/** * ClickUp node config — connection, browse, search, create task with list fields. */ import React, { useEffect, useState, useCallback, useMemo, useRef } from 'react'; import type { NodeConfigRendererProps } from './types'; import { fetchConnections, fetchBrowse, fetchClickupList, fetchClickupTask, fetchClickupTeam, fetchClickupListFields, loadClickupListTasksForDropdown, type UserConnection, type BrowseEntry, type ApiRequestFunction, } from '../../../../api/workflowApi'; import { SharepointBrowseTree } from '../../../FolderTree/SharepointBrowseTree'; import { HybridStaticRefField } from '../shared/HybridStaticRefField'; import { StatischKontextSelect, shouldShowStaticControl, } from '../shared/RefSourceSelect'; import { useAutomation2DataFlow } from '../../context/Automation2DataFlowContext'; import { isRef, isValue, createValue, resolvePreview } from '../shared/dataRef'; import { buildSyncFromClickUpList, findClosestUpstreamFormNode, type ClickUpFieldLike, } from '../shared/clickupFormSync'; import styles from '../../editor/Automation2FlowEditor.module.css'; /** ClickUp browse: list folder paths end with /list/{id} */ const LIST_SEGMENT_RE = /\/list\/([^/]+)$/; /** List id from stored path (e.g. /team/…/list/{id}/…) when listId param is empty. */ function parseListIdFromPath(path: unknown): string | null { if (typeof path !== 'string' || !path.trim()) return null; const m = path.match(/\/list\/([^/?#]+)/); return m && m[1] ? m[1].trim() : null; } function teamIdFromBrowseEntry(e: BrowseEntry): string | null { const m = e.path.match(/^\/team\/([^/]+)$/); if (m) return m[1]; const raw = e.metadata?.id; if (typeof raw === 'string' && raw) return raw; if (typeof raw === 'number') return String(raw); return null; } /** Recursively collect list “tables” under /team/{teamId} (spaces, folders, folderless lists). */ async function collectListsUnderTeam( request: ApiRequestFunction, instanceId: string, connectionId: string, teamPath: string ): Promise> { const acc: Array<{ id: string; name: string }> = []; const seen = new Set(); const visit = async (path: string) => { const { items } = await fetchBrowse(request, instanceId, connectionId, 'clickup', path); for (const e of items) { if (!e.isFolder) continue; if (LIST_SEGMENT_RE.test(e.path)) { const mid = e.path.match(LIST_SEGMENT_RE); if (mid && !seen.has(mid[1])) { seen.add(mid[1]); acc.push({ id: mid[1], name: e.name }); } } else { await visit(e.path); } } }; await visit(teamPath); acc.sort((a, b) => a.name.localeCompare(b.name, 'de')); return acc; } const browseDetailsStyle: React.CSSProperties = { marginTop: 12, border: '1px solid var(--border-color, #e0e0e0)', borderRadius: 6, background: 'var(--bg-secondary, #f8f9fa)', overflow: 'hidden', }; const browseSummaryStyle: React.CSSProperties = { padding: '0.5rem 0.75rem', cursor: 'pointer', fontWeight: 500, fontSize: '0.875rem', display: 'flex', alignItems: 'center', gap: '0.5rem', userSelect: 'none', }; const browseBodyStyle: React.CSSProperties = { padding: '0.5rem 0.75rem', borderTop: '1px solid var(--border-color, #e0e0e0)', maxHeight: 280, overflowY: 'auto', }; function browseTitle(nodeType: string): string { switch (nodeType) { case 'clickup.listTasks': return 'Liste wählen (Ordner bis /team/…/list/…)'; case 'clickup.getTask': case 'clickup.updateTask': case 'clickup.uploadAttachment': return 'Aufgabe wählen (Pfad …/task/…)'; default: return 'ClickUp durchsuchen'; } } function isListPicker(nodeType: string): boolean { return nodeType === 'clickup.listTasks'; } function isTaskPicker(nodeType: string): boolean { return ( nodeType === 'clickup.getTask' || nodeType === 'clickup.updateTask' || nodeType === 'clickup.uploadAttachment' ); } const needsTeamList = (nt: string) => nt === 'clickup.searchTasks' || nt === 'clickup.createTask' || nt === 'clickup.updateTask'; /** Status names from GET /list/{id} (same as ClickUp table columns). */ function parseListStatuses(data: Record): Array<{ status: string; orderindex: number }> { if (data && typeof data === 'object' && 'error' in data && (data as { error?: unknown }).error) { return []; } const nested = data.list as Record | undefined; const list = nested && typeof nested === 'object' ? nested : data; let raw = list?.statuses; if (!Array.isArray(raw) && Array.isArray(data.statuses)) { raw = data.statuses; } if (!Array.isArray(raw)) return []; const out: Array<{ status: string; orderindex: number }> = []; raw.forEach((s, idx) => { if (typeof s === 'string' && s.trim()) { out.push({ status: s.trim(), orderindex: idx }); return; } if (!s || typeof s !== 'object') return; const ob = s as Record; let st: string | undefined; if (typeof ob.status === 'string' && ob.status) { st = ob.status; } else if (ob.status && typeof ob.status === 'object' && ob.status !== null) { const inner = (ob.status as Record).status; if (typeof inner === 'string' && inner) st = inner; } if (!st && typeof ob.name === 'string' && ob.name) st = ob.name; const oi = ob.orderindex; if (st) { const o = typeof oi === 'number' ? oi : typeof oi === 'string' ? parseInt(oi, 10) : 0; out.push({ status: st, orderindex: Number.isFinite(o) ? o : 0 }); } }); out.sort((a, b) => a.orderindex - b.orderindex); return out; } /** List id from GET /task/{id} when only task id is known (ref / static). */ function listIdFromClickUpTaskApi(data: Record): string | null { const root = data.task && typeof data.task === 'object' && data.task !== null ? (data.task as Record) : data; const list = root.list; if (list && typeof list === 'object' && list !== null) { const id = (list as Record).id; if (id != null && String(id).trim()) return String(id); } if (root.list_id != null && String(root.list_id).trim()) return String(root.list_id); return null; } /** Reduces bogus GET /task calls (placeholders, garbage). */ function isPlausibleClickUpTaskId(tid: string): boolean { const t = tid.trim(); if (t.length < 4 || t.length > 64) return false; if (t.includes('{{') || t.includes('}}')) return false; return /^[\w-]+$/.test(t); } /** Members from GET /team/{id} for assignee multi-select. */ function parseTeamMembers(data: Record): Array<{ id: string; username: string }> { const team = (data.team as Record | undefined) ?? data; const members = team?.members; if (!Array.isArray(members)) return []; const out: Array<{ id: string; username: string }> = []; for (const m of members) { if (!m || typeof m !== 'object') continue; const ob = m as Record; const user = ob.user; const u = user && typeof user === 'object' ? (user as Record) : ob; const id = u.id != null ? String(u.id) : ''; if (!id) continue; const username = String(u.username ?? u.email ?? id); out.push({ id, username }); } out.sort((a, b) => a.username.localeCompare(b.username, 'de')); return out; } type ClickUpField = Record; /** ClickUp API uses option UUID/string id as value, not orderindex (see custom fields docs). */ function normalizeFieldType(raw: unknown): string { return String(raw ?? 'short_text') .trim() .toLowerCase() .replace(/-/g, '_') .replace(/\s+/g, '_'); } function dropdownOptions(field: ClickUpField): Array<{ label: string; value: string }> { const tc = field.type_config as Record | undefined; if (!tc) return []; let options = tc.options as unknown[] | undefined; if (!options?.length && Array.isArray(tc.dropdowns)) { const d0 = (tc.dropdowns as unknown[])[0] as Record | undefined; options = d0?.options as unknown[] | undefined; } if (!Array.isArray(options)) return []; const out: Array<{ label: string; value: string }> = []; for (const o of options) { if (!o || typeof o !== 'object') continue; const ob = o as Record; const name = String(ob.name ?? ob.label ?? ''); const idRaw = ob.id; const id = idRaw != null && idRaw !== '' ? String(idRaw) : ''; const oi = ob.orderindex; const orderindex = typeof oi === 'number' ? oi : typeof oi === 'string' ? parseInt(oi, 10) : NaN; const fallback = Number.isFinite(orderindex) ? String(orderindex) : ''; const value = id || fallback; if (!value) continue; out.push({ label: name, value }); } return out; } function fieldUnsupported(ft: string): boolean { return ['tasks', 'user', 'users'].includes(ft); } /** Target list for a list_relationship field (ClickUp type_config varies by version). */ function linkedListIdFromRelationshipField(field: ClickUpField): string | null { const tc = (field.type_config ?? {}) as Record; const asId = (v: unknown): string | null => { if (typeof v === 'string' && v.trim()) return v.trim(); if (typeof v === 'number' && Number.isFinite(v)) return String(v); return null; }; const directKeys = [ 'linked_list_id', 'list_id', 'related_list_id', 'relationship_list_id', 'relation_list_id', 'target_list_id', 'resource_id', ]; for (const k of directKeys) { const raw = tc[k]; const id = asId(raw); if (id) return id; if (raw && typeof raw === 'object' && raw !== null) { const nested = asId((raw as Record).id); if (nested) return nested; } } if (String(tc.resource_type ?? '').toLowerCase() === 'list') { const id = asId(tc.resource_id); if (id) return id; } const rel = tc.relationship; if (rel && typeof rel === 'object' && rel !== null) { const r = rel as Record; const fromRel = asId( r.list_id ?? r.id ?? r.target_id ?? r.linked_list_id ?? r.resource_id ); if (fromRel) return fromRel; } const nestedKeys = ['list', 'linked_list', 'related_list', 'resource', 'related_resource'] as const; for (const k of nestedKeys) { const raw = tc[k]; if (raw == null) continue; const id = asId(raw) ?? (typeof raw === 'object' ? asId((raw as Record).id) : null); if (id) return id; } return null; } /** ClickUp relationship / tasks-style fields: { add: taskIds, rem: [] }. */ function relationshipPayloadFromValue(value: unknown): { add: string[]; rem: string[] } | null { if (value == null) return null; if (isRef(value)) return null; if (isValue(value)) { const v = value.value; if (v === '' || v === null || v === undefined) return null; if (typeof v === 'object' && v !== null && 'add' in v && Array.isArray((v as { add: unknown }).add)) { return v as { add: string[]; rem: string[] }; } if (typeof v === 'string' && v) return { add: [v], rem: [] }; } if (typeof value === 'object' && value !== null && 'add' in value) { return value as { add: string[]; rem: string[] }; } if (typeof value === 'string' && value) return { add: [value], rem: [] }; return null; } function selectedTaskIdFromRelationship(value: unknown): string { const p = relationshipPayloadFromValue(value); if (!p?.add?.length) return ''; return String(p.add[0]); } function ClickUpListRelationshipFieldRow({ fname, connectionId, request, linkedListId, value, onChange, }: { fname: string; connectionId: string; request: ApiRequestFunction; linkedListId: string; value: unknown; onChange: (v: unknown) => void; }) { const dataFlow = useAutomation2DataFlow(); const [tasks, setTasks] = useState>([]); const [loading, setLoading] = useState(false); const [loadError, setLoadError] = useState(null); const hasSources = dataFlow && dataFlow.getAvailableSourceIds().some((id) => { const n = dataFlow.nodes.find((x) => x.id === id); return n?.type !== 'trigger.manual'; }); useEffect(() => { let cancelled = false; setLoading(true); setLoadError(null); loadClickupListTasksForDropdown(request, connectionId, linkedListId) .then((rows) => { if (!cancelled) setTasks(rows); }) .catch(() => { if (!cancelled) { setTasks([]); setLoadError('Aufgaben konnten nicht geladen werden.'); } }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, [request, connectionId, linkedListId]); const sel = selectedTaskIdFromRelationship(value); return (
{loadError ? (

{loadError}

) : null} {hasSources ? (
) : null} {shouldShowStaticControl(value, Boolean(hasSources)) && ( <> {loading ? (

Verknüpfte Aufgaben werden geladen…

) : ( )} )}
); } /** Selected dropdown option id (string); supports legacy numeric orderindex stored as number. */ function staticDropdownStringValue(value: unknown): string { if (value == null) return ''; if (isRef(value)) return ''; if (isValue(value)) { const v = value.value; if (v === '' || v === null || v === undefined) return ''; return String(v); } return String(value ?? ''); } function staticCheckbox(value: unknown): boolean { if (value == null) return false; if (isValue(value)) return Boolean(value.value); return Boolean(value); } /** One custom field row: static and/or context ref. */ function fieldConfigHintMessage(fname: string, ft: string): string { return `${fname} (${ft})`; } function ClickUpCustomFieldRow({ field, value, onChange, connectionId, request, parentListId, }: { field: ClickUpField; value: unknown; onChange: (v: unknown) => void; connectionId?: string; request?: ApiRequestFunction; /** List where the new task is created — fallback if relationship omits linked list (same-list). */ parentListId?: string; }) { const dataFlow = useAutomation2DataFlow(); const fid = String(field.id ?? ''); const fname = String(field.name ?? fid); const ft = normalizeFieldType(field.type); const hasSources = dataFlow && dataFlow.getAvailableSourceIds().some((id) => { const n = dataFlow.nodes.find((x) => x.id === id); return n?.type !== 'trigger.manual'; }); if (fieldUnsupported(ft)) { return (

{fname} ({ft}): nur über „Zusätzliches JSON“ setzbar.

); } if (ft === 'list_relationship') { const linkedId = linkedListIdFromRelationshipField(field) ?? (parentListId?.trim() || null); if (!connectionId || !request) { return (

{fname} (List-Beziehung): Verbindung fehlt — Feld kann nicht geladen werden.

); } if (!linkedId) { return (

{fname} (List-Beziehung): Verknüpfte Liste nicht in den Felddaten — in ClickUp prüfen oder Support.

); } return ( ); } if (ft === 'drop_down' || ft === 'dropdown') { const opts = dropdownOptions(field); if (!opts.length) { return (

{fieldConfigHintMessage(fname, ft)}: Keine Dropdown-Optionen in den Felddaten (type_config.options).

); } return (
{hasSources ? (
) : null} {shouldShowStaticControl(value, Boolean(hasSources)) && ( )}
); } if (ft === 'checkbox') { const checked = staticCheckbox(value); return (
{hasSources ? (
) : null} {shouldShowStaticControl(value, Boolean(hasSources)) && ( )}
); } if (ft === 'date') { const ms = isValue(value) ? Number(value.value) : typeof value === 'number' ? value : 0; const day = ms > 0 ? new Date(ms).toISOString().slice(0, 10) : ''; return (
{hasSources ? (
) : null} {shouldShowStaticControl(value, Boolean(hasSources)) && ( { const t = e.target.value ? new Date(e.target.value).getTime() : ''; onChange(createValue(t === '' ? '' : t)); }} /> )}
); } if (ft === 'number' || ft === 'currency') { return ( ); } if (ft === 'text' || ft === 'long_text' || ft === 'short_text' || ft === 'email' || ft === 'phone' || ft === 'url') { const multiline = ft === 'text' || ft === 'long_text'; return ( ); } return ( ); } function ClickUpTaskDueDateRow({ value, onChange, }: { value: unknown; onChange: (v: unknown) => void; }) { const dataFlow = useAutomation2DataFlow(); const hasSources = dataFlow && dataFlow.getAvailableSourceIds().some((id) => { const n = dataFlow.nodes.find((x) => x.id === id); return n?.type !== 'trigger.manual'; }); const ms = isValue(value) ? Number(value.value) : typeof value === 'number' ? value : 0; const day = ms > 0 ? new Date(ms).toISOString().slice(0, 10) : ''; return (
{hasSources ? (
) : null} {shouldShowStaticControl(value, Boolean(hasSources)) && ( { const t = e.target.value ? new Date(e.target.value).getTime() : ''; onChange(createValue(t === '' ? '' : t)); }} /> )}
); } function ClickUpTimeEstimateHoursRow({ value, onChange, }: { value: unknown; onChange: (v: unknown) => void; }) { const dataFlow = useAutomation2DataFlow(); const hasSources = dataFlow && dataFlow.getAvailableSourceIds().some((id) => { const n = dataFlow.nodes.find((x) => x.id === id); return n?.type !== 'trigger.manual'; }); const h = isValue(value) ? Number(value.value) : typeof value === 'number' ? value : 0; const hours = h > 0 && Number.isFinite(h) ? h : ''; return (
{hasSources ? (
) : null} {shouldShowStaticControl(value, Boolean(hasSources)) && ( { const raw = e.target.value; if (raw === '') onChange(createValue('')); else { const n = parseFloat(raw); onChange(createValue(Number.isFinite(n) ? n : '')); } }} /> )}
); } /** One row for clickup.updateTask — field + value (+ custom field id). */ export type ClickUpTaskUpdateEntryRow = { rowId: string; fieldKey: string; customFieldId: unknown; value: unknown; }; function newClickUpTaskUpdateEntryRow(): ClickUpTaskUpdateEntryRow { return { rowId: `row_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`, fieldKey: 'name', customFieldId: createValue(''), value: createValue(''), }; } const UPDATE_TASK_FIELD_OPTIONS: Array<{ value: string; label: string }> = [ { value: 'name', label: 'Titel (name)' }, { value: 'description', label: 'Beschreibung' }, { value: 'status', label: 'Status' }, { value: 'priority', label: 'Priorität (1–4)' }, { value: 'due_date', label: 'Fälligkeit (Datum oder ms)' }, { value: 'time_estimate_h', label: 'Zeitschätzung (Stunden)' }, { value: 'time_estimate_ms', label: 'Zeitschätzung (ms)' }, { value: 'assignees', label: 'Zugewiesene' }, { value: 'custom_field', label: 'Benutzerdefiniertes Feld' }, ]; function normalizeTaskUpdateEntries(raw: unknown): ClickUpTaskUpdateEntryRow[] { if (!Array.isArray(raw)) return []; return raw.map((r, i) => { if (r && typeof r === 'object' && !Array.isArray(r)) { const o = r as Record; return { rowId: String(o.rowId ?? `row_${i}`), fieldKey: String(o.fieldKey ?? 'name'), customFieldId: o.customFieldId ?? createValue(''), value: o.value ?? createValue(''), }; } return newClickUpTaskUpdateEntryRow(); }); } /** Static task id for dropdown (refs → empty). */ function taskIdStaticForDropdown(taskId: unknown): string { if (isRef(taskId)) return ''; if (isValue(taskId)) return String(taskId.value ?? '').trim(); if (typeof taskId === 'string' || typeof taskId === 'number') return String(taskId).trim(); return ''; } /** Pick an existing task from a list (same API as Form „ClickUp-Aufgabe“). */ function ClickUpTaskFromListDropdown({ connectionId, listId, request, taskId, onSetTaskId, }: { connectionId: string; listId: string; request: ApiRequestFunction; taskId: unknown; onSetTaskId: (v: unknown) => void; }) { const [tasks, setTasks] = useState>([]); const [loading, setLoading] = useState(false); const [err, setErr] = useState(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('Aufgaben konnten nicht geladen werden.'); } }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, [request, connectionId, listId]); const sel = taskIdStaticForDropdown(taskId); return (
{err ? (

{err}

) : null} {loading ? ( Aufgaben werden geladen… ) : ( )}

Workspace und Liste oben wählen. Oder Task-ID unten per Kontext-Referenz setzen (dann entfällt die Listenauswahl oben).

); } function ClickUpUpdateTaskEntriesEditor({ rows, onChangeRows, connectionId, listId, pathForListId, request, taskIdParam, teamMembers, teamMembersLoading, parentListStatuses, parentListStatusesLoading, }: { rows: ClickUpTaskUpdateEntryRow[]; onChangeRows: (next: ClickUpTaskUpdateEntryRow[]) => void; connectionId: string; /** Resolved list id (param or parsed from path). */ listId: string; /** Raw path — fallback to parse /list/{id}/ when listId param empty. */ pathForListId: string; request: ApiRequestFunction; taskIdParam: unknown; teamMembers: Array<{ id: string; username: string }>; teamMembersLoading: boolean; /** Same list fetch as parent — avoids empty child dropdown when listId is set but child effect lags. */ parentListStatuses: Array<{ status: string; orderindex: number }>; parentListStatusesLoading: boolean; }) { const dataFlow = useAutomation2DataFlow(); const [resolvedStatuses, setResolvedStatuses] = useState>( [] ); const [resolvedStatusesLoading, setResolvedStatusesLoading] = useState(false); const previewFingerprint = useMemo( () => JSON.stringify(dataFlow?.nodeOutputsPreview ?? {}), [dataFlow?.nodeOutputsPreview] ); /** GET /list/{id}; wenn keine List-ID: GET /task/{id} nur bei plausibler Task-ID (Vorschau/Statisch). */ useEffect(() => { let cancelled = false; (async () => { const cid = (connectionId || '').trim(); if (!cid || !request) { setResolvedStatuses([]); setResolvedStatusesLoading(false); return; } let lid = (listId || '').trim(); if (!lid) { lid = parseListIdFromPath(pathForListId) ?? ''; } const lidFromListOrPath = lid; if (!lid && taskIdParam != null && dataFlow) { let tid = ''; if (isRef(taskIdParam)) { const r = resolvePreview(taskIdParam, dataFlow.nodeOutputsPreview); tid = r != null && r !== '' ? String(r) : ''; } else if (isValue(taskIdParam)) { tid = String(taskIdParam.value ?? '').trim(); } else if (typeof taskIdParam === 'string' || typeof taskIdParam === 'number') { tid = String(taskIdParam).trim(); } if (tid && isPlausibleClickUpTaskId(tid)) { setResolvedStatusesLoading(true); try { const task = await fetchClickupTask(request, cid, tid); if (cancelled) return; if ( task && typeof task === 'object' && !('error' in task && (task as { error?: unknown }).error) ) { const fromTask = listIdFromClickUpTaskApi(task as Record) || ''; if (fromTask) lid = fromTask; } } catch { lid = lidFromListOrPath; } } } if (!lid) { setResolvedStatuses([]); setResolvedStatusesLoading(false); return; } setResolvedStatusesLoading(true); try { const data = await fetchClickupList(request, cid, lid); if (cancelled) return; if ( data && typeof data === 'object' && 'error' in data && (data as { error?: unknown }).error ) { setResolvedStatuses([]); } else { setResolvedStatuses(parseListStatuses(data as Record)); } } catch { if (!cancelled) setResolvedStatuses([]); } finally { if (!cancelled) setResolvedStatusesLoading(false); } })(); return () => { cancelled = true; }; }, [connectionId, listId, pathForListId, request, taskIdParam, previewFingerprint, dataFlow]); const statusOptions = useMemo(() => { if (resolvedStatuses.length > 0) return resolvedStatuses; return parentListStatuses ?? []; }, [resolvedStatuses, parentListStatuses]); const statusOptionsLoading = resolvedStatusesLoading || (resolvedStatuses.length === 0 && parentListStatusesLoading && Boolean((listId || '').trim() || parseListIdFromPath(pathForListId))); const updateRow = (rowId: string, patch: Partial) => { onChangeRows(rows.map((r) => (r.rowId === rowId ? { ...r, ...patch } : r))); }; const removeRow = (rowId: string) => { onChangeRows(rows.filter((r) => r.rowId !== rowId)); }; const assigneeIdsFromValue = (v: unknown): string[] => { if (isRef(v)) return []; const raw = isValue(v) ? v.value : v; if (Array.isArray(raw)) return raw.map((x) => String(x)).filter(Boolean); if (typeof raw === 'string' && raw.trim()) { try { const p = JSON.parse(raw) as unknown; if (Array.isArray(p)) return p.map((x) => String(x)).filter(Boolean); } catch { return []; } } return []; }; const renderValueEditor = (row: ClickUpTaskUpdateEntryRow) => { const fk = row.fieldKey; const commonHybrid = (multiline?: boolean, placeholder?: string, inputType?: 'text' | 'number') => ( updateRow(row.rowId, { value: v })} multiline={multiline} inputType={inputType} placeholder={placeholder} pathPickMode="exclude_forms" /> ); if (fk === 'custom_field') { return ( <> updateRow(row.rowId, { customFieldId: v })} placeholder="Feld-ID" pathPickMode="exclude_forms" /> updateRow(row.rowId, { value: v })} multiline placeholder="Wert" pathPickMode="exclude_forms" /> ); } if (fk === 'status') { if (isRef(row.value)) { return ( updateRow(row.rowId, { value: v })} placeholder="Kontext mit Status-String" pathPickMode="exclude_forms" /> ); } const staticVal = isValue(row.value) ? String(row.value.value ?? '') : String(row.value ?? ''); if (statusOptionsLoading) { return (
); } return (
{statusOptions.length === 0 && (

Liste wählen {' '} (Kasten direkt über den Zeilen) oder gültige Task-ID mit Vorschau; alternativ Path mit{' '} /list/…/.

)}
); } if (fk === 'priority') { const pv = isRef(row.value) ? '' : isValue(row.value) ? String(row.value.value ?? '') : String(row.value ?? ''); return (
); } if (fk === 'due_date') { return ( <> updateRow(row.rowId, { value: v })} /> {commonHybrid(false, 'Alternativ nur Referenz')} ); } if (fk === 'time_estimate_h') { return ( <> updateRow(row.rowId, { value: v })} /> {commonHybrid(false, 'Alternativ: Referenz (Stunden)')} ); } if (fk === 'time_estimate_ms') { return commonHybrid(false, 'ms', 'number'); } if (fk === 'assignees') { const selected = new Set(assigneeIdsFromValue(row.value)); return ( <> {teamMembersLoading ? ( Mitglieder werden geladen… ) : teamMembers.length === 0 ? (

Keine Mitglieder — Workspace oben wählen oder JSON / Referenz unten.

) : (
{teamMembers.map((m) => ( ))}
)} {commonHybrid(false, '[123,456] oder JSON-Array')} ); } if (fk === 'description') { return ( updateRow(row.rowId, { value: v })} multiline placeholder="Beschreibung" pathPickMode="exclude_forms" /> ); } if (fk === 'name') { return ( updateRow(row.rowId, { value: v })} placeholder="Titel" pathPickMode="exclude_forms" /> ); } return commonHybrid(false); }; return (

Pro Zeile ein Feld. Status und Priorität wie in ClickUp (Dropdown). Referenzen ohne Formular-Payload (nur Knoten wie „Aufgabe erstellen“).

{rows.map((row) => (
{renderValueEditor(row)}
))}
); } export const ClickUpNodeConfig: React.FC = ({ params, updateParam, instanceId, request, mergeNodeParameters, nodeType = 'clickup.listTasks', }) => { const dataFlow = useAutomation2DataFlow(); const lastFormSyncSigRef = useRef(''); const [connections, setConnections] = useState([]); const [browseOpen, setBrowseOpen] = useState(false); const [connectionsLoading, setConnectionsLoading] = useState(false); const [searchTeams, setSearchTeams] = useState>([]); const [searchTeamsLoading, setSearchTeamsLoading] = useState(false); const [searchLists, setSearchLists] = useState>([]); const [searchListsLoading, setSearchListsLoading] = useState(false); const [listFields, setListFields] = useState([]); const [listFieldsLoading, setListFieldsLoading] = useState(false); const [listStatuses, setListStatuses] = useState>([]); const [listStatusesLoading, setListStatusesLoading] = useState(false); const [teamMembers, setTeamMembers] = useState>([]); const [teamMembersLoading, setTeamMembersLoading] = useState(false); const connectionId = (params.connectionId as string) ?? ''; const path = (params.path as string) ?? ''; const teamIdParam = (params.teamId as string) ?? ''; const listIdParam = (params.listId as string) ?? ''; /** listId param or list id parsed from path — loads statuses when Task-GET fails (ref / 404). */ const effectiveListIdForStatuses = useMemo(() => { const lid = ((params.listId as string) ?? '').trim(); if (lid) return lid; return parseListIdFromPath(path) ?? ''; }, [params.listId, path]); const customFieldValues = useMemo( () => (params.customFieldValues as Record | undefined) ?? {}, [params.customFieldValues] ); const taskAssigneeIds = useMemo(() => { const raw = params.taskAssigneeIds; if (Array.isArray(raw)) { return raw.map((x) => String(x)).filter(Boolean); } if (typeof raw === 'string' && raw.trim()) { try { const p = JSON.parse(raw) as unknown; if (Array.isArray(p)) return p.map((x) => String(x)).filter(Boolean); } catch { return []; } } return []; }, [params.taskAssigneeIds]); /** Stunden (API); Fallback: alte Speicherung in ms → Anzeige in Stunden */ const timeEstimateHoursValue = useMemo(() => { const th = params.taskTimeEstimateHours; if (th != null && th !== '') return th; const tm = params.taskTimeEstimateMs; if (isValue(tm) && typeof tm.value === 'number' && tm.value > 0) { return createValue(tm.value / (3600 * 1000)); } if (typeof tm === 'number' && tm > 0) return createValue(tm / (3600 * 1000)); return createValue(''); }, [params.taskTimeEstimateHours, params.taskTimeEstimateMs]); const runFormSyncFromList = useCallback( (force: boolean) => { if (!mergeNodeParameters || !connectionId || !teamIdParam || !listIdParam) return; if (!dataFlow) return; if (listFieldsLoading) return; if (listStatusesLoading) return; const formNode = findClosestUpstreamFormNode( dataFlow.currentNodeId, dataFlow.nodes, dataFlow.connections ); if (!formNode) return; const sig = `${listIdParam}|${listFields.map((f) => String((f as ClickUpFieldLike).id ?? '')).sort().join(',')}|${listStatuses.map((s) => s.status).join('\x00')}`; if (!force && lastFormSyncSigRef.current === sig) return; lastFormSyncSigRef.current = sig; const built = buildSyncFromClickUpList({ formNodeId: formNode.id, listFields: listFields as ClickUpFieldLike[], listStatuses, connectionId, teamId: teamIdParam, listId: listIdParam, }); if (formNode.type === 'input.form') { mergeNodeParameters(formNode.id, { fields: built.inputFormFields }); } else if (formNode.type === 'trigger.form') { mergeNodeParameters(formNode.id, { formFields: built.triggerFormFields }); } for (const [k, v] of Object.entries(built.clickupPatch)) { updateParam(k, v); } updateParam('taskTimeEstimateMs', createValue('')); }, [ mergeNodeParameters, connectionId, teamIdParam, listIdParam, listFields, listFieldsLoading, listStatuses, listStatusesLoading, dataFlow, updateParam, ] ); const clickupConnections = useMemo( () => connections.filter((c) => c.authority === 'clickup'), [connections] ); useEffect(() => { if (instanceId && request) { setConnectionsLoading(true); fetchConnections(request, instanceId) .then(setConnections) .catch(() => setConnections([])) .finally(() => setConnectionsLoading(false)); } }, [instanceId, request]); useEffect(() => { if (!needsTeamList(nodeType) || !instanceId || !request || !connectionId) { setSearchTeams([]); return; } let cancelled = false; setSearchTeamsLoading(true); fetchBrowse(request, instanceId, connectionId, 'clickup', '/') .then((r) => { if (cancelled) return; const rows: Array<{ id: string; name: string }> = []; for (const e of r.items ?? []) { const tid = teamIdFromBrowseEntry(e); if (tid) rows.push({ id: tid, name: e.name }); } setSearchTeams(rows); }) .catch(() => { if (!cancelled) setSearchTeams([]); }) .finally(() => { if (!cancelled) setSearchTeamsLoading(false); }); return () => { cancelled = true; }; }, [nodeType, instanceId, request, connectionId]); useEffect(() => { if (!needsTeamList(nodeType) || !instanceId || !request || !connectionId || !teamIdParam) { setSearchLists([]); return; } let cancelled = false; setSearchListsLoading(true); const teamPath = `/team/${teamIdParam}`; collectListsUnderTeam(request, instanceId, connectionId, teamPath) .then((rows) => { if (!cancelled) setSearchLists(rows); }) .catch(() => { if (!cancelled) setSearchLists([]); }) .finally(() => { if (!cancelled) setSearchListsLoading(false); }); return () => { cancelled = true; }; }, [nodeType, instanceId, request, connectionId, teamIdParam]); useEffect(() => { if (nodeType !== 'clickup.createTask' || !connectionId || !listIdParam || !request) { setListFields([]); return; } let cancelled = false; setListFieldsLoading(true); fetchClickupListFields(request, connectionId, listIdParam) .then((data) => { if (cancelled) return; const raw = data?.fields; const list = Array.isArray(raw) ? raw : Array.isArray(data) ? (data as ClickUpField[]) : []; setListFields(list as ClickUpField[]); }) .catch(() => { if (!cancelled) setListFields([]); }) .finally(() => { if (!cancelled) setListFieldsLoading(false); }); return () => { cancelled = true; }; }, [nodeType, connectionId, listIdParam, request]); useEffect(() => { if ( (nodeType !== 'clickup.createTask' && nodeType !== 'clickup.updateTask') || !connectionId || !effectiveListIdForStatuses || !request ) { setListStatuses([]); return; } let cancelled = false; setListStatusesLoading(true); fetchClickupList(request, connectionId, effectiveListIdForStatuses) .then((data) => { if (cancelled) return; if (data && typeof data === 'object' && 'error' in data && (data as { error?: unknown }).error) { setListStatuses([]); return; } setListStatuses(parseListStatuses(data as Record)); }) .catch(() => { if (!cancelled) setListStatuses([]); }) .finally(() => { if (!cancelled) setListStatusesLoading(false); }); return () => { cancelled = true; }; }, [nodeType, connectionId, effectiveListIdForStatuses, request]); useEffect(() => { if ( (nodeType !== 'clickup.createTask' && nodeType !== 'clickup.updateTask') || !connectionId || !teamIdParam || !request ) { setTeamMembers([]); return; } let cancelled = false; setTeamMembersLoading(true); fetchClickupTeam(request, connectionId, teamIdParam) .then((data) => { if (cancelled) return; if (data && typeof data === 'object' && 'error' in data && (data as { error?: unknown }).error) { setTeamMembers([]); return; } setTeamMembers(parseTeamMembers(data as Record)); }) .catch(() => { if (!cancelled) setTeamMembers([]); }) .finally(() => { if (!cancelled) setTeamMembersLoading(false); }); return () => { cancelled = true; }; }, [nodeType, connectionId, teamIdParam, request]); useEffect(() => { if (nodeType !== 'clickup.createTask') return; if (params.autoSyncFormWithList === false) return; if (!mergeNodeParameters) return; runFormSyncFromList(false); }, [ nodeType, params.autoSyncFormWithList, mergeNodeParameters, listIdParam, listFields, listFieldsLoading, connectionId, teamIdParam, runFormSyncFromList, ]); const loadChildren = useCallback( async (pathToLoad: string): Promise => { if (!instanceId || !request || !connectionId) return []; const r = await fetchBrowse(request, instanceId, connectionId, 'clickup', pathToLoad); return r?.items ?? []; }, [instanceId, request, connectionId] ); const selectPath = useCallback( (p: string) => { updateParam('path', p); setBrowseOpen(false); }, [updateParam] ); const setCustomField = useCallback( (fieldId: string, v: unknown) => { updateParam('customFieldValues', { ...customFieldValues, [fieldId]: v }); }, [customFieldValues, updateParam] ); const showBrowse = connectionId && (isListPicker(nodeType) || isTaskPicker(nodeType)); return ( <>
{nodeType === 'clickup.searchTasks' && ( <>
updateParam('query', e.target.value)} placeholder="Stichwort für die Aufgabensuche" />
Erweitert
updateParam('page', parseInt(e.target.value, 10) || 0)} />
{listIdParam ? (
) : null}
)} {nodeType === 'clickup.createTask' && ( <>
{mergeNodeParameters && teamIdParam && listIdParam ? (

Sucht den nächsten Formular-Knoten (input.form oder{' '} trigger.form) stromaufwärts, legt dessen Felder wie die ClickUp-Liste an und setzt hier die Datenquellen (payload.…) auf dieses Formular. Anschließend können Sie Felder im Formular-Knoten anpassen.

{!dataFlow || !findClosestUpstreamFormNode( dataFlow.currentNodeId, dataFlow.nodes, dataFlow.connections ) ? (

Kein Formular-Knoten stromaufwärts verbunden — zuerst ein Formular vor diesen Node ziehen und verbinden.

) : null}
) : null} {teamIdParam && listIdParam ? (

Status, Priorität, Fälligkeit, Zuweisungen und Zeitschätzung — dieselben Spalten wie in der Listenansicht (nicht die benutzerdefinierten Felder darunter).

{listStatusesLoading ? ( Status wird geladen… ) : ( )}
updateParam('taskDueDateMs', v)} />
{teamMembersLoading ? ( Mitglieder werden geladen… ) : teamMembers.length === 0 ? ( Keine Mitglieder geladen. ) : (
{teamMembers.map((m) => ( ))}
)}
{ updateParam('taskTimeEstimateHours', v); updateParam('taskTimeEstimateMs', createValue('')); }} />
) : null} {listIdParam ? (
{listFieldsLoading ? (

Felder werden geladen…

) : listFields.length === 0 ? (

Keine benutzerdefinierten Felder oder keine Berechtigung.

) : ( listFields.map((f) => { const id = String(f.id ?? ''); if (!id) return null; return ( setCustomField(id, v)} connectionId={connectionId} request={request} parentListId={listIdParam} /> ); }) )}
) : null}
Erweitert (JSON)