// use client 'use client'; // imports import clsx from 'clsx'; import dotProp from 'dot-prop'; import copyToClipboard from 'copy-to-clipboard'; import { Handle, Position, NodeResizer } from '@xyflow/react'; import React, { useEffect, useRef, useState, useContext, useMemo } from 'react'; // icons import { IconSettings, IconEye, IconTick, IconWarning, IconEyeClosed, IconMaximise, IconMinimise } from '@chirper/react/src/icons'; // locals import Input from '../Input'; import Timer from '../Timer'; import Config from './Config'; import Loading from '../Loading'; import Tooltip from '../Tooltip'; import ApiSignal from '../../signals/Api'; import ButtonIcon from '../Button/Icon'; import { useComputed, useSignal } from '../../signal'; import { output as OutputSignal, selected as SelectedSignal } from '../Workflow'; // timeout let timeout; const debounce = (fn, wait) => { clearTimeout(timeout); timeout = setTimeout(() => { fn(); }, wait); }; /** * component studio line * * @param props * @returns */ export const ComponentNodeLine = (props = {}) => { // return jsx return (
{props.children}
); }; /** * workflow node output * * @param props */ export const ComponentNodeOutput = (props = {}) => { // output return (

{props.output?.name || 'N/A'}

); }; /** * workflow node output * * @param props */ export const ComponentNodeInput = (props = {}) => { // output return (

{props.input?.name || 'N/A'}

); }; /** * workflow node input * * @param param0 * @returns */ const ComponentNode = (props = {}) => { // signals const [api] = useSignal(ApiSignal); const [sOutput] = useSignal(OutputSignal); const [sSelected, setSelected] = useSignal(SelectedSignal); // use computed const simpleOutput = useMemo(() => { return { node : sOutput?.node, state : sOutput?.state, message : sOutput?.message, running : sOutput?.running, }; }, [sOutput, props.output, props.variant]); const actualOutput = useMemo(() => { // actual output return props.variant === 'message' ? props.output : sOutput?.output; }, [sOutput, props.output, props.variant]); // output value const outputValue = useMemo(() => { // output return (props.outputs || []).reduce((accum, item) => { // if item if (actualOutput?.[props.id]?.[item.id]) { accum[item.name] = actualOutput?.[props.id]?.[item.id]; } // return accum return accum; }, {}); }, [props.id, actualOutput, props.outputs]); const inputValue = useMemo(() => { return (props.inputs || []).reduce((accum, item) => { // get input const nodeInput = props.item?.input?.[item.id]; // check input if (!nodeInput) return accum; // get output const inputValue = dotProp.get(actualOutput || {}, nodeInput.split('-').join('.')); // if input value if (inputValue) { // set to accum accum[item.name] = inputValue; } // return accum return accum; }, {}) }, [props.item, props.inputs]); // ref const baseRef = useRef(null); // state const [name, setName] = useState(props.item?.name || props.node?.name || ''); const [config, setConfig] = useState(false); const [endedAt, setEndedAt] = useState(null); const [startedAt, setStartedAt] = useState(null); // is selected const isSelected = useMemo(() => { // return return sSelected === props.id; }, [sSelected === props.id]); // icon component const Icon = useMemo(() => { // icon component return props.iconComponent || IconTick; }, [props.iconComponent]); const Element = useMemo(() => { return props.Element; }, [props.Element]); /** * handle copy * * @param e */ const handleCopy = (e) => { // check if within `input` element if (['TEXTAREA', 'INPUT'].includes(e.target.nodeName)) return; // prevent default e.preventDefault(); e.stopPropagation(); // json string const jsonString = JSON.stringify(props.data, null, 2); // copy copyToClipboard(jsonString); }; // use effect useEffect(() => { // check selected value if (!isSelected) return; // add listener document.addEventListener('copy', handleCopy); // remove listener return () => document.removeEventListener('copy', handleCopy); }, [isSelected]); useEffect(() => { // variant if (props.variant === 'message') return; // debounce debounce(() => { // on change props.onChange('name', name); }, 500) }, [name]); useEffect(() => { // check output value if ((simpleOutput?.node !== props.id) && !Object.keys(outputValue).length) { // reset setEndedAt(null); setStartedAt(null); // return return; } // check state if (!startedAt && (Object.keys(outputValue).length || simpleOutput?.node === props.id || simpleOutput?.running?.includes(props.id))) { // set started at setStartedAt(new Date().getTime()); } // check ended at if (startedAt && Object.keys(outputValue).length) { // ended at setEndedAt(new Date().getTime()); } }, [startedAt, Object.keys(outputValue).length, simpleOutput?.node, simpleOutput?.running?.length]); useEffect(() => { // check variant if (!props.data.onUpdate) return; if (props.variant === 'message') return; // update node internals props.data.onUpdate(props.id); // timeout const timeout = setTimeout(() => { // update node internals props.data.onUpdate(props.id); }, 500); // return return () => clearTimeout(timeout); }, [props.data?.position?.minimised, Object.keys(outputValue).length, Object.keys(inputValue).length]); // return jsx if (props.variant === 'message') return ( !!(Object.keys(outputValue).length || props.running?.includes(props.id)) && (
{!props.isLast && (
)}
{(props.state !== 'completed' && !Object.keys(outputValue).length) ? ( (props.state === 'failed') ? ( ) : ( ) ) : (props.icon || ( ))}
{((props.current === props.id) && (props.state === 'failed')) ? (

{props.message}

) : ( <> {name || props.title}
{props.children} {!!Element && ( )}
)}
) ); // return jsx return ( <>
isSelected ? setSelected(null) : setSelected(props.id)} className={clsx( 'flex flex-1 flex-col py-3 space-y-3 w-full min-h-full card !bg-primary-LIGHT dark:!bg-gray-DARK', endedAt ? ( (simpleOutput?.state === 'failed') && (simpleOutput?.node === props.id) ? '!ring-red-500' : '!ring-green-500' ) : ( isSelected ? '!ring-primary-500' : '!ring-gray-200/20 dark:!ring-gray-400' ), )} >
{!!(simpleOutput?.running?.includes(props.id) && (simpleOutput?.state !== 'failed')) && ( )}
{name || props.title}
{!!props.description && (

{props.description}

)}
{!!(startedAt && (simpleOutput?.state !== 'failed')) && ( )} props.data.onChange('position.minimised', !props.data?.position?.minimised)} color="primary" > {props.data?.position?.minimised ? ( ) : ( )} setConfig(true)} color="primary" >
{!!((simpleOutput?.node === props.id) && (simpleOutput?.state === 'failed')) && (

{simpleOutput?.message}

)} {!props.data?.position?.minimised && (
{props.children && (
{props.children}
)}
{!!Element && ( )}
)} {!!props.inputs?.length && (
{(props.inputs || []).map((input) => ( ))}
)} {!!props.outputs?.length && (
{(props.outputs || []).map((output) => ( ))}
)}
setConfig(false)}>
setName(e.target.value)} placeholder={api.translate('node:form.name.placeholder', 'Node Name')} />
props.item.onChange('spec.hidden', !props.item.spec?.hidden)} > {props.item.spec?.hidden ? ( ) : ( )}
); }; /** * @description - Workflow Node Input component */ export default ComponentNode;