|
| 1 | +import {Handle, Node, NodeProps, Position, useStore} from "@xyflow/react"; |
| 2 | +import React, {CSSProperties, memo} from "react"; |
| 3 | +import "./FunctionNodeComponent.style.scss"; |
| 4 | +import {FunctionNodeComponentProps} from "./FunctionNodeComponent"; |
| 5 | +import {FileTabsService} from "@code0-tech/pictor/dist/components/file-tabs/FileTabs.service"; |
| 6 | +import {Badge, Card, Flex, Text, useService, useStore as usePictorStore} from "@code0-tech/pictor"; |
| 7 | +import {useFlowValidation} from "@edition/flow/hooks/Flow.validation.hook"; |
| 8 | +import {IconVariable} from "@tabler/icons-react"; |
| 9 | +import {FlowService} from "@edition/flow/services/Flow.service"; |
| 10 | +import {FunctionService} from "@edition/function/services/Function.service"; |
| 11 | +import {LiteralBadgeComponent} from "@edition/datatype/components/badges/LiteralBadgeComponent"; |
| 12 | +import {ReferenceBadgeComponent} from "@edition/datatype/components/badges/ReferenceBadgeComponent"; |
| 13 | +import {NodeBadgeComponent} from "@edition/datatype/components/badges/NodeBadgeComponent"; |
| 14 | +import {FunctionFileDefaultComponent} from "@edition/function/components/files/FunctionFileDefaultComponent"; |
| 15 | +import {NodeFunction} from "@code0-tech/sagittarius-graphql-types"; |
| 16 | +import {underlineBySeverity} from "@core/util/inspection"; |
| 17 | +import {icon, IconString} from "@core/util/icons"; |
| 18 | +import {FALLBACK_FUNCTION_DISPLAY_MESSAGE, FALLBACK_FUNCTION_NAME} from "@core/util/fallback-translations"; |
| 19 | + |
| 20 | +export type FunctionNodeSquareComponentProps = NodeProps<Node<FunctionNodeComponentProps>> |
| 21 | + |
| 22 | +export const FunctionNodeSquareComponent: React.FC<FunctionNodeSquareComponentProps> = memo((props) => { |
| 23 | + const {data, id} = props |
| 24 | + |
| 25 | + const fileTabsService = useService(FileTabsService) |
| 26 | + const fileTabsStore = usePictorStore(FileTabsService) |
| 27 | + const flowService = useService(FlowService) |
| 28 | + const flowStore = usePictorStore(FlowService) |
| 29 | + const functionService = useService(FunctionService) |
| 30 | + const functionStore = usePictorStore(FunctionService) |
| 31 | + |
| 32 | + const node = React.useMemo( |
| 33 | + () => flowService.getNodeById(data.flowId, data.nodeId), |
| 34 | + [flowStore, data] |
| 35 | + ) |
| 36 | + const definition = React.useMemo( |
| 37 | + () => node ? functionService.getById(node.functionDefinition?.id!!) : undefined, |
| 38 | + [functionStore, data, node] |
| 39 | + ) |
| 40 | + |
| 41 | + const DisplayIcon = icon(definition?.displayIcon as IconString) |
| 42 | + |
| 43 | + const validation = useFlowValidation(data.flowId) |
| 44 | + |
| 45 | + const nodeValidations = React.useMemo( |
| 46 | + () => validation?.filter(v => v.nodeId === data.nodeId && v.parameterIndex === null), |
| 47 | + [validation] |
| 48 | + ) |
| 49 | + |
| 50 | + const nodeValidationStyle: CSSProperties = |
| 51 | + nodeValidations?.length |
| 52 | + ? underlineBySeverity[nodeValidations[0].type] |
| 53 | + : {}; |
| 54 | + |
| 55 | + const activeTabId = React.useMemo(() => { |
| 56 | + return fileTabsService.getActiveTab()?.id |
| 57 | + }, [fileTabsStore, fileTabsService]); |
| 58 | + |
| 59 | + const firstItem = useStore((s) => { |
| 60 | + const children = s.nodes.filter((n) => n.parentId === props.parentId); |
| 61 | + let start: any | undefined = undefined; |
| 62 | + children.forEach((n) => { |
| 63 | + const idx = (n.data as any)?.index ?? Infinity; |
| 64 | + const startIdx = (start?.data as any)?.index ?? Infinity; |
| 65 | + if (!start || idx < startIdx) { |
| 66 | + start = n; |
| 67 | + } |
| 68 | + }); |
| 69 | + return start; |
| 70 | + }) |
| 71 | + |
| 72 | + const splitTemplate = (str: string) => |
| 73 | + str |
| 74 | + .split(/(\$\{[^}]+\})/) |
| 75 | + .filter(Boolean) |
| 76 | + .flatMap(part => |
| 77 | + part.startsWith("${") |
| 78 | + ? [part.slice(2, -1)] // variable name ohne ${} |
| 79 | + : part.split(/(\s*,\s*)/) // Kommas einzeln extrahieren |
| 80 | + .filter(Boolean) |
| 81 | + .flatMap(p => p.trim() === "," ? [","] : p.trim() ? [p.trim()] : []) |
| 82 | + ); |
| 83 | + |
| 84 | + const displayMessage = React.useMemo(() => splitTemplate(definition?.displayMessages?.[0]?.content ?? FALLBACK_FUNCTION_DISPLAY_MESSAGE).map(item => { |
| 85 | + const nodeParameter = node?.parameters?.nodes?.find((_, index) => { |
| 86 | + const parameterDefinition = definition?.parameterDefinitions?.nodes?.[index] |
| 87 | + return parameterDefinition?.identifier == item |
| 88 | + }) |
| 89 | + |
| 90 | + const parameterDefinition = definition?.parameterDefinitions?.nodes?.find(pd => pd?.identifier == item) |
| 91 | + const parameterIndex = parameterDefinition ? definition?.parameterDefinitions?.nodes?.findIndex(p => p?.id === parameterDefinition.id) : undefined |
| 92 | + const parameterValidation = validation?.filter(v => v.parameterIndex === parameterIndex && v.nodeId === node?.id) |
| 93 | + const decorationStyle: CSSProperties = |
| 94 | + parameterValidation?.length |
| 95 | + ? underlineBySeverity[parameterValidation[0].type] |
| 96 | + : {}; |
| 97 | + |
| 98 | + if (parameterDefinition) { |
| 99 | + switch (nodeParameter?.value?.__typename) { |
| 100 | + case "LiteralValue": |
| 101 | + return <div style={{...decorationStyle, display: "inline-block"}}> |
| 102 | + <LiteralBadgeComponent value={nodeParameter.value}/> |
| 103 | + </div> |
| 104 | + case "ReferenceValue": |
| 105 | + return <div style={{...decorationStyle, display: "inline-block"}}> |
| 106 | + <ReferenceBadgeComponent value={nodeParameter.value}/> |
| 107 | + </div> |
| 108 | + case "NodeFunctionIdWrapper": |
| 109 | + return <div style={{...decorationStyle, display: "inline-block"}}> |
| 110 | + <NodeBadgeComponent value={nodeParameter.value}/> |
| 111 | + <Handle |
| 112 | + key={parameterIndex} |
| 113 | + type={"target"} |
| 114 | + position={Position.Right} |
| 115 | + id={`param-${parameterIndex}`} |
| 116 | + isConnectable={false} |
| 117 | + className={"d-flow-node__handle d-flow-node__handle--target"} |
| 118 | + /> |
| 119 | + </div> |
| 120 | + } |
| 121 | + return <div style={{...decorationStyle, display: "inline-block"}}> |
| 122 | + <Badge style={{verticalAlign: "middle"}} border> |
| 123 | + <Text size={"sm"}> |
| 124 | + {item} |
| 125 | + </Text> |
| 126 | + </Badge> |
| 127 | + </div> |
| 128 | + } |
| 129 | + return " " + String(item) + " " |
| 130 | + }), [flowStore, functionStore, data, definition, validation]) |
| 131 | + |
| 132 | + React.useEffect(() => { |
| 133 | + if (!node?.id) return |
| 134 | + fileTabsService.registerTab({ |
| 135 | + id: node.id, |
| 136 | + active: false, |
| 137 | + closeable: true, |
| 138 | + children: <> |
| 139 | + <DisplayIcon color={data.color} size={12}/> |
| 140 | + <Text size={"sm"}>{definition?.names?.[0]?.content ?? FALLBACK_FUNCTION_NAME}</Text> |
| 141 | + </>, |
| 142 | + content: <FunctionFileDefaultComponent flowId={props.data.flowId} node={node}/> |
| 143 | + }) |
| 144 | + }, []) |
| 145 | + |
| 146 | + const isReferenced = React.useMemo(() => { |
| 147 | + |
| 148 | + const activeNode = flowService.getNodeById(data.flowId, activeTabId as NodeFunction['id']) |
| 149 | + const isActiveNodeReferencingCurrentNode = activeNode?.parameters?.nodes?.some(p => p?.value?.__typename === "ReferenceValue" && p.value.nodeFunctionId === node?.id) |
| 150 | + const hasReferences = activeNode?.parameters?.nodes?.some(p => p?.value?.__typename === "ReferenceValue") |
| 151 | + |
| 152 | + if (isActiveNodeReferencingCurrentNode) { |
| 153 | + return true |
| 154 | + } else if (hasReferences && !isActiveNodeReferencingCurrentNode && activeTabId !== data.nodeId) { |
| 155 | + return false |
| 156 | + } |
| 157 | + |
| 158 | + return undefined |
| 159 | + |
| 160 | + }, [flowStore, activeTabId, data.flowId, node]) |
| 161 | + |
| 162 | + return ( |
| 163 | + <Flex align={"center"} style={{flexDirection: "column", position: "relative"}}> |
| 164 | + <Card |
| 165 | + data-qa-selector={"flow-builder-node"} |
| 166 | + key={id} |
| 167 | + data-flow-refernce={id} |
| 168 | + paddingSize={"xs"} |
| 169 | + p={"1"} |
| 170 | + w={"60px"} |
| 171 | + h={"60px"} |
| 172 | + display={"flex"} |
| 173 | + align={"center"} |
| 174 | + justify={"center"} |
| 175 | + outline={firstItem.id === id} |
| 176 | + borderColor={activeTabId == id ? "info" : undefined} |
| 177 | + className={`d-flow-node ${activeTabId == id ? "d-flow-node--active" : ""} ${isReferenced === false ? "d-flow-node--notReferenced" : ""}`} |
| 178 | + color={"primary"} style={{ |
| 179 | + ...(isReferenced === true ? {boxShadow: `0 0 5rem 0 ${withAlpha(data.color, 0.25)}`} : {}), |
| 180 | + }}> |
| 181 | + |
| 182 | + <Handle |
| 183 | + isConnectable={false} |
| 184 | + draggable={false} |
| 185 | + type="target" |
| 186 | + className={"d-flow-node__handle d-flow-node__handle--target"} |
| 187 | + style={{...(data.isParameter ? {right: "2px"} : {top: "2px"})}} |
| 188 | + position={data.isParameter ? Position.Right : Position.Top} |
| 189 | + /> |
| 190 | + |
| 191 | + {/* Ausgang */} |
| 192 | + <Handle |
| 193 | + isConnectable={false} |
| 194 | + type="source" |
| 195 | + style={{...(data.isParameter ? {left: "2px"} : {bottom: "2px"})}} |
| 196 | + className={"d-flow-node__handle d-flow-node__handle--source"} |
| 197 | + position={data.isParameter ? Position.Left : Position.Bottom} |
| 198 | + /> |
| 199 | + |
| 200 | + { |
| 201 | + isReferenced === true ? ( |
| 202 | + <div className={"d-flow-node__isReferenced"} style={{ |
| 203 | + position: "absolute", |
| 204 | + top: "50%", |
| 205 | + left: firstItem.id === id ? "-0.733rem" : "-0.5rem", |
| 206 | + transform: "translate(-100%, -50%)", |
| 207 | + display: "flex" |
| 208 | + }}> |
| 209 | + <IconVariable className={"d-flow-node__isReferenced-icon"} color={data.color} size={13}/> |
| 210 | + </div> |
| 211 | + ) : null |
| 212 | + } |
| 213 | + |
| 214 | + |
| 215 | + <Flex align={"center"} style={{flexDirection: "column", gap: "0.35rem", ...nodeValidationStyle}}> |
| 216 | + <DisplayIcon color={data.color} size={16}/> |
| 217 | + </Flex> |
| 218 | + </Card> |
| 219 | + <Text pos={"relative"} w={"60px"} mt={0.35} size={"md"} style={{textAlign: "center"}}> |
| 220 | + <Text size={"sm"}>{definition?.names?.[0]?.content ?? FALLBACK_FUNCTION_NAME}</Text> |
| 221 | + </Text> |
| 222 | + </Flex> |
| 223 | + ); |
| 224 | +}) |
| 225 | + |
| 226 | +type RGBA = { |
| 227 | + r: number |
| 228 | + g: number |
| 229 | + b: number |
| 230 | + a: number |
| 231 | +} |
| 232 | + |
| 233 | +const clamp01 = (v: number) => Math.min(Math.max(v, 0), 1) |
| 234 | + |
| 235 | +const parseCssColorToRgba = (color: string): RGBA => { |
| 236 | + if (typeof document === "undefined") { |
| 237 | + return {r: 0, g: 0, b: 0, a: 1} |
| 238 | + } |
| 239 | + |
| 240 | + const el = document.createElement("span") |
| 241 | + el.style.color = color |
| 242 | + document.body.appendChild(el) |
| 243 | + |
| 244 | + const computed = getComputedStyle(el).color |
| 245 | + document.body.removeChild(el) |
| 246 | + |
| 247 | + const match = computed.match( |
| 248 | + /rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)(?:\s*,\s*([\d.]+))?\s*\)/ |
| 249 | + ) |
| 250 | + |
| 251 | + if (!match) { |
| 252 | + return {r: 0, g: 0, b: 0, a: 1} |
| 253 | + } |
| 254 | + |
| 255 | + return { |
| 256 | + r: Math.round(Number(match[1])), |
| 257 | + g: Math.round(Number(match[2])), |
| 258 | + b: Math.round(Number(match[3])), |
| 259 | + a: match[4] !== undefined ? Number(match[4]) : 1, |
| 260 | + } |
| 261 | +} |
| 262 | + |
| 263 | +const mixColorRgb = (color: string, level: number) => { |
| 264 | + const w = clamp01(level * 0.1) |
| 265 | + |
| 266 | + const c1 = parseCssColorToRgba(color) |
| 267 | + const c2 = parseCssColorToRgba("#070514") |
| 268 | + |
| 269 | + const mix = (a: number, b: number) => |
| 270 | + Math.round(a * (1 - w) + b * w) |
| 271 | + |
| 272 | + return `rgb(${mix(c1.r, c2.r)}, ${mix(c1.g, c2.g)}, ${mix(c1.b, c2.b)})` |
| 273 | +} |
| 274 | + |
| 275 | +const withAlpha = (color: string, alpha: number) => { |
| 276 | + const c = parseCssColorToRgba(color) |
| 277 | + return `rgba(${c.r}, ${c.g}, ${c.b}, ${clamp01(alpha)})` |
| 278 | +} |
0 commit comments