/** * Start node (Zeitplan) — Karten-UI mit Konfiguration unter der gewählten Option. */ import React, { useState, useEffect, useCallback, useRef } from 'react'; import { AnimatePresence, LayoutGroup, motion, useReducedMotion } from 'framer-motion'; import type { NodeConfigRendererProps } from '../configs/types'; import { type ScheduleSpec, type ScheduleMode, type IntervalUnit, type CalendarPeriod, buildCronFromSpec, scheduleSpecFromParams, WEEKDAYS_MO_SO, } from '../runtime/scheduleCron'; import styles from '../../editor/Automation2FlowEditor.module.css'; import { useLanguage } from '../../../../providers/language/LanguageContext'; function _getModeOptions(t: (key: string) => string): { value: ScheduleMode; title: string; subtitle: string }[] { return [ { value: 'daily', title: t('scheduleStartNodeConfig.taeglich'), subtitle: t('scheduleStartNodeConfig.jedenTagZurGleichenZeit') }, { value: 'weekdays', title: t('scheduleStartNodeConfig.werktage'), subtitle: t('scheduleStartNodeConfig.montagBisFreitag') }, { value: 'weekly', title: t('scheduleStartNodeConfig.bestimmteTage'), subtitle: t('scheduleStartNodeConfig.wochentageAuswaehlen') }, { value: 'calendar', title: t('scheduleStartNodeConfig.einAndererZeitraum'), subtitle: t('scheduleStartNodeConfig.monatlichOderJaehrlich') }, { value: 'interval', title: t('scheduleStartNodeConfig.intervall'), subtitle: t('scheduleStartNodeConfig.inRegelmaessigenAbstaenden') }, ]; } const MONTH_NAMES_DE = [ 'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember', ]; function _getIntervalUnits(t: (key: string) => string): { value: IntervalUnit; label: string; title: string }[] { return [ { value: 'seconds', label: t('scheduleStartNodeConfig.sek'), title: t('scheduleStartNodeConfig.sekunden') }, { value: 'minutes', label: t('scheduleStartNodeConfig.min'), title: t('scheduleStartNodeConfig.minuten') }, { value: 'hours', label: t('scheduleStartNodeConfig.h'), title: t('scheduleStartNodeConfig.stunden') }, { value: 'days', label: t('scheduleStartNodeConfig.d'), title: t('scheduleStartNodeConfig.tage') }, { value: 'years', label: t('scheduleStartNodeConfig.a'), title: t('scheduleStartNodeConfig.jahre') }, ]; } function timeString(hour: number, minute: number): string { return `${String(Math.max(0, Math.min(23, hour))).padStart(2, '0')}:${String(Math.max(0, Math.min(59, minute))).padStart(2, '0')}`; } function commitSpec(next: ScheduleSpec, updateParam: (key: string, value: unknown) => void) { updateParam('schedule', next); updateParam('cron', buildCronFromSpec(next)); } function clampInterval(value: number, unit: IntervalUnit): number { const v = Math.max(1, Math.floor(value) || 1); switch (unit) { case 'seconds': return Math.min(59, v); case 'minutes': return Math.min(59, v); case 'hours': return Math.min(23, v); case 'days': return Math.min(31, v); case 'years': return Math.min(99, v); default: return v; } } const EASE_SMOOTH = [0.33, 1, 0.68, 1] as const; export const ScheduleStartNodeConfig: React.FC = ({ params, updateParam }) => { const { t } = useLanguage(); const modeOptions = _getModeOptions(t); const intervalUnits = _getIntervalUnits(t); const [spec, setSpec] = useState(() => scheduleSpecFromParams(params)); const prefersReducedMotion = useReducedMotion(); const specModeRef = useRef(spec.mode); specModeRef.current = spec.mode; useEffect(() => { const derived = scheduleSpecFromParams(params as Record); console.log('[ScheduleStartNode] useEffect params → setSpec', { paramsCron: params.cron, paramsSchedule: params.schedule, derivedMode: derived.mode, previousSpecMode: specModeRef.current, }); setSpec(derived); }, [params.cron, params.schedule]); useEffect(() => { console.log('[ScheduleStartNode] spec.mode changed (UI sollte passen)', { specMode: spec.mode, cssBlockBase: styles.scheduleModeBlock, cssBlockActive: styles.scheduleModeBlockActive, }); }, [spec.mode]); const push = useCallback( (next: ScheduleSpec) => { setSpec(next); commitSpec(next, updateParam); }, [updateParam] ); const setMode = (mode: ScheduleMode) => { console.log('[ScheduleStartNode] setMode', { from: spec.mode, to: mode, refMode: specModeRef.current, }); const base: ScheduleSpec = { ...spec, mode }; if (mode === 'weekly' && base.weekdays.length === 0) { base.weekdays = [1, 2, 3, 4, 5]; } if (mode === 'calendar') { base.calendarPeriod = base.calendarPeriod ?? 'monthly'; } push(base); }; const onModeCardPointerEvent = ( phase: 'pointerdown' | 'click', e: React.PointerEvent | React.MouseEvent, o: { value: ScheduleMode; title: string; subtitle: string } ) => { const el = e.target as HTMLElement; const cur = e.currentTarget as HTMLElement; const isLast = o.value === 'interval'; if (!isLast) return; const cx = 'clientX' in e ? e.clientX : 0; const cy = 'clientY' in e ? e.clientY : 0; const hit = typeof document !== 'undefined' ? document.elementFromPoint(cx, cy) : null; const hitH = hit as HTMLElement | null; console.log(`[ScheduleStartNode] ${phase} — unterstes Element (Intervall)`, { intendedMode: o.value, title: o.title, specModeClosure: spec.mode, specModeRef: specModeRef.current, eventPhase: e.nativeEvent.eventPhase, target: { tag: el?.tagName, className: el?.className, id: el?.id }, currentTarget: { tag: cur?.tagName, className: cur?.className }, clientX: cx, clientY: cy, pointerId: 'pointerId' in e ? (e as React.PointerEvent).pointerId : undefined, isTrusted: e.nativeEvent.isTrusted, elementFromPoint: hitH ? { tag: hitH.tagName, className: hitH.className, dataScheduleMode: hitH.closest?.('[data-schedule-mode]')?.getAttribute('data-schedule-mode'), textSlice: (hitH.textContent ?? '').slice(0, 60), } : null, }); }; const onTimeChange = (ev: React.ChangeEvent) => { const v = ev.target.value; if (!v) return; const [hs, ms] = v.split(':'); const hour = Number(hs); const minute = Number(ms); if (Number.isNaN(hour) || Number.isNaN(minute)) return; push({ ...spec, hour, minute }); }; const toggleWeekday = (cronDow: number) => { const set = new Set(spec.weekdays); if (set.has(cronDow)) set.delete(cronDow); else set.add(cronDow); let weekdays = [...set]; if (weekdays.length === 0) weekdays = [cronDow]; push({ ...spec, weekdays }); }; const setCalendarPeriod = (calendarPeriod: CalendarPeriod) => { push({ ...spec, calendarPeriod }); }; const setIntervalUnit = (intervalUnit: IntervalUnit) => { const next = { ...spec, intervalUnit }; next.intervalValue = clampInterval(next.intervalValue, intervalUnit); push(next); }; const panelTransition = prefersReducedMotion ? { duration: 0 } : { height: { duration: 0.44, ease: EASE_SMOOTH }, opacity: { duration: 0.3, ease: 'easeOut' as const }, }; return (

Legen Sie fest, wann dieser Workflow automatisch laufen soll. Der technische Cron-Ausdruck wird unten automatisch erzeugt.

{modeOptions.map((o) => ( onModeCardPointerEvent('pointerdown', e, o)} onClick={(e) => { onModeCardPointerEvent('click', e, o); setMode(o.value); }} whileTap={prefersReducedMotion ? undefined : { scale: 0.992 }} transition={{ type: 'spring', stiffness: 520, damping: 32 }} > {o.title} {o.subtitle} {spec.mode === o.value && (
{o.value === 'daily' && ( )} {o.value === 'weekdays' && ( )} {o.value === 'weekly' && ( <>
Wochentage
{WEEKDAYS_MO_SO.map(({ cronDow, label }) => ( ))}
)} {o.value === 'calendar' && ( <>
{spec.calendarPeriod === 'monthly' && ( )} {spec.calendarPeriod === 'yearly' && (
)} )} {o.value === 'interval' && (
{t('scheduleStartNodeConfig.alle')} push({ ...spec, intervalValue: clampInterval(Number(e.target.value) || 1, spec.intervalUnit), }) } />
)}
)}
))}
); };