Skip to content

Commit ea1bef9

Browse files
committed
feat: add FunctionNodeSquareComponent and integrate it into FlowBuilderComponent
1 parent aff466a commit ea1bef9

2 files changed

Lines changed: 280 additions & 0 deletions

File tree

src/packages/ce/src/flow/components/builder/FlowBuilderComponent.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {FlowPanelControlComponent} from "@edition/flow/components/panels/FlowPan
3030
import {FlowPanelUpdateComponent} from "@edition/flow/components/panels/FlowPanelUpdateComponent";
3131
import {FileTabsService} from "@code0-tech/pictor/dist/components/file-tabs/FileTabs.service";
3232
import {FlowPanelExportComponent} from "@edition/flow/components/panels/FlowPanelExportComponent";
33+
import {FunctionNodeSquareComponent} from "@edition/function/components/nodes/FunctionNodeSquareComponent";
3334

3435
/**
3536
* Dynamically layouts a tree of nodes and their parameter nodes for a flow-based editor.
@@ -622,6 +623,7 @@ const InternalFlowBuilder: React.FC<FlowBuilderProps> = (props) => {
622623

623624
const nodeTypes = React.useMemo(() => ({
624625
default: FunctionNodeDefaultComponent,
626+
square: FunctionNodeSquareComponent,
625627
group: FunctionNodeGroupComponent,
626628
trigger: FunctionNodeTriggerComponent,
627629
}), [])
Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
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

Comments
 (0)