From e55f625ad594e1ae2d241e9db96bb8b1832350c1 Mon Sep 17 00:00:00 2001 From: nicosammito Date: Thu, 5 Mar 2026 00:17:02 +0100 Subject: [PATCH 01/35] feat: add data type validation interfaces and components --- .../src/datatype/components/DataTypeInput.tsx | 71 ++ .../components/DataTypeInputLiteralBadge.tsx | 20 + .../components/DataTypeInputNodeBadge.tsx | 66 ++ .../DataTypeInputReferenceBadge.tsx | 40 + .../datatype/components/DataTypeJSONInput.tsx | 154 ++++ .../DataTypeJSONInputEditDialog.tsx | 182 ++++ .../components/DataTypeJSONInputTree.tsx | 166 ++++ .../datatype/components/DataTypeTextInput.tsx | 182 ++++ .../components/DataTypeTypeInput.style.scss | 64 ++ .../datatype/components/DataTypeTypeInput.tsx | 182 ++++ .../DataTypeTypeInputEditDialog.tsx | 176 ++++ .../ce/src/datatype/services/DataType.view.ts | 113 +++ .../src/datatype/services/Datatype.service.ts | 289 ++++++- .../services/rules/DataTypeContainsKeyRule.ts | 57 ++ .../rules/DataTypeContainsTypeRule.ts | 68 ++ .../rules/DataTypeItemOfCollectionRule.ts | 13 + .../services/rules/DataTypeNumberRangeRule.ts | 11 + .../services/rules/DataTypeParentRule.ts | 21 + .../services/rules/DataTypeRegexRule.ts | 11 + .../services/rules/DataTypeReturnTypeRule.ts | 126 +++ .../datatype/services/rules/DataTypeRule.ts | 23 + .../datatype/services/rules/DataTypeRules.ts | 20 + .../services/variants/DataTypeNodeVariant.ts | 10 + .../services/variants/DataTypeVariant.ts | 5 + .../services/variants/DataTypeVariants.ts | 9 + .../flow/components/DFlowFolder.style.scss | 99 +++ .../ce/src/flow/components/DFlowFolder.tsx | 389 +++++++++ .../components/DFlowFolderContextMenu.tsx | 90 ++ .../components/DFlowFolderDeleteDialog.tsx | 59 ++ .../components/DFlowFolderRenameDialog.tsx | 72 ++ .../flow/components/FlowBuilder.style.scss | 27 + .../ce/src/flow/components/FlowBuilder.tsx | 807 ++++++++++++++++++ .../src/flow/components/FlowBuilder.util.ts | 95 +++ .../src/flow/components/FlowBuilderEdge.tsx | 91 ++ .../src/flow/components/FlowPanelControl.tsx | 95 +++ .../src/flow/components/FlowPanelLayout.tsx | 56 ++ .../ce/src/flow/components/FlowPanelSize.tsx | 47 + .../src/flow/components/FlowPanelUpdate.tsx | 122 +++ .../src/flow/hooks/DataTypeValidation.hook.ts | 54 ++ .../ce/src/flow/hooks/Flow.edges.hook.ts | 165 ++++ .../ce/src/flow/hooks/Flow.nodes.hook.ts | 218 +++++ .../ce/src/flow/hooks/NodeValidation.hook.ts | 159 ++++ .../ce/src/flow/hooks/ValueValidation.hook.ts | 28 + src/packages/ce/src/flow/utils/generics.ts | 749 ++++++++++++++++ .../function/components/DFlowNode.style.scss | 32 + .../ce/src/function/components/DFlowNode.ts | 11 + .../components/DFlowNodeDefaultCard.tsx | 170 ++++ .../components/DFlowNodeGroupCard.tsx | 86 ++ .../components/DFlowNodeTriggerCard.tsx | 93 ++ .../components/DFlowSuggestion.view.ts | 21 + .../components/DFlowSuggestionMenu.tsx | 73 ++ .../components/DFlowSuggestionMenu.util.tsx | 59 ++ .../components/DFlowSuggestionMenuFooter.tsx | 53 ++ .../DFlowSuggestionMenuSearchBar.tsx | 18 + .../DFlowSuggestionSearchInput.style.scss | 17 + .../components/DFlowSuggestionSearchInput.tsx | 39 + .../function/components/DFlowTabDefault.tsx | 156 ++++ .../function/components/DFlowTabTrigger.tsx | 121 +++ .../ce/src/function/components/DFlowTabs.tsx | 163 ++++ .../hooks/DFlowDataTypeSuggestions.hook.tsx | 27 + .../hooks/DFlowFunction.return.hook.ts | 19 + .../hooks/DFlowFunctionSuggestions.hook.tsx | 81 ++ .../function/hooks/DFlowNode.return.hook.ts | 43 + .../hooks/DFlowNodeReference.return.hook.ts | 147 ++++ .../hooks/DFlowReferenceSuggestions.hook.tsx | 337 ++++++++ .../function/hooks/DFlowSuggestion.hook.tsx | 49 ++ .../hooks/DFlowValueSuggestions.hook.tsx | 57 ++ 67 files changed, 7366 insertions(+), 7 deletions(-) create mode 100644 src/packages/ce/src/datatype/components/DataTypeInput.tsx create mode 100644 src/packages/ce/src/datatype/components/DataTypeInputLiteralBadge.tsx create mode 100644 src/packages/ce/src/datatype/components/DataTypeInputNodeBadge.tsx create mode 100644 src/packages/ce/src/datatype/components/DataTypeInputReferenceBadge.tsx create mode 100644 src/packages/ce/src/datatype/components/DataTypeJSONInput.tsx create mode 100644 src/packages/ce/src/datatype/components/DataTypeJSONInputEditDialog.tsx create mode 100644 src/packages/ce/src/datatype/components/DataTypeJSONInputTree.tsx create mode 100644 src/packages/ce/src/datatype/components/DataTypeTextInput.tsx create mode 100644 src/packages/ce/src/datatype/components/DataTypeTypeInput.style.scss create mode 100644 src/packages/ce/src/datatype/components/DataTypeTypeInput.tsx create mode 100644 src/packages/ce/src/datatype/components/DataTypeTypeInputEditDialog.tsx create mode 100644 src/packages/ce/src/datatype/services/DataType.view.ts create mode 100644 src/packages/ce/src/datatype/services/rules/DataTypeContainsKeyRule.ts create mode 100644 src/packages/ce/src/datatype/services/rules/DataTypeContainsTypeRule.ts create mode 100644 src/packages/ce/src/datatype/services/rules/DataTypeItemOfCollectionRule.ts create mode 100644 src/packages/ce/src/datatype/services/rules/DataTypeNumberRangeRule.ts create mode 100644 src/packages/ce/src/datatype/services/rules/DataTypeParentRule.ts create mode 100644 src/packages/ce/src/datatype/services/rules/DataTypeRegexRule.ts create mode 100644 src/packages/ce/src/datatype/services/rules/DataTypeReturnTypeRule.ts create mode 100644 src/packages/ce/src/datatype/services/rules/DataTypeRule.ts create mode 100644 src/packages/ce/src/datatype/services/rules/DataTypeRules.ts create mode 100644 src/packages/ce/src/datatype/services/variants/DataTypeNodeVariant.ts create mode 100644 src/packages/ce/src/datatype/services/variants/DataTypeVariant.ts create mode 100644 src/packages/ce/src/datatype/services/variants/DataTypeVariants.ts create mode 100644 src/packages/ce/src/flow/components/DFlowFolder.style.scss create mode 100644 src/packages/ce/src/flow/components/DFlowFolder.tsx create mode 100644 src/packages/ce/src/flow/components/DFlowFolderContextMenu.tsx create mode 100644 src/packages/ce/src/flow/components/DFlowFolderDeleteDialog.tsx create mode 100644 src/packages/ce/src/flow/components/DFlowFolderRenameDialog.tsx create mode 100644 src/packages/ce/src/flow/components/FlowBuilder.style.scss create mode 100644 src/packages/ce/src/flow/components/FlowBuilder.tsx create mode 100644 src/packages/ce/src/flow/components/FlowBuilder.util.ts create mode 100644 src/packages/ce/src/flow/components/FlowBuilderEdge.tsx create mode 100644 src/packages/ce/src/flow/components/FlowPanelControl.tsx create mode 100644 src/packages/ce/src/flow/components/FlowPanelLayout.tsx create mode 100644 src/packages/ce/src/flow/components/FlowPanelSize.tsx create mode 100644 src/packages/ce/src/flow/components/FlowPanelUpdate.tsx create mode 100644 src/packages/ce/src/flow/hooks/DataTypeValidation.hook.ts create mode 100644 src/packages/ce/src/flow/hooks/Flow.edges.hook.ts create mode 100644 src/packages/ce/src/flow/hooks/Flow.nodes.hook.ts create mode 100644 src/packages/ce/src/flow/hooks/NodeValidation.hook.ts create mode 100644 src/packages/ce/src/flow/hooks/ValueValidation.hook.ts create mode 100644 src/packages/ce/src/flow/utils/generics.ts create mode 100644 src/packages/ce/src/function/components/DFlowNode.style.scss create mode 100644 src/packages/ce/src/function/components/DFlowNode.ts create mode 100644 src/packages/ce/src/function/components/DFlowNodeDefaultCard.tsx create mode 100644 src/packages/ce/src/function/components/DFlowNodeGroupCard.tsx create mode 100644 src/packages/ce/src/function/components/DFlowNodeTriggerCard.tsx create mode 100644 src/packages/ce/src/function/components/DFlowSuggestion.view.ts create mode 100644 src/packages/ce/src/function/components/DFlowSuggestionMenu.tsx create mode 100644 src/packages/ce/src/function/components/DFlowSuggestionMenu.util.tsx create mode 100644 src/packages/ce/src/function/components/DFlowSuggestionMenuFooter.tsx create mode 100644 src/packages/ce/src/function/components/DFlowSuggestionMenuSearchBar.tsx create mode 100644 src/packages/ce/src/function/components/DFlowSuggestionSearchInput.style.scss create mode 100644 src/packages/ce/src/function/components/DFlowSuggestionSearchInput.tsx create mode 100644 src/packages/ce/src/function/components/DFlowTabDefault.tsx create mode 100644 src/packages/ce/src/function/components/DFlowTabTrigger.tsx create mode 100644 src/packages/ce/src/function/components/DFlowTabs.tsx create mode 100644 src/packages/ce/src/function/hooks/DFlowDataTypeSuggestions.hook.tsx create mode 100644 src/packages/ce/src/function/hooks/DFlowFunction.return.hook.ts create mode 100644 src/packages/ce/src/function/hooks/DFlowFunctionSuggestions.hook.tsx create mode 100644 src/packages/ce/src/function/hooks/DFlowNode.return.hook.ts create mode 100644 src/packages/ce/src/function/hooks/DFlowNodeReference.return.hook.ts create mode 100644 src/packages/ce/src/function/hooks/DFlowReferenceSuggestions.hook.tsx create mode 100644 src/packages/ce/src/function/hooks/DFlowSuggestion.hook.tsx create mode 100644 src/packages/ce/src/function/hooks/DFlowValueSuggestions.hook.tsx diff --git a/src/packages/ce/src/datatype/components/DataTypeInput.tsx b/src/packages/ce/src/datatype/components/DataTypeInput.tsx new file mode 100644 index 00000000..e51538ca --- /dev/null +++ b/src/packages/ce/src/datatype/components/DataTypeInput.tsx @@ -0,0 +1,71 @@ +import {Flow, NodeFunction, NodeParameter} from "@code0-tech/sagittarius-graphql-types"; +import React from "react"; +import {DataTypeTextInput} from "./DataTypeTextInput"; +import {DataTypeJSONInput} from "./DataTypeJSONInput"; +import {InputProps, useService, useStore} from "@code0-tech/pictor"; +import {FlowService} from "@edition/flow/services/Flow.service"; +import {DatatypeService} from "@edition/datatype/services/Datatype.service"; +import {FunctionService} from "@edition/function/services/Function.service"; + +export interface DataTypeInputProps extends Omit, "wrapperComponent" | "type"> { + flowId: Flow['id'] + nodeId: NodeFunction['id'] + parameterId: NodeParameter['id'] + clearable?: boolean + onClear?: (event: React.MouseEvent) => void +} + +export const DataTypeInput: React.FC = (props) => { + + const {flowId, nodeId, parameterId, ...rest} = props + + const flowService = useService(FlowService) + const flowStore = useStore(FlowService) + const dataTypeService = useService(DatatypeService) + const dataTypeStore = useStore(DatatypeService) + const functionService = useService(FunctionService) + const functionStore = useStore(FunctionService) + + const node = React.useMemo( + () => flowService.getNodeById(flowId, nodeId), + [flowStore, flowId, nodeId] + ) + + const parameter = React.useMemo( + () => node?.parameters?.nodes?.find(p => p?.id === parameterId), + [node, parameterId] + ) + + const functionDefinition = React.useMemo( + () => functionService.getById(node?.functionDefinition?.id!), + [functionStore, node] + ) + + const parameterDefinition = React.useMemo( + () => functionDefinition?.parameterDefinitions?.find(pd => pd.id === parameter?.parameterDefinition?.id), + [functionDefinition, parameter] + ) + + const dataType = React.useMemo( + () => dataTypeService.getDataType(parameterDefinition?.dataTypeIdentifier!), + [dataTypeStore, parameterDefinition] + ) + + switch (dataType?.variant) { + case "ARRAY": + case "OBJECT": + return + default: + return + } +} \ No newline at end of file diff --git a/src/packages/ce/src/datatype/components/DataTypeInputLiteralBadge.tsx b/src/packages/ce/src/datatype/components/DataTypeInputLiteralBadge.tsx new file mode 100644 index 00000000..6745023d --- /dev/null +++ b/src/packages/ce/src/datatype/components/DataTypeInputLiteralBadge.tsx @@ -0,0 +1,20 @@ +import React from "react"; +import {LiteralValue} from "@code0-tech/sagittarius-graphql-types"; +import {Badge, BadgeType, Text} from "@code0-tech/pictor"; + +export interface DataTypeInputLiteralBadgeProps extends Omit { + value: LiteralValue +} + +export const DataTypeInputLiteralBadge: React.FC = (props) => { + + const {value, ...rest} = props + + return + + {String(value.value)} + + +} \ No newline at end of file diff --git a/src/packages/ce/src/datatype/components/DataTypeInputNodeBadge.tsx b/src/packages/ce/src/datatype/components/DataTypeInputNodeBadge.tsx new file mode 100644 index 00000000..d013d553 --- /dev/null +++ b/src/packages/ce/src/datatype/components/DataTypeInputNodeBadge.tsx @@ -0,0 +1,66 @@ +import {Flow, NodeFunction, NodeFunctionIdWrapper} from "@code0-tech/sagittarius-graphql-types"; +import React from "react"; +import {IconBolt, IconNote} from "@tabler/icons-react"; +import { + Badge, + BadgeType, + FlowTypeView, + FunctionDefinitionView, + hashToColor, Text, + useService, + useStore +} from "@code0-tech/pictor"; +import {FunctionService} from "@edition/function/services/Function.service"; +import {FlowService} from "@edition/flow/services/Flow.service"; +import {FlowTypeService} from "@edition/flowtype/services/FlowTypeService"; + +export interface DataTypeInputNodeBadgeProps extends Omit { + value: NodeFunction | NodeFunctionIdWrapper + flowId: Flow['id'] + definition?: FunctionDefinitionView | FlowTypeView +} + +export const DataTypeInputNodeBadge: React.FC = (props) => { + + const {value, flowId, definition, ...rest} = props + + const functionService = definition || useService(FunctionService) + const functionStore = definition || useStore(FunctionService) + const flowService = definition || useService(FlowService) + const flowStore = definition || useStore(FlowService) + const flowTypeService = definition || useService(FlowTypeService) + const flowTypeStore = definition || useStore(FlowTypeService) + + const isTrigger = value.__typename === "NodeFunctionIdWrapper" && !value.id + + const node: NodeFunction | FlowTypeView | NodeFunctionIdWrapper | undefined = React.useMemo(() => { + if (isTrigger && !definition) { + const flow = (flowService as FlowService).getById(flowId) + return (flowTypeService as FlowTypeService).getById(flow?.type?.id) + } + return value.__typename === "NodeFunction" || definition ? value : (flowService as FlowService).getNodeById(flowId, value.id) + }, [flowStore, flowTypeStore]) + + const name = React.useMemo(() => { + if (definition) { + return definition.names?.[0]?.content + } else if (isTrigger && node instanceof FlowTypeView) { + return node.names?.[0]?.content + } + return (functionService as FunctionService).getById((node as NodeFunction)?.functionDefinition?.id)?.names?.[0]?.content + }, [functionStore, node]) + + return + { + isTrigger + ? + : + } + + {String(name)} + + +} \ No newline at end of file diff --git a/src/packages/ce/src/datatype/components/DataTypeInputReferenceBadge.tsx b/src/packages/ce/src/datatype/components/DataTypeInputReferenceBadge.tsx new file mode 100644 index 00000000..3a77c57e --- /dev/null +++ b/src/packages/ce/src/datatype/components/DataTypeInputReferenceBadge.tsx @@ -0,0 +1,40 @@ +import {Flow, ReferenceValue} from "@code0-tech/sagittarius-graphql-types"; +import React from "react"; +import {DataTypeInputNodeBadge} from "./DataTypeInputNodeBadge"; +import {IconVariable} from "@tabler/icons-react"; +import {Badge, BadgeType, Flex, FlowTypeView, FunctionDefinitionView, Text} from "@code0-tech/pictor"; + +export interface DataTypeInputReferenceBadgeProps extends Omit { + value: ReferenceValue + flowId: Flow['id'] + definition?: FunctionDefinitionView | FlowTypeView +} + +export const DataTypeInputReferenceBadge: React.FC = (props) => { + + const {value, flowId, definition, ...rest} = props + const content = React.useMemo(() => { + if (flowId) { + return + + {"inputTypeIdentifier" in value && value.inputTypeIdentifier ? "." + value.inputTypeIdentifier : ""} + {value.referencePath ? "." + (value.referencePath?.map(path => path.path).join(".") ?? "") : ""} + + } + return `undefined` + }, [value]) + + return + + + {content} + + +} \ No newline at end of file diff --git a/src/packages/ce/src/datatype/components/DataTypeJSONInput.tsx b/src/packages/ce/src/datatype/components/DataTypeJSONInput.tsx new file mode 100644 index 00000000..22c84d68 --- /dev/null +++ b/src/packages/ce/src/datatype/components/DataTypeJSONInput.tsx @@ -0,0 +1,154 @@ +import React from "react" +import {IconAlignLeft, IconEdit, IconX} from "@tabler/icons-react" +import "./DataTypeTypeInput.style.scss" +import {DataTypeJSONInputTree} from "./DataTypeJSONInputTree"; +import {DataTypeInputProps} from "./DataTypeInput"; +import {LiteralValue, NodeFunction, NodeParameterValue} from "@code0-tech/sagittarius-graphql-types"; +import {DataTypeInputNodeBadge} from "./DataTypeInputNodeBadge"; +import {DataTypeInputReferenceBadge} from "./DataTypeInputReferenceBadge"; +import {useSuggestions} from "@edition/function/hooks/DFlowSuggestion.hook"; +import { + Button, + Card, + DFlowDataTypeReactiveService, + DFlowFunctionReactiveService, + DFlowReactiveService, Flex, InputDescription, InputLabel, InputMessage, Text, + useService, + useStore +} from "@code0-tech/pictor"; +import {ButtonGroup} from "@code0-tech/pictor/dist/components/button-group/ButtonGroup"; +import {DFlowSuggestionMenu} from "@edition/function/components/DFlowSuggestionMenu"; +import {DataTypeJSONInputEditDialog} from "@edition/datatype/components/DataTypeJSONInputEditDialog"; + +export interface EditableJSONEntry { + key: string + value: LiteralValue | null + path: string[] +} + +export type DataTypeJSONInputProps = DataTypeInputProps + +export const DataTypeJSONInput: React.FC = (props) => { + + + const {flowId, nodeId, parameterId, title, description, formValidation, onChange} = props + + const flowService = useService(DFlowReactiveService) + const flowStore = useStore(DFlowReactiveService) + const dataTypeService = useService(DFlowDataTypeReactiveService) + const dataTypeStore = useStore(DFlowDataTypeReactiveService) + const functionService = useService(DFlowFunctionReactiveService) + const functionStore = useStore(DFlowFunctionReactiveService) + + const node = React.useMemo( + () => flowService.getNodeById(flowId, nodeId), + [flowStore, flowId, nodeId] + ) + + const parameter = React.useMemo( + () => node?.parameters?.nodes?.find(p => p?.id === parameterId), + [node, parameterId] + ) + + const functionDefinition = React.useMemo( + () => functionService.getById(node?.functionDefinition?.id!), + [functionStore, node] + ) + + const parameterDefinition = React.useMemo( + () => functionDefinition?.parameterDefinitions?.find(pd => pd.id === parameter?.parameterDefinition?.id), + [functionDefinition, parameter] + ) + + const initialValue: NodeParameterValue | undefined = React.useMemo(() => { + if (!parameter?.value || (parameter?.value?.__typename === "LiteralValue" && parameter.value.value == null)) { + return dataTypeService.getValueFromType(parameterDefinition?.dataTypeIdentifier!) + } + return parameter?.value + }, [parameter, parameterDefinition, dataTypeStore]) + + + const suggestions = useSuggestions(flowId, nodeId, parameterId) + + const [value, setValue] = React.useState(initialValue) + const [editDialogOpen, setEditDialogOpen] = React.useState(false) + const [editEntry, setEditEntry] = React.useState(null) + const [collapsedState, setCollapsedStateRaw] = React.useState>({}) + + const setCollapsedState = (path: string[], collapsed: boolean) => { + setCollapsedStateRaw(prev => ({...prev, [path.join(".")]: collapsed})) + } + + const handleEntryClick = (entry: EditableJSONEntry) => { + setEditEntry(entry) + setEditDialogOpen(true) + } + + const handleClear = React.useCallback(() => { + setValue(dataTypeService.getValueFromType(parameterDefinition?.dataTypeIdentifier!)) + }, [parameter, parameterDefinition, dataTypeStore]) + + React.useEffect(() => { + formValidation?.setValue(value) + // @ts-ignore + onChange?.() + }, [value]) + + return ( + <> + {value?.__typename === "LiteralValue" && ( + setEditDialogOpen(open)} + onObjectChange={v => setValue(v ?? undefined)} + /> + )} + {title} + {description} + + + + {"Object"} + + + setValue(suggestion.value)} + triggerContent={}/> + + + + + + {value?.__typename === "NodeFunction" || value?.__typename === "NodeFunctionIdWrapper" ? ( + + ) : value?.__typename === "ReferenceValue" ? ( + + ) : ( + + )} + + + {!formValidation?.valid && formValidation?.notValidMessage && ( + {formValidation.notValidMessage} + )} + + ) +} diff --git a/src/packages/ce/src/datatype/components/DataTypeJSONInputEditDialog.tsx b/src/packages/ce/src/datatype/components/DataTypeJSONInputEditDialog.tsx new file mode 100644 index 00000000..36a07ce5 --- /dev/null +++ b/src/packages/ce/src/datatype/components/DataTypeJSONInputEditDialog.tsx @@ -0,0 +1,182 @@ +import React from "react" +import {DataTypeJSONInputTree} from "./DataTypeJSONInputTree"; +import {LiteralValue} from "@code0-tech/sagittarius-graphql-types"; +import {EditableJSONEntry} from "@edition/datatype/components/DataTypeJSONInput"; +import { + Button, + Dialog, + DialogClose, + DialogContent, + DialogOverlay, + DialogPortal, + DLayout, DResizableHandle, DResizablePanel, DResizablePanelGroup, + Flex, ScrollArea, ScrollAreaScrollbar, ScrollAreaThumb, ScrollAreaViewport, Spacing, Text +} from "@code0-tech/pictor"; +import {IconX} from "@tabler/icons-react"; +import {Editor} from "@code0-tech/pictor/dist/components/editor/Editor"; + +export interface DataTypeJSONInputEditDialogProps { + open: boolean + entry: EditableJSONEntry | null + value: LiteralValue | null + onOpenChange?: (open: boolean) => void + onObjectChange?: (object: LiteralValue | null) => void +} + +function getValueAtPath(obj: LiteralValue | null, path: string[]): unknown { + if (!obj || !Array.isArray(path) || path.length === 0) return obj?.value + // Traverse .value recursively if nested + let current: any = obj.value + for (const key of path) { + if (current && typeof current === 'object' && key in current) { + current = current[key] + } else { + return undefined + } + } + return current +} + +function setValueAtPath(obj: LiteralValue | null, path: string[], value: unknown): LiteralValue | null { + if (!obj) return null + if (path.length === 0) return { ...obj, value } + const [key, ...rest] = path + if (Array.isArray(obj.value)) { + const idx = Number(key) + const newArr = [...obj.value] + if (rest.length > 0 && typeof newArr[idx] === 'object' && newArr[idx] !== null) { + newArr[idx] = setValueAtPath({ ...obj, value: newArr[idx] }, rest, value)?.value + } else { + newArr[idx] = value + } + return { ...obj, value: newArr } + } else if (typeof obj.value === 'object' && obj.value !== null) { + const newObj = { ...obj.value } + if (rest.length > 0 && typeof newObj[key] === 'object' && newObj[key] !== null) { + newObj[key] = setValueAtPath({ ...obj, value: newObj[key] }, rest, value)?.value + } else { + newObj[key] = value + } + return { ...obj, value: newObj } + } else { + // Not an object/array, just replace + return { ...obj, value } + } +} + +export const DataTypeJSONInputEditDialog: React.FC = (props) => { + const { + open, + entry, + value, + onObjectChange, + onOpenChange + } = props + + const [editOpen, setEditOpen] = React.useState(open) + const [collapsedState, setCollapsedStateRaw] = React.useState>({}) + const [activePath, setActivePath] = React.useState(entry?.path ?? []) + const [editedObject, setEditedObject] = React.useState(value) + const [editorValue, setEditorValue] = React.useState(getValueAtPath(value, entry?.path ?? [])) + const clickTimeout = React.useRef(null) + + React.useEffect(() => { + setEditorValue(getValueAtPath(editedObject, activePath)) + }, [activePath]) + + React.useEffect(() => { + setActivePath(entry?.path ?? []) + setEditedObject(value) + }, [entry]) + + React.useEffect(() => { + setEditOpen(open) + }, [open]) + + const setCollapsedState = (path: string[], collapsed: boolean) => { + setCollapsedStateRaw(prev => ({...prev, [path.join(".")]: collapsed})) + } + + const handleEntryClick = (clickedEntry: EditableJSONEntry) => { + if (clickTimeout.current) clearTimeout(clickTimeout.current) + clickTimeout.current = setTimeout(() => { + setActivePath(clickedEntry.path ?? []) + }, 200) + } + + const handleRuleDoubleClick = (currentPath: string[], isCollapsed: boolean) => { + if (clickTimeout.current) clearTimeout(clickTimeout.current) + setCollapsedState(currentPath, !isCollapsed) + } + + const handleEditorChange = (val: unknown) => { + const updated = setValueAtPath(editedObject, activePath, val) + setEditedObject(updated) + onObjectChange?.(updated) + } + + const suggestions = () => null + const tokenHighlights = {} + + return ( + onOpenChange?.(open)}> + + + { + const target = e.target as HTMLElement + if (target.closest("[data-slot=resizable-handle]") || target.closest("[data-slot=resizable-panel]")) { + e.preventDefault() + } + }} w={"75%"} h={"75%"} style={{padding: "2px"}}> + + {entry?.key ?? "Edit Object"} + + + + + }> + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} + diff --git a/src/packages/ce/src/datatype/components/DataTypeJSONInputTree.tsx b/src/packages/ce/src/datatype/components/DataTypeJSONInputTree.tsx new file mode 100644 index 00000000..657aa415 --- /dev/null +++ b/src/packages/ce/src/datatype/components/DataTypeJSONInputTree.tsx @@ -0,0 +1,166 @@ +import {EditableJSONEntry} from "./DataTypeJSONInput" +import React from "react" +import {IconChevronDown, IconChevronUp} from "@tabler/icons-react" +import {LiteralValue} from "@code0-tech/sagittarius-graphql-types" +import {Badge, Flex, hashToColor, Text} from "@code0-tech/pictor"; + +export interface DataTypeJSONInputTreeProps { + object: LiteralValue + parentKey?: string + isRoot?: boolean + onEntryClick: (entry: EditableJSONEntry) => void + collapsedState: Record + setCollapsedState: (path: string[], collapsed: boolean) => void + path?: string[] + activePath?: string[] | null + onDoubleClick?: (path: string[], isCollapsed: boolean) => void + parentColor?: string +} + +export const DataTypeJSONInputTree: React.FC = (props) => { + const { + object, + parentKey, + isRoot = !parentKey, + onEntryClick, + collapsedState, + setCollapsedState, + path = [], + activePath = null, + onDoubleClick, + parentColor, + } = props + + const value = isRoot ? object?.value : object + if (typeof value !== "object" || value === null) return null + + const clickTimeout = React.useRef(null) + const CLICK_DELAY = 250 // ms + + const handleClick = (entry: EditableJSONEntry) => { + if (clickTimeout.current) clearTimeout(clickTimeout.current) + clickTimeout.current = setTimeout(() => { + onEntryClick(entry) + clickTimeout.current = null + }, CLICK_DELAY) + } + + const handleDoubleClick = (currentPath: string[], isCollapsed: boolean) => { + if (clickTimeout.current) { + clearTimeout(clickTimeout.current) + clickTimeout.current = null + } + if (onDoubleClick) { + onDoubleClick(currentPath, isCollapsed) + } else { + setCollapsedState(currentPath, !isCollapsed) + } + } + + React.useEffect(() => { + const currentPath = path ?? [] + const pathKey = (isRoot ? ["root"] : currentPath).join(".") + if (currentPath.length > 1 && collapsedState[pathKey] === undefined) { + setCollapsedState(currentPath.length === 0 ? ["root"] : currentPath, true) + } + }, [path, isRoot, collapsedState, setCollapsedState]) + + const renderRoot = () => { + const currentPath = [...path] + const pathKey = "root" + const isCollapsed = collapsedState[pathKey] || false + const isCollapsable = typeof value === "object" && value !== null && (Array.isArray(value) ? value.length > 0 : Object.keys(value).length > 0) + const isActive = Array.isArray(activePath) && activePath.length === 0 && parentKey === undefined + const icon = isCollapsable ? (isCollapsed ? : ) : null + return ( +
{ + e.stopPropagation() + handleClick({key: pathKey, value: object, path: currentPath}) + }} + onDoubleClick={e => { + e.stopPropagation() + handleDoubleClick(currentPath, isCollapsed) + }} + aria-selected={isActive || undefined} + > + + {icon} + {Array.isArray(value) ? "is a list of" : "is a nested object"} + + {!isCollapsed &&
    {renderNodes}
} +
+ ) + } + + const renderNodes = Array.isArray(value) || (value && typeof value === 'object') + ? Object.entries(value as Record).map(([key, val]) => { + const currentPath = [...path, key] + const pathKey = currentPath.join(".") + const isCollapsed = collapsedState[pathKey] || false + const isActive = activePath && activePath.length > 0 && currentPath.join(".") === activePath.join(".") + const parentColorValue = parentColor ?? hashToColor("root") + const isCollapsable = typeof (val as any) === "object" && (val as any) !== null && (Array.isArray((val as any)) ? (val as any).length > 0 : Object.keys((val as any) ?? {}).length > 0) + const collapsableColor = isCollapsable ? hashToColor(pathKey) : parentColorValue + const icon = isCollapsable ? (isCollapsed ? : ) : null + const label = isCollapsable ? ( + + {icon} + + {key} + + {Array.isArray((val as any)) ? "is a list of" : "is a nested object"} + + ) : ( + + + {key} + + has value + + {String((val as any))} + + + ) + const childTree = isCollapsable && !isCollapsed ? ( + + ) : null + return ( +
  • +
    { + e.stopPropagation() + handleClick({key, value: val as LiteralValue, path: currentPath}) + }} + onDoubleClick={e => { + e.stopPropagation() + handleDoubleClick(currentPath, isCollapsed) + }} + > + {label} + {childTree} +
    +
  • + ) + }) + : null + + const rootNode = renderRoot() + const nodes = rootNode && isRoot ? [rootNode] : renderNodes + const validNodes = (nodes ?? []).filter(Boolean) + if (validNodes.length === 0) return null + return
      {validNodes}
    +} \ No newline at end of file diff --git a/src/packages/ce/src/datatype/components/DataTypeTextInput.tsx b/src/packages/ce/src/datatype/components/DataTypeTextInput.tsx new file mode 100644 index 00000000..89b34624 --- /dev/null +++ b/src/packages/ce/src/datatype/components/DataTypeTextInput.tsx @@ -0,0 +1,182 @@ +import React from "react"; +import {ReferenceValue} from "@code0-tech/sagittarius-graphql-types"; +import {DataTypeInputNodeBadge} from "./DataTypeInputNodeBadge"; +import {DataTypeInputReferenceBadge} from "./DataTypeInputReferenceBadge"; +import {DataTypeInputProps} from "./DataTypeInput"; +import {InputSyntaxSegment, MenuItem, Text, TextInput, useService} from "@code0-tech/pictor"; +import {FunctionService} from "@edition/function/services/Function.service"; +import {FlowService} from "@edition/flow/services/Flow.service"; +import {FlowTypeService} from "@edition/flowtype/services/FlowTypeService"; +import {useSuggestions} from "@edition/function/hooks/DFlowSuggestion.hook"; +import {DFlowSuggestionMenuFooter} from "@edition/function/components/DFlowSuggestionMenuFooter"; +import {toInputSuggestions} from "@edition/function/components/DFlowSuggestionMenu.util"; +import {DFlowSuggestion} from "@edition/function/components/DFlowSuggestion.view"; + +export type DataTypeTextInputProps = DataTypeInputProps + +export const splitTextAndObjects = (input: string) => { + const result: (string | Record)[] = [] + + let currentText = "" + let currentObject = "" + let braceLevel = 0 + let inString: '"' | "'" | "" = "" + let escaped = false + + const pushText = () => { + if (currentText) result.push(currentText) + currentText = "" + } + + const parseObject = (value: string) => { + try { + return JSON.parse(value) + } catch { + try { + return JSON.parse( + value + .replace(/'/g, `"`) + .replace(/([{,]\s*)([A-Za-z_$][\w$]*)(\s*:)/g, `$1"$2"$3`) + ) + } catch { + return {} + } + } + } + + input.split("").forEach(char => { + if (braceLevel > 0) { + currentObject += char + + if (escaped) { + escaped = false + return + } + + if (char === "\\") { + escaped = true + return + } + + if (inString) { + if (char === inString) inString = "" + return + } + + if (char === `"` || char === `'`) { + inString = char as any + return + } + + if (char === "{") braceLevel++ + if (char === "}") braceLevel-- + + if (braceLevel === 0) { + result.push(parseObject(currentObject)) + currentObject = "" + } + + return + } + + if (char === "{") { + pushText() + braceLevel = 1 + currentObject = "{" + return + } + + currentText += char + }) + + pushText() + return result +} + +export const DataTypeTextInput: React.FC = (props) => { + + const {flowId, nodeId, parameterId, ...rest} = props + + const functionService = useService(FunctionService) + const flowService = useService(FlowService) + const flowTypeService = useService(FlowTypeService) + + const flow = React.useMemo(() => { + return flowService.getById(flowId) + }, [flowService, flowId]) + + const suggestions = rest.suggestions || useSuggestions(flowId, nodeId, parameterId) + + const transformSyntax = React.useCallback((value: string | null): InputSyntaxSegment[] => { + + const textValue = (value === null || value === undefined ? value : String(value ?? ""))! + let cursor = 0 + + const buildTextSegment = (text: string): InputSyntaxSegment => { + const segment = { + type: "text", + value: text, + start: cursor, + end: cursor + text.length, + visualLength: text.length, + content: text, + } as InputSyntaxSegment + cursor += text.length + return segment + } + + const buildBlockSegment = (node: React.ReactNode, value: Record): InputSyntaxSegment => { + const segment = { + type: "block", + value: value, + start: cursor, + end: cursor + JSON.stringify(value).length, + visualLength: 1, + content: node, + } as InputSyntaxSegment + cursor += JSON.stringify(value).length + return segment + } + + return splitTextAndObjects(textValue).map(value => { + + if (typeof value !== "object") { + return buildTextSegment(value) + } + + if (value?.__typename === "NodeFunctionIdWrapper" || value?.__typename === "NodeFunction") { + const node = value?.__typename === "NodeFunction" ? value : flowService.getNodeById(flowId, value.id) + return buildBlockSegment( + , + value + ) + } + + if (value?.__typename === "ReferenceValue") { + const node = (value as ReferenceValue).nodeFunctionId === "gid://sagittarius/NodeFunction/-1" ? flowTypeService.getById(flow?.type?.id) : functionService.getById(flowService.getNodeById(flowId, (value as ReferenceValue).nodeFunctionId)?.functionDefinition?.id) + return buildBlockSegment( + , + value + ) + } + + if (value?.__typename === "LiteralValue") { + return buildTextSegment(value.value) + } + + return buildTextSegment(value as any as string) + }) + }, [functionService, flowService]) + + return No suggestion found} + suggestionsFooter={} + filterSuggestionsByLastToken + enforceUniqueSuggestions + validationUsesSyntax + transformSyntax={transformSyntax} + suggestions={rest.suggestions ? rest.suggestions : toInputSuggestions(suggestions as DFlowSuggestion[])} + {...rest} + + /> +} diff --git a/src/packages/ce/src/datatype/components/DataTypeTypeInput.style.scss b/src/packages/ce/src/datatype/components/DataTypeTypeInput.style.scss new file mode 100644 index 00000000..6a0522fd --- /dev/null +++ b/src/packages/ce/src/datatype/components/DataTypeTypeInput.style.scss @@ -0,0 +1,64 @@ +@use "../../styles/helpers"; +@use "../../styles/box"; +@use "../../styles/variables"; + +ul { + list-style: none; + margin: 0; + margin-inline-start: 2px; + margin-block-start: variables.$xxs; + padding-inline-start: variables.$xxs; + text-wrap: nowrap; + position: relative; + display: flex; + flex-direction: column; + gap: variables.$xxs; +} + +li { + position: relative; + padding-inline-start: variables.$xs; + list-style: none; + + &:before { + content: ''; + position: absolute; + background: transparent; + width: 0.7rem; + left: -0.35rem; + height: 20px; + top: 5px; + transform: translateY(-50%); + border-left: 2px solid helpers.backgroundColor(variables.$tertiary); + border-bottom: 2px solid helpers.backgroundColor(variables.$tertiary); + border-bottom-left-radius: 0.5rem; + } + + &:not(:last-child):after { + content: ''; + position: absolute; + width: 2px; + height: 100%; + background: helpers.backgroundColor(variables.$tertiary); + top: 0; + left: -0.35rem; + } +} + +.rule { + + padding: variables.$xxs / 2 variables.$xxs; + margin-left: -1 * variables.$xxs; + width: calc(100% + (0.35rem)); + + & { + @include helpers.borderRadius(); + @include box.box(variables.$primary); + @include box.boxHover(variables.$secondary); + @include box.boxActive(variables.$secondary); + @include helpers.fontStyle(); + box-shadow: none; + //border-top: 1px solid helpers.borderColor(); + cursor: pointer; + } +} \ No newline at end of file diff --git a/src/packages/ce/src/datatype/components/DataTypeTypeInput.tsx b/src/packages/ce/src/datatype/components/DataTypeTypeInput.tsx new file mode 100644 index 00000000..cf3a6e68 --- /dev/null +++ b/src/packages/ce/src/datatype/components/DataTypeTypeInput.tsx @@ -0,0 +1,182 @@ +import { + DataTypeIdentifier, + DataTypeRule, + DataTypeRulesContainsKeyConfig, + Maybe +} from "@code0-tech/sagittarius-graphql-types"; +import React from "react"; +import {IconEdit} from "@tabler/icons-react"; +import "./DataTypeTypeInput.style.scss" +import {DataTypeTypeInputEditDialog} from "./DataTypeTypeInputEditDialog"; +import { + Badge, + Button, + Card, + Flex, hashToColor, + InputDescription, + InputLabel, + Text, + useService, + useStore, + ValidationProps +} from "@code0-tech/pictor"; +import {DatatypeService} from "@edition/datatype/services/Datatype.service"; +import CardSection from "@code0-tech/pictor/dist/components/card/CardSection"; + +export interface DataTypeTypeInputProps extends ValidationProps { + onChange?: (value: DataTypeIdentifier | null) => void + description?: string + label?: string +} + +export interface DataTypeTypeInputRuleTreeProps { + dataTypeIdentifier: DataTypeIdentifier + parentRule?: Maybe + isRoot?: boolean +} + +export const DataTypeTypeInput: React.FC = (props) => { + + const {initialValue, defaultValue, value, label, description} = props + const initValue = value ?? initialValue ?? defaultValue ?? null + + const dataTypeService = useService(DatatypeService) + const dataTypeStore = useStore(DatatypeService) + + const [editOpen, setEditOpen] = React.useState(false) + + const initialDataType = React.useMemo(() => { + return dataTypeService.getDataType(initValue!) + }, [dataTypeStore, initValue]) + + return
    + setEditOpen(open)}/> + {label} + {description} + + + + + {(initialDataType?.name?.[0].content) ?? "Unnamed Data Type"} + + + + + + + + + + +
    +} + +export const DFlowInputDataTypeRuleTree: React.FC = (props) => { + const {dataTypeIdentifier, parentRule, isRoot = !parentRule} = props + + const dataTypeService = useService(DatatypeService) + const dataTypeStore = useStore(DatatypeService) + + const genericMap = React.useMemo(() => { + const rules = dataTypeIdentifier.genericType?.genericMappers ?? []; + return new Map(rules.map(g => [g.target!!, g] as const)) + }, [dataTypeIdentifier]) + + const currentDataTypes = React.useMemo(() => { + if (dataTypeIdentifier.genericKey) { + const genericEntry = genericMap.get(dataTypeIdentifier.genericKey) + return genericEntry?.sourceDataTypeIdentifiers?.map(id => dataTypeService.getDataType(id)) ?? [] + } + return [dataTypeService.getDataType(dataTypeIdentifier)] + }, [dataTypeStore, dataTypeIdentifier, genericMap]) + + const resolveChildIdentifier = React.useCallback((childRawIdentifier: DataTypeIdentifier): DataTypeIdentifier | null => { + if (!childRawIdentifier) return null + if (childRawIdentifier.genericKey) { + return genericMap.get(childRawIdentifier.genericKey)?.sourceDataTypeIdentifiers?.[0] ?? null + } + return childRawIdentifier + }, [genericMap]) + + const nodes = React.useMemo(() => { + return currentDataTypes.flatMap((dataType, typeIndex) => { + const rules = dataType?.rules?.nodes ?? [] + if (!rules.length) return [] + + return rules.map((rule, ruleIndex) => { + const key = `${typeIndex}-${ruleIndex}` + const rawChildId = (rule?.config as any)?.dataTypeIdentifier as DataTypeIdentifier + const childId = resolveChildIdentifier(rawChildId) + + if (rule?.variant === "PARENT_TYPE" && childId) { + return + } + + if (!childId) return null + + const childType = dataTypeService.getDataType(childId) + const isChildPrimitive = childType?.variant === "PRIMITIVE" + const typeName = childType?.name?.[0]?.content + + let label: React.ReactNode = null + if (rule?.variant === "CONTAINS_KEY") { + const keyConfig = rule?.config as DataTypeRulesContainsKeyConfig + label = ( + + + {keyConfig?.key} + + + {parentRule?.variant === "CONTAINS_KEY" ? "is a field inside" : "is a field"} + {isChildPrimitive ? " of type" : ""} + + {isChildPrimitive && ( + + {typeName} + + )} + + ) + } else if (rule?.variant === "CONTAINS_TYPE") { + const prevKey = (parentRule?.config as DataTypeRulesContainsKeyConfig)?.key + label = ( + + Inside + {prevKey && ( + + {prevKey} + + )} + , each entity has + + ) + } + + const childTree = + + if (isRoot) return {label} {childTree} + + return
  • {label} {childTree}
  • + }) + }) + }, [currentDataTypes, isRoot, resolveChildIdentifier, dataTypeService, parentRule]) + + const validNodes = nodes.filter(Boolean) + if (validNodes.length === 0) return null + + if (isRoot || parentRule?.variant === "PARENT_TYPE") { + return <>{validNodes} + } + + return
      {validNodes}
    +} diff --git a/src/packages/ce/src/datatype/components/DataTypeTypeInputEditDialog.tsx b/src/packages/ce/src/datatype/components/DataTypeTypeInputEditDialog.tsx new file mode 100644 index 00000000..8252dd35 --- /dev/null +++ b/src/packages/ce/src/datatype/components/DataTypeTypeInputEditDialog.tsx @@ -0,0 +1,176 @@ +import React from "react"; +import {DataTypeIdentifier, LiteralValue} from "@code0-tech/sagittarius-graphql-types"; +import {DFlowInputDataTypeRuleTree} from "./DataTypeTypeInput"; +import {CompletionContext, CompletionResult} from "@codemirror/autocomplete"; +import {syntaxTree} from "@codemirror/language"; +import {IconX} from "@tabler/icons-react"; +import { + Badge, + Button, + Dialog, + DialogClose, + DialogContent, + DialogOverlay, + DialogPortal, + DialogTitle, + DLayout, + DResizableHandle, + DResizablePanel, + DResizablePanelGroup, + Flex, + hashToColor, + ScrollArea, + ScrollAreaScrollbar, + ScrollAreaThumb, + ScrollAreaViewport, + Spacing, + Text, + useService, + useStore +} from "@code0-tech/pictor"; +import {DatatypeService} from "@edition/datatype/services/Datatype.service"; +import {Editor, EditorTokenHighlights} from "@code0-tech/pictor/dist/components/editor/Editor"; + +export interface DataTypeTypeInputEditDialogProps { + dataTypeIdentifier: DataTypeIdentifier + open?: boolean + onOpenChange?: (open: boolean) => void + onDataTypeChange?: (dataTypeIdentifier: DataTypeIdentifier) => void +} + +export const DataTypeTypeInputEditDialog: React.FC = (props) => { + + const {open, onOpenChange, onDataTypeChange} = props + + const dataTypeService = useService(DatatypeService) + const dataTypeStore = useStore(DatatypeService) + + const [editOpen, setEditOpen] = React.useState(open) + const [dataTypeIdentifier, setDataTypeIdentifier] = React.useState(props.dataTypeIdentifier) + + React.useEffect(() => { + setEditOpen(open) + setDataTypeIdentifier(props.dataTypeIdentifier) + }, [open]) + + const editorValue = React.useMemo(() => { + return dataTypeService.getValueFromType(dataTypeIdentifier) as LiteralValue + }, [dataTypeStore]) + + const initialDataType = React.useMemo(() => { + return dataTypeService.getDataType(dataTypeIdentifier!) + }, [dataTypeStore, dataTypeIdentifier]) + + const suggestions = (context: CompletionContext): CompletionResult | null => { + + const word = context.matchBefore(/\w*/) + + if (!word || (word.from === word.to && !context.explicit)) { + return null; + } + + const node = syntaxTree(context.state).resolveInner(context.pos, -1); + const prevNode = syntaxTree(context.state).resolveInner(context.pos, 0); + + if (node.name === "Property" || prevNode.name === "Property") { + return { + from: word.from, + options: [ + { + label: "Text", + type: "type", + apply: `"Text"`, + }, + { + label: "Boolean", + type: "type", + apply: `true`, + }, + { + label: "Number", + type: "type", + apply: `1`, + }, + ] + } + } + return null + } + + const myRenderMap: EditorTokenHighlights = { + bool: ({content}) => { + return + Boolean + + }, + string: ({content}) => { + return + Text + + }, + number: ({content}) => { + return + Number + + } + } + + return onOpenChange?.(open)}> + + + { + const target = e.target as HTMLElement; + + if (target.closest("[data-slot=resizable-handle]") || target.closest("[data-slot=resizable-panel]")) { + e.preventDefault(); + } + }} w={"75%"} h={"75%"} style={{ + padding: "2px", + }}> + + + + {initialDataType?.name?.[0].content ?? "Unnamed Data Type"} + + + + + }> + + + + + + + + + + + + + + + + + + + { + const dataTypeIdentifier = dataTypeService.getTypeFromValue({ + __typename: "LiteralValue", + value: value + }) + onDataTypeChange?.(dataTypeIdentifier!) + setDataTypeIdentifier(dataTypeIdentifier!) + }}/> + + + + + + +} \ No newline at end of file diff --git a/src/packages/ce/src/datatype/services/DataType.view.ts b/src/packages/ce/src/datatype/services/DataType.view.ts new file mode 100644 index 00000000..71a3c852 --- /dev/null +++ b/src/packages/ce/src/datatype/services/DataType.view.ts @@ -0,0 +1,113 @@ +import { + DataType, DataTypeIdentifier, DataTypeIdentifierConnection, + DataTypeRuleConnection, + DataTypeVariant, Maybe, Runtime, Scalars, Translation, +} from "@code0-tech/sagittarius-graphql-types"; +import {attachDataTypeIdentifiers, resolveDataTypeIdentifiers} from "@edition/flow/components/FlowBuilder.util"; + + +export class DataTypeView { + + /** Name of the function */ + private readonly _aliases?: Maybe>; + /** Time when this DataType was created */ + private readonly _createdAt?: Maybe; + /** The data type identifiers that are referenced in this data type and its rules */ + private readonly _dataTypeIdentifiers?: Maybe; + /** Display message of the function */ + private readonly _displayMessages?: Maybe>; + /** Generic keys of the datatype */ + private readonly _genericKeys?: Maybe>; + /** Global ID of this DataType */ + private readonly _id?: Maybe; + /** The identifier scoped to the namespace */ + private readonly _identifier?: Maybe; + /** Names of the flow type setting */ + private readonly _name?: Maybe>; + /** Rules of the datatype */ + private readonly _rules?: Maybe; + /** The namespace where this datatype belongs to */ + private readonly _runtime?: Maybe; + /** Time when this DataType was last updated */ + private readonly _updatedAt?: Maybe; + /** The type of the datatype */ + private readonly _variant?: Maybe; + + constructor(dataType: DataType) { + this._aliases = dataType.aliases; + this._createdAt = dataType.createdAt; + this._dataTypeIdentifiers = dataType.dataTypeIdentifiers; + this._displayMessages = dataType.displayMessages; + this._genericKeys = dataType.genericKeys; + this._id = dataType.id; + this._identifier = dataType.identifier; + this._name = dataType.name; + this._runtime = dataType.runtime; + this._rules = attachDataTypeIdentifiers(resolveDataTypeIdentifiers((this._dataTypeIdentifiers?.nodes ?? []) as DataTypeIdentifier[]), dataType.rules); + this._updatedAt = dataType.updatedAt; + this._variant = dataType.variant; + } + + get aliases(): Maybe> | undefined { + return this._aliases; + } + + get createdAt(): Maybe | undefined { + return this._createdAt; + } + + get dataTypeIdentifiers(): Maybe | undefined { + return this._dataTypeIdentifiers; + } + + get displayMessages(): Maybe> | undefined { + return this._displayMessages; + } + + get genericKeys(): Maybe> | undefined { + return this._genericKeys; + } + + get id(): Maybe | undefined { + return this._id; + } + + get identifier(): Maybe | undefined { + return this._identifier; + } + + get name(): Maybe> | undefined { + return this._name; + } + + get runtime(): Maybe | undefined { + return this._runtime; + } + + get rules(): Maybe | undefined { + return this._rules; + } + + get updatedAt(): Maybe | undefined { + return this._updatedAt; + } + + get variant(): Maybe | undefined { + return this._variant; + } + + get json(): DataType { + return { + id: this._id, + createdAt: this._createdAt, + updatedAt: this._updatedAt, + identifier: this._identifier, + name: this._name, + runtime: this._runtime, + variant: this._variant, + genericKeys: this._genericKeys, + rules: this._rules, + dataTypeIdentifiers: this._dataTypeIdentifiers + } + } +} \ No newline at end of file diff --git a/src/packages/ce/src/datatype/services/Datatype.service.ts b/src/packages/ce/src/datatype/services/Datatype.service.ts index 61f546ec..ce9bed77 100644 --- a/src/packages/ce/src/datatype/services/Datatype.service.ts +++ b/src/packages/ce/src/datatype/services/Datatype.service.ts @@ -1,14 +1,29 @@ -import { - DataTypeView, DFlowDataTypeDependencies, - DFlowDataTypeReactiveService, - ReactiveArrayStore -} from "@code0-tech/pictor"; +import {DFlowDataTypeDependencies, ReactiveArrayService, ReactiveArrayStore} from "@code0-tech/pictor"; import {GraphqlClient} from "@core/util/graphql-client"; -import {DataType, Query} from "@code0-tech/sagittarius-graphql-types"; +import { + DataType, + DataTypeIdentifier, + DataTypeRule, + DataTypeRulesContainsKeyConfig, + DataTypeRulesInputTypesConfig, + DataTypeRulesVariant, + Flow, + GenericMapper, + LiteralValue, + Maybe, + NodeFunctionIdWrapper, + NodeParameterValue, + Query +} from "@code0-tech/sagittarius-graphql-types"; import dataTypeQuery from "@edition/datatype/services/queries/DataTypes.query.graphql" import {View} from "@code0-tech/pictor/dist/utils/view"; +import {useValueValidation} from "@edition/flow/hooks/ValueValidation.hook"; +import {findReturnNode} from "@edition/datatype/services/rules/DataTypeReturnTypeRule"; +import {md5} from "js-md5"; +import {resolveType} from "@edition/flow/utils/generics"; +import {DataTypeView} from "@edition/datatype/services/DataType.view"; -export class DatatypeService extends DFlowDataTypeReactiveService { +export class DatatypeService extends ReactiveArrayService { private readonly client: GraphqlClient private i = 0 @@ -61,4 +76,264 @@ export class DatatypeService extends DFlowDataTypeReactiveService { return dataType !== undefined } + getDataType(type: DataTypeIdentifier, dependencies?: DFlowDataTypeDependencies): DataTypeView | undefined { + if (!type) return undefined + if ((type as DataTypeIdentifier).genericKey) return undefined + const dataType = type.dataType ?? type.genericType?.dataType + const identifier = dataType?.identifier + const id = dataType?.id + + if (dataType?.rules) { + return new DataTypeView(dataType) + } + + return this.values().find(value => { + return value.identifier == identifier || value.id == id + }); + } + + getDataTypeFromValue(value: NodeParameterValue, flow?: Flow, dependencies?: DFlowDataTypeDependencies): DataTypeView | undefined { + + if (!value) return undefined + + if (value.__typename == "LiteralValue") { + //hardcode primitive types (NUMBER, BOOLEAN, TEXT) + if (Array.isArray(value.value) && Array.from(value.value).length > 0) return this.getDataType({dataType: {identifier: "LIST"}}) + if (typeof value.value === "object") return this.getDataType({dataType: {identifier: "OBJECT"}}, dependencies) + if (typeof value.value === "string") return this.getDataType({dataType: {identifier: "TEXT"}}, dependencies) + if (typeof value.value === "number") return this.getDataType({dataType: {identifier: "NUMBER"}}, dependencies) + if (typeof value.value === "boolean") return this.getDataType({dataType: {identifier: "BOOLEAN"}}, dependencies) + } + + const matchingDataTypes = this.values(dependencies).filter(type => { + if (value.__typename === "NodeFunctionIdWrapper" && (type.variant != "NODE" || !flow)) return false + return useValueValidation(value, type, this, flow) + }) + + return matchingDataTypes[matchingDataTypes.length - 1] + + } + + getValueFromType(dataTypeIdentifier: DataTypeIdentifier, flow?: Flow, dependencies?: DFlowDataTypeDependencies): LiteralValue | undefined { + const type = this.getDataType(dataTypeIdentifier, dependencies) + if (!type) return undefined + + if (type.identifier === "TEXT") return {__typename: "LiteralValue", value: ""} + if (type.identifier === "NUMBER") return {__typename: "LiteralValue", value: 0} + if (type.identifier === "BOOLEAN") return {__typename: "LiteralValue", value: false} + + const rules = type.rules?.nodes ?? [] + if (rules.length === 0) return {__typename: "LiteralValue", value: null} + + const isList = rules.some(rule => rule?.variant === "CONTAINS_TYPE") + const isObject = !isList && rules.some(rule => rule?.variant === "CONTAINS_KEY" || rule?.variant === "PARENT_TYPE") + + if (!isList && !isObject) { + return { + __typename: "LiteralValue", + value: null + } + } + + const mappedValues = rules.map(rule => { + if (!rule) return undefined + + if (rule.variant === "CONTAINS_TYPE" && isList) { + // @ts-ignore + const configId = rule.config?.dataTypeIdentifier as DataTypeIdentifier + if (configId) { + const mapper = configId.genericKey && dataTypeIdentifier.genericType?.genericMappers + ? dataTypeIdentifier.genericType.genericMappers.find(m => m.target === configId.genericKey) + : undefined + const resolvedId = mapper?.sourceDataTypeIdentifiers?.[0] ?? configId + + const nestedVal = this.getValueFromType(resolvedId, flow, dependencies) + if (nestedVal && nestedVal.__typename === "LiteralValue") { + return nestedVal.value + } + } + } + + if (rule.variant === "CONTAINS_KEY" && isObject) { + const keyConfig = rule.config as DataTypeRulesContainsKeyConfig + if (keyConfig?.key && keyConfig?.dataTypeIdentifier) { + const mapper = keyConfig.dataTypeIdentifier?.genericKey && dataTypeIdentifier.genericType?.genericMappers + ? dataTypeIdentifier.genericType.genericMappers.find(m => m.target === keyConfig.dataTypeIdentifier?.genericKey) + : undefined + const resolvedId = mapper?.sourceDataTypeIdentifiers?.[0] ?? keyConfig.dataTypeIdentifier + + const nestedVal = this.getValueFromType(resolvedId, flow, dependencies) + if (nestedVal && nestedVal.__typename === "LiteralValue") { + return {[keyConfig.key]: nestedVal.value} + } + } + } + + if (rule.variant === "PARENT_TYPE" && isObject) { + // @ts-ignore + const configId = rule.config?.dataTypeIdentifier as DataTypeIdentifier + if (configId) { + const mapper = configId.genericKey && dataTypeIdentifier.genericType?.genericMappers + ? dataTypeIdentifier.genericType.genericMappers.find(m => m.target === configId.genericKey) + : undefined + const resolvedId = mapper?.sourceDataTypeIdentifiers?.[0] ?? configId + + const nestedVal = this.getValueFromType(resolvedId, flow, dependencies) + if (nestedVal && nestedVal.__typename === "LiteralValue" && typeof nestedVal.value === "object" && !Array.isArray(nestedVal.value)) { + return nestedVal.value + } + } + } + + return undefined + }).filter(val => val !== undefined) + + return { + __typename: "LiteralValue", + value: isList ? mappedValues : Object.assign({}, ...mappedValues) + } + } + + getTypeFromValue(value: NodeParameterValue, flow?: Flow, dependencies?: DFlowDataTypeDependencies): Maybe | undefined { + + if (!value) return undefined + + const dataType = this.getDataTypeFromValue(value, flow, dependencies) + if ((dataType?.genericKeys?.length ?? 0) <= 0 || !dataType?.genericKeys) return { + dataType: { + id: dataType?.id, + identifier: dataType?.identifier + } + } + + //TODO: missing generic combinations + const genericMapper: GenericMapper[] = dataType.genericKeys.map(genericKey => { + + // @ts-ignore + const ruleThatIncludesGenericKey: Maybe | undefined = dataType.rules?.nodes?.find((rule: DataTypeRule) => { + // @ts-ignore + return ("dataTypeIdentifier" in (rule?.config ?? {}) && rule?.config?.dataTypeIdentifier?.genericKey == genericKey) + || ("inputTypes" in (rule?.config as DataTypeRulesInputTypesConfig ?? {})) && (rule.config as DataTypeRulesInputTypesConfig).inputTypes?.some(inputType => inputType.dataTypeIdentifier?.genericKey == genericKey) + }) + + if (ruleThatIncludesGenericKey + && ruleThatIncludesGenericKey.variant == "CONTAINS_TYPE" + && "value" in value && value?.value + && dataType.variant === "ARRAY") { + + return { + sourceDataTypeIdentifiers: [this.getTypeFromValue({ + __typename: "LiteralValue", + value: ((value as LiteralValue).value as Array)[0] + }, flow, dependencies)], + target: genericKey + } as GenericMapper + } + + if (ruleThatIncludesGenericKey + && ruleThatIncludesGenericKey.variant == "CONTAINS_KEY" + && "value" in value && value?.value + && dataType.variant === "OBJECT") { + return { + sourceDataTypeIdentifiers: [this.getTypeFromValue({ + __typename: "LiteralValue", + /* @ts-ignore */ + value: (value.value as Object)[((ruleThatIncludesGenericKey.config as DataTypeRulesContainsKeyConfig)?.key ?? "")] + }, flow, dependencies)], + target: genericKey + } as GenericMapper + } + + if (ruleThatIncludesGenericKey + && ruleThatIncludesGenericKey.variant == "RETURN_TYPE" + && dataType.variant === "NODE") { + + const foundReturnFunction = findReturnNode(value as NodeFunctionIdWrapper, flow!!) + const returnValue = foundReturnFunction?.parameters?.nodes?.[0]?.value; + + return { + sourceDataTypeIdentifiers: [this.getTypeFromValue(returnValue ?? { + __typename: "LiteralValue", + value: null + }, flow, dependencies)], + target: genericKey + } as GenericMapper + } + + if (ruleThatIncludesGenericKey + && ruleThatIncludesGenericKey.variant == "INPUT_TYPES" + && dataType.variant === "NODE") { + return { + sourceDataTypeIdentifiers: [{ + genericKey: genericKey + }], + target: genericKey + } as GenericMapper + } + + if (ruleThatIncludesGenericKey + && ruleThatIncludesGenericKey.variant == "PARENT_TYPE" + && dataType.identifier === "OBJECT" + && value.__typename === "LiteralValue" + && value.value) { + const rules: Array = Object.entries(value.value).map(innerValue => { + return { + __typename: "DataTypeRule", + variant: "CONTAINS_KEY" as DataTypeRulesVariant.ContainsKey, + config: { + key: innerValue[0]!, + dataTypeIdentifier: this.getTypeFromValue({ + __typename: "LiteralValue", + value: innerValue[1]! + }, flow, dependencies) ?? null + } + } + }) + + const innerDataType = new DataTypeView({ + ...dataType.json, + genericKeys: [], + identifier: md5(String(value.value)), + rules: { + nodes: rules + } + }) + return { + sourceDataTypeIdentifiers: [{ + dataType: innerDataType.json + }], + target: genericKey + } as GenericMapper + } + + return null + }).filter(mapper => !!mapper) + + const resolvedType: DataTypeIdentifier = genericMapper.length > 0 ? { + genericType: { + dataType: { + id: dataType.id, + identifier: dataType.identifier, + }, + genericMappers: genericMapper + } + } : { + dataType: { + id: dataType.id, + identifier: dataType.identifier, + } + } + + return resolveType(resolvedType, this) + + } + + hasDataTypes(types: DataTypeIdentifier[], dependencies?: DFlowDataTypeDependencies): boolean { + return types.every(type => { + return this.values(dependencies).find(value => { + return value.id === (type.genericType?.dataType?.id ?? type.dataType?.id) + }) + }) + } + } \ No newline at end of file diff --git a/src/packages/ce/src/datatype/services/rules/DataTypeContainsKeyRule.ts b/src/packages/ce/src/datatype/services/rules/DataTypeContainsKeyRule.ts new file mode 100644 index 00000000..da5befbf --- /dev/null +++ b/src/packages/ce/src/datatype/services/rules/DataTypeContainsKeyRule.ts @@ -0,0 +1,57 @@ +import {DataTypeRule, genericMapping, staticImplements} from "./DataTypeRule"; +import type { + DataTypeRulesContainsKeyConfig, + Flow, + GenericMapper, + LiteralValue, + NodeParameterValue +} from "@code0-tech/sagittarius-graphql-types"; +import {useValueValidation} from "@edition/flow/hooks/ValueValidation.hook"; +import {DatatypeService} from "@edition/datatype/services/Datatype.service"; + + +@staticImplements() +export class DataTypeContainsKeyRule { + public static validate(value: NodeParameterValue, config: DataTypeRulesContainsKeyConfig, generics?: Map, flow?: Flow, dataTypeService?: DatatypeService): boolean { + + const genericMapper = generics?.get(config?.dataTypeIdentifier?.genericKey!!) + const genericTypes = generics?.get(config?.dataTypeIdentifier?.genericKey!!)?.sourceDataTypeIdentifiers + const genericCombination = generics?.get(config?.dataTypeIdentifier?.genericKey!!)?.genericCombinationStrategies + + //TODO: seperate general validation + //if (!(isObject(value))) return false + if ((config?.key ?? "") in value && config?.dataTypeIdentifier?.genericKey && !genericMapper && !dataTypeService?.getDataType(config.dataTypeIdentifier)) return true + + if (!(dataTypeService?.getDataType(config.dataTypeIdentifier!!) || genericMapper)) return false + + //use of generic key but datatypes does not exist + if (genericMapper && !dataTypeService?.hasDataTypes(genericTypes!!)) return false + + //check if all generic combinations are set + if (genericMapper && !(((genericCombination?.length ?? 0) + 1) == genericTypes!!.length)) return false + + //use generic given type for checking against value + if (config?.dataTypeIdentifier?.genericKey && genericMapper && genericTypes) { + const checkAllTypes: boolean[] = genericTypes.map(genericType => { + return useValueValidation({__typename: "LiteralValue", value: (value as LiteralValue).value[(config?.key ?? "")]}, dataTypeService?.getDataType(genericType)!!, dataTypeService!!, flow, ((genericType.genericType)!!.genericMappers as GenericMapper[])) + }) + + const combination = checkAllTypes.length > 1 ? checkAllTypes.reduce((previousValue, currentValue, currentIndex) => { + if (genericCombination && genericCombination[currentIndex - 1].type == "OR") { + return previousValue || currentValue + } + + return previousValue && currentValue + }) : checkAllTypes[0] + + return ((config?.key ?? "") in value) && combination + } + + //normal datatype link + if (config?.dataTypeIdentifier?.dataType) { + return ((config?.key ?? "") in (value as LiteralValue).value) && useValueValidation({__typename: "LiteralValue", value: (value as LiteralValue).value[(config?.key ?? "")]}, dataTypeService?.getDataType(config.dataTypeIdentifier)!!, dataTypeService!!) + } + + return ((config?.key ?? "") in (value as LiteralValue).value) && useValueValidation({__typename: "LiteralValue", value: (value as LiteralValue).value[(config?.key ?? "")]}, dataTypeService?.getDataType(config.dataTypeIdentifier!!)!!, dataTypeService!!, flow, genericMapping(config?.dataTypeIdentifier?.genericType?.genericMappers!!, generics)) + } +} \ No newline at end of file diff --git a/src/packages/ce/src/datatype/services/rules/DataTypeContainsTypeRule.ts b/src/packages/ce/src/datatype/services/rules/DataTypeContainsTypeRule.ts new file mode 100644 index 00000000..84bf57d1 --- /dev/null +++ b/src/packages/ce/src/datatype/services/rules/DataTypeContainsTypeRule.ts @@ -0,0 +1,68 @@ +import {DataTypeRule, genericMapping, staticImplements} from "./DataTypeRule"; +import type { + DataTypeRulesContainsKeyConfig, + Flow, + GenericMapper, + GenericType, + LiteralValue, + NodeParameterValue +} from "@code0-tech/sagittarius-graphql-types"; +import {DatatypeService} from "@edition/datatype/services/Datatype.service"; +import {useValueValidation} from "@edition/flow/hooks/ValueValidation.hook"; + +@staticImplements() +export class DataTypeContainsTypeRule { + public static validate(value: NodeParameterValue, config: DataTypeRulesContainsKeyConfig, generics?: Map, flow?: Flow, dataTypeService?: DatatypeService): boolean { + + const genericMapper = generics?.get(config?.dataTypeIdentifier?.genericKey!!) + const genericTypes = generics?.get(config?.dataTypeIdentifier?.genericKey!!)?.sourceDataTypeIdentifiers + const genericCombination = generics?.get(config?.dataTypeIdentifier?.genericKey!!)?.genericCombinationStrategies + + //TODO: seperate general validation + if ("value" in value && !(Array.isArray(value.value))) return false + + if (config?.dataTypeIdentifier?.genericKey && !genericMapper && !dataTypeService?.getDataType(config.dataTypeIdentifier)) return true + + if (!(dataTypeService?.getDataType(config.dataTypeIdentifier!!) || genericMapper)) return false + + //use of generic key but datatype does not exist + if (genericMapper && !dataTypeService?.hasDataTypes(genericTypes!!)) return false + + //check if all generic combinations are set + if (genericMapper && !(((genericCombination?.length ?? 0) + 1) == genericTypes!!.length)) return false + + //use generic given type for checking against value + if (config?.dataTypeIdentifier?.genericKey && genericMapper && genericTypes) { + const checkAllTypes: boolean[] = genericTypes.map(genericType => { + return (value as LiteralValue).value.every((value1: any) => { + if (genericType.genericType) { + return useValueValidation({ + __typename: "LiteralValue", + value: value1 + }, dataTypeService?.getDataType(genericType)!!, dataTypeService!!, flow, ((genericType.genericType as GenericType)!!.genericMappers as GenericMapper[])) + } + return useValueValidation({ + __typename: "LiteralValue", + value: value1 + }, dataTypeService?.getDataType(genericType)!!, dataTypeService!!, flow) + }) + }) + + return checkAllTypes.length > 1 ? checkAllTypes.reduce((previousValue, currentValue, currentIndex) => { + if (genericCombination && genericCombination[currentIndex - 1].type == "OR") { + return previousValue || currentValue + } + + return previousValue && currentValue + }) : checkAllTypes[0] + } + + //normal datatype link + if (config?.dataTypeIdentifier?.dataType) { + return (value as LiteralValue).value.every((value1: any) => useValueValidation({__typename: "LiteralValue", value: value1}, dataTypeService?.getDataType(config.dataTypeIdentifier!!)!!, dataTypeService!!)) + } + + return (value as LiteralValue).value.every((value1: any) => useValueValidation({__typename: "LiteralValue", value: value1}, dataTypeService?.getDataType(config.dataTypeIdentifier!!)!!, dataTypeService!!, flow, genericMapping((config.dataTypeIdentifier?.genericType as GenericType).genericMappers!!, generics))) + + } +} \ No newline at end of file diff --git a/src/packages/ce/src/datatype/services/rules/DataTypeItemOfCollectionRule.ts b/src/packages/ce/src/datatype/services/rules/DataTypeItemOfCollectionRule.ts new file mode 100644 index 00000000..6f00d186 --- /dev/null +++ b/src/packages/ce/src/datatype/services/rules/DataTypeItemOfCollectionRule.ts @@ -0,0 +1,13 @@ +import {DataTypeRule, staticImplements} from "./DataTypeRule"; +import type {DataTypeRulesItemOfCollectionConfig, NodeParameterValue} from "@code0-tech/sagittarius-graphql-types"; + +/** + * @todo deep equality check for arrays and objects + */ +@staticImplements() +export class DataTypeItemOfCollectionRule { + public static validate(value: NodeParameterValue, config: DataTypeRulesItemOfCollectionConfig): boolean { + if (!config.items) return false + return "value" in value && config.items.includes(value.value) + } +} \ No newline at end of file diff --git a/src/packages/ce/src/datatype/services/rules/DataTypeNumberRangeRule.ts b/src/packages/ce/src/datatype/services/rules/DataTypeNumberRangeRule.ts new file mode 100644 index 00000000..d4ac80b9 --- /dev/null +++ b/src/packages/ce/src/datatype/services/rules/DataTypeNumberRangeRule.ts @@ -0,0 +1,11 @@ +import {DataTypeRule, staticImplements} from "./DataTypeRule"; +import type {DataTypeRulesNumberRangeConfig, NodeParameterValue} from "@code0-tech/sagittarius-graphql-types"; + +@staticImplements() +export class DataTypeRangeRule { + public static validate(value: NodeParameterValue, config: DataTypeRulesNumberRangeConfig): boolean { + if (value.__typename !== 'LiteralValue') return false + if (!config.from || !config.to) return false + return value.value >= config.from && value.value <= config.to + } +} \ No newline at end of file diff --git a/src/packages/ce/src/datatype/services/rules/DataTypeParentRule.ts b/src/packages/ce/src/datatype/services/rules/DataTypeParentRule.ts new file mode 100644 index 00000000..02ceee70 --- /dev/null +++ b/src/packages/ce/src/datatype/services/rules/DataTypeParentRule.ts @@ -0,0 +1,21 @@ +import {DataTypeRule, staticImplements} from "./DataTypeRule"; +import type {DataTypeIdentifier, Flow, GenericMapper, NodeParameterValue} from "@code0-tech/sagittarius-graphql-types"; +import {useValueValidation} from "@edition/flow/hooks/ValueValidation.hook"; +import {replaceGenericKeysInType} from "@edition/flow/utils/generics"; +import {DatatypeService} from "@edition/datatype/services/Datatype.service"; + +export interface DFlowDataTypeParentRuleConfig { + type: DataTypeIdentifier +} + +@staticImplements() +export class DataTypeParentRule { + public static validate(value: NodeParameterValue, config: DFlowDataTypeParentRuleConfig, generics?: Map, flow?: Flow, dataTypeService?: DatatypeService): boolean { + + const replacedType = generics ? replaceGenericKeysInType(config.type, generics) : config.type + + if (!dataTypeService) return false + return useValueValidation(value, dataTypeService.getDataType(replacedType)!!, dataTypeService, flow, Array.from(generics!!, ([_, value]) => value)) + + } +} \ No newline at end of file diff --git a/src/packages/ce/src/datatype/services/rules/DataTypeRegexRule.ts b/src/packages/ce/src/datatype/services/rules/DataTypeRegexRule.ts new file mode 100644 index 00000000..db326d76 --- /dev/null +++ b/src/packages/ce/src/datatype/services/rules/DataTypeRegexRule.ts @@ -0,0 +1,11 @@ +import {DataTypeRule, staticImplements} from "./DataTypeRule"; +import type {DataTypeRulesRegexConfig, NodeParameterValue} from "@code0-tech/sagittarius-graphql-types"; + +@staticImplements() +export class DataTypeRegexRule { + public static validate(value: NodeParameterValue, config: DataTypeRulesRegexConfig): boolean { + if (value?.__typename != 'LiteralValue') return false + if (!config.pattern) return false + return new RegExp(config.pattern).test(String(value.value)) + } +} \ No newline at end of file diff --git a/src/packages/ce/src/datatype/services/rules/DataTypeReturnTypeRule.ts b/src/packages/ce/src/datatype/services/rules/DataTypeReturnTypeRule.ts new file mode 100644 index 00000000..23ede6b4 --- /dev/null +++ b/src/packages/ce/src/datatype/services/rules/DataTypeReturnTypeRule.ts @@ -0,0 +1,126 @@ +import {DataTypeRule, genericMapping, staticImplements} from "./DataTypeRule"; +import type { + DataTypeRulesReturnTypeConfig, + Flow, + GenericMapper, + GenericType, + NodeFunction, + NodeFunctionIdWrapper, + NodeParameterValue, + ReferenceValue +} from "@code0-tech/sagittarius-graphql-types"; +import {DatatypeService} from "@edition/datatype/services/Datatype.service"; +import {FunctionService} from "@edition/function/services/Function.service"; +import {useDataTypeValidation} from "@edition/flow/hooks/DataTypeValidation.hook"; +import {useValueValidation} from "@edition/flow/hooks/ValueValidation.hook"; +import {useReturnType} from "@edition/function/hooks/DFlowFunction.return.hook"; + +//TODO: simple use useReturnType function +@staticImplements() +export class DataTypeReturnTypeRule { + public static validate( + value: NodeParameterValue, + config: DataTypeRulesReturnTypeConfig, + generics?: Map, + flow?: Flow, + dataTypeService?: DatatypeService, + functionService?: FunctionService + ): boolean { + + const genericMapper = generics?.get(config?.dataTypeIdentifier?.genericKey!!) + const genericTypes = generics?.get(config?.dataTypeIdentifier?.genericKey!!)?.sourceDataTypeIdentifiers + const genericCombination = generics?.get(config?.dataTypeIdentifier?.genericKey!!)?.genericCombinationStrategies + + if (value.__typename != "NodeFunctionIdWrapper") return false + + if (config?.dataTypeIdentifier?.genericKey && !genericMapper && !dataTypeService?.getDataType(config.dataTypeIdentifier)) return true + + const foundReturnFunction = findReturnNode(value, flow!!) + if (!foundReturnFunction) return false + if (!foundReturnFunction?.parameters?.nodes?.[0]?.value) return false + + if (!(dataTypeService?.getDataType(config.dataTypeIdentifier!!) || genericMapper)) return false + + //use of generic key but datatypes does not exist + if (genericMapper && !dataTypeService?.hasDataTypes(genericTypes!!)) return false + + //check if all generic combinations are set + if (genericMapper && !(((genericCombination?.length ?? 0) + 1) == genericTypes!!.length)) return false + + if (foundReturnFunction?.parameters?.nodes?.[0]?.value?.__typename === "ReferenceValue") { + + const value = (foundReturnFunction?.parameters?.nodes?.[0]?.value as ReferenceValue) + const node = flow?.nodes?.nodes?.find(node => node?.id === value.nodeFunctionId) as NodeFunction + const funcDef = functionService?.getById(node?.functionDefinition?.id!) + const values = node.parameters?.nodes?.map(p => p?.value!) ?? [] + + //use generic given type for checking against value + if (config?.dataTypeIdentifier?.genericKey && genericMapper && genericTypes) { + + const checkAllTypes: boolean[] = genericTypes.map(genericType => { + return useDataTypeValidation(dataTypeService?.getDataType(genericType)!!, dataTypeService?.getDataType(useReturnType(funcDef!, values, dataTypeService, functionService!)!)!) + }) + + return checkAllTypes.length > 1 ? checkAllTypes.reduce((previousValue, currentValue, currentIndex) => { + if (genericCombination && genericCombination[currentIndex - 1].type == "OR") { + return previousValue || currentValue + } + + return previousValue && currentValue + }) : checkAllTypes[0] + } + + if (config?.dataTypeIdentifier?.dataType) { + return useDataTypeValidation(dataTypeService?.getDataType(config.dataTypeIdentifier!)!, dataTypeService?.getDataType(useReturnType(funcDef!, values, dataTypeService, functionService!)!)!) + } + + } else if (foundReturnFunction?.parameters?.nodes?.[0]?.value?.__typename == "NodeFunctionIdWrapper") { + //TODO : allow function as return value + } else { + + //use generic given type for checking against value + if (config?.dataTypeIdentifier?.genericKey && genericMapper && genericTypes) { + + const checkAllTypes: boolean[] = genericTypes.map(genericType => { + return useValueValidation(foundReturnFunction?.parameters?.nodes?.[0]?.value!, dataTypeService?.getDataType(genericType)!!, dataTypeService!!, flow, ((genericType.genericType as GenericType)!!.genericMappers as GenericMapper[])) + }) + + return checkAllTypes.length > 1 ? checkAllTypes.reduce((previousValue, currentValue, currentIndex) => { + if (genericCombination && genericCombination[currentIndex - 1].type == "OR") { + return previousValue || currentValue + } + + return previousValue && currentValue + }) : checkAllTypes[0] + } + + if (config?.dataTypeIdentifier?.dataType) { + return useValueValidation(foundReturnFunction?.parameters?.nodes?.[0]?.value!, dataTypeService?.getDataType(config.dataTypeIdentifier!)!, dataTypeService!!) + } + + return useValueValidation(foundReturnFunction?.parameters?.nodes?.[0]?.value!, dataTypeService?.getDataType(config.dataTypeIdentifier!)!, dataTypeService!!, flow, genericMapping(config.dataTypeIdentifier?.genericType?.genericMappers!, generics)) + + } + + return false + + } +} + +export const findReturnNode = (n: NodeFunctionIdWrapper, flow: Flow): NodeFunction | undefined => { + + + const node = flow.nodes?.nodes?.find(node => node?.id === n.id) as NodeFunction | undefined + if (!node) return undefined + if (node?.functionDefinition?.runtimeFunctionDefinition?.identifier === 'std::control::return') return node + + if (node && node.nextNodeId) { + const found = findReturnNode({ + id: node.nextNodeId, + __typename: "NodeFunctionIdWrapper" + }, flow) + if (found) return found + } + + return undefined +} \ No newline at end of file diff --git a/src/packages/ce/src/datatype/services/rules/DataTypeRule.ts b/src/packages/ce/src/datatype/services/rules/DataTypeRule.ts new file mode 100644 index 00000000..e95cbb6d --- /dev/null +++ b/src/packages/ce/src/datatype/services/rules/DataTypeRule.ts @@ -0,0 +1,23 @@ +import type {Flow, GenericMapper, NodeParameterValue} from "@code0-tech/sagittarius-graphql-types"; +import {DatatypeService} from "@edition/datatype/services/Datatype.service"; +import {FunctionService} from "@edition/function/services/Function.service"; + +export interface DataTypeRule { + validate(value: NodeParameterValue, config: object, generics?: Map, flow?: Flow, dataTypeService?: DatatypeService, functionService?: FunctionService): boolean +} + +export const staticImplements = () => { + return (constructor: U) => constructor +} + +export const genericMapping = (to?: GenericMapper[], from?: Map): GenericMapper[] | undefined => { + + if (!to || !from) return [] + + return to.map(generic => ({ + ...generic, + target: generic.target, + sources: generic?.sourceDataTypeIdentifiers?.map(type => from?.get(type.genericKey!!)?.sourceDataTypeIdentifiers!!).flat(), + genericCombinationStrategies: generic?.sourceDataTypeIdentifiers?.map(type => from?.get(type.genericKey!!)?.genericCombinationStrategies!!).flat() + })) +} \ No newline at end of file diff --git a/src/packages/ce/src/datatype/services/rules/DataTypeRules.ts b/src/packages/ce/src/datatype/services/rules/DataTypeRules.ts new file mode 100644 index 00000000..9640321a --- /dev/null +++ b/src/packages/ce/src/datatype/services/rules/DataTypeRules.ts @@ -0,0 +1,20 @@ +import {DataTypeRegexRule} from "./DataTypeRegexRule"; +import {DataTypeRangeRule} from "./DataTypeNumberRangeRule"; +import { + DataTypeItemOfCollectionRule +} from "./DataTypeItemOfCollectionRule"; +import {DataTypeContainsTypeRule} from "./DataTypeContainsTypeRule"; +import {DataTypeContainsKeyRule} from "./DataTypeContainsKeyRule"; +import {DataTypeRule} from "./DataTypeRule"; +import {DataTypeReturnTypeRule} from "./DataTypeReturnTypeRule"; +import type {DataTypeRulesVariant} from "@code0-tech/sagittarius-graphql-types"; + +export const RuleMap = new Map([ + ["REGEX" as DataTypeRulesVariant.Regex, DataTypeRegexRule], + ["NUMBER_RANGE" as DataTypeRulesVariant.NumberRange, DataTypeRangeRule], + ["ITEM_OF_COLLECTION" as DataTypeRulesVariant.ItemOfCollection, DataTypeItemOfCollectionRule], + ["CONTAINS_TYPE" as DataTypeRulesVariant.ContainsType, DataTypeContainsTypeRule], + ["CONTAINS_KEY" as DataTypeRulesVariant.ContainsKey, DataTypeContainsKeyRule], + ["RETURN_TYPE" as DataTypeRulesVariant.ReturnType, DataTypeReturnTypeRule] + +]) \ No newline at end of file diff --git a/src/packages/ce/src/datatype/services/variants/DataTypeNodeVariant.ts b/src/packages/ce/src/datatype/services/variants/DataTypeNodeVariant.ts new file mode 100644 index 00000000..9293c162 --- /dev/null +++ b/src/packages/ce/src/datatype/services/variants/DataTypeNodeVariant.ts @@ -0,0 +1,10 @@ +import type {NodeParameterValue} from "@code0-tech/sagittarius-graphql-types"; +import {DataTypeVariant} from "./DataTypeVariant"; +import {staticImplements} from "../rules/DataTypeRule"; + +@staticImplements() +export class DataTypeNodeVariant { + public static validate(value: NodeParameterValue): boolean { + return value.__typename == 'NodeFunctionIdWrapper'; + } +} \ No newline at end of file diff --git a/src/packages/ce/src/datatype/services/variants/DataTypeVariant.ts b/src/packages/ce/src/datatype/services/variants/DataTypeVariant.ts new file mode 100644 index 00000000..46e3cdca --- /dev/null +++ b/src/packages/ce/src/datatype/services/variants/DataTypeVariant.ts @@ -0,0 +1,5 @@ +import type {NodeParameterValue} from "@code0-tech/sagittarius-graphql-types"; + +export interface DataTypeVariant { + validate(value: NodeParameterValue): boolean +} \ No newline at end of file diff --git a/src/packages/ce/src/datatype/services/variants/DataTypeVariants.ts b/src/packages/ce/src/datatype/services/variants/DataTypeVariants.ts new file mode 100644 index 00000000..1ac8bd65 --- /dev/null +++ b/src/packages/ce/src/datatype/services/variants/DataTypeVariants.ts @@ -0,0 +1,9 @@ +import {DataTypeNodeVariant} from "./DataTypeNodeVariant"; + +import {DataTypeVariant} from "@code0-tech/sagittarius-graphql-types"; +import {DataTypeVariant as DTVariant} from "./DataTypeVariant"; + +export const VariantsMap = new Map([ + ["NODE" as DataTypeVariant.Node, DataTypeNodeVariant], + +]) \ No newline at end of file diff --git a/src/packages/ce/src/flow/components/DFlowFolder.style.scss b/src/packages/ce/src/flow/components/DFlowFolder.style.scss new file mode 100644 index 00000000..0b6f07d6 --- /dev/null +++ b/src/packages/ce/src/flow/components/DFlowFolder.style.scss @@ -0,0 +1,99 @@ +@use "../../styles/helpers"; +@use "../../styles/box"; +@use "../../styles/variables"; + +.d-folder { + padding: variables.$xxs variables.$xs; + display: flex; + white-space: nowrap; + flex-wrap: nowrap; + gap: variables.$xxs; + cursor: pointer; + align-items: center; + justify-content: space-between; + font-size: variables.$sm; + + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + + &__root { + width: 100%; + height: 100%; + box-sizing: border-box; + } + + & { + @include helpers.borderRadius(); + @include box.boxHover(variables.$tertiary); + @include helpers.fontStyle(); + box-shadow: none; + background: transparent; + } + + &[data-state=open] { + @include box.boxActiveStyle(variables.$tertiary); + box-shadow: none; + } + + &__icon, &__status, &__item-icon { + display: flex; + align-items: center; + justify-content: center; + } + + &__content { + $spacing: variables.$xxs; + + margin-left: variables.$xs; + position: relative; + padding-left: calc(5px + $spacing); + + &:before { + height: 100%; + width: 2px; + background: helpers.backgroundColor(variables.$tertiary); + position: absolute; + content: ""; + left: 5px; + } + } + + &__item { + padding: variables.$xxs variables.$xs; + display: flex; + white-space: nowrap; + flex-wrap: nowrap; + gap: variables.$xxs; + align-items: center; + cursor: pointer; + font-size: variables.$sm; + position: relative; + margin: (variables.$xxs / 2) 0; + + & { + @include helpers.borderRadius(); + @include box.boxHover(variables.$tertiary); + @include box.boxActive(variables.$tertiary); + @include helpers.fontStyle(); + box-shadow: none; + background: transparent; + } + + &-hover-card { + @include box.box(variables.$tertiary); + @include box.boxActiveStyle(variables.$tertiary); + @include box.boxHover(variables.$tertiary); + @include box.boxActive(variables.$tertiary); + @include helpers.borderRadius(); + cursor: pointer; + } + + &[data-state=open], &--active { + @include box.boxActiveStyle(variables.$tertiary); + } + } +} \ No newline at end of file diff --git a/src/packages/ce/src/flow/components/DFlowFolder.tsx b/src/packages/ce/src/flow/components/DFlowFolder.tsx new file mode 100644 index 00000000..4736f576 --- /dev/null +++ b/src/packages/ce/src/flow/components/DFlowFolder.tsx @@ -0,0 +1,389 @@ +"use client" + +import "./DFlowFolder.style.scss" +import React from "react" +import {Code0Component, mergeCode0Props, useService, useStore} from "../../utils" +import {IconFile, IconFolderFilled, IconFolderOpen} from "@tabler/icons-react" +import type {Flow, FlowType, Namespace, NamespaceProject, Scalars} from "@code0-tech/sagittarius-graphql-types" +import {DFlowReactiveService} from "../d-flow" +import {ScrollArea, ScrollAreaScrollbar, ScrollAreaThumb, ScrollAreaViewport} from "../scroll-area/ScrollArea" +import {Flex} from "../flex/Flex" +import {Text} from "../text/Text" +import { + DFlowFolderContextMenu, + DFlowFolderContextMenuGroupData, + DFlowFolderContextMenuItemData +} from "./DFlowFolderContextMenu"; +import {hashToColor} from "../d-flow/DFlow.util"; +import {HoverCard, HoverCardContent, HoverCardPortal, HoverCardTrigger} from "@radix-ui/react-hover-card"; + + +export interface DFlowFolderProps { + activeFlowId: Scalars["FlowID"]["output"] + namespaceId: Namespace['id'] + projectId: NamespaceProject['id'] + onRename?: (contextData: DFlowFolderContextMenuGroupData | DFlowFolderContextMenuItemData) => void + onDelete?: (contextData: DFlowFolderContextMenuGroupData | DFlowFolderContextMenuItemData) => void + onCreate?: (type: FlowType['id']) => void + onSelect?: (flow: Flow) => void +} + +export type DFlowFolderHandle = { + openAll: () => void + closeAll: () => void + openActivePath: () => void +} + +type OpenMode = "default" | "allOpen" | "allClosed" | "activePath" + +export interface DFlowFolderGroupProps extends DFlowFolderProps, Omit, "onSelect"> { + name: string + children: React.ReactElement | React.ReactElement[] | React.ReactElement | React.ReactElement[] + defaultOpen?: boolean + flows: Flow[] +} + +export interface DFlowFolderItemProps extends DFlowFolderProps, Omit, "onSelect"> { + name: string + path: string + active?: boolean + flow: Flow +} + +export const DFlowFolder = React.forwardRef((props, ref) => { + + const {activeFlowId, namespaceId, projectId} = props + + const flowService = useService(DFlowReactiveService) + const flowStore = useStore(DFlowReactiveService) + + type TreeNode = { + name: string + path: string + children: Record + flow?: Flow + } + + const normalizePath = (p: string) => { + const trimmed = p.replace(/\/+$/g, "") + + return trimmed.split("/").filter((seg, idx) => { + return idx === 0 || seg.length > 0 + }) + } + + const flows = React.useMemo(() => { + const raw = (flowService.values?.({namespaceId, projectId}) ?? []) as Flow[] + return raw.filter(f => !!f?.name) + }, [flowStore]) + + const activePathSegments = React.useMemo(() => { + const active = flows.find(f => f.id === activeFlowId) + if (!active?.name) return [] + return normalizePath(active.name) + }, [flows, activeFlowId]) + + const tree = React.useMemo(() => { + const root: TreeNode = {name: "", path: "", children: {}} + for (const flow of flows) { + const segs = normalizePath(flow.name as string) + if (segs.length === 0) continue + + let cur = root + let acc = "" + for (let i = 0; i < segs.length; i++) { + const seg = segs[i] + // Behandle führenden leeren String (von führendem /) speziell + if (i === 0 && seg === "") { + acc = "/" + // Erstelle Root-Level Ordner für führenden Slash + if (!cur.children["/"]) { + cur.children["/"] = { + name: "/", + path: "/", + children: {} + } + } + cur = cur.children["/"] + continue + } + + acc = acc === "/" ? `/${seg}` : (acc ? `${acc}/${seg}` : seg) + + if (i === segs.length - 1) { + // leaf (Flow) + if (!cur.children[seg]) { + cur.children[seg] = { + name: seg, + path: acc, + children: {}, + flow + } + } else { + // falls es bereits einen Knoten gibt, hänge Flow an + cur.children[seg].flow = flow + } + } else { + // folder + if (!cur.children[seg]) { + cur.children[seg] = { + name: seg, + path: acc, + children: {} + } + } + cur = cur.children[seg] + } + } + } + return root + }, [flows]) + + const isPrefixOfActive = React.useCallback((nodePath: string) => { + if (!nodePath) return false + const segs = nodePath.split("/").filter(Boolean) + return segs.every((s, i) => activePathSegments[i] === s) + }, [activePathSegments]) + + const [openMode, setOpenMode] = React.useState("default") + const [resetEpoch, setResetEpoch] = React.useState(0) + + const openAll = React.useCallback(() => { + setOpenMode("allOpen") + setResetEpoch(v => v + 1) + }, []) + + const closeAll = React.useCallback(() => { + setOpenMode("allClosed") + setResetEpoch(v => v + 1) + }, []) + + const openActivePath = React.useCallback(() => { + setOpenMode("activePath") + setResetEpoch(v => v + 1) + }, []) + + React.useImperativeHandle(ref, () => ({ + openAll, + closeAll, + openActivePath + }), [openAll, closeAll, openActivePath]) + + const computeDefaultOpen = React.useCallback((folderPath: string) => { + if (openMode === "allOpen") return true + if (openMode === "allClosed") return false + return isPrefixOfActive(folderPath) + }, [isPrefixOfActive, openMode]) + + const renderChildren = React.useCallback((childrenMap: Record) => { + const nodes = Object.values(childrenMap) + + const folders = nodes.filter(n => !n.flow) + const items = nodes.filter(n => !!n.flow) + + folders.sort((a, b) => a.name.localeCompare(b.name)) + items.sort((a, b) => a.name.localeCompare(b.name)) + + return ( + <> + {folders.map(folder => ( + value.flow!!)} + defaultOpen={computeDefaultOpen(folder.path)} + {...props} + > + {renderChildren(folder.children)} + + ))} + {items.map(item => ( + + ))} + + ) + }, [activeFlowId, computeDefaultOpen, resetEpoch]) + + return ( + + + +
    + {renderChildren(tree.children)} +
    +
    +
    + + + +
    + ) + +}) + +export const DFlowFolderGroup: React.FC = (props) => { + + const { + name, + flows, + defaultOpen = false, + children, + onCreate, + onDelete, + onRename, + activeFlowId, + namespaceId, + projectId, + ...code0Props + } = props + const [open, setOpen] = React.useState(defaultOpen) + const contextMenuProps = {onCreate, onDelete, onRename, activeFlowId, namespaceId, projectId} + + return <> + +
    setOpen(prevState => !prevState)} {...mergeCode0Props(`d-folder`, code0Props)}> + + {open ? : } + {name} + +
    +
    +
    + {open ? children : null} +
    + +} + +export const DFlowFolderItem: React.FC = (props) => { + + const { + name, + path, + flow, + onSelect, + active, + onCreate, + onDelete, + onRename, + activeFlowId, + namespaceId, + projectId, + ...code0Props + } = props + + const wrapperRef = React.useRef(null) + const [text, setText] = React.useState(name) + + React.useEffect(() => { + + const resizeObserverWrapper = new ResizeObserver(([entry]) => { + const wrapperWidth = entry.contentRect.width + const newText = truncateText(name, wrapperWidth) + setText(newText) + }) + + + if (wrapperRef.current) resizeObserverWrapper.observe(wrapperRef.current) + return () => { + resizeObserverWrapper.disconnect() + }; + }, [wrapperRef]) + + + const contextMenuProps = {onCreate, onDelete, onRename, activeFlowId, namespaceId, projectId} + + return +
    + + +
    onSelect?.(flow)}> + + + {text} + +
    +
    + + +
    onSelect?.(flow)} style={{padding: "0.35rem 0.7rem"}}> + + + {props.name} + +
    +
    +
    +
    +
    +
    + +} + + +type TruncateMode = "start" | "middle" | "end"; + +export function truncateText( + value: string, + wrapperWidth: number, + mode: TruncateMode = "middle", + ellipsis = "…" +): string { + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d")!; + + ctx.font = "400 16px Inter, sans-serif"; + const letterSpacing = -1.9; + + const measure = (text: string) => + ctx.measureText(text).width + (text.length - 1) * letterSpacing; + + if (measure(value) <= wrapperWidth) return value; + + let low = 0; + let high = value.length; + let best = ellipsis; + + while (low <= high) { + const mid = Math.floor((low + high) / 2); + let candidate: string; + + if (mode === "end") { + candidate = value.slice(0, mid) + ellipsis; + } else if (mode === "start") { + candidate = ellipsis + value.slice(value.length - mid); + } else { + const half = Math.floor(mid / 2); + candidate = value.slice(0, half) + ellipsis + value.slice(value.length - half); + } + + const width = measure(candidate); + + if (width > wrapperWidth - 8) { + high = mid - 1; + } else { + best = candidate; + low = mid + 1; + } + } + + return best; +} diff --git a/src/packages/ce/src/flow/components/DFlowFolderContextMenu.tsx b/src/packages/ce/src/flow/components/DFlowFolderContextMenu.tsx new file mode 100644 index 00000000..207343e4 --- /dev/null +++ b/src/packages/ce/src/flow/components/DFlowFolderContextMenu.tsx @@ -0,0 +1,90 @@ +import {DFlowFolderProps} from "./DFlowFolder"; +import React from "react"; +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuLabel, + ContextMenuPortal, + ContextMenuSeparator, + ContextMenuSub, + ContextMenuSubContent, + ContextMenuSubTrigger, + ContextMenuTrigger +} from "../context-menu/ContextMenu"; +import {Flex} from "../flex/Flex"; +import {Text} from "../text/Text"; +import {IconChevronRight, IconEdit, IconTrash} from "@tabler/icons-react"; +import {useService, useStore} from "../../utils"; +import {DFlowTypeReactiveService} from "../d-flow-type"; +import {Flow} from "@code0-tech/sagittarius-graphql-types"; + +export interface DFlowFolderContextMenuGroupData { + name: string + flow: Flow[] + type: "folder" +} + +export interface DFlowFolderContextMenuItemData { + name: string + flow: Flow + type: "item" +} + +export interface DFlowFolderContextMenuProps extends DFlowFolderProps { + children: React.ReactNode + contextData?: DFlowFolderContextMenuGroupData | DFlowFolderContextMenuItemData +} + +export const DFlowFolderContextMenu: React.FC = (props) => { + + const {children} = props + + const flowTypeService = useService(DFlowTypeReactiveService) + const flowTypeStore = useStore(DFlowTypeReactiveService) + + const flowTypes = React.useMemo(() => flowTypeService.values(), [flowTypeStore]) + + return <> + + + {children} + + + + + + + Create new flow + + + + + Flow types + {flowTypes.map(flowType => { + return { + props.onCreate?.(flowType.id) + }}> + {flowType.names!![0]?.content ?? flowType.id} + + })} + + {props.contextData ? ( + <> + + props.onRename?.(props.contextData!)}> + + Rename + + props.onDelete?.(props.contextData!)}> + + Delete + + + ) : null} + + + + + +} \ No newline at end of file diff --git a/src/packages/ce/src/flow/components/DFlowFolderDeleteDialog.tsx b/src/packages/ce/src/flow/components/DFlowFolderDeleteDialog.tsx new file mode 100644 index 00000000..e4f7f67a --- /dev/null +++ b/src/packages/ce/src/flow/components/DFlowFolderDeleteDialog.tsx @@ -0,0 +1,59 @@ +import {Dialog, DialogClose, DialogContent, DialogOverlay, DialogPortal} from "../dialog/Dialog"; +import {Text} from "../text/Text"; +import {Badge} from "../badge/Badge"; +import {Flex} from "../flex/Flex"; +import {Button} from "../button/Button"; +import React from "react"; +import {DFlowFolderContextMenuGroupData, DFlowFolderContextMenuItemData} from "./DFlowFolderContextMenu"; +import {Flow} from "@code0-tech/sagittarius-graphql-types"; + +export interface DFlowFolderDeleteDialogProps { + open?: boolean + onOpenChange?: (open: boolean) => void + contextData: DFlowFolderContextMenuGroupData | DFlowFolderContextMenuItemData + onDelete?: (flow: Flow) => void +} + +export const DFlowFolderDeleteDialog: React.FC = (props) => { + + const {open} = props + + const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(open) + + React.useEffect(() => { + setDeleteDialogOpen(open) + }, [open]) + + return props.onOpenChange?.(open)}> + + + + + {props.contextData.type == "item" ? "Are you sure you want to remove flow" : "Are you sure you want to remove folder"} {" "} + + {props.contextData.name} + {" "} + {props.contextData.type == "folder" ? ", all flows and sub-folders inside " : ""}from the this + project? + + + + + + + + + + + + +} \ No newline at end of file diff --git a/src/packages/ce/src/flow/components/DFlowFolderRenameDialog.tsx b/src/packages/ce/src/flow/components/DFlowFolderRenameDialog.tsx new file mode 100644 index 00000000..3b07233c --- /dev/null +++ b/src/packages/ce/src/flow/components/DFlowFolderRenameDialog.tsx @@ -0,0 +1,72 @@ +import React from "react"; +import {Dialog, DialogClose, DialogContent, DialogOverlay, DialogPortal} from "../dialog/Dialog"; +import {DFlowFolderItemPathInput} from "./DFlowFolderItemPathInput"; +import {Flex} from "../flex/Flex"; +import {Button} from "../button/Button"; +import {DFlowFolderContextMenuGroupData, DFlowFolderContextMenuItemData} from "./DFlowFolderContextMenu"; +import {useForm} from "../form"; +import {Flow} from "@code0-tech/sagittarius-graphql-types"; + +export interface DFlowFolderRenameDialogProps { + contextData: DFlowFolderContextMenuGroupData | DFlowFolderContextMenuItemData + open?: boolean + onOpenChange?: (open: boolean) => void + onRename?: (flow: Flow, newName: string) => void +} + +export const DFlowFolderRenameDialog: React.FC = (props) => { + const {open} = props + + const [renameDialogOpen, setRenameDialogOpen] = React.useState(open) + const initialValues = React.useMemo(() => ({ + path: props.contextData.name + }), []) + + React.useEffect(() => { + setRenameDialogOpen(open) + }, [open]) + + const [inputs, validate] = useForm({ + initialValues: initialValues, + validate: { + path: (value) => { + return null + } + }, + onSubmit: (values) => { + if (props.contextData.type === "item") { + props.onRename?.(props.contextData.flow, values.path) + } else if (props.contextData.type === "folder") { + props.contextData.flow.forEach(flow => { + const newName = flow.name?.replace(props.contextData.name, values.path) ?? flow.name + props.onRename?.(flow, newName!) + }) + } + } + }) + + return { + props.onOpenChange?.(open) + }}> + + + +
    + +
    + + + + + + + + +
    +
    +
    +} \ No newline at end of file diff --git a/src/packages/ce/src/flow/components/FlowBuilder.style.scss b/src/packages/ce/src/flow/components/FlowBuilder.style.scss new file mode 100644 index 00000000..23cc1e64 --- /dev/null +++ b/src/packages/ce/src/flow/components/FlowBuilder.style.scss @@ -0,0 +1,27 @@ +.flow { + overflow: hidden; + + &[data-tree-visibility=false] { + .react-flow__node, .react-flow__edgelabel-renderer, .react-flow__edge { + opacity: 0; + } + } + + &[data-tree-visibility=true] { + .react-flow__node, .react-flow__edgelabel-renderer, .react-flow__edge { + opacity: 100%; + } + } + + .react-flow__node { + background: transparent; + padding: unset; + box-shadow: none !important; + border: none; + border-radius: unset; + color: unset; + width: unset; + font-size: unset; + text-align: unset; + } +} \ No newline at end of file diff --git a/src/packages/ce/src/flow/components/FlowBuilder.tsx b/src/packages/ce/src/flow/components/FlowBuilder.tsx new file mode 100644 index 00000000..c7dbcbe7 --- /dev/null +++ b/src/packages/ce/src/flow/components/FlowBuilder.tsx @@ -0,0 +1,807 @@ +import { + Background, + BackgroundVariant, + Edge, + Node, + ReactFlow, + ReactFlowProvider, + useEdgesState, + useNodesState, + useReactFlow, + useUpdateNodeInternals, + ViewportPortal +} from "@xyflow/react"; +import React from "react"; +import '@xyflow/react/dist/style.css'; +import "./FlowBuilder.style.scss" +import {FlowBuilderEdge} from "./FlowBuilderEdge"; +import {Flow, type Namespace, type NamespaceProject} from "@code0-tech/sagittarius-graphql-types"; +import {LineWobble} from 'ldrs/react' +import 'ldrs/react/LineWobble.css' +import {useFlowNodes} from "@edition/flow/hooks/Flow.nodes.hook"; +import { + Code0ComponentProps, DFlowPanelControl, + DFlowPanelLayout, + DFlowPanelSize, DFlowValidation, + mergeCode0Props, + Spacing, Text +} from "@code0-tech/pictor"; +import {DFlowNodeDefaultCard} from "@edition/function/components/DFlowNodeDefaultCard"; +import {DFlowNodeGroupCard} from "@edition/function/components/DFlowNodeGroupCard"; +import {DFlowNodeTriggerCard} from "@edition/function/components/DFlowNodeTriggerCard"; +import {DFlowPanelUpdate} from "@code0-tech/pictor/dist/components/d-flow-panel/DFlowPanelUpdate"; +import {useEdges} from "@edition/flow/hooks/Flow.edges.hook"; + +/** + * Dynamically layouts a tree of nodes and their parameter nodes for a flow-based editor. + * - Main nodes are stacked vertically with fixed vertical spacing. + * - Parameter nodes are centered vertically alongside their parent node and recursively laid out horizontally. + * - Sub-parameter nodes do NOT influence the vertical stacking of main nodes; only direct parameters are considered. + * + * @param nodes Array of all nodes to be positioned. Each node should have at least: id, measured?.width, measured?.height, data?.isParameter, data?.parentId, and optionally data?.paramIndex. + * @param edges Array of edge objects, unchanged by this function (used only for return type symmetry). + * @returns An object containing the new positioned nodes and the unchanged edges. + */ +const getLayoutElements = (nodes: Node[], dirtyIds?: Set) => { + if (!dirtyIds || dirtyIds.size === 0) return { nodes } + + /* Konstanten */ + const V = 50; // vertical gap Node ↕ Node + const H = 50; // horizontal gap Parent → Param + const PAD = 16; // inner padding einer Group (links+rechts / oben+unten) + const EPS = 0.25; // Toleranz gegen Rundungsdrift + + // Wir iterieren, bis Group-Maße stabil sind + let pass = 0 + let changed = false + + // ---------------------------- + // 1) Relationen einmalig ermitteln + // ---------------------------- + const rfKidIds = new Map() + const paramIds = new Map() + + for (const n of nodes) { + const link = (n.data as any)?.parentNodeId + if (link) { + const arr = paramIds.get(link) ?? [] + arr.push(n.id) + paramIds.set(link, arr) + } + if (n.parentId && !link) { + const arr = rfKidIds.get(n.parentId) ?? [] + arr.push(n.id) + rfKidIds.set(n.parentId, arr) + } + } + + const byId = new Map(nodes.map(n => [n.id, n] as const)) + + const rfKids = new Map() + for (const [k, ids] of rfKidIds) { + const arr: Node[] = new Array(ids.length) + for (let i = 0; i < ids.length; i++) arr[i] = byId.get(ids[i])! + rfKids.set(k, arr) + } + + const params = new Map() + for (const [k, ids] of paramIds) { + const arr: Node[] = new Array(ids.length) + for (let i = 0; i < ids.length; i++) arr[i] = byId.get(ids[i])! + params.set(k, arr) + } + + type Size = { w: number; h: number } + type Pos = { x: number; y: number } + + // ---------------------------- + // 2) Working state (nur Daten, KEINE Node-Mutationen) + // ---------------------------- + // Positionen (Top-Left, RF-Koordinaten, relativ zu Parent falls parentId) + const posTL = new Map() + + // Group-Measured/Style (width/height) werden im Bounding geändert + // -> diese Maps repräsentieren den "aktuellen" Stand über Passes hinweg + const styleWH = new Map() + const measuredWH = new Map() + + // baseSizes als Startpunkt (non-group) + wird für groups nach Bounding aktualisiert + const baseSizes = new Map() + for (const n of nodes) { + const styleW = typeof n.style?.width === "number" ? (n.style.width as number) : undefined + const styleH = typeof n.style?.height === "number" ? (n.style.height as number) : undefined + const mw = n.measured?.width && n.measured.width > 0 ? n.measured.width : undefined + const mh = n.measured?.height && n.measured.height > 0 ? n.measured.height : undefined + baseSizes.set(n.id, { w: styleW ?? mw ?? 200, h: styleH ?? mh ?? 80 }) + } + + const getStyleW = (n: Node) => styleWH.get(n.id)?.width + const getStyleH = (n: Node) => styleWH.get(n.id)?.height + + const sizeCache = new Map() + const size = (n: Node): Size => { + const cached = sizeCache.get(n.id) + if (cached) return cached + + // non-group: aus baseSizes + if (n.type !== "group") { + const s = baseSizes.get(n.id)! + sizeCache.set(n.id, s) + return s + } + + // group: wenn width/height explizit gesetzt (aus styleWH), dann das nehmen + const sw = getStyleW(n) + const sh = getStyleH(n) + if (sw !== undefined && sh !== undefined) { + const s = { w: sw, h: sh } + sizeCache.set(n.id, s) + return s + } + + // sonst aus Kindern ableiten + const kids = rfKids.get(n.id) ?? [] + let stackH = 0 + let wMax = 0 + let count = 0 + + for (const k of kids) { + const ks = size(k) + stackH += ks.h + if (ks.w > wMax) wMax = ks.w + count++ + } + stackH += V * Math.max(0, count - 1) + + const g = { w: wMax + 2 * PAD, h: (count ? stackH : 0) + 2 * PAD } + sizeCache.set(n.id, g) + return g + } + + // ---------------------------- + // 3) Layout-Iteration (wie bisher), nur dass wir am Ende posTL/styleWH/measuredWH updaten + // ---------------------------- + do { + changed = false + pass++ + + sizeCache.clear() + for (const n of nodes) size(n) + + // relatives Layout (Center in globalen Koordinaten) + const relCenter = new Map() + + // Unterkante je rechter Spalten-"Band", damit Parameter nicht kollidieren + const columnBottom = new Map() + const colKey = (x: number) => Math.round(x / 10) + + const layoutIter = (root: Node, cx: number, cy: number): number => { + type Frame = { + node: Node + cx: number + cy: number + phase: number + w?: number + h?: number + right?: Node[] + rightIndex?: number + py?: number + rightBottom?: number + childKey?: number + childPs?: Size + lastChildBottom?: number + + gParams?: Node[] + gSizes?: Size[] + gIndex?: number + gx?: number + gy?: number + rowBottom?: number + + kids?: Node[] + kidIndex?: number + curY?: number + bottom?: number + } + + const stack: Frame[] = [{ node: root, cx, cy, phase: 0 }] + let returnBottom = 0 + + while (stack.length) { + const f = stack[stack.length - 1] + switch (f.phase) { + case 0: { + relCenter.set(f.node.id, { x: f.cx, y: f.cy }) + const { w, h } = size(f.node) + f.w = w + f.h = h + + const paramsOf = params.get(f.node.id) ?? [] + const right: Node[] = [] + const gParams: Node[] = [] + for (const p of paramsOf) { + if (p.type === "group") gParams.push(p) + else right.push(p) + } + right.sort((a, b) => (+(a.data as any)?.paramIndex) - (+(b.data as any)?.paramIndex)) + gParams.sort((a, b) => (+(a.data as any)?.paramIndex) - (+(b.data as any)?.paramIndex)) + + f.right = right + f.gParams = gParams + + let total = 0 + for (const p of right) total += size(p).h + total += V * Math.max(0, right.length - 1) + + f.py = f.cy - total / 2 + f.rightBottom = f.cy + h / 2 + f.rightIndex = 0 + f.phase = 1 + break + } + + case 1: { + if (f.rightIndex! < f.right!.length) { + const p = f.right![f.rightIndex!] + const ps = size(p) + const px = f.cx + f.w! / 2 + H + ps.w / 2 + let pcy = f.py! + ps.h / 2 + + const key = colKey(px) + const occ = columnBottom.get(key) ?? Number.NEGATIVE_INFINITY + const minTop = occ + V + const desiredTop = pcy - ps.h / 2 + + if (desiredTop < minTop) { + pcy = minTop + ps.h / 2 + f.py = pcy - ps.h / 2 + } + + f.childKey = key + f.childPs = ps + stack.push({ node: p, cx: px, cy: pcy, phase: 0 }) + f.phase = 10 + } else { + f.bottom = Math.max(f.cy + f.h! / 2, f.rightBottom!) + f.phase = 2 + } + break + } + + case 10: { + const subBottom = f.lastChildBottom! + columnBottom.set( + f.childKey!, + Math.max(columnBottom.get(f.childKey!) ?? Number.NEGATIVE_INFINITY, subBottom) + ) + f.rightBottom = Math.max(f.rightBottom!, subBottom) + f.py = Math.max(f.py! + f.childPs!.h + V, subBottom + V) + f.rightIndex!++ + f.phase = 1 + break + } + + case 2: { + if (f.gParams && f.gParams.length) { + const gSizes: Size[] = [] + let rowW = 0 + for (const g of f.gParams) { + const gs = size(g) + gSizes.push(gs) + rowW += gs.w + } + rowW += H * (f.gParams.length - 1) + + f.gSizes = gSizes + f.gx = f.cx - rowW / 2 + f.gy = f.bottom! + V + f.rowBottom = f.bottom + f.gIndex = 0 + f.phase = 3 + } else { + f.phase = 4 + } + break + } + + case 3: { + if (f.gIndex! < f.gParams!.length) { + const g = f.gParams![f.gIndex!] + const gs = f.gSizes![f.gIndex!] + const gcx = f.gx! + gs.w / 2 + const gcy = f.gy! + gs.h / 2 + f.gx! += gs.w + H + + stack.push({ node: g, cx: gcx, cy: gcy, phase: 0 }) + f.childPs = gs + f.phase = 30 + } else { + f.bottom = f.rowBottom + f.phase = 4 + } + break + } + + case 30: { + const subBottom = f.lastChildBottom! + f.rowBottom = Math.max(f.rowBottom!, subBottom) + f.gIndex!++ + f.phase = 3 + break + } + + case 4: { + if (f.node.type === "group") { + const kidsAll = rfKids.get(f.node.id) ?? [] + const kids: Node[] = [] + for (const k of kidsAll) { + if (!(k.data as any)?.parentNodeId) kids.push(k) + } + f.kids = kids + f.kidIndex = 0 + f.curY = f.cy - f.h! / 2 + PAD + f.phase = 5 + } else { + f.phase = 6 + } + break + } + + case 5: { + if (f.kidIndex! < f.kids!.length) { + const k = f.kids![f.kidIndex!] + const ks = size(k) + const ky = f.curY! + ks.h / 2 + + stack.push({ node: k, cx: f.cx, cy: ky, phase: 0 }) + f.childPs = ks + f.phase = 50 + } else { + const contentBottom = f.curY! - V + f.bottom = Math.max(f.bottom!, contentBottom + PAD) + f.phase = 6 + } + break + } + + case 50: { + const subBottom = f.lastChildBottom! + f.curY = subBottom + V + f.kidIndex!++ + f.phase = 5 + break + } + + case 6: { + const finished = stack.pop()! + if (stack.length) { + stack[stack.length - 1].lastChildBottom = finished.bottom + } else { + returnBottom = finished.bottom! + } + break + } + } + } + + return returnBottom + } + + // Root-Nodes stapeln + let yCursor = 0 + for (const r of nodes) { + if (!(r.data as any)?.parentNodeId && !r.parentId) { + const b = layoutIter(r, 0, yCursor + size(r).h / 2) + yCursor = b + V + } + } + + // rel (Center) → absTL_initial (global Top-Left) + const absTL_initial = new Map() + for (const n of nodes) { + const { w, h } = size(n) + const c = relCenter.get(n.id)! + absTL_initial.set(n.id, { x: c.x - w / 2, y: c.y - h / 2 }) + } + + // initial posTL setzen (in RF-Koordinaten, relativ zu Parent) + for (const n of nodes) { + const tl = absTL_initial.get(n.id)! + let px = tl.x + let py = tl.y + + if (n.parentId) { + const pTL = absTL_initial.get(n.parentId)! + px -= pTL.x + py -= pTL.y + } + + const prev = posTL.get(n.id) + if (!prev || Math.abs(prev.x - px) > EPS || Math.abs(prev.y - py) > EPS) { + posTL.set(n.id, { x: px, y: py }) + changed = true + } + } + + // Bounding-Korrektur jeder Group + const depth = (g: Node) => { + let d = 0 + let p: Node | undefined = g + while (p?.parentId) { + d++ + p = byId.get(p.parentId) + if (!p) break + } + return d + } + + const groups: Node[] = [] + for (const n of nodes) if (n.type === "group") groups.push(n) + groups.sort((a, b) => depth(b) - depth(a)) + + const childSize = (n: Node): Size => { + const sw = typeof n.style?.width === "number" ? (n.style.width as number) : undefined + const sh = typeof n.style?.height === "number" ? (n.style.height as number) : undefined + const s = baseSizes.get(n.id)! + return { w: sw ?? s.w, h: sh ?? s.h } + } + + for (const g of groups) { + const direct: Node[] = [] + for (const k of nodes) { + if (k.parentId === g.id) direct.push(k) + } + + if (!direct.length) { + // minimal group size + const gw = getStyleW(g) ?? (typeof g.style?.width === "number" ? (g.style.width as number) : 2 * PAD) + const gh = getStyleH(g) ?? (typeof g.style?.height === "number" ? (g.style.height as number) : 2 * PAD) + styleWH.set(g.id, { width: gw, height: gh }) + measuredWH.set(g.id, { width: gw, height: gh }) + baseSizes.set(g.id, { w: gw, h: gh }) + continue + } + + let minX = Number.POSITIVE_INFINITY + let minY = Number.POSITIVE_INFINITY + let maxX = Number.NEGATIVE_INFINITY + let maxY = Number.NEGATIVE_INFINITY + + for (const k of direct) { + const ks = childSize(k) + const p = posTL.get(k.id)! + if (p.x < minX) minX = p.x + if (p.y < minY) minY = p.y + if (p.x + ks.w > maxX) maxX = p.x + ks.w + if (p.y + ks.h > maxY) maxY = p.y + ks.h + } + + const dx = minX - PAD + const dy = minY - PAD + + if (Math.abs(dx) > EPS || Math.abs(dy) > EPS) { + for (const k of direct) { + const p = posTL.get(k.id)! + const nx = p.x - dx + const ny = p.y - dy + + if (Math.abs(p.x - nx) > EPS || Math.abs(p.y - ny) > EPS) { + posTL.set(k.id, { x: nx, y: ny }) + changed = true + } + } + } + + const newW = (maxX - minX) + 2 * PAD + const newH = (maxY - minY) + 2 * PAD + + const oldW = getStyleW(g) ?? (typeof g.style?.width === "number" ? (g.style.width as number) : size(g).w) + const oldH = getStyleH(g) ?? (typeof g.style?.height === "number" ? (g.style.height as number) : size(g).h) + + if (Math.abs(newW - oldW) > EPS || Math.abs(newH - oldH) > EPS) changed = true + + styleWH.set(g.id, { width: newW, height: newH }) + measuredWH.set(g.id, { width: newW, height: newH }) + baseSizes.set(g.id, { w: newW, h: newH }) + } + + // Größen-Cache invalidieren (Group-Styles haben sich ggf. geändert) + sizeCache.clear() + for (const n of nodes) size(n) + + // absTL nach Bounding mit NEUEN Größen + const absTL_after = new Map() + const absCenterAfter = new Map() + for (const n of nodes) { + const s = size(n) + const c = relCenter.get(n.id)! + absCenterAfter.set(n.id, c) + absTL_after.set(n.id, { x: c.x - s.w / 2, y: c.y - s.h / 2 }) + } + + // Param-Group-Row nach Bounding sauber zentrieren + for (const parent of nodes) { + const pGroups: Node[] = [] + const paramList = params.get(parent.id) ?? [] + for (const p of paramList) if (p.type === "group") pGroups.push(p) + if (!pGroups.length) continue + + const ordered = pGroups.slice().sort((a, b) => + (+((a.data as any)?.paramIndex) || 0) - (+((b.data as any)?.paramIndex) || 0) + ) + + const widths: number[] = [] + for (const g of ordered) { + const sw = getStyleW(g) ?? (typeof g.style?.width === "number" ? (g.style.width as number) : undefined) + widths.push(sw ?? size(g).w) + } + + let rowW = 0 + for (const w of widths) rowW += w + rowW += H * (ordered.length - 1) + + const pCenterX = absCenterAfter.get(parent.id)!.x + let gx = pCenterX - rowW / 2 + + for (let i = 0; i < ordered.length; i++) { + const g = ordered[i] + const containerTL = g.parentId ? absTL_after.get(g.parentId)! : { x: 0, y: 0 } + const cur = posTL.get(g.id)! + const nx = gx - containerTL.x + const ny = cur.y + + if (Math.abs(cur.x - nx) > EPS || Math.abs(cur.y - ny) > EPS) { + posTL.set(g.id, { x: nx, y: ny }) + changed = true + } + gx += widths[i] + H + } + } + + } while (changed && pass < 5) + + // ---------------------------- + // 4) Output: Structural Sharing (nur wirklich geänderte Nodes neu bauen) + // ---------------------------- + const out = nodes.map((n) => { + const nextP = posTL.get(n.id) + const nextM = measuredWH.get(n.id) + const nextS = styleWH.get(n.id) + + let isChanged = false + + if (nextP) { + const op = n.position ?? { x: 0, y: 0 } + if (Math.abs(op.x - nextP.x) > EPS || Math.abs(op.y - nextP.y) > EPS) isChanged = true + } + + if (nextM) { + const om = n.measured ?? ({ width: 0, height: 0 } as any) + if (Math.abs((om as any).width - nextM.width) > EPS || Math.abs((om as any).height - nextM.height) > EPS) isChanged = true + } + + if (nextS) { + const ow = typeof (n.style as any)?.width === "number" ? (n.style as any).width : undefined + const oh = typeof (n.style as any)?.height === "number" ? (n.style as any).height : undefined + if (ow !== nextS.width || oh !== nextS.height) isChanged = true + } + + if (!isChanged) return n + + return { + ...n, + position: nextP ?? n.position, + measured: nextM ? ({ ...(n.measured as any), width: nextM.width, height: nextM.height } as any) : n.measured, + style: nextS + ? ({ ...(n.style as any), width: nextS.width, height: nextS.height } as any) + : n.style, + } as Node + }) + + return { nodes: out } +} + +const getCachedLayoutElements = React.cache(getLayoutElements) + +export interface FlowBuilderProps extends Code0ComponentProps { + flowId: Flow['id'] + namespaceId: Namespace['id'] + projectId: NamespaceProject['id'] +} + +export const FlowBuilder: React.FC = (props) => { + return + + +} + +const InternalFlowBuilder: React.FC = (props) => { + const {flowId, namespaceId, projectId, ...rest} = props + + const nodeTypes = React.useMemo(() => ({ + default: DFlowNodeDefaultCard, + group: DFlowNodeGroupCard, + trigger: DFlowNodeTriggerCard, + }), []) + + const edgeTypes = React.useMemo(() => ({ + default: FlowBuilderEdge, + }), []) + + const initialNodes = useFlowNodes(flowId, namespaceId, projectId) + const initialEdges = useEdges(flowId, namespaceId, projectId) + + const [nodes, setNodes] = useNodesState([]) + const [edges, setEdges, edgeChangeEvent] = useEdgesState([]) + const [showTree, setShowTree] = React.useState(false) + + const updateNodeInternals = useUpdateNodeInternals() + + const {fitView} = useReactFlow() + const didFitViewRef = React.useRef(false) + + const revalidateHandles = React.useCallback((ids: string[]) => { + requestAnimationFrame(() => { + ids.forEach(id => updateNodeInternals(id)) + }) + }, [updateNodeInternals]) + + const nodeChangeEvent = React.useCallback((changes: any) => { + const changedIds: string[] = Array.from(new Set( + changes + .filter((c: any) => c.type === 'dimensions' || c.type === 'position') + .map((c: any) => c.id) + )) + + const dimensionMap = new Map() + changes + .filter((c: any) => c.type === 'dimensions') + .forEach((c: any) => dimensionMap.set(c.id, c.dimensions)) + + setNodes(prevNodes => { + const localNodes = prevNodes.map(node => { + if (!dimensionMap.has(node.id)) return node; + const dims = dimensionMap.get(node.id) || {} + return { + ...node, + measured: { + width: dims.width ?? node.measured?.width ?? 0, + height: dims.height ?? node.measured?.height ?? 0, + } + } as Node; + }) + + const layouted = getCachedLayoutElements(localNodes, new Set(changedIds)) + return layouted.nodes as Node[]; + }) + + revalidateHandles(changedIds) + }, [revalidateHandles]) + + React.useEffect(() => { + const localNodes = initialNodes.map(value => { + const nodeEls = !value.measured ? document.querySelectorAll("[data-id='" + value.id + "']") : []; + return { + ...value, + measured: { + width: value.measured?.width ?? (nodeEls[0] as any)?.clientWidth ?? 0, + height: value.measured?.height ?? (nodeEls[0] as any)?.clientHeight ?? 0, + } + } as unknown as Node + }) + + const layouted = getCachedLayoutElements(localNodes, new Set(localNodes.map(n => n.id))) + setNodes(layouted.nodes as Node[]) + setEdges(initialEdges as Edge[]) + + revalidateHandles((layouted.nodes as Node[]).map(n => n.id)) + + }, [initialNodes, initialEdges, revalidateHandles]) + + React.useEffect(() => { + if (didFitViewRef.current) return + if (nodes.length <= 0) return + + didFitViewRef.current = true + + requestAnimationFrame(() => { + requestAnimationFrame(() => { + setTimeout(async () => { + await fitView({ + padding: "64px", + maxZoom: 1 + }) + setShowTree(true) + }, 1000) + }) + }) + }, [nodes, didFitViewRef]) + + return ( + + + {!showTree ? ( + +
    + + + + + + We are running requests to prepare your flow. This may take a few moments. + +
    +
    + ) : null} + {showTree ? ( + <> + + + + + + + + ) : null} +
    + ) +} + +const LoadingFlowText: React.FC = () => { + + const [index, setIndex] = React.useState(0) + + const loadingTexts = [ + "Preparing flow data", + "Loading node and edge definitions", + "Calculating perfect layout" + ] + + React.useEffect(() => { + const id = setInterval(() => { + setIndex(i => (i + 1) % loadingTexts.length) + }, 2000) + + return () => clearInterval(id) + }, []) + + return ( + + {loadingTexts[index]} + + ) +} \ No newline at end of file diff --git a/src/packages/ce/src/flow/components/FlowBuilder.util.ts b/src/packages/ce/src/flow/components/FlowBuilder.util.ts new file mode 100644 index 00000000..eaa2e760 --- /dev/null +++ b/src/packages/ce/src/flow/components/FlowBuilder.util.ts @@ -0,0 +1,95 @@ +import {DataTypeIdentifier} from "@code0-tech/sagittarius-graphql-types"; + +export const attachDataTypeIdentifiers = ( + identifiers: DataTypeIdentifier[], + input: T +): T => { + const map = new Map( + identifiers + .filter(x => x?.id) + .map(x => [x.id!, x]) + ) + + const walk = (v: any): any => + Array.isArray(v) + ? v.map(walk) + : v && typeof v === "object" + ? { + ...Object.fromEntries( + Object.entries(v).map(([k, val]) => [k, walk(val)]) + ), + ...(v.dataTypeIdentifierId + ? {dataTypeIdentifier: map.get(v.dataTypeIdentifierId) ?? null} + : {}), + ...(Array.isArray(v.sourceDataTypeIdentifierIds) + ? { + sourceDataTypeIdentifiers: v.sourceDataTypeIdentifierIds + .map((id: any) => map.get(id)) + .filter(Boolean) + } + : {}) + } + : v + + return walk(input) +} + +export const resolveDataTypeIdentifiers = ( + list: DataTypeIdentifier[] +): DataTypeIdentifier[] => { + const byId = new Map(list.filter(x => x?.id).map(x => [x.id!, x])) + const memo = new Map() + + const build = (x?: DataTypeIdentifier | null): DataTypeIdentifier | null => { + if (!x?.id) return x as any + if (memo.has(x.id)) return memo.get(x.id)! + + const out: DataTypeIdentifier = {...(x as any)} + memo.set(x.id, out) + + if (x.genericType) { + out.genericType = {...(x.genericType as any)} + + const mappers = (x.genericType as any).genericMappers + if (Array.isArray(mappers)) { + ;(out.genericType as any).genericMappers = mappers.map((m: any) => ({ + ...m, + ...(Array.isArray(m.sourceDataTypeIdentifierIds) + ? { + sourceDataTypeIdentifiers: m.sourceDataTypeIdentifierIds + .map((id: any) => build(byId.get(id) ?? null)) + .filter(Boolean), + } + : {}), + })) as any + } + } + + return out + } + + const resolved = list.map(x => build(x)!).filter(Boolean) as DataTypeIdentifier[] + + const resolveDataType = (dt: any) => { + if (!dt) return dt + + const nodes = dt?.dataTypeIdentifiers?.nodes + if (!Array.isArray(nodes)) return dt + + const localResolved = resolveDataTypeIdentifiers(nodes) + + const dtWithNodes = { + ...dt, + dataTypeIdentifiers: {...dt.dataTypeIdentifiers, nodes: localResolved}, + } + + return attachDataTypeIdentifiers(localResolved, dtWithNodes) + } + + resolved.forEach((id: any) => { + if (id?.dataType) id.dataType = resolveDataType(id.dataType) + if (id?.genericType?.dataType) id.genericType.dataType = resolveDataType(id.genericType.dataType) + }) + + return resolved +} \ No newline at end of file diff --git a/src/packages/ce/src/flow/components/FlowBuilderEdge.tsx b/src/packages/ce/src/flow/components/FlowBuilderEdge.tsx new file mode 100644 index 00000000..82455d89 --- /dev/null +++ b/src/packages/ce/src/flow/components/FlowBuilderEdge.tsx @@ -0,0 +1,91 @@ +import {BaseEdge, Edge, EdgeLabelRenderer, EdgeProps, getSmoothStepPath, Position} from "@xyflow/react"; +import React, {memo} from "react"; +import {Flow, NodeFunction} from "@code0-tech/sagittarius-graphql-types"; +import {Badge, Code0Component} from "@code0-tech/pictor"; + +export interface FlowBuilderEdgeDataProps extends Code0Component { + //some data we will use + color?: string + type: 'parameter' | 'suggestion' | 'group' | 'default' + parentNodeId?: NodeFunction['id'] | null + flowId: Flow['id'] +} + +// @ts-ignore +export type FlowBuilderEdgeProps = EdgeProps> + +export const FlowBuilderEdge: React.FC = memo((props) => { + const { + sourceX, + sourceY, + targetX, + targetY, + id, + data, + label, + style, + ...rest + } = props + + const [edgePath, labelX, labelY] = getSmoothStepPath({ + sourceX, + sourceY, + sourcePosition: data?.type === "parameter" ? Position.Left : Position.Bottom, + targetX, + targetY, + targetPosition: data?.type === "parameter" ? Position.Right : Position.Top, + borderRadius: 16, + stepPosition: 0.5, + }) + const color = data?.color ?? "#ffffff" + const gradientId = `dflow-edge-gradient-${id}` + + return ( + <> + {/* Gradient-Definition für genau diese Edge */} + + + {/* Start: volle Farbe */} + + {/* Ende: gleiche Farbe, aber transparent */} + + + + + + + {label ? ( + +
    + + {label} + +
    +
    + ) : null} + + ) +}) \ No newline at end of file diff --git a/src/packages/ce/src/flow/components/FlowPanelControl.tsx b/src/packages/ce/src/flow/components/FlowPanelControl.tsx new file mode 100644 index 00000000..9727f3b2 --- /dev/null +++ b/src/packages/ce/src/flow/components/FlowPanelControl.tsx @@ -0,0 +1,95 @@ +import React from "react"; +import {ButtonGroup} from "../button-group/ButtonGroup"; +import {Button} from "../button/Button"; +import {Panel} from "@xyflow/react"; +import {useService, useStore} from "../../utils"; +import {FileTabsService} from "../file-tabs/FileTabs.service"; +import {DFlowReactiveService} from "../d-flow"; +import {Flow, NodeFunction} from "@code0-tech/sagittarius-graphql-types"; +import {Tooltip, TooltipArrow, TooltipContent, TooltipPortal, TooltipTrigger} from "../tooltip/Tooltip"; +import {Text} from "../text/Text"; +import {DFlowSuggestionMenu} from "../d-flow-suggestion/DFlowSuggestionMenu"; +import {useSuggestions} from "../d-flow-suggestion/DFlowSuggestion.hook"; + +export interface DFlowPanelControlProps { + flowId: Flow['id'] +} + +export const FlowPanelControl: React.FC = (props) => { + + //props + const {flowId} = props + + //services and stores + const fileTabsService = useService(FileTabsService) + const fileTabsStore = useStore(FileTabsService) + const flowService = useService(DFlowReactiveService) + const flowStore = useStore(DFlowReactiveService) + const [, startTransition] = React.useTransition() + + //memoized values + const activeTab = React.useMemo(() => { + return fileTabsService.values().find((t: any) => (t as any).active) + }, [fileTabsStore, fileTabsService]) + + const result = useSuggestions(flowId, activeTab?.content?.props?.node?.id as NodeFunction['id'] | undefined) + + //callbacks + const deleteActiveNode = React.useCallback(() => { + if (!activeTab) return + if (!(activeTab?.content?.props?.flowId as Flow['id'])) return + // @ts-ignore + startTransition(async () => { + const linkedNodes = flowService.getLinkedNodesById(flowId, activeTab?.content?.props?.node.id) + linkedNodes.forEach(node => { + if (node.id) fileTabsService.deleteById(node.id) + }) + + await flowService.deleteNodeById((activeTab?.content?.props?.flowId as Flow['id']), (activeTab?.content?.props?.node.id as NodeFunction['id'])) + }) + }, [activeTab, flowService, flowStore]) + + const addNodeToFlow = React.useCallback((suggestion: any) => { + if (flowId && suggestion.value.__typename === "NodeFunction" && "node" in activeTab!.content?.props) { + startTransition(async () => { + await flowService.addNextNodeById(flowId, (activeTab?.content?.props.node.id as NodeFunction['id']) ?? undefined, suggestion.value) + }) + } else { + startTransition(async () => { + await flowService.addNextNodeById(flowId, null, suggestion.value) + }) + } + }, [flowId, flowService, flowStore, activeTab]) + + return + + + + + + + + Select a node to delete it + + + + + Add next node + + }/> + + + +} diff --git a/src/packages/ce/src/flow/components/FlowPanelLayout.tsx b/src/packages/ce/src/flow/components/FlowPanelLayout.tsx new file mode 100644 index 00000000..bf01d0e6 --- /dev/null +++ b/src/packages/ce/src/flow/components/FlowPanelLayout.tsx @@ -0,0 +1,56 @@ +import React from "react"; +import {SegmentedControl, SegmentedControlItem} from "../segmented-control/SegmentedControl"; +import {IconLayout, IconLayoutDistributeHorizontal, IconLayoutDistributeVertical} from "@tabler/icons-react"; +import {Panel} from "@xyflow/react"; +import {Tooltip, TooltipArrow, TooltipContent, TooltipPortal, TooltipTrigger} from "../tooltip/Tooltip"; +import {Text} from "../text/Text"; + +export interface DFlowPanelLayoutProps { + +} + +export const FlowPanelLayout: React.FC = (props) => { + + const {} = props + + return + + + + + + + + + + Vertical layout + + + + + + + + + + + + Horizontal layout + + + + + + + + + + + + Manual layout + + + + + +} \ No newline at end of file diff --git a/src/packages/ce/src/flow/components/FlowPanelSize.tsx b/src/packages/ce/src/flow/components/FlowPanelSize.tsx new file mode 100644 index 00000000..90ec7e68 --- /dev/null +++ b/src/packages/ce/src/flow/components/FlowPanelSize.tsx @@ -0,0 +1,47 @@ +import React from "react"; +import {Panel, useReactFlow, useViewport} from "@xyflow/react"; +import {ButtonGroup} from "../button-group/ButtonGroup"; +import {Button} from "../button/Button"; +import {IconFocusCentered, IconMinus, IconPlus} from "@tabler/icons-react"; +import {Badge} from "../badge/Badge"; +import {Flex} from "../flex/Flex"; +import {Text} from "../text/Text"; + +export const FlowPanelSize: React.FC = () => { + + const viewport = useViewport(); + const reactFlow = useReactFlow(); + + const zoomIn = () => { + reactFlow.zoomIn() + } + + const zoomOut = () => { + reactFlow.zoomOut() + } + + const center = () => { + reactFlow.fitView() + } + + const getCurrentZoomInPercent = () => { + return Math.round(viewport.zoom * 100); + } + + return + + + + + + + + {getCurrentZoomInPercent()}% + + + + +} \ No newline at end of file diff --git a/src/packages/ce/src/flow/components/FlowPanelUpdate.tsx b/src/packages/ce/src/flow/components/FlowPanelUpdate.tsx new file mode 100644 index 00000000..40e67d92 --- /dev/null +++ b/src/packages/ce/src/flow/components/FlowPanelUpdate.tsx @@ -0,0 +1,122 @@ +import React from "react"; +import {IconCloudCheck, IconCloudUpload} from "@tabler/icons-react"; +import {Panel} from "@xyflow/react"; +import {Tooltip, TooltipArrow, TooltipContent, TooltipPortal, TooltipTrigger} from "../tooltip/Tooltip"; +import {Text} from "../text/Text"; +import {Flow} from "@code0-tech/sagittarius-graphql-types"; +import {useService, useStore} from "../../utils"; +import {DFlowReactiveService} from "../d-flow"; +import {Badge} from "../badge/Badge"; +import {Button} from "../button/Button"; + +export interface DFlowPanelUpdateProps { + flowId: Flow['id'] +} + +export const FlowPanelUpdate: React.FC = (props) => { + + const {flowId} = props + + const flowService = useService(DFlowReactiveService) + const flowStore = useStore(DFlowReactiveService) + const [loading, startTransition] = React.useTransition() + + const flow = React.useMemo(() => flowService.getById(flowId), [flowId, flowStore]) + + const edited = React.useMemo( + () => !!flow?.editedAt && new Date(flow?.updatedAt ?? Date.now()).getTime() != new Date(flow?.editedAt ?? Date.now()).getTime(), + [flow, flowStore] + ) + const lastSave = React.useMemo( + () => { + return formatTimeAgo(flow?.updatedAt ?? Date.now()) + }, + [flow, flowStore] + ) + + const flowUpdate = React.useCallback(() => { + const flowInput = flowService.getPayloadById(flowId) + if (!flowId) return + + + startTransition(async () => { + await flowService.flowUpdate({ + flowInput: flowInput!, + flowId: flowId! + }) + }) + + }, [flowId, flowService]) + + return + {edited ? ( + + + + + + + + Changes are also saved automatically within 1 minute + + + + ) : ( + + + + + + + + Last save {lastSave}.
    Everything is synced.
    +
    +
    +
    + )} +
    +} + +const formatTimeAgo = (input: Date | number | string) => { + const now = Date.now() + + let pastMs: number + + if (input instanceof Date) { + pastMs = input.getTime() + } else if (typeof input === "number") { + pastMs = input < 1e12 ? input * 1000 : input // sec vs ms + } else { + // string: if numeric => unix, else ISO date string + const n = Number(input) + pastMs = Number.isFinite(n) + ? (n < 1e12 ? n * 1000 : n) + : new Date(input).getTime() + } + + if (!Number.isFinite(pastMs)) return "just now" + + let diff = now - pastMs + if (diff <= 0) return "just now" + + const MIN = 60_000 + const HOUR = 60 * MIN + const DAY = 24 * HOUR + const YEAR = 365 * DAY + + const years = Math.floor(diff / YEAR); diff %= YEAR + const days = Math.floor(diff / DAY); diff %= DAY + const hours = Math.floor(diff / HOUR); diff %= HOUR + const minutes = Math.floor(diff / MIN) + + const parts = [ + years && `${years}y`, + days && `${days}d`, + hours && `${hours}h`, + minutes && `${minutes}m`, + ].filter(Boolean) as string[] + + return parts.length ? `${parts.join(" ")} ago` : "just now" +} \ No newline at end of file diff --git a/src/packages/ce/src/flow/hooks/DataTypeValidation.hook.ts b/src/packages/ce/src/flow/hooks/DataTypeValidation.hook.ts new file mode 100644 index 00000000..3f89a436 --- /dev/null +++ b/src/packages/ce/src/flow/hooks/DataTypeValidation.hook.ts @@ -0,0 +1,54 @@ +import {DataTypeView} from "@edition/datatype/services/DataType.view"; + +const IGNORE_ID_KEYS = ["id", "__typename", "createdAt", "updatedAt", "aliases", "displayMessages", "name", "runtime"]; + +export const useDataTypeValidation = ( + firstDataType: DataTypeView, + secondDataType: DataTypeView +) => { + + if (firstDataType?.variant !== secondDataType?.variant) return false + + const isObject = (value: unknown): value is Record => { + return value !== null && typeof value === "object" + } + + const isDeepEqual = (value1: unknown, value2: unknown): boolean => { + if (value1 === value2) return true + + const value1IsArray = Array.isArray(value1) + const value2IsArray = Array.isArray(value2) + + if (value1IsArray || value2IsArray) { + if (!value1IsArray || !value2IsArray) return false + + if (value1.length !== value2.length) return false + + return (value1 as unknown[]).every((entry, index) => isDeepEqual(entry, (value2 as unknown[])[index])) + } + + if (isObject(value1) && isObject(value2)) { + const objKeys1 = Object.keys(value1) + const objKeys2 = Object.keys(value2) + + if (objKeys1.length !== objKeys2.length) return false + + return objKeys1.every(key => { + if (IGNORE_ID_KEYS.includes(key)) return true // Ignore IDs in deep comparison + return isDeepEqual((value1 as Record)[key], (value2 as Record)[key]) + }) + } + + return false + } + + const firstRules = firstDataType.rules?.nodes ?? [] + const secondRules = secondDataType.rules?.nodes ?? [] + + if (!firstRules.length && !secondRules.length) return true + if (!firstRules.length || !secondRules.length) return false + + if (firstRules.length !== secondRules.length) return false + + return firstRules.every((rule, index) => isDeepEqual(rule, secondRules[index])) +} diff --git a/src/packages/ce/src/flow/hooks/Flow.edges.hook.ts b/src/packages/ce/src/flow/hooks/Flow.edges.hook.ts new file mode 100644 index 00000000..45a1308e --- /dev/null +++ b/src/packages/ce/src/flow/hooks/Flow.edges.hook.ts @@ -0,0 +1,165 @@ +import {Edge} from "@xyflow/react"; +import React from "react"; +import type {Flow, Namespace, NamespaceProject, NodeFunction} from "@code0-tech/sagittarius-graphql-types"; +import {hashToColor, useService, useStore} from "@code0-tech/pictor"; +import {FlowService} from "@edition/flow/services/Flow.service"; +import {FunctionService} from "@edition/function/services/Function.service"; +import {DatatypeService} from "@edition/datatype/services/Datatype.service"; + +// @ts-ignore +export const useEdges = (flowId: Flow['id'], namespaceId?: Namespace['id'], projectId?: NamespaceProject['id']): Edge[] => { + + const flowService = useService(FlowService); + const flowStore = useStore(FlowService) + const functionService = useService(FunctionService); + const functionStore = useStore(FunctionService) + const dataTypeService = useService(DatatypeService); + const dataTypeStore = useStore(DatatypeService) + + const flow = React.useMemo(() => flowService.getById(flowId, {namespaceId, projectId}), [flowId, flowStore]) + + return React.useMemo(() => { + if (!flow) return [] + if (functionStore.length <= 0) return [] + if (dataTypeStore.length <= 0) return [] + + // @ts-ignore + const edges: Edge[] = [] + + const groupsWithValue = new Map(); + + let idCounter = 0; + + const traverse = ( + node: NodeFunction, + parentNode?: NodeFunction, + isParameter = false + ): string => { + if (!node) return "" + + if (node.id == flow.startingNodeId) { + edges.push({ + id: `trigger-${node.id}-next`, + source: flow.id as string, + target: node.id!, + data: { + color: "#ffffff", + type: 'default', + flowId: flowId, + parentNodeId: parentNode?.id + }, + deletable: false, + selectable: false, + }); + } + + if (parentNode?.id && !isParameter) { + const startGroups = groupsWithValue.get(parentNode.id) ?? []; + + if (startGroups.length > 0) { + startGroups.forEach((gId, idx) => edges.push({ + id: `${gId}-${node.id}-next-${idx}`, + source: gId, + target: node.id!, + data: { + color: "#ffffff", + type: 'default', + flowId: flowId, + parentNodeId: parentNode?.id + }, + deletable: false, + selectable: false, + })); + } else { + edges.push({ + id: `${parentNode.id}-${node.id}-next`, + source: parentNode.id, + target: node.id!, + data: { + color: "#ffffff", + type: 'default', + flowId: flowId, + parentNodeId: parentNode.id + }, + deletable: false, + selectable: false, + }); + } + } + + node.parameters?.nodes?.forEach((param) => { + const parameterValue = param?.value; + const parameterDefinition = functionService.getById(node.functionDefinition?.id!!)?.parameterDefinitions?.find(p => p.id === param?.parameterDefinition?.id); + const parameterDataTypeIdentifier = parameterDefinition?.dataTypeIdentifier; + const parameterDataType = parameterDataTypeIdentifier ? dataTypeService.getDataType(parameterDataTypeIdentifier) : undefined; + + if (!parameterValue) return + + if (parameterDataType?.variant === "NODE") { + if (parameterValue && parameterValue.__typename === "NodeFunctionIdWrapper") { + + const groupId = `${node.id}-group-${idCounter++}`; + + edges.push({ + id: `${node.id}-${groupId}-param-${param.id}`, + source: node.id!, + target: groupId, + deletable: false, + selectable: false, + animated: true, + label: parameterDefinition?.names!![0]?.content ?? param.id, + data: { + color: hashToColor(parameterValue?.id || ""), + type: 'group', + flowId: flowId, + parentNodeId: parentNode?.id + }, + }); + + (groupsWithValue.get(node.id!) ?? (groupsWithValue.set(node.id!, []), groupsWithValue.get(node.id!)!)).push(groupId); + + traverse( + flowService.getNodeById(flowId, parameterValue.id)!, + node, + true + ); + } + } else if (parameterValue && parameterValue.__typename === "NodeFunctionIdWrapper") { + const subFnId = traverse( + flowService.getNodeById(flowId, parameterValue.id)!, + node, + true + ); + + edges.push({ + id: `${subFnId}-${node.id}-param-${param.id}`, + source: subFnId, + target: node.id!, + targetHandle: `param-${param.id}`, + animated: true, + deletable: false, + selectable: false, + data: { + color: hashToColor(parameterValue?.id || ""), + type: 'parameter', + flowId: flowId, + parentNodeId: parentNode?.id + }, + }); + } + }); + + if (node.nextNodeId) { + traverse(flowService.getNodeById(flow.id!!, node.nextNodeId!!)!!, node); + } + + return node.id!; + }; + + if (flow.startingNodeId) { + traverse(flowService.getNodeById(flow.id!!, flow.startingNodeId!!)!!, undefined, false); + } + + return edges + }, [flow, flowStore, functionStore, dataTypeStore]); +}; \ No newline at end of file diff --git a/src/packages/ce/src/flow/hooks/Flow.nodes.hook.ts b/src/packages/ce/src/flow/hooks/Flow.nodes.hook.ts new file mode 100644 index 00000000..b69e091e --- /dev/null +++ b/src/packages/ce/src/flow/hooks/Flow.nodes.hook.ts @@ -0,0 +1,218 @@ +import {Node} from "@xyflow/react"; +import type {Flow, Namespace, NamespaceProject, NodeFunction} from "@code0-tech/sagittarius-graphql-types"; +import React from "react"; +import {hashToColor, useService, useStore} from "@code0-tech/pictor"; +import {FlowService} from "@edition/flow/services/Flow.service"; +import {FunctionService} from "@edition/function/services/Function.service"; +import {DatatypeService} from "@edition/datatype/services/Datatype.service"; +import {DFlowNodeProps} from "@code0-tech/pictor/dist/components/d-flow-node/DFlowNode"; + +const packageNodes = new Map([ + ['std', 'default'], +]); + +/** + * Returns the value of the best-matching key from a Map. + * + * Matching priority: + * 1) Exact match + * 2) Longest prefix match (with bonus if the prefix ends at a token boundary) + * 3) Largest common prefix length (with small boundary bonus) + * + * Token boundaries are characters in /[:._\-\/\s]+/ (e.g., "::", ".", "_", "-", "/", whitespace). + * + * Performance: + * - O(N * M), where N = number of keys, M = average key length (string comparisons only). + * + */ +const bestMatchValue = (map: Map, input: string): string => { + if (!input || map.size === 0) return "" + + const SEP = /[:._\-\/\s]+/; + const normInput = input.trim().toLowerCase(); + + let bestKey: string | null = null; + let bestScore = -Infinity; + + for (const [key, value] of map.entries()) { + const normKey = key.trim().toLowerCase(); + + // (1) Exact match → immediately return (strongest possible score) + if (normInput === normKey) { + return value; + } + + let score = 0; + + // (2) Prefix match + if (normInput.startsWith(normKey)) { + score = 2000 + normKey.length * 2; + + // Bonus if the prefix ends at a clean token boundary (or equals the whole input) + const boundaryChar = normInput.charAt(normKey.length); // '' if out of range + if (boundaryChar === "" || SEP.test(boundaryChar)) { + score += 200; + } + } else { + // (3) Largest common prefix (LCP) + const max = Math.min(normInput.length, normKey.length); + let lcp = 0; + while (lcp < max && normInput.charCodeAt(lcp) === normKey.charCodeAt(lcp)) { + lcp++; + } + if (lcp > 0) { + score = 1000 + lcp; + + // Small bonus if LCP ends at a boundary on either side + const inBoundaryChar = normInput.charAt(lcp); + const keyBoundaryChar = normKey.charAt(lcp); + if ( + inBoundaryChar === "" || + SEP.test(inBoundaryChar) || + keyBoundaryChar === "" || + SEP.test(keyBoundaryChar) + ) { + score += 50; + } + } + } + + // Best candidate so far? Tie-breaker favors longer key (more specific) + if (score > bestScore) { + bestScore = score; + bestKey = key; + } else if (score === bestScore && bestKey !== null && key.length > bestKey.length) { + bestKey = key; + } else if (score === bestScore && bestKey === null) { + bestKey = key; + } + } + + return bestKey !== null ? map.get(bestKey)! : ""; +}; + +// @ts-ignore +export const useFlowNodes = (flowId: Flow["id"], namespaceId?: Namespace["id"], projectId?: NamespaceProject["id"]): Node[] => { + + const flowService = useService(FlowService) + const flowStore = useStore(FlowService) + const functionService = useService(FunctionService) + const functionStore = useStore(FunctionService) + const dataTypeService = useService(DatatypeService) + const dataTypeStore = useStore(DatatypeService) + + const flow = React.useMemo(() => flowService.getById(flowId, {namespaceId, projectId}), [flowId, flowStore]); + + return React.useMemo(() => { + if (!flow) return [] + if (functionStore.length <= 0) return [] + if (dataTypeStore.length <= 0) return [] + + const nodes: Node[] = []; + const visited = new Set(); + + let groupCounter = 0; + let globalIndex = 0; + + // Trigger node (ID == flow.id -> edge compatible) + nodes.push({ + id: `${flow.id}`, + type: "trigger", + position: {x: 0, y: 0}, + draggable: false, + data: { + flowId: flowId, + nodeId: undefined, + color: hashToColor(flowId!), + }, + }); + + const traverse = ( + node: NodeFunction, + isParameter = false, + parentId?: NodeFunction['id'], + parentGroup?: string + ) => { + if (!node?.id) return; + + const nodeId = node.id; + + if (!visited.has(nodeId)) { + visited.add(nodeId); + + nodes.push({ + id: nodeId, + type: bestMatchValue(packageNodes, node.functionDefinition?.identifier ?? ""), + position: {x: 0, y: 0}, + draggable: false, + parentId: parentGroup, + extent: parentGroup ? "parent" : undefined, + data: { + nodeId: nodeId, + isParameter: isParameter, + flowId: flowId, + parentNodeId: isParameter ? parentId : undefined, + index: ++globalIndex, + color: hashToColor(nodeId), + }, + }); + } + + const definition = node.functionDefinition?.id + ? functionService.getById(node.functionDefinition.id) + : undefined; + + node.parameters?.nodes?.forEach(param => { + const value = param?.value; + if (!value || value.__typename !== "NodeFunctionIdWrapper") return; + + const paramDef = definition?.parameterDefinitions?.find(p => p.id === param?.parameterDefinition?.id); + const dataType = paramDef?.dataTypeIdentifier + ? dataTypeService.getDataType(paramDef.dataTypeIdentifier) + : undefined; + + if (dataType?.variant === "NODE") { + const groupId = `${nodeId}-group-${groupCounter++}`; + + if (!visited.has(groupId)) { + visited.add(groupId); + + nodes.push({ + id: groupId, + type: "group", + position: {x: 0, y: 0}, + draggable: false, + parentId: parentGroup, + extent: parentGroup ? "parent" : undefined, + data: { + isParameter: true, + parentNodeId: nodeId, + nodeId: nodeId, + flowId: flowId, + color: hashToColor(value.id!), + }, + }); + } + + const child = flowService.getNodeById(flowId, value.id); + if (child) traverse(child, false, undefined, groupId); + } else { + const child = flowService.getNodeById(flowId, value.id); + if (child) traverse(child, true, nodeId, parentGroup); + } + }); + + if (node.nextNodeId) { + const next = flowService.getNodeById(flow.id, node.nextNodeId); + if (next) traverse(next, false, undefined, parentGroup); + } + }; + + if (flow.startingNodeId) { + const start = flowService.getNodeById(flow.id, flow.startingNodeId); + if (start) traverse(start); + } + + return nodes; + }, [flow, flowStore, functionStore, dataTypeStore]); +}; \ No newline at end of file diff --git a/src/packages/ce/src/flow/hooks/NodeValidation.hook.ts b/src/packages/ce/src/flow/hooks/NodeValidation.hook.ts new file mode 100644 index 00000000..cfb06cb9 --- /dev/null +++ b/src/packages/ce/src/flow/hooks/NodeValidation.hook.ts @@ -0,0 +1,159 @@ +import React from "react" +import type { + Flow, + NodeFunction, + NodeParameter, + NodeParameterValue, + ReferenceValue +} from "@code0-tech/sagittarius-graphql-types" +import {useDataTypeValidation} from "./DataTypeValidation.hook" +import {useValueValidation} from "./ValueValidation.hook" +import {DataTypeView} from "@edition/datatype/services/DataType.view"; +import { + GenericMap, + replaceGenericKeysInDataTypeObject, + replaceGenericKeysInType, + resolveGenericKeys +} from "@edition/flow/utils/generics"; +import { + InspectionSeverity, + useService, + useStore, + ValidationResult +} from "@code0-tech/pictor"; +import {useReturnType} from "@edition/function/hooks/DFlowFunction.return.hook"; +import {getReferenceType} from "@edition/function/hooks/DFlowNodeReference.return.hook"; +import {FunctionService} from "@edition/function/services/Function.service"; +import {FlowService} from "@edition/flow/services/Flow.service"; +import {FlowTypeService} from "@edition/flowtype/services/FlowTypeService"; +import {DatatypeService} from "@edition/datatype/services/Datatype.service"; + +const isReference = (value: NodeParameterValue) => + value.__typename === "ReferenceValue" + +const isNode = (value: NodeParameterValue) => + value.__typename === "NodeFunctionIdWrapper" + +const resolveDataTypeWithGenerics = ( + dataType: DataTypeView, + genericMap: GenericMap +) => + new DataTypeView( + replaceGenericKeysInDataTypeObject(dataType.json!, genericMap) + ) + +const errorResult = ( + parameterId: NodeParameter['id'], + expected?: DataTypeView, + actual?: DataTypeView +): ValidationResult => ({ + parameterId, + type: InspectionSeverity.ERROR, + message: [{ + code: "en-US", + content: `Argument of type ${actual?.name!![0]?.content} is not assignable to parameter of type ${expected?.name!![0]?.content}` + }] +}) + +export const useNodeValidation = ( + nodeId: NodeFunction['id'], + flowId: Flow['id'] +): ValidationResult[] | null => { + + const functionService = useService(FunctionService) + const functionStore = useStore(FunctionService) + const flowService = useService(FlowService) + const flowStore = useStore(FlowService) + const flowTypeService = useService(FlowTypeService) + const flowTypeStore = useStore(FlowTypeService) + const dataTypeService = useService(DatatypeService) + const dataTypeStore = useStore(DatatypeService) + + const flow = flowService.getById(flowId) + const node = flowService.getNodeById(flowId, nodeId) + const values = node?.parameters?.nodes?.map(p => p?.value!!) ?? [] + const functionDefinition = functionService.getById(node?.functionDefinition?.id) + const parameters = functionDefinition?.parameterDefinitions ?? [] + const genericKeys = functionDefinition?.genericKeys ?? [] + const genericMap = React.useMemo( + () => resolveGenericKeys(functionDefinition!, values, dataTypeService, functionService, flow), + [functionDefinition, values, dataTypeService, dataTypeStore, flow, flowStore] + ) + + const resolveValueType = React.useCallback( + (value: NodeParameterValue, expectedDT?: DataTypeView) => { + + //TODO seperate check for flow input, return type and parameter type to properly resolve variables + if ((isNode(value) && expectedDT?.variant !== "NODE")) { + const node = flowService.getNodeById(flowId, value.__typename == "NodeFunctionIdWrapper" ? value.id : value.__typename === "ReferenceValue" ? value.nodeFunctionId : undefined) + const fn = functionService.getById(node?.functionDefinition?.id!!)!! + const params = node?.parameters?.nodes?.map(p => p?.value!!) ?? [] + return useReturnType(fn, params, dataTypeService, functionService) + } else if (isReference(value)) { + const node = flowService.getNodeById(flowId, value.__typename == "NodeFunctionIdWrapper" ? value.id : value.__typename === "ReferenceValue" ? value.nodeFunctionId : undefined) + const fn = functionService.getById(node?.functionDefinition?.id!!)!! + const flowType = flowTypeService.getById(flow?.type?.id)?.json() + return getReferenceType(value as ReferenceValue, dataTypeService, functionService, fn, node, flowType) + } + return dataTypeService.getTypeFromValue(value, flow) + }, + [dataTypeService, flow, flowId, flowService, functionService, flowTypeStore] + ) + + return React.useMemo(() => { + const errors: ValidationResult[] = [] + + for (let i = 0; i < parameters.length; i++) { + const parameter = parameters[i] + const value = values[i] + const nodeParameter = node?.parameters?.nodes?.find(p => p?.parameterDefinition?.id === parameter.id) + if (!value) continue + + const expectedType = parameter.dataTypeIdentifier + const expectedResolvedType = replaceGenericKeysInType(expectedType!, genericMap) + const expectedDT = dataTypeService.getDataType(expectedResolvedType) + const valueType = resolveValueType(value, expectedDT) + const valueDT = dataTypeService.getDataType(valueType!!) + + if (!expectedDT || !valueDT) { + errors.push(errorResult(nodeParameter?.id, expectedDT, valueDT)) + continue + } + + const isGeneric = + !!expectedType?.genericType || + (!!expectedType?.genericKey && genericKeys.includes(expectedType.genericKey)) + + let isValid = true + + if (isGeneric) { + const resolvedExpectedDT = resolveDataTypeWithGenerics(expectedDT, genericMap) + if (isReference(value) || (isNode(value) && expectedDT.variant !== "NODE")) { + const resolvedValueDT = resolveDataTypeWithGenerics(valueDT, genericMap) + isValid = useDataTypeValidation(resolvedExpectedDT, resolvedValueDT) + } else { + isValid = useValueValidation( + value, + resolvedExpectedDT, + dataTypeService, + flow, + expectedResolvedType?.genericType?.genericMappers!, + functionService + ) + } + } else { + if (isReference(value) || (isNode(value) && expectedDT.variant !== "NODE")) { + isValid = useDataTypeValidation(expectedDT, valueDT) + } else { + isValid = useValueValidation(value, expectedDT, dataTypeService, flow, [], functionService) + } + } + + if (!isValid) { + errors.push(errorResult(nodeParameter?.id, expectedDT, valueDT)) + } + } + + return errors.length > 0 ? errors : null + }, [flow, node, values, functionDefinition, parameters, genericKeys, genericMap, resolveValueType, nodeId, flowId, functionStore, flowStore, dataTypeStore, dataTypeService]) +} \ No newline at end of file diff --git a/src/packages/ce/src/flow/hooks/ValueValidation.hook.ts b/src/packages/ce/src/flow/hooks/ValueValidation.hook.ts new file mode 100644 index 00000000..818806df --- /dev/null +++ b/src/packages/ce/src/flow/hooks/ValueValidation.hook.ts @@ -0,0 +1,28 @@ +import type {Flow, GenericMapper, NodeParameterValue} from "@code0-tech/sagittarius-graphql-types"; +import {DataTypeView} from "@edition/datatype/services/DataType.view"; +import {DatatypeService} from "@edition/datatype/services/Datatype.service"; +import {FunctionService} from "@edition/function/services/Function.service"; +import {RuleMap} from "@edition/datatype/services/rules/DataTypeRules"; +import {VariantsMap} from "@edition/datatype/services/variants/DataTypeVariants"; + +export const useValueValidation = ( + value: NodeParameterValue, + dataType: DataTypeView, + dataTypeService: DatatypeService, + flow?: Flow, + generics?: GenericMapper[], + functionService?: FunctionService, +): boolean => { + + const map = new Map(generics?.map(generic => [generic.target!!, generic])) + + const isRulesValid = dataType?.rules?.nodes?.every(rule => { + if (!rule || !rule.variant || !rule.config) return false + if (!RuleMap.get(rule.variant)) return true //TODO; missing parent type rule + return RuleMap.get(rule.variant)?.validate(value, rule.config, map, flow, dataTypeService, functionService) + }) ?? true + + const isVariantValid = VariantsMap.get(dataType.variant!!)?.validate(value) ?? true + + return isRulesValid && isVariantValid +} \ No newline at end of file diff --git a/src/packages/ce/src/flow/utils/generics.ts b/src/packages/ce/src/flow/utils/generics.ts new file mode 100644 index 00000000..2aeeb883 --- /dev/null +++ b/src/packages/ce/src/flow/utils/generics.ts @@ -0,0 +1,749 @@ + +import type { + DataType, + DataTypeIdentifier, + DataTypeRule, + DataTypeRuleConnection, + DataTypeRulesConfig, + Flow, + GenericCombinationStrategyType, + GenericMapper, + GenericType, + NodeParameterValue +} from "@code0-tech/sagittarius-graphql-types"; +import {useReturnType} from "@edition/function/hooks/DFlowFunction.return.hook"; +import {FunctionDefinitionView} from "@code0-tech/pictor"; +import {DatatypeService} from "@edition/datatype/services/Datatype.service"; +import {FunctionService} from "@edition/function/services/Function.service"; + +const GENERIC_PLACEHOLDER = "GENERIC"; + +type IdentifierLike = DataTypeIdentifier | string | undefined | null; + +type GenericMappingResult = Record; + +type GenericReplacement = DataTypeIdentifier | GenericMapper; + +export type GenericMap = Map; +export type GenericTargetMap = Map; + +const isPlainObject = (value: unknown): value is Record => { + return typeof value === "object" && value !== null && !Array.isArray(value); +}; + +const isDataTypeIdentifier = (value: unknown): value is DataTypeIdentifier => { + if (!isPlainObject(value)) return false; + return ( + "genericKey" in value || + "genericType" in value || + "dataType" in value + ); +}; + +const isGenericMapper = (value: GenericMapper): value is GenericMapper => { + return isPlainObject(value) && "target" in value && Array.isArray((value as GenericMapper).sourceDataTypeIdentifiers); +}; + +const isDataType = (value: unknown): value is DataType => { + return isPlainObject(value) && "variant" in value && "identifier" in value; +}; + +const isDataTypeRule = (value: unknown): value is DataTypeRule => { + return isPlainObject(value) && "variant" in value && "config" in value; +}; + +const extractIdentifierId = (identifier: IdentifierLike): string | undefined => { + if (!identifier) return undefined; + if (typeof identifier === "string") return identifier; + return ( + identifier?.dataType?.identifier ?? + identifier?.genericType?.dataType?.identifier + ) ?? undefined; +}; + +const extractIdentifierGenericKey = ( + identifier: IdentifierLike, + genericKeys?: Set +): string | undefined => { + if (!identifier) return undefined; + if (typeof identifier === "string") { + if (genericKeys && genericKeys.has(identifier)) return identifier; + return undefined; + } + return identifier.genericKey ?? undefined; +}; + +const getIdentifierMappers = (identifier: IdentifierLike): GenericMapper[] => { + if (!identifier || typeof identifier === "string") return []; + return identifier.genericType?.genericMappers ?? []; +}; + +const cloneMapperWithSources = ( + mapper: GenericMapper, + sourceDataTypeIdentifiers: DataTypeIdentifier[] +): GenericMapper => { + return { + ...mapper, + sourceDataTypeIdentifiers: sourceDataTypeIdentifiers + }; +}; + +const toCombinationTypes = (mapper: GenericMapper): Set => { + const strategies = mapper.genericCombinationStrategies ?? []; + return new Set(strategies.map(strategy => strategy.type!!)); +}; + +const normalizeObjectForComparison = (value: unknown): unknown => { + if (Array.isArray(value)) { + return value.map(normalizeObjectForComparison); + } + + if (isPlainObject(value)) { + const normalized: Record = {}; + Object.entries(value).forEach(([key, val]) => { + if (val === undefined || val === null) return + if (key === "__typename" || key === "id" || key === "createdAt" || key === "updatedAt") return; + normalized[key] = normalizeObjectForComparison(val); + }); + return normalized; + } + + return value; +}; + +const identifiersMatch = ( + source: IdentifierLike, + target: IdentifierLike +): boolean => { + if (!target) return !source; + if (typeof target === "string") { + if (target === GENERIC_PLACEHOLDER) return true; + return extractIdentifierId(source) === target; + } + + const genericKey = target.genericKey; + if (genericKey) return true; + + const targetId = extractIdentifierId(target); + const sourceId = extractIdentifierId(source); + return !!targetId && targetId === sourceId; +}; + +export const replaceIdentifiersInConfig = ( + config: DataTypeRulesConfig, + genericMap: GenericMap +): DataTypeRulesConfig => { + switch (config.__typename) { + case "DataTypeRulesContainsKeyConfig": + case "DataTypeRulesContainsTypeConfig": + case "DataTypeRulesReturnTypeConfig": + case "DataTypeRulesParentTypeConfig": { + const identifier = (config as { dataTypeIdentifier?: DataTypeIdentifier }).dataTypeIdentifier; + if (!identifier) return config; + return { + ...config, + dataTypeIdentifier: replaceGenericKeysInType(identifier, genericMap) + } as DataTypeRulesConfig; + } + case "DataTypeRulesInputTypesConfig": { + const typedConfig = config; + const inputTypes = typedConfig.inputTypes?.map(inputType => ({ + ...inputType, + dataTypeIdentifier: replaceGenericKeysInType(inputType.dataTypeIdentifier!!, genericMap) + })); + return { + ...typedConfig, + inputTypes + }; + } + default: + return config; + } +}; + +export const replaceIdentifiersInRule = ( + rule: DataTypeRule, + genericMap: GenericMap +): DataTypeRule => { + switch (rule.variant) { + case "CONTAINS_KEY": { + const config = replaceIdentifiersInConfig({ + ...rule.config, + __typename: "DataTypeRulesContainsKeyConfig" + }!, genericMap) + return { + ...rule, + config + } + } + case "CONTAINS_TYPE": { + const config = replaceIdentifiersInConfig({ + ...rule.config, + __typename: "DataTypeRulesContainsTypeConfig" + }!, genericMap) + return { + ...rule, + config + } + } + case "RETURN_TYPE": { + const config = replaceIdentifiersInConfig({ + ...rule.config, + __typename: "DataTypeRulesReturnTypeConfig" + }!, genericMap) + return { + ...rule, + config + } + } + case "INPUT_TYPES": { + const config = replaceIdentifiersInConfig({ + ...rule.config, + __typename: "DataTypeRulesInputTypesConfig" + }!, genericMap) + return { + ...rule, + config + } + } + case "PARENT_TYPE": { + const config = replaceIdentifiersInConfig({ + ...rule.config, + __typename: "DataTypeRulesParentTypeConfig" + }!, genericMap) + return { + ...rule, + config + } + } + default: + return rule; + } +}; + +export const resolveGenericKeyMappings = ( + parameterType: DataTypeIdentifier, + valueType: DataTypeIdentifier, + genericKeys: string[] +): GenericMappingResult => { + const result: GenericMappingResult = {}; + const genericKeySet = new Set(genericKeys); + + const recurse = (param: DataTypeIdentifier, value: DataTypeIdentifier) => { + if (!param || !value) return; + + const paramKey = extractIdentifierGenericKey(param, genericKeySet); + if (paramKey && genericKeySet.has(paramKey)) { + if (isDataTypeIdentifier(value)) { + result[paramKey] = value; + } + return; + } + + const paramMappers = getIdentifierMappers(param); + if (paramMappers.length === 0) return; + + const valueMappers = getIdentifierMappers(value); + + for (const paramMapper of paramMappers) { + const matchingValueMapper = valueMappers.find(mapper => mapper.target === paramMapper.target); + if (!matchingValueMapper) continue; + + const keysInSources = (paramMapper.sourceDataTypeIdentifiers ?? []) + .map(source => extractIdentifierGenericKey(source, genericKeySet)) + .filter((key): key is string => !!key && genericKeySet.has(key)); + + const combination = toCombinationTypes(paramMapper); + const valueSources = matchingValueMapper.sourceDataTypeIdentifiers ?? []; + + if ( + (combination.has("AND" as GenericCombinationStrategyType.And) || combination.has("OR" as GenericCombinationStrategyType.Or)) && + valueSources.length === 1 && + keysInSources.length === (paramMapper.sourceDataTypeIdentifiers?.length ?? 0) + ) { + for (const key of keysInSources) { + result[key] = valueSources[0]; + } + } else { + const length = Math.min(paramMapper.sourceDataTypeIdentifiers?.length ?? 0, valueSources.length); + for (let index = 0; index < length; index++) { + recurse(paramMapper.sourceDataTypeIdentifiers!![index], valueSources[index]); + } + } + } + }; + + recurse(parameterType, valueType); + return result; +}; + +export const replaceGenericKeysInType = ( + type: DataTypeIdentifier, + genericMap: GenericMap +): DataTypeIdentifier => { + if (!isDataTypeIdentifier(type)) return type; + + const {genericKey, genericType} = type; + + if (genericKey && genericMap.has(genericKey)) { + const replacement = genericMap.get(genericKey); + if (replacement && isDataTypeIdentifier(replacement)) { + return replacement; + } + return type; + } + + if (!genericType) return type; + + const resolvedMappers = (genericType.genericMappers ?? []).map(mapper => { + const resolvedSources: DataTypeIdentifier[] = []; + + for (const source of mapper.sourceDataTypeIdentifiers ?? []) { + if (!source) continue; + const sourceKey = source.genericKey; + if (sourceKey && genericMap.has(sourceKey)) { + const replacement = genericMap.get(sourceKey); + if (replacement && isGenericMapper(replacement as GenericMapper)) { + resolvedSources.push(...(replacement as GenericMapper).sourceDataTypeIdentifiers!!); + } else if (replacement && isDataTypeIdentifier(replacement)) { + resolvedSources.push(replacement); + } else { + resolvedSources.push(source); + } + } else if (isDataTypeIdentifier(source)) { + resolvedSources.push(replaceGenericKeysInType(source, genericMap)); + } else { + resolvedSources.push(source as DataTypeIdentifier); + } + } + + return cloneMapperWithSources(mapper, resolvedSources); + }); + + return { + ...type, + genericType: { + ...genericType, + genericMappers: resolvedMappers + } + }; +}; + +export const resolveAllGenericKeysInDataTypeObject = ( + genericObj: DataType, + concreteObj: DataType, + genericKeys: string[] +): Record => { + const result: Record = {}; + const unresolved = new Set(genericKeys); + + const visit = ( + genericNode: unknown, + concreteNode: unknown, + parentMapper?: GenericMapper + ) => { + if (!genericNode || !concreteNode || unresolved.size === 0) return; + + if (isDataTypeIdentifier(genericNode)) { + const key = genericNode.genericKey; + if (key && unresolved.has(key)) { + if (parentMapper) { + result[key] = parentMapper; + } else if (isGenericMapper(concreteNode as GenericMapper)) { + result[key] = concreteNode as GenericMapper; + } else if (isDataTypeIdentifier(concreteNode)) { + result[key] = concreteNode; + } + unresolved.delete(key); + if (unresolved.size === 0) return; + } + } + + if (isGenericMapper(genericNode as GenericMapper) && isGenericMapper(concreteNode as GenericMapper)) { + const length = Math.min((genericNode as GenericMapper).sourceDataTypeIdentifiers?.length!!, (concreteNode as GenericMapper).sourceDataTypeIdentifiers?.length!!); + for (let index = 0; index < length; index++) { + visit((genericNode as GenericMapper).sourceDataTypeIdentifiers!![index], (concreteNode as GenericMapper).sourceDataTypeIdentifiers!![index], concreteNode as GenericMapper); + if (unresolved.size === 0) return; + } + return; + } + + if (isDataType(genericNode) && isDataType(concreteNode)) { + const genericRules = genericNode.rules?.nodes ?? []; + const concreteRules = concreteNode.rules?.nodes ?? []; + const length = Math.min(genericRules.length, concreteRules.length); + + for (let index = 0; index < length; index++) { + const genericRule = genericRules[index]; + const concreteRule = concreteRules[index]; + if (!genericRule || !concreteRule) continue; + visit(genericRule, concreteRule); + if (unresolved.size === 0) return; + } + return; + } + + if (isDataTypeRule(genericNode) && isDataTypeRule(concreteNode)) { + visit(genericNode.config, concreteNode.config); + return; + } + + if (Array.isArray(genericNode) && Array.isArray(concreteNode)) { + const length = Math.min(genericNode.length, concreteNode.length); + for (let index = 0; index < length; index++) { + visit(genericNode[index], concreteNode[index], parentMapper); + if (unresolved.size === 0) return; + } + return; + } + + if (isPlainObject(genericNode) && isPlainObject(concreteNode)) { + for (const key of Object.keys(genericNode)) { + if (!(key in concreteNode)) continue; + if (key === "__typename") continue; + visit((genericNode as Record)[key], (concreteNode as Record)[key], parentMapper); + if (unresolved.size === 0) return; + } + } + }; + + visit(genericObj, concreteObj); + return result; +}; + +export const replaceGenericKeysInDataTypeObject = ( + dataType: DataType, + genericMap: GenericMap +): DataType => { + + const resolvedRules = dataType.rules + ? { + ...dataType.rules, + nodes: dataType.rules.nodes?.map(rule => { + if (!rule) return rule; + return { + ...rule, + config: replaceIdentifiersInConfig(rule.config!!, genericMap) + } as DataTypeRule; + }) + } + : undefined; + + return { + ...dataType, + rules: resolvedRules as DataTypeRuleConnection + }; +}; + +export const resolveGenericKeys = ( + func: FunctionDefinitionView, + values: NodeParameterValue[], + dataTypeService: DatatypeService, + functionService: FunctionService, + flow?: Flow +): GenericMap => { + const genericMap: GenericMap = new Map(); + const genericKeys = func?.genericKeys ?? []; + + if (!func?.parameterDefinitions || genericKeys.length <= 0) return genericMap; + + const genericKeySet = new Set(genericKeys); + + func.parameterDefinitions.forEach((parameter, index) => { + const parameterType = parameter.dataTypeIdentifier as DataTypeIdentifier; + const value = values[index]; + const valueType = value?.__typename === "ReferenceValue" ? + (() => { + const node = flow?.nodes?.nodes?.find(n => n?.id === value.nodeFunctionId) + const lvalues = node?.parameters?.nodes?.map(p => p?.value!!) ?? [] + const funDef = functionService.getById(node?.functionDefinition?.id!!) + return useReturnType(funDef!, lvalues, dataTypeService, functionService) + })() + : dataTypeService.getTypeFromValue(value, flow) as DataTypeIdentifier; + + if (!parameterType || !valueType) return; + + const mappings = resolveGenericKeyMappings( + parameterType, + valueType, + genericKeys + ); + + for (const [key, identifier] of Object.entries(mappings)) { + if (!genericKeySet.has(key)) continue; + if (!genericMap.has(key)) { + genericMap.set(key, identifier); + } + } + }); + + return genericMap; +}; + +export function isMatchingDataTypeObject( + source: DataType, + target: DataType +): boolean { + if (source.variant !== target.variant) return false; + const targetRules = target.rules?.nodes ?? []; + if (targetRules.length === 0) return true; + + const sourceRules = source.rules?.nodes ?? []; + + for (const targetRule of targetRules) { + if (!targetRule) continue; + const found = sourceRules.some(sourceRule => { + if (!sourceRule) return false; + return ruleMatches(sourceRule, targetRule); + }); + if (!found) return false; + } + + return true; +} + +function ruleMatches(sourceRule: DataTypeRule, targetRule: DataTypeRule): boolean { + if (sourceRule.variant !== targetRule.variant) return false; + + switch (targetRule.variant) { + case "CONTAINS_TYPE": + case "RETURN_TYPE": + return identifiersMatch( + (sourceRule.config as { dataTypeIdentifier?: DataTypeIdentifier }).dataTypeIdentifier, + (targetRule.config as { dataTypeIdentifier?: DataTypeIdentifier }).dataTypeIdentifier + ); + case "CONTAINS_KEY": { + const sourceConfig = sourceRule.config as { key: string; dataTypeIdentifier?: DataTypeIdentifier }; + const targetConfig = targetRule.config as { key: string; dataTypeIdentifier?: DataTypeIdentifier }; + if (sourceConfig.key !== targetConfig.key) return false; + return identifiersMatch(sourceConfig.dataTypeIdentifier, targetConfig.dataTypeIdentifier); + } + case "INPUT_TYPES": { + const sourceConfig = sourceRule.config as { + inputTypes?: Array<{ dataTypeIdentifier: DataTypeIdentifier }> + }; + const targetConfig = targetRule.config as { + inputTypes?: Array<{ dataTypeIdentifier: DataTypeIdentifier }> + }; + const targetInputTypes = targetConfig.inputTypes ?? []; + const sourceInputTypes = sourceConfig.inputTypes ?? []; + return targetInputTypes.every(targetInput => + sourceInputTypes.some(sourceInput => + identifiersMatch(sourceInput.dataTypeIdentifier, targetInput.dataTypeIdentifier) + ) + ); + } + case "ITEM_OF_COLLECTION": { + const sourceItems = (sourceRule.config as { items?: unknown[] }).items ?? []; + const targetItems = (targetRule.config as { items?: unknown[] }).items ?? []; + if (sourceItems.length !== targetItems.length) return false; + return sourceItems.every((item, index) => item === targetItems[index]); + } + case "NUMBER_RANGE": { + const sourceConfig = sourceRule.config as { from: number; to: number; steps?: number }; + const targetConfig = targetRule.config as { from: number; to: number; steps?: number }; + return ( + sourceConfig.from === targetConfig.from && + sourceConfig.to === targetConfig.to && + sourceConfig.steps === targetConfig.steps + ); + } + case "REGEX": { + const sourcePattern = (sourceRule.config as { pattern: string }).pattern; + const targetPattern = (targetRule.config as { pattern: string }).pattern; + return sourcePattern === targetPattern; + } + default: + return JSON.stringify(normalizeObjectForComparison(sourceRule.config)) === + JSON.stringify(normalizeObjectForComparison(targetRule.config)); + } +} + +export function isMatchingType( + source: DataTypeIdentifier, + target: DataTypeIdentifier +): boolean { + const wildcard = (value: unknown): boolean => { + if (value === GENERIC_PLACEHOLDER) return true; + if (isDataTypeIdentifier(value) && value.genericKey) return true; + return false; + }; + + const deepMatch = (s: any, t: any): boolean => { + if (s && s.identifier && t && t.identifier && s?.identifier === t?.identifier) return true + if (wildcard(s) || wildcard(t)) return true; + if (s == null || t == null) return s === t; + + if (Array.isArray(t)) { + if (!Array.isArray(s) || s.length !== t.length) return false; + return s.every((value, index) => deepMatch(value, t[index])); + } + + if (isPlainObject(t)) { + if (!isPlainObject(s)) return false; + const keys = Object.keys(t); + return keys.every(key => deepMatch((s as Record)[key], (t as Record)[key])); + } + + return s === t; + }; + + const normalizedSource = normalizeObjectForComparison(source); + const normalizedTarget = normalizeObjectForComparison(target); + return deepMatch(normalizedSource, normalizedTarget); +} + +export const resolveType = ( + type: DataTypeIdentifier, + service: DatatypeService +): DataTypeIdentifier => { + const dataType = service.getDataType(type); + if (!dataType) return type; + + if (dataType.variant === "ARRAY" && dataType.identifier !== "LIST") { + const listType = service.getDataType({dataType: {identifier: "LIST"}}) + const innerTypeRule = dataType.rules?.nodes?.find(rule => rule?.variant === "CONTAINS_TYPE"); + const innerIdentifier = (innerTypeRule?.config as { + dataTypeIdentifier?: DataTypeIdentifier + })?.dataTypeIdentifier; + if (innerIdentifier && listType) { + const [genericKey] = listType?.genericKeys!; + if (!genericKey) return type; + return { + genericType: { + dataType: listType?.json as DataType, + genericMappers: [ + { + target: genericKey, + sourceDataTypeIdentifiers: [resolveType(innerIdentifier, service)] + } + ] + } + } as DataTypeIdentifier; + } + } + + return type; +}; + +const sortValue = (value: unknown): unknown => { + if (Array.isArray(value)) { + const mapped = value.map(sortValue); + if (mapped.length <= 1) return mapped; + + const allPlainObjects = mapped.every(isPlainObject); + if (allPlainObjects) { + return [...mapped].sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b))); + } + + const allPrimitive = mapped.every(item => !Array.isArray(item) && !isPlainObject(item)); + if (allPrimitive) { + return [...mapped].sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b))); + } + + return mapped; + } + + if (isPlainObject(value)) { + const recordValue = value as Record; + return Object.keys(recordValue) + .sort() + .reduce>((acc, key) => { + acc[key] = sortValue(recordValue[key]); + return acc; + }, {}); + } + + return value; +}; + +export const targetForGenericKey = (func: FunctionDefinitionView, target: DataTypeIdentifier): GenericTargetMap => { + if (!target.genericType) return new Map() + + const genericKeySet = new Set(func.genericKeys ?? []); + const targetMap: GenericTargetMap = new Map(); + + for (const mapper of target.genericType.genericMappers ?? []) { + const targetKey = mapper.target!; + if (genericKeySet.has(targetKey)) continue; + + for (const source of mapper.sourceDataTypeIdentifiers ?? []) { + const sourceKey = source.genericKey; + if (sourceKey && genericKeySet.has(sourceKey)) { + targetMap.set(sourceKey, targetKey); + } + } + } + + return targetMap + +} + +export const replaceGenericsAndSortType = ( + type: DataTypeIdentifier, + genericKeys: string[] = [] +): DataTypeIdentifier => { + const genericKeySet = new Set(genericKeys); + + const shouldReplace = (key: string | undefined | null): boolean => { + if (!key) return false; + if (genericKeySet.size === 0) return true; + return genericKeySet.has(key); + }; + + const replaceMapper = (mapper: GenericMapper): GenericMapper => { + const replaceTarget = shouldReplace(mapper.target); + const target = replaceTarget ? GENERIC_PLACEHOLDER : mapper.target; + const sources = (mapper.sourceDataTypeIdentifiers ?? []).map(source => visit(source) as DataTypeIdentifier); + + return sortValue({ + ...mapper, + target, + sourceDataTypeIdentifiers: sources + }) as GenericMapper; + }; + + const replaceIdentifier = (identifier: DataTypeIdentifier): DataTypeIdentifier => { + const replaceKey = shouldReplace(identifier.genericKey); + const genericKey = replaceKey ? GENERIC_PLACEHOLDER : identifier.genericKey; + + const replaced = { + ...identifier, + genericKey, + dataType: identifier.dataType ? (visit(identifier.dataType) as DataType) : identifier.dataType, + genericType: identifier.genericType ? (visit(identifier.genericType) as GenericType) : identifier.genericType + }; + + return sortValue(replaced) as DataTypeIdentifier; + }; + + function visit(value: unknown): unknown { + if (value == null) return value; + + if (Array.isArray(value)) { + const mapped = value.map(item => visit(item)); + return sortValue(mapped); + } + + if (isDataTypeIdentifier(value)) { + return replaceIdentifier(value as DataTypeIdentifier); + } + + if (isGenericMapper(value as GenericMapper)) { + return replaceMapper(value as GenericMapper); + } + + if (isPlainObject(value)) { + const entries = Object.entries(value as Record).reduce>((acc, [key, val]) => { + acc[key] = visit(val); + return acc; + }, {}); + + return sortValue(entries); + } + + return value; + } + + return visit(type) as DataTypeIdentifier; +}; diff --git a/src/packages/ce/src/function/components/DFlowNode.style.scss b/src/packages/ce/src/function/components/DFlowNode.style.scss new file mode 100644 index 00000000..d7eecf01 --- /dev/null +++ b/src/packages/ce/src/function/components/DFlowNode.style.scss @@ -0,0 +1,32 @@ +@use "../../styles/variables"; +@use "../../styles/helpers"; +@use "../../styles/box"; + +.d-flow-node { + border: 1px solid helpers.borderColor(); + box-shadow: none; + text-wrap: nowrap; + + &__handle { + border: none !important; + padding: 1px; + z-index: -1; + opacity: 0; + } + + &__inspection { + top: 0; + transform: translateY(-50%); + padding: variables.$xxs; + + & { + @include box.box(variables.$primary); + @include helpers.borderRadius(); + position: absolute; + } + } + + &--active { + box-shadow: 0 0 0 1px rgba(variables.$info, .5); + } +} \ No newline at end of file diff --git a/src/packages/ce/src/function/components/DFlowNode.ts b/src/packages/ce/src/function/components/DFlowNode.ts new file mode 100644 index 00000000..1da74226 --- /dev/null +++ b/src/packages/ce/src/function/components/DFlowNode.ts @@ -0,0 +1,11 @@ +import {Code0Component} from "../../utils"; +import {Flow, NodeFunction} from "@code0-tech/sagittarius-graphql-types"; + +export interface DFlowNodeProps extends Record, Code0Component { + nodeId: NodeFunction['id'] + flowId: Flow['id'] + color: string + parentNodeId?: NodeFunction['id'] + isParameter?: boolean + index?: number +} \ No newline at end of file diff --git a/src/packages/ce/src/function/components/DFlowNodeDefaultCard.tsx b/src/packages/ce/src/function/components/DFlowNodeDefaultCard.tsx new file mode 100644 index 00000000..a52778f1 --- /dev/null +++ b/src/packages/ce/src/function/components/DFlowNodeDefaultCard.tsx @@ -0,0 +1,170 @@ +import {underlineBySeverity} from "../../utils"; +import {Handle, Node, NodeProps, Position, useReactFlow, useStore} from "@xyflow/react"; +import React, {CSSProperties, memo} from "react"; +import {Card} from "../card/Card"; +import "./DFlowNode.style.scss"; +import {Flex} from "../flex/Flex"; +import {IconNote} from "@tabler/icons-react"; +import {Text} from "../text/Text"; +import {useService, useStore as usePictorStore} from "../../utils/contextStore"; +import {DFlowFunctionReactiveService} from "../d-flow-function"; +import {useNodeValidation} from "../d-flow-validation/DNodeValidation.hook"; +import {DFlowReactiveService} from "../d-flow"; +import {FileTabsService} from "../file-tabs/FileTabs.service"; +import {DFlowTabDefault} from "../d-flow-file/DFlowTabDefault"; +import {Badge} from "../badge/Badge"; +import {DFlowInputLiteralBadge} from "../d-flow-input/DFlowInputLiteralBadge"; +import {DFlowInputReferenceBadge} from "../d-flow-input/DFlowInputReferenceBadge"; +import {DFlowInputNodeBadge} from "../d-flow-input/DFlowInputNodeBadge"; +import {DFlowNodeProps} from "./DFlowNode"; + +export type DFlowNodeDefaultCardProps = NodeProps> + +export const DFlowNodeDefaultCard: React.FC = memo((props) => { + const {data, id, width = 0, height = 0} = props + + const viewportWidth = useStore(s => s.width); + const viewportHeight = useStore(s => s.height); + const flowInstance = useReactFlow() + const fileTabsService = useService(FileTabsService) + const fileTabsStore = usePictorStore(FileTabsService) + const flowService = useService(DFlowReactiveService) + const flowStore = usePictorStore(DFlowReactiveService) + const functionService = useService(DFlowFunctionReactiveService) + const functionStore = usePictorStore(DFlowFunctionReactiveService) + + const node = React.useMemo(() => flowService.getNodeById(data.flowId, data.nodeId), [flowStore, data]) + const definition = React.useMemo(() => node ? functionService.getById(node.functionDefinition?.id!!) : undefined, [functionStore, data, node]) + const validation = useNodeValidation(data.nodeId, data.flowId) + const activeTabId = React.useMemo(() => { + return fileTabsService.getActiveTab()?.id + }, [fileTabsStore, fileTabsService]); + + const firstItem = useStore((s) => { + const children = s.nodes.filter((n) => n.parentId === props.parentId); + let start: any | undefined = undefined; + children.forEach((n) => { + const idx = (n.data as any)?.index ?? Infinity; + const startIdx = (start?.data as any)?.index ?? Infinity; + if (!start || idx < startIdx) { + start = n; + } + }); + return start; + }) + + const splitTemplate = (str: string) => + str + .split(/(\$\{[^}]+\})/) + .filter(Boolean) + .flatMap(part => + part.startsWith("${") + ? [part.slice(2, -1)] // variable name ohne ${} + : part.split(/(\s*,\s*)/) // Kommas einzeln extrahieren + .filter(Boolean) + .flatMap(p => p.trim() === "," ? [","] : p.trim() ? [p.trim()] : []) + ); + + const displayMessage = React.useMemo(() => splitTemplate(definition?.displayMessages!![0]?.content ?? "").map(item => { + const param = node?.parameters?.nodes?.find(p => { + const parameterDefinition = definition?.parameterDefinitions?.find(pd => pd.id == p?.parameterDefinition?.id) + return parameterDefinition?.identifier == item + }) + + const parameterValidation = validation?.filter(v => v.parameterId === param?.id) + const decorationStyle: CSSProperties = + parameterValidation?.length + ? underlineBySeverity[parameterValidation[0].type] + : {}; + + if (param) { + switch (param?.value?.__typename) { + case "LiteralValue": + return
    + +
    + case "ReferenceValue": + return
    + +
    + case "NodeFunctionIdWrapper": + return
    + + +
    + } + return + + {item} + + + } + return " " + String(item) + " " + }), [flowStore, functionStore, data, definition]) + + React.useEffect(() => { + if (!node?.id) return + fileTabsService.registerTab({ + id: node.id, + active: false, + closeable: true, + children: <> + + {definition?.names!![0]?.content} + , + content: + }) + }, [node?.id, definition, data]) + + return ( + { + flowInstance.setViewport({ + x: (viewportWidth / 2) + (props.positionAbsoluteX * -1) - (width / 2), + y: (viewportHeight / 2) + (props.positionAbsoluteY * -1) - (height / 2), + zoom: 1 + }, { + duration: 250, + }) + fileTabsService.activateTab(node?.id!!) + }} style={{position: "relative"}}> + + + + {/* Ausgang */} + + + + {displayMessage} + + + ); +}) diff --git a/src/packages/ce/src/function/components/DFlowNodeGroupCard.tsx b/src/packages/ce/src/function/components/DFlowNodeGroupCard.tsx new file mode 100644 index 00000000..0bb7cc8e --- /dev/null +++ b/src/packages/ce/src/function/components/DFlowNodeGroupCard.tsx @@ -0,0 +1,86 @@ +import React, {memo} from "react"; +import {Handle, Node, NodeProps, Position} from "@xyflow/react"; +import {Card} from "../card/Card"; +import {DFlowNodeProps} from "./DFlowNode"; + +export type DFlowNodeGroupCardProps = NodeProps> + +export const DFlowNodeGroupCard: React.FC = memo((props) => { + + const {data, id} = props + + return ( + + + + + ); +}); + +/* =========================== + Color utilities + =========================== */ + +const clamp01 = (v: number) => Math.min(Math.max(v, 0), 1) + +const parseCssColorToRgba = (color: string): any => { + if (typeof document === "undefined") { + return {r: 0, g: 0, b: 0, a: 1} + } + + const el = document.createElement("span") + el.style.color = color + document.body.appendChild(el) + + const computed = getComputedStyle(el).color + document.body.removeChild(el) + + const match = computed.match( + /rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)(?:\s*,\s*([\d.]+))?\s*\)/ + ) + + if (!match) { + return {r: 0, g: 0, b: 0, a: 1} + } + + return { + r: Math.round(Number(match[1])), + g: Math.round(Number(match[2])), + b: Math.round(Number(match[3])), + a: match[4] !== undefined ? Number(match[4]) : 1, + } +} + +const mixColorRgb = (color: string, level: number) => { + const w = clamp01(level * 0.1) + + const c1 = parseCssColorToRgba(color) + const c2 = parseCssColorToRgba("#030014") + + const mix = (a: number, b: number) => + Math.round(a * (1 - w) + b * w) + + return `rgb(${mix(c1.r, c2.r)}, ${mix(c1.g, c2.g)}, ${mix(c1.b, c2.b)})` +} + +const withAlpha = (color: string, alpha: number) => { + const c = parseCssColorToRgba(color) + return `rgba(${c.r}, ${c.g}, ${c.b}, ${clamp01(alpha)})` +} diff --git a/src/packages/ce/src/function/components/DFlowNodeTriggerCard.tsx b/src/packages/ce/src/function/components/DFlowNodeTriggerCard.tsx new file mode 100644 index 00000000..9851ae2e --- /dev/null +++ b/src/packages/ce/src/function/components/DFlowNodeTriggerCard.tsx @@ -0,0 +1,93 @@ +import React, {memo} from "react"; +import {Handle, Node, NodeProps, Position, useReactFlow, useStore} from "@xyflow/react"; +import {Text} from "../text/Text"; +import {useService, useStore as usePictorStore} from "../../utils/contextStore"; +import {FileTabsService} from "../file-tabs/FileTabs.service"; +import {Card} from "../card/Card"; +import {Flex} from "../flex/Flex"; +import {IconBolt} from "@tabler/icons-react"; +import {DFlowTabTrigger} from "../d-flow-file/DFlowTabTrigger"; +import {DFlowTypeReactiveService} from "../d-flow-type"; +import {Badge} from "../badge/Badge"; +import {DFlowNodeProps} from "./DFlowNode"; +import {DFlowReactiveService} from "../d-flow"; + + +export type DFlowNodeTriggerCardProps = NodeProps> + +export const DFlowNodeTriggerCard: React.FC = memo((props) => { + + const {data, id} = props + const fileTabsService = useService(FileTabsService) + const flowInstance = useReactFlow() + const flowTypeService = useService(DFlowTypeReactiveService) + const flowTypeStore = usePictorStore(DFlowTypeReactiveService) + const flowService = useService(DFlowReactiveService) + const flowStore = usePictorStore(DFlowReactiveService) + + const flow = React.useMemo(() => flowService.getById(data.flowId), [flowStore, data]) + const definition = React.useMemo(() => flow ? flowTypeService.getById(flow.type?.id) : undefined, [flowTypeStore, flow]) + + const width = props.width ?? 0 + const height = props.height ?? 0 + const viewportWidth = useStore(s => s.width) + const viewportHeight = useStore(s => s.height) + + React.useEffect(() => { + if (!definition?.id || !flow) return + fileTabsService.registerTab({ + id: definition?.id!!, + active: true, + closeable: true, + children: <> + + {definition?.names!![0]?.content} + , + content: , + show: true + }) + }, [definition, data.instance, fileTabsService, flow]) + + return { + flowInstance.setViewport({ + x: (viewportWidth / 2) + (props.positionAbsoluteX * -1) - (width / 2), + y: (viewportHeight / 2) + (props.positionAbsoluteY * -1) - (height / 2), + zoom: 1 + }, { + duration: 250, + }) + fileTabsService.activateTab(definition?.id!!) + }}> + + + Starting node + + + + + + {definition?.displayMessages!![0]?.content ?? definition?.id} + + + + + + + +}) \ No newline at end of file diff --git a/src/packages/ce/src/function/components/DFlowSuggestion.view.ts b/src/packages/ce/src/function/components/DFlowSuggestion.view.ts new file mode 100644 index 00000000..ea1230b2 --- /dev/null +++ b/src/packages/ce/src/function/components/DFlowSuggestion.view.ts @@ -0,0 +1,21 @@ +import type { + LiteralValue, + NodeFunction, + ReferenceValue +} from "@code0-tech/sagittarius-graphql-types"; + +export enum DFlowSuggestionType { + REF_OBJECT, + VALUE, + FUNCTION, + FUNCTION_COMBINATION, + DATA_TYPE, +} + +export interface DFlowSuggestion { + + displayText: string[] + path: number[] + value: LiteralValue | ReferenceValue | NodeFunction + type: DFlowSuggestionType +} \ No newline at end of file diff --git a/src/packages/ce/src/function/components/DFlowSuggestionMenu.tsx b/src/packages/ce/src/function/components/DFlowSuggestionMenu.tsx new file mode 100644 index 00000000..bf23d3f9 --- /dev/null +++ b/src/packages/ce/src/function/components/DFlowSuggestionMenu.tsx @@ -0,0 +1,73 @@ +import {Menu, MenuPortal, MenuSeparator, MenuTrigger} from "../menu/Menu"; +import React from "react"; +import {DFlowSuggestion} from "./DFlowSuggestion.view"; +import {DFlowSuggestionMenuFooter} from "./DFlowSuggestionMenuFooter"; +import { + InputSuggestionMenuContent, + InputSuggestionMenuContentItems, + InputSuggestionMenuContentItemsHandle +} from "../form"; +import {toInputSuggestions} from "./DFlowSuggestionMenu.util"; +import {DFlowSuggestionMenuSearchBar} from "./DFlowSuggestionMenuSearchBar"; +import {useStoreApi} from "@xyflow/react"; +import {Card} from "../card/Card"; + +export interface DFlowSuggestionMenuProps { + triggerContent: React.ReactNode + suggestions?: DFlowSuggestion[] + onSuggestionSelect?: (suggestion: DFlowSuggestion) => void +} + +export const DFlowSuggestionMenu: React.FC = (props) => { + + const {suggestions = [], triggerContent, onSuggestionSelect = () => {}} = props + + const menuRef = React.useRef(null); // Ref to suggestion list + const [stateSuggestions, setStateSuggestions] = React.useState(suggestions) + + React.useEffect(() => { + setStateSuggestions(suggestions) + }, [suggestions]) + + return + + {triggerContent} + + + + { + + if (event.key === "ArrowDown") { + event.preventDefault(); + menuRef.current?.focusFirstItem(); // Navigate down + } else if (event.key === "ArrowUp") { + event.preventDefault(); + menuRef.current?.focusLastItem(); // Navigate up + } + + // @ts-ignore + const searchTerm = event.target.value + setStateSuggestions(suggestions.filter(suggestion => { + return suggestion.displayText.some(text => { + return text.includes(searchTerm) + }) + })) + event.preventDefault() + return false + }}/> + + { + onSuggestionSelect(suggestion.valueData as DFlowSuggestion) + }} + /> + + + + + + +} \ No newline at end of file diff --git a/src/packages/ce/src/function/components/DFlowSuggestionMenu.util.tsx b/src/packages/ce/src/function/components/DFlowSuggestionMenu.util.tsx new file mode 100644 index 00000000..1c3a78ca --- /dev/null +++ b/src/packages/ce/src/function/components/DFlowSuggestionMenu.util.tsx @@ -0,0 +1,59 @@ +import {DFlowSuggestion, DFlowSuggestionType} from "./DFlowSuggestion.view"; +import {InputSuggestion} from "../form"; +import React from "react"; +import {IconCircleDot, IconCirclesRelation, IconFileFunctionFilled} from "@tabler/icons-react"; +import {Text} from "../text/Text"; + +export const toInputSuggestions = (suggestions: DFlowSuggestion[]): InputSuggestion[] => { + + const staticGroupLabels: Partial> = { + [DFlowSuggestionType.VALUE]: "Values", + [DFlowSuggestionType.REF_OBJECT]: "Variables", + [DFlowSuggestionType.DATA_TYPE]: "Datatypes", + } + + return suggestions.map(suggestion => { + + const iconMap: Record = { + [DFlowSuggestionType.FUNCTION]: , + [DFlowSuggestionType.FUNCTION_COMBINATION]: , + [DFlowSuggestionType.REF_OBJECT]: , + [DFlowSuggestionType.VALUE]: , + [DFlowSuggestionType.DATA_TYPE]: , + } + + const children: React.ReactNode = <> + {iconMap[suggestion.type]} +
    + + {suggestion.displayText.map((text, idx) => ( + {text} + ))} + +
    + + + let groupLabel: string | undefined = staticGroupLabels[suggestion.type] + + if (suggestion.type === DFlowSuggestionType.FUNCTION || suggestion.type === DFlowSuggestionType.FUNCTION_COMBINATION) { + const runtimeIdentifier = suggestion.value.__typename === "NodeFunction" + ? suggestion.value.functionDefinition?.runtimeFunctionDefinition?.identifier + : undefined + + if (runtimeIdentifier) { + const [runtime, pkg] = runtimeIdentifier.split("::") + if (runtime && pkg) { + groupLabel = `${runtime}::${pkg}` + } + } + } + + return { + children, + insertMode: "replace", + valueData: suggestion, + value: suggestion.value, + groupBy: groupLabel, + }; + }) +} diff --git a/src/packages/ce/src/function/components/DFlowSuggestionMenuFooter.tsx b/src/packages/ce/src/function/components/DFlowSuggestionMenuFooter.tsx new file mode 100644 index 00000000..b4c9dfd2 --- /dev/null +++ b/src/packages/ce/src/function/components/DFlowSuggestionMenuFooter.tsx @@ -0,0 +1,53 @@ +"use client"; + +import React from "react"; +import {MenuLabel} from "../menu/Menu"; +import { + IconArrowsShuffle, + IconBulb, + IconCircleDot, + IconCirclesRelation, + IconCornerDownLeft, + IconFileFunctionFilled +} from "@tabler/icons-react"; +import {Text} from "../text/Text"; +import {Flex} from "../flex/Flex"; +import {Tooltip, TooltipContent, TooltipPortal, TooltipTrigger} from "../tooltip/Tooltip"; + +export const DFlowSuggestionMenuFooter: React.FC = () => { + return + + Press to insert + + + + + + + + + + + FUNCTION + + + + FUNCTION COMBINATION + + + + VARIABLE + + + + VALUE + + + + + + + + + +} \ No newline at end of file diff --git a/src/packages/ce/src/function/components/DFlowSuggestionMenuSearchBar.tsx b/src/packages/ce/src/function/components/DFlowSuggestionMenuSearchBar.tsx new file mode 100644 index 00000000..21064a22 --- /dev/null +++ b/src/packages/ce/src/function/components/DFlowSuggestionMenuSearchBar.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import {DFlowSuggestionSearchInput} from "./DFlowSuggestionSearchInput"; +import {IconSearch} from "@tabler/icons-react"; +import {Code0Component} from "../../utils"; + +export interface DFlowSuggestionMenuSearchBarProps extends Code0Component { + onType: (event: React.KeyboardEvent) => void +} + +export const DFlowSuggestionMenuSearchBar: React.FC = (props) => { + return props.onType(event)} + clearable + style={{background: "none", boxShadow: "none"}} + autoFocus + left={} + /> +} \ No newline at end of file diff --git a/src/packages/ce/src/function/components/DFlowSuggestionSearchInput.style.scss b/src/packages/ce/src/function/components/DFlowSuggestionSearchInput.style.scss new file mode 100644 index 00000000..e9e8d036 --- /dev/null +++ b/src/packages/ce/src/function/components/DFlowSuggestionSearchInput.style.scss @@ -0,0 +1,17 @@ +@use "../../styles/variables"; +@use "../../styles/helpers"; + +.d-flow-suggestion-search-input { + border: none !important; + background: none !important; + margin: -1 * variables.$xxs; + padding-left: variables.$xxs; + padding-right: variables.$xxs; + border-radius: 0 !important; + box-shadow: none !important; + + > input { + padding-top: 0 !important; + padding-bottom: 0 !important; + } +} \ No newline at end of file diff --git a/src/packages/ce/src/function/components/DFlowSuggestionSearchInput.tsx b/src/packages/ce/src/function/components/DFlowSuggestionSearchInput.tsx new file mode 100644 index 00000000..52ea31ad --- /dev/null +++ b/src/packages/ce/src/function/components/DFlowSuggestionSearchInput.tsx @@ -0,0 +1,39 @@ +import {InputProps, Input} from "../form"; +import React, {RefObject} from "react"; +import {Button} from "../button/Button"; +import {IconX} from "@tabler/icons-react"; +import "./DFlowSuggestionSearchInput.style.scss" +import {clearInputElement} from "../form/Input.utils"; + +interface DFlowSuggestionSearchInputProps extends Omit, "wrapperComponent" | "type"> { + //defaults to false + clearable?: boolean +} + +export const DFlowSuggestionSearchInput: React.ForwardRefExoticComponent = React.forwardRef((props, ref: RefObject) => { + ref = ref || React.useRef(null) + + const { + clearable = false, + right, + ...rest + } = props + + const toClearable = () => { + clearInputElement(ref.current) + } + + const rightAction = [right] + clearable && rightAction.push() + + + return } + {...rest} + /> +}) diff --git a/src/packages/ce/src/function/components/DFlowTabDefault.tsx b/src/packages/ce/src/function/components/DFlowTabDefault.tsx new file mode 100644 index 00000000..cad74833 --- /dev/null +++ b/src/packages/ce/src/function/components/DFlowTabDefault.tsx @@ -0,0 +1,156 @@ +import React from "react"; +import {useForm} from "../form"; +import {Flex} from "../flex/Flex"; +import {useService, useStore} from "../../utils"; +import {DFlowFunctionReactiveService, ParameterDefinitionView} from "../d-flow-function"; +import {DFlowReactiveService} from "../d-flow"; +import { + Flow, + LiteralValue, + NodeFunction, + NodeParameterValue, + ReferenceValue, + Scalars +} from "@code0-tech/sagittarius-graphql-types"; +import {InputSyntaxSegment} from "../form"; +import {useNodeValidation} from "../d-flow-validation/DNodeValidation.hook"; +import {FileTabsService} from "../file-tabs/FileTabs.service"; +import {DFlowInput} from "../d-flow-input/DFlowInput"; + +export interface DFlowTabDefaultProps { + node: NodeFunction + flowId: Flow['id'] +} + +export const DFlowTabDefault: React.FC = (props) => { + + const {node, flowId} = props + + const functionService = useService(DFlowFunctionReactiveService) + const functionStore = useStore(DFlowFunctionReactiveService) + const flowService = useService(DFlowReactiveService) + const flowStore = useStore(DFlowReactiveService) + const fileTabsService = useService(FileTabsService) + const validation = useNodeValidation(node.id, flowId) + + const changedParameters = React.useRef>(new Set()) + const [, startTransition] = React.useTransition() + + const definition = React.useMemo(() => { + return functionService.getById(node.functionDefinition?.id!!) + }, [functionStore]) + + const paramDefinitions = React.useMemo(() => { + const map: Record = {} + definition?.parameterDefinitions?.forEach(pd => { + map[pd.id!!] = pd + }) + return map + }, [definition]) + + const sortedParameters = React.useMemo(() => { + return [...(node.parameters?.nodes || [])].sort((a, b) => a!!.id!!.localeCompare(b?.id!!)) + }, [node]) + + const initialValues = React.useMemo(() => { + const values: Record = {} + sortedParameters.forEach(parameter => { + values[parameter?.id!!] = parameter?.value?.__typename === "LiteralValue" ? (typeof parameter.value?.value === "object" && parameter.value?.value != null ? JSON.stringify(parameter.value?.value) : parameter.value.value) : parameter?.value != null ? JSON.stringify(parameter?.value) : parameter?.value + }) + return values + }, [sortedParameters]) + + const validations = React.useMemo(() => { + const values: Record = {} + sortedParameters.forEach(parameter => { + values[parameter?.id!!] = (_: any) => { + const validationForParameter = validation?.find(v => v.parameterId === parameter?.id) + if (validationForParameter) { + return validationForParameter.message!![0]?.content || "Invalid value" + } + return null + } + }) + return values + }, [sortedParameters, validation]) + + const onSubmit = React.useCallback((values: any) => { + startTransition(async () => { + for (const paramDefinitions1 of sortedParameters) { + if (!changedParameters.current.has(paramDefinitions1?.id!!)) continue; + const syntaxSegment = values[paramDefinitions1?.id!] + const previousValue = paramDefinitions1?.value as NodeParameterValue + const syntaxValue = syntaxSegment?.[0]?.value ?? syntaxSegment?.value ?? syntaxSegment as NodeFunction | LiteralValue | ReferenceValue + + if (previousValue && previousValue.__typename === "NodeFunctionIdWrapper" && previousValue.id) { + const linkedNodes = flowService.getLinkedNodesById(flowId, previousValue.id) + linkedNodes.reverse().forEach(node => { + if (node.id) fileTabsService.deleteById(node.id) + }) + } + + if (!syntaxValue || !syntaxSegment) { + await flowService.setParameterValue(flowId, node.id!!, paramDefinitions1!!.id!!, undefined); + } + + try { + const parsedSyntaxValue = Number.isNaN(Number(syntaxValue)) ? JSON.parse(syntaxValue) : syntaxValue + if (!parsedSyntaxValue?.__typename) { + await flowService.setParameterValue(flowId, node.id!!, paramDefinitions1!!.id!!, syntaxValue ? { + __typename: "LiteralValue", + value: parsedSyntaxValue === null || parsedSyntaxValue === undefined ? String(parsedSyntaxValue) : parsedSyntaxValue + } : undefined); + continue; + } + } catch (e) { + if (!syntaxValue?.__typename) { + await flowService.setParameterValue(flowId, node.id!!, paramDefinitions1!!.id!!, syntaxValue ? { + __typename: "LiteralValue", + value: syntaxValue, + } : undefined); + continue; + } + } + + const parsedSyntaxValue = typeof syntaxValue === "object" ? syntaxValue : JSON.parse(syntaxValue) + + await flowService.setParameterValue(flowId, node.id!!, paramDefinitions1!!.id!!, parsedSyntaxValue.__typename === "LiteralValue" ? (!!parsedSyntaxValue.value ? parsedSyntaxValue : undefined) : parsedSyntaxValue); + } + changedParameters.current.clear() + }) + }, [flowStore, sortedParameters]) + + const [inputs, validate] = useForm>({ + initialValues: initialValues, + validate: validations, + truthyValidationBeforeSubmit: false, + onSubmit: onSubmit + }) + + return + {sortedParameters.map(parameter => { + + if (!parameter) return null + + const parameterDefinition = paramDefinitions[parameter?.parameterDefinition?.id!!] + const title = parameterDefinition?.names ? parameterDefinition?.names!![0]?.content : parameterDefinition?.id + const description = parameterDefinition?.descriptions ? parameterDefinition?.descriptions!![0]?.content : JSON.stringify(parameterDefinition?.dataTypeIdentifier) + + return
    + {/*@ts-ignore*/} + { + changedParameters.current.add(parameter.id!!) + validate() + }} + {...inputs.getInputProps(parameter.id!!)} + /> +
    + })} +
    +} diff --git a/src/packages/ce/src/function/components/DFlowTabTrigger.tsx b/src/packages/ce/src/function/components/DFlowTabTrigger.tsx new file mode 100644 index 00000000..d72ec833 --- /dev/null +++ b/src/packages/ce/src/function/components/DFlowTabTrigger.tsx @@ -0,0 +1,121 @@ +import React from "react"; +import {useService} from "../../utils"; +import {DFlowReactiveService} from "../d-flow"; +import {Flex} from "../flex/Flex"; +import {DFlowTypeReactiveService} from "../d-flow-type"; +import {DFlowSuggestion} from "../d-flow-suggestion"; +import {useValueSuggestions} from "../d-flow-suggestion/DFlowValueSuggestions.hook"; +import {useDataTypeSuggestions} from "../d-flow-suggestion/DFlowDataTypeSuggestions.hook"; +import {toInputSuggestions} from "../d-flow-suggestion/DFlowSuggestionMenu.util"; +import {DataType, DataTypeRulesVariant, Flow, NodeParameterValue, LiteralValue, Scalars} from "@code0-tech/sagittarius-graphql-types"; +import {DFlowInputDefault} from "../d-flow-input/DFlowInputDefault"; +import {DFlowInputDataType} from "../d-flow-input/DataTypeTypeInput"; +import {DFlowDataTypeReactiveService} from "../d-flow-data-type"; + +export interface DFlowTabTriggerProps { + instance: Flow +} + +export const DFlowTabTrigger: React.FC = (props) => { + + const {instance} = props + + const flowTypeService = useService(DFlowTypeReactiveService) + const flowService = useService(DFlowReactiveService) + const dataTypeService = useService(DFlowDataTypeReactiveService) + const [, startTransition] = React.useTransition() + + const definition = flowTypeService.getById(instance.type?.id!!) + + const suggestionsById: Record = {} + definition?.flowTypeSettings?.forEach(settingDefinition => { + const dataTypeIdentifier = {dataType: settingDefinition.dataType} + const valueSuggestions = useValueSuggestions(dataTypeIdentifier) + const dataTypeSuggestions = useDataTypeSuggestions(dataTypeIdentifier) + suggestionsById[settingDefinition.identifier!!] = [ + ...valueSuggestions, + ...dataTypeSuggestions, + ].sort() + }) + + const testDataType = dataTypeService.getTypeFromValue({ + __typename: "LiteralValue", + value: { + body: { + users: [ + { + username: "john_doe", + email: "test@test.de", + } + ], + test: "sd" + }, + headers: { + username: "john_doe", + email: "sd", + } + } + }) + + return + {definition?.inputType ? console.log(dataTypeIdentifier)}/> : null} + {definition?.flowTypeSettings?.map(settingDefinition => { + const setting = instance.settings?.nodes?.find(s => s?.flowSettingIdentifier == settingDefinition.identifier) + const title = settingDefinition.names!![0]?.content ?? "" + const description = settingDefinition?.descriptions!![0]?.content ?? "" + const result = suggestionsById[settingDefinition.identifier!!] + + + const defaultValue = setting?.value?.__typename === "LiteralValue" ? typeof setting?.value == "object" ? JSON.stringify(setting?.value) : setting?.value : typeof setting?.value == "object" ? JSON.stringify(setting?.value) : setting?.value + + const submitValue = (value: NodeParameterValue) => { + startTransition(async () => { + if (value?.__typename == "LiteralValue" && settingDefinition.identifier) { + await flowService.setSettingValue(props.instance.id, String(settingDefinition.identifier), value.value) + } else if (settingDefinition.identifier) { + await flowService.setSettingValue(props.instance.id, String(settingDefinition.identifier), value) + } + }) + + } + + const submitValueEvent = (event: any) => { + try { + const value = JSON.parse(event.target.value) as Scalars['JSON']['output'] + if (value.__typename == "LiteralValue") { + submitValue(value.value) + return + } + submitValue(value) + } catch (e) { + submitValue({ + value: event.target.innerText, + __typename: "LiteralValue" + } as LiteralValue) + } + } + + return
    + { + submitValue(suggestion.value) + }} + suggestions={toInputSuggestions(result)} + /> +
    + })} +
    +} diff --git a/src/packages/ce/src/function/components/DFlowTabs.tsx b/src/packages/ce/src/function/components/DFlowTabs.tsx new file mode 100644 index 00000000..1cb44a02 --- /dev/null +++ b/src/packages/ce/src/function/components/DFlowTabs.tsx @@ -0,0 +1,163 @@ +import {useService, useStore} from "../../utils"; +import {FileTabsService} from "../file-tabs/FileTabs.service"; +import {FileTabs, FileTabsContent, FileTabsList, FileTabsTrigger} from "../file-tabs/FileTabs"; +import React from "react"; +import {Menu, MenuContent, MenuItem, MenuLabel, MenuPortal, MenuSeparator, MenuTrigger} from "../menu/Menu"; +import {Button} from "../button/Button"; +import {IconDotsVertical, IconPlus} from "@tabler/icons-react"; +import {FileTabsView} from "../file-tabs/FileTabs.view"; +import {DLayout} from "../d-layout/DLayout"; +import {ButtonGroup} from "../button-group/ButtonGroup"; +import {Flow, type Namespace, type NamespaceProject} from "@code0-tech/sagittarius-graphql-types"; +import {DFlowReactiveService} from "../d-flow"; +import {DFlowTypeReactiveService} from "../d-flow-type"; + +export interface DFlowTabsProps { + flowId: Flow['id'] + namespaceId: Namespace['id'] + projectId: NamespaceProject['id'] +} + +export const DFlowTabs: React.FC = (props) => { + + const {flowId, namespaceId, projectId} = props + + const fileTabsService = useService(FileTabsService) + const fileTabsStore = useStore(FileTabsService) + const flowService = useService(DFlowReactiveService) + const flowStore = useStore(DFlowReactiveService) + const flowTypeService = useService(DFlowTypeReactiveService) + const flowTypeStore = useStore(DFlowTypeReactiveService) + const id = React.useId() + + const flow = React.useMemo(() => flowService.getById(flowId, {namespaceId, projectId}), [flowStore]) + const flowType = React.useMemo(() => flowTypeService.getById(flow?.type?.id!!), [flowTypeStore, flow]) + const activeTabId = React.useMemo(() => { + return fileTabsStore.find((t: any) => (t as any).active)?.id ?? fileTabsService.getActiveTab()?.id; + }, [fileTabsStore, fileTabsService]) + + const triggerTab = React.useMemo(() => { + if (!flowType?.id) return undefined + return fileTabsService.values().find((tab: FileTabsView) => tab.id === flowType.id) + }, [fileTabsStore, flowType]) + + const visibleTabs = React.useMemo(() => { + return fileTabsService.values().filter((tab: FileTabsView) => tab.show) + }, [fileTabsStore, triggerTab]) + + const hiddenTabs = React.useMemo(() => { + return fileTabsService.values().filter((tab: FileTabsView) => !tab.show && tab.id !== triggerTab?.id) + }, [fileTabsStore, triggerTab]) + + React.useEffect(() => { + setTimeout(() => { + const parent = document.querySelector("[data-id=" + '"' + id + '"' + "]") as HTMLDivElement + const tabList = parent.querySelector(".file-tabs__list-content") as HTMLDivElement + const trigger = tabList.querySelector("[data-value=" + '"' + fileTabsService.getActiveTab()?.id + '"' + "]") as HTMLDivElement + + if (tabList && trigger) { + const offset = (trigger.offsetLeft + (trigger.offsetWidth / 2)) - (tabList.offsetWidth / 2) + tabList.scrollLeft = 0 //reset to 0 + tabList.scrollBy({ + left: offset, + behavior: 'smooth' + }); + } + }, 0) + }, [activeTabId, id]) + + return ( + { + fileTabsService.activateTab(value); + }} + > + + + + + + + + Starting Node + {triggerTab && + fileTabsService.activateTab(triggerTab.id!!)}> + {triggerTab.children} + } + + Opened Nodes + {visibleTabs.map((tab: FileTabsView) => ( + { + fileTabsService.activateTab(tab.id!) + }}> + {tab.children} + + ))} + + Available Node + {hiddenTabs.map((tab: FileTabsView) => ( + { + fileTabsService.activateTab(tab.id!) + }}> + {tab.children} + + ))} + + + + + + + + + + + fileTabsService.clearAll()}>Close all tabs + fileTabsService.clearWithoutActive()}>Close other + tabs + + fileTabsService.clearLeft()}>Close all tabs to + left + fileTabsService.clearRight()}>Close all tabs to + right + + + + + } + > + {visibleTabs.map((tab: FileTabsView, _: number) => { + return tab.show && { + fileTabsService.removeTabById(tab.id!!) + }} + > + {tab.children} + + })} + }> + <> + {fileTabsService.values().map((tab: FileTabsView) => ( + + {tab.content} + + ))} + + + + ); + +} \ No newline at end of file diff --git a/src/packages/ce/src/function/hooks/DFlowDataTypeSuggestions.hook.tsx b/src/packages/ce/src/function/hooks/DFlowDataTypeSuggestions.hook.tsx new file mode 100644 index 00000000..53027f78 --- /dev/null +++ b/src/packages/ce/src/function/hooks/DFlowDataTypeSuggestions.hook.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import {useService, useStore} from "../../utils"; +import {DFlowDataTypeReactiveService} from "../d-flow-data-type"; +import {DFlowSuggestion, DFlowSuggestionType} from "./DFlowSuggestion.view"; +import type {DataTypeIdentifier} from "@code0-tech/sagittarius-graphql-types"; + +export const useDataTypeSuggestions = (dataTypeIdentifier?: DataTypeIdentifier): DFlowSuggestion[] => { + const dataTypeService = useService(DFlowDataTypeReactiveService) + const dataTypeStore = useStore(DFlowDataTypeReactiveService) + + const dataType = React.useMemo(() => ( + dataTypeIdentifier ? dataTypeService?.getDataType(dataTypeIdentifier) : undefined + ), [dataTypeIdentifier, dataTypeService, dataTypeStore]) + + // @ts-ignore + return React.useMemo(() => { + if (!dataType || dataType.variant !== "DATA_TYPE") return [] + + return dataTypeService.values().map(nextDataType => ({ + path: [], + type: DFlowSuggestionType.DATA_TYPE, + displayText: [nextDataType.name!![0]?.content!], + /*@ts-ignore*/ + value: nextDataType.json, + })) + }, [dataType, dataTypeService, dataTypeStore]) +} diff --git a/src/packages/ce/src/function/hooks/DFlowFunction.return.hook.ts b/src/packages/ce/src/function/hooks/DFlowFunction.return.hook.ts new file mode 100644 index 00000000..fb759bdb --- /dev/null +++ b/src/packages/ce/src/function/hooks/DFlowFunction.return.hook.ts @@ -0,0 +1,19 @@ +import {FunctionDefinitionView} from "./DFlowFunction.view"; +import {DFlowDataTypeReactiveService} from "../d-flow-data-type"; +import {replaceGenericKeysInType, resolveGenericKeys} from "../../utils/generics"; +import type {DataTypeIdentifier, NodeParameterValue} from "@code0-tech/sagittarius-graphql-types"; +import {DFlowFunctionReactiveService} from "./DFlowFunction.service"; + +export const useReturnType = ( + func: FunctionDefinitionView, + values: NodeParameterValue[], + dataTypeService: DFlowDataTypeReactiveService, + functionService: DFlowFunctionReactiveService, +): DataTypeIdentifier | null => { + + if (!func?.returnType) return null + + const genericTypeMap = resolveGenericKeys(func, values, dataTypeService, functionService) + return replaceGenericKeysInType(func.returnType, genericTypeMap) + +} \ No newline at end of file diff --git a/src/packages/ce/src/function/hooks/DFlowFunctionSuggestions.hook.tsx b/src/packages/ce/src/function/hooks/DFlowFunctionSuggestions.hook.tsx new file mode 100644 index 00000000..61f43884 --- /dev/null +++ b/src/packages/ce/src/function/hooks/DFlowFunctionSuggestions.hook.tsx @@ -0,0 +1,81 @@ +import React from "react"; +import {useService, useStore} from "../../utils"; +import {DFlowDataTypeReactiveService} from "../d-flow-data-type"; +import {DFlowSuggestion, DFlowSuggestionType} from "./DFlowSuggestion.view"; +import {DFlowFunctionReactiveService} from "../d-flow-function"; +import {isMatchingType, replaceGenericsAndSortType, resolveType} from "../../utils/generics"; +import type { + DataTypeIdentifier, + LiteralValue, + NodeFunction, + ReferenceValue +} from "@code0-tech/sagittarius-graphql-types"; + +export const useFunctionSuggestions = ( + dataTypeIdentifier?: DataTypeIdentifier, + genericKeys: string[] = [] +): DFlowSuggestion[] => { + const dataTypeService = useService(DFlowDataTypeReactiveService) + const functionService = useService(DFlowFunctionReactiveService) + const dataTypeStore = useStore(DFlowDataTypeReactiveService) + const functionStore = useStore(DFlowFunctionReactiveService) + + const dataType = React.useMemo(() => ( + dataTypeIdentifier ? dataTypeService?.getDataType(dataTypeIdentifier) : undefined + ), [dataTypeIdentifier, dataTypeService, dataTypeStore]) + + const resolvedType = React.useMemo(() => ( + dataTypeIdentifier ? replaceGenericsAndSortType(resolveType(dataTypeIdentifier, dataTypeService), genericKeys) : undefined + ), [dataTypeIdentifier, dataTypeService, dataTypeStore, genericKeys]) + + return React.useMemo(() => { + const matchingFunctions = functionService.values().filter(funcDefinition => { + if (!dataTypeIdentifier || !resolvedType) return true + if (funcDefinition.runtimeFunctionDefinition?.identifier == "std::control::return") return false + if (dataType?.variant === "NODE") return true + if (!funcDefinition.returnType) return false + if (!funcDefinition.genericKeys) return false + const resolvedReturnType = replaceGenericsAndSortType(resolveType(funcDefinition.returnType, dataTypeService), funcDefinition.genericKeys) + return isMatchingType(resolvedType, resolvedReturnType) + }).sort((a, b) => { + const [rA, pA, fA] = a.runtimeFunctionDefinition!!.identifier!!.split("::"); + const [rB, pB, fB] = b.runtimeFunctionDefinition!!.identifier!!.split("::"); + + const runtimeCmp = rA.localeCompare(rB); + if (runtimeCmp !== 0) return runtimeCmp; + + const packageCmp = pA.localeCompare(pB); + if (packageCmp !== 0) return packageCmp; + + return fA.localeCompare(fB); + }) + + return matchingFunctions.map(funcDefinition => { + const nodeFunctionSuggestion: LiteralValue | ReferenceValue | NodeFunction = { + __typename: "NodeFunction", + id: `gid://sagittarius/NodeFunction/1`, + functionDefinition: { + id: funcDefinition.id, + runtimeFunctionDefinition: funcDefinition.runtimeFunctionDefinition + }, + parameters: { + nodes: (funcDefinition.parameterDefinitions?.map((definition, index) => { + return { + id: `gid://sagittarius/NodeParameter/${index}`, + parameterDefinition: { + id: definition.id + } + } + }) ?? []) + } + } + + return { + path: [], + type: DFlowSuggestionType.FUNCTION, + displayText: [funcDefinition.names!![0]?.content as string], + value: nodeFunctionSuggestion, + } + }) + }, [dataType, dataTypeIdentifier, dataTypeService, functionService, functionStore, resolvedType, dataTypeStore]) +} diff --git a/src/packages/ce/src/function/hooks/DFlowNode.return.hook.ts b/src/packages/ce/src/function/hooks/DFlowNode.return.hook.ts new file mode 100644 index 00000000..1debb81e --- /dev/null +++ b/src/packages/ce/src/function/hooks/DFlowNode.return.hook.ts @@ -0,0 +1,43 @@ +import {replaceGenericKeysInType, resolveGenericKeys} from "../../utils/generics"; +import type {DataTypeIdentifier, Flow, NodeFunction} from "@code0-tech/sagittarius-graphql-types"; +import {useService, useStore} from "../../utils"; +import {DFlowFunctionReactiveService} from "../d-flow-function"; +import {DFlowDataTypeReactiveService} from "../d-flow-data-type"; +import {DFlowReactiveService} from "../d-flow"; +import React from "react"; + +export const useReturnTypes = ( + flowId: Flow['id'] +): Map => { + + const flowService = useService(DFlowReactiveService) + const flowStore = useStore(DFlowReactiveService) + const functionService = useService(DFlowFunctionReactiveService) + const functionStore = useStore(DFlowFunctionReactiveService) + const dataTypeService = useService(DFlowDataTypeReactiveService) + + return React.useMemo(() => { + const flow = flowService.getById(flowId) + return getReturnTypesForFlow(flow!, functionService, dataTypeService) + }, [flowId, flowStore, functionStore, dataTypeService]) + +} + +export function getReturnTypesForFlow( + flow: Flow, + functionService: DFlowFunctionReactiveService, + dataTypeService: DFlowDataTypeReactiveService +): Map { + const nodes = flow?.nodes?.nodes; + const result = new Map(); + nodes?.forEach(node => { + const values = node?.parameters?.nodes?.map(p => p?.value!) ?? []; + const func = functionService.getById(node?.functionDefinition?.id!!); + const genericTypeMap = resolveGenericKeys(func!, values, dataTypeService, functionService); + const returnType = func?.returnType ? replaceGenericKeysInType(func.returnType, genericTypeMap) : null; + if (node?.id) { + result.set(node.id, returnType); + } + }); + return result; +} \ No newline at end of file diff --git a/src/packages/ce/src/function/hooks/DFlowNodeReference.return.hook.ts b/src/packages/ce/src/function/hooks/DFlowNodeReference.return.hook.ts new file mode 100644 index 00000000..56c38e3a --- /dev/null +++ b/src/packages/ce/src/function/hooks/DFlowNodeReference.return.hook.ts @@ -0,0 +1,147 @@ +import { + DataTypeIdentifier, + Flow, FlowType, + NodeFunction, + ReferenceValue +} from "@code0-tech/sagittarius-graphql-types"; +import {DFlowFunctionReactiveService, FunctionDefinitionView} from "../d-flow-function"; +import {DFlowDataTypeReactiveService} from "../d-flow-data-type"; +import { + replaceGenericKeysInType, + resolveType, + resolveGenericKeys, + targetForGenericKey +} from "../../utils/generics"; + +export const getReferenceType = ( + reference: ReferenceValue, + dataTypeService: DFlowDataTypeReactiveService, + functionService: DFlowFunctionReactiveService, + functionDefinition?: FunctionDefinitionView, + node?: NodeFunction, + flowType?: FlowType +): DataTypeIdentifier | undefined => { + let typeIdentifier: DataTypeIdentifier | undefined = undefined; + let genericTypeMap: Map = new Map(); + + // 1. Return-Type eines Knotens + if (reference.nodeFunctionId && (reference.inputIndex === undefined || reference.parameterIndex === undefined)) { + const funcDef = functionDefinition; + if (funcDef && funcDef.returnType) { + typeIdentifier = funcDef.returnType; + if (node && node.parameters && funcDef.parameterDefinitions) { + const nodeValues = node.parameters.nodes?.map(p => p?.value).filter(Boolean) as any[]; + genericTypeMap = resolveGenericKeys(funcDef, nodeValues, dataTypeService, functionService); + if (typeIdentifier) { + const genericTargetMap = targetForGenericKey(funcDef, typeIdentifier); + const genericMap = new Map( + Array.from(genericTypeMap.entries()) + .filter(([, v]) => v && v.__typename === "DataTypeIdentifier") + ); + const resolvedGenericMap = new Map( + [...genericMap.entries()].map(([key, value]) => [genericTargetMap.get(key) ?? key, value]) + ); + typeIdentifier = replaceGenericKeysInType(typeIdentifier, resolvedGenericMap); + } + } + typeIdentifier = typeIdentifier ? resolveType(typeIdentifier, dataTypeService) : undefined; + } + } + + // 2. Input-Type eines Knotens + if ( + reference.nodeFunctionId && + reference.inputIndex !== undefined && reference.inputIndex !== null && + reference.parameterIndex !== undefined && reference.parameterIndex !== null + ) { + const funcDef = functionDefinition; + if (funcDef && funcDef.parameterDefinitions) { + const paramDef = funcDef.parameterDefinitions[reference.parameterIndex]; + if (paramDef && paramDef.dataTypeIdentifier) { + const paramDataType = dataTypeService.getDataType(paramDef.dataTypeIdentifier); + if (paramDataType && paramDataType.rules?.nodes) { + const inputTypesRule = paramDataType.rules.nodes.find((r: any) => r?.variant === "INPUT_TYPES"); + if (inputTypesRule && inputTypesRule.config) { + const config = inputTypesRule.config as { inputTypes?: any[] }; + if ( + Array.isArray(config.inputTypes) && + reference.inputIndex !== undefined && reference.inputIndex !== null + ) { + const inputType = config.inputTypes[reference.inputIndex]; + if (inputType && inputType.dataTypeIdentifier) { + typeIdentifier = inputType.dataTypeIdentifier; + if (node && node.parameters && funcDef.parameterDefinitions && typeIdentifier) { + const nodeValues = node.parameters.nodes?.map(p => p?.value).filter(Boolean) as any[]; + genericTypeMap = resolveGenericKeys(funcDef, nodeValues, dataTypeService, functionService); + const genericTargetMap = targetForGenericKey(funcDef, typeIdentifier); + const genericMap = new Map( + Array.from(genericTypeMap.entries()) + .filter(([, v]) => v && v.__typename === "DataTypeIdentifier") + ); + const resolvedGenericMap = new Map( + [...genericMap.entries()].map(([key, value]) => [genericTargetMap.get(key) ?? key, value]) + ); + typeIdentifier = replaceGenericKeysInType(typeIdentifier, resolvedGenericMap); + } + typeIdentifier = typeIdentifier ? resolveType(typeIdentifier, dataTypeService) : undefined; + } + } + } + } + // Fallback: Haupttyp + if (!typeIdentifier) { + typeIdentifier = paramDef.dataTypeIdentifier; + if (node && node.parameters && funcDef.parameterDefinitions && typeIdentifier) { + const nodeValues = node.parameters.nodes?.map(p => p?.value).filter(Boolean) as any[]; + genericTypeMap = resolveGenericKeys(funcDef, nodeValues, dataTypeService, functionService); + const genericTargetMap = targetForGenericKey(funcDef, typeIdentifier); + const genericMap = new Map( + Array.from(genericTypeMap.entries()) + .filter(([, v]) => v && v.__typename === "DataTypeIdentifier") + ); + const resolvedGenericMap = new Map( + [...genericMap.entries()].map(([key, value]) => [genericTargetMap.get(key) ?? key, value]) + ); + typeIdentifier = replaceGenericKeysInType(typeIdentifier, resolvedGenericMap); + } + typeIdentifier = typeIdentifier ? resolveType(typeIdentifier, dataTypeService) : undefined; + } + } + } + } + + // 3. Flow Input Type + if (!reference.nodeFunctionId && flowType && flowType.inputType) { + typeIdentifier = {dataType: flowType.inputType}; + typeIdentifier = typeIdentifier ? resolveType(typeIdentifier, dataTypeService) : undefined; + } + + // 4. referencePath ablaufen und Typ weiter auflösen (rekursiv wie referenceExtraction) + function resolveReferencePath( + dataTypeIdentifier: DataTypeIdentifier | undefined, + referencePath: { path: string }[] | undefined + ): DataTypeIdentifier | undefined { + if (!dataTypeIdentifier || !referencePath || referencePath.length === 0) return dataTypeIdentifier; + const [current, ...rest] = referencePath; + const dataType = dataTypeIdentifier.dataType ? dataTypeService.getDataType(dataTypeIdentifier) : dataTypeIdentifier.genericType?.dataType; + if (!dataType || !dataType.rules?.nodes) return dataTypeIdentifier; + const containsKeyRule = dataType.rules.nodes.find( + (rule: any) => rule?.variant === "CONTAINS_KEY" && rule.config?.key === current.path + ); + if (!containsKeyRule || !containsKeyRule.config) return dataTypeIdentifier; + const config = containsKeyRule.config as { dataTypeIdentifier?: DataTypeIdentifier }; + if (!config.dataTypeIdentifier) return dataTypeIdentifier; + return resolveReferencePath(config.dataTypeIdentifier, rest); + } + + if (typeIdentifier && reference.referencePath && reference.referencePath.length > 0) { + const filteredPath = reference.referencePath + .map(p => ({ path: typeof p.path === 'string' ? p.path : undefined })) + .filter(p => !!p.path) as { path: string }[]; + if (filteredPath.length !== reference.referencePath.length) return typeIdentifier ? resolveType(typeIdentifier, dataTypeService) : undefined; + const resolved = resolveReferencePath(typeIdentifier, filteredPath); + return resolved ? resolveType(resolved, dataTypeService) : undefined; + } + + return typeIdentifier ? resolveType(typeIdentifier, dataTypeService) : undefined; +} \ No newline at end of file diff --git a/src/packages/ce/src/function/hooks/DFlowReferenceSuggestions.hook.tsx b/src/packages/ce/src/function/hooks/DFlowReferenceSuggestions.hook.tsx new file mode 100644 index 00000000..fdbfc08b --- /dev/null +++ b/src/packages/ce/src/function/hooks/DFlowReferenceSuggestions.hook.tsx @@ -0,0 +1,337 @@ +import React from "react"; +import {useService, useStore} from "../../utils"; +import {DFlowDataTypeReactiveService} from "../d-flow-data-type"; +import {DFlowSuggestion, DFlowSuggestionType} from "./DFlowSuggestion.view"; +import { + isMatchingType, + replaceGenericKeysInType, + replaceGenericsAndSortType, + resolveGenericKeys, + resolveType, + targetForGenericKey +} from "../../utils/generics"; +import { + DataType, + DataTypeIdentifier, + DataTypeRulesContainsKeyConfig, + DataTypeRulesInputTypesConfig, + Flow, + Maybe, + NodeFunction, + NodeFunctionIdWrapper, + NodeParameterValue, + ReferenceValue +} from "@code0-tech/sagittarius-graphql-types"; +import {DFlowFunctionReactiveService} from "../d-flow-function"; +import {DFlowReactiveService} from "../d-flow"; +import {useReturnType} from "../d-flow-function/DFlowFunction.return.hook"; +import {DFlowTypeReactiveService} from "../d-flow-type"; + +interface ExtendedReferenceValue extends ReferenceValue { + inputTypeIdentifier?: string + dataTypeIdentifier: DataTypeIdentifier + node: number + depth: number + scope: number[] +} + +interface ReferenceValueContext extends ReferenceValue { + node: number + depth: number + inputTypeIdentifier?: string + scope: number[] +} + +export const useReferenceSuggestions = ( + flowId: Flow['id'], + nodeId?: NodeFunction['id'], + dataTypeIdentifier?: DataTypeIdentifier, + genericKeys: string[] = [] +): DFlowSuggestion[] => { + const dataTypeService = useService(DFlowDataTypeReactiveService) + const dataTypeStore = useStore(DFlowDataTypeReactiveService) + const flowService = useService(DFlowReactiveService) + const flowStore = useStore(DFlowReactiveService) + + const nodeContexts = useNodeContext(flowId) + const nodeContext = React.useMemo(() => ( + nodeId ? nodeContexts?.find(context => context.nodeFunctionId === nodeId) : undefined + ), [nodeContexts, nodeId]) + const nodeParameters = React.useMemo(() => { + if (!nodeId) return [] + const node = flowService.getNodeById(flowId, nodeId) + return node?.parameters?.nodes?.map(p => p?.value).filter((value): value is NodeFunctionIdWrapper => value?.__typename === "NodeFunctionIdWrapper") ?? [] + }, [flowId, nodeId, flowService, flowStore]) + + const resolvedType = React.useMemo(() => ( + dataTypeIdentifier ? replaceGenericsAndSortType(resolveType(dataTypeIdentifier, dataTypeService), genericKeys) : undefined + ), [dataTypeIdentifier, dataTypeService, dataTypeStore, genericKeys]) + + const refObjects = useRefObjects(flowId) + + return React.useMemo(() => { + if (!resolvedType || !nodeContext) return [] + + const {depth, scope, node} = nodeContext + return refObjects.flatMap(value => { + if (value.node === null || value.node === undefined) return [] + if (value.depth === null || value.depth === undefined) return [] + if (value.scope === null || value.scope === undefined) return [] + + const isInputTypeRef = value.parameterIndex !== undefined && value.inputIndex !== undefined + const isInputTypeScopeMatch = isInputTypeRef + ? value.scope?.every((scopeId, index) => scope?.[index] === scopeId) + : true + if (isInputTypeRef && !isInputTypeScopeMatch) return [] + if (nodeParameters.some(param => param.id === value.nodeFunctionId)) return [] + if (!isInputTypeRef && value.node >= node!) return [] + if (value.depth > depth!) return [] + if (value.scope.some(r => !scope!.includes(r))) return [] + + const resolvedRefObjectType = replaceGenericsAndSortType(resolveType(value.dataTypeIdentifier!, dataTypeService), []) + if (!isMatchingType(resolvedType, resolvedRefObjectType)) return [] + + return [{ + path: [], + type: DFlowSuggestionType.REF_OBJECT, + displayText: [`${value.depth}-${value.scope}-${value.node || ''}-${value.referencePath?.map(path => path.path).join(".") ?? ""}`], + value: value as ReferenceValue, + }] + }) + }, [dataTypeService, nodeContext, nodeParameters, refObjects, resolvedType]) +} + + +/** + * Walks the flow starting at its startingNode (depth-first, left-to-right) and collects + * all RefObjects (variables/outputs) with contextual metadata: + * - depth: nesting level (root 0; +1 per NODE-parameter sub-block) + * - scope: PATH of scope ids as number[], e.g. [0], [0,1], [0,2], [0,2,3] ... + * (root is [0]; each NODE-parameter group appends a new unique id) + * - node: GLOBAL visit index across the entire flow (1-based, strictly increasing) + * + * Notes: + * - A NODE-typed parameter opens a new group/lane: depth+1 and scopePath+[newId]. + * - Functions passed as non-NODE parameters are traversed in the SAME depth/scopePath. + * - The `node` id is incremented globally for every visited node and shared by all + * RefObjects (inputs from rules and the return value) produced by that node. + */ +const useRefObjects = (flowId: Flow['id']): Array => { + + const dataTypeService = useService(DFlowDataTypeReactiveService) + const dataTypeStore = useStore(DFlowDataTypeReactiveService) + const functionService = useService(DFlowFunctionReactiveService) + const flowService = useService(DFlowReactiveService) + const flowStore = useStore(DFlowReactiveService) + const flowTypeService = useService(DFlowTypeReactiveService) + const flowTypeStore = useStore(DFlowTypeReactiveService) + + const flow = React.useMemo( + () => flowService.getById(flowId), + [flowId, flowStore] + ) + + const flowType = React.useMemo( + () => flowTypeService.getById(flow?.type?.id!), + [flow?.type?.id, flowTypeStore] + ) + + const nodeContexts = useNodeContext(flowId) + + const nodeSuggestions = React.useMemo(() => { + return flow?.nodes?.nodes?.map(node => { + + const nodeValues = node?.parameters?.nodes?.map(p => p?.value!!) ?? [] + const functionDefinition = functionService.getById(node?.functionDefinition?.id) + const resolvedReturnType = useReturnType(functionDefinition!, nodeValues as NodeParameterValue[], dataTypeService, functionService) + const nodeContext = nodeContexts?.find(context => context.nodeFunctionId === node?.id) + + if (resolvedReturnType && nodeContext) { + return referenceExtraction(nodeContext, resolvedReturnType, dataTypeService) + } + + return {} as ExtendedReferenceValue + + }) ?? [] + }, [flow]) + + const flowInputSuggestions = React.useMemo(() => { + return referenceExtraction({ + node: 0, + depth: 0, + scope: [0], + }, { + dataType: flowType?.inputType + }, dataTypeService) + }, [flow]) + + const inputSuggestions: ExtendedReferenceValue[] = React.useMemo(() => { + if (!flow?.nodes?.nodes?.length) return [] + + return flow.nodes.nodes.flatMap((node) => { + const functionDefinition = functionService.getById(node?.functionDefinition?.id) + if (!functionDefinition) return [] + const nodeValues = + node?.parameters?.nodes?.map((p) => p?.value!).filter(Boolean) ?? [] + + return (functionDefinition.parameterDefinitions ?? []).flatMap((paramDef, index) => { + const dataTypeIdentifier = paramDef?.dataTypeIdentifier + if (!dataTypeIdentifier) return [] + + const pType = dataTypeService.getDataType(dataTypeIdentifier) + if (!pType || pType.variant !== "NODE") return [] + + + const paramInstance = node?.parameters?.nodes?.find((p) => p?.parameterDefinition?.id === paramDef?.id) + if (!paramInstance?.value || paramInstance.value.__typename !== "NodeFunctionIdWrapper") return [] + + const paramNodeContext = nodeContexts?.find( + (context) => paramInstance?.value?.__typename === "NodeFunctionIdWrapper" && context.nodeFunctionId === paramInstance.value?.id + ) + + if (!paramNodeContext) return [] + + const inputTypeRules = + pType.rules?.nodes?.filter((r) => r?.variant === "INPUT_TYPES") ?? [] + + const genericTypeMap = resolveGenericKeys(functionDefinition, nodeValues, dataTypeService, functionService) + const genericTargetMap = targetForGenericKey(functionDefinition, dataTypeIdentifier) + const resolvedGenericMap = new Map( + [...genericTypeMap].map(([key, value]) => [genericTargetMap.get(key) ?? key, value]) + ) + + return inputTypeRules.flatMap((rule) => { + const config = rule?.config as DataTypeRulesInputTypesConfig | undefined + const inputTypes = config?.inputTypes ?? [] + + return inputTypes.flatMap((inputType, inputIndex) => { + const resolved = replaceGenericKeysInType( + inputType.dataTypeIdentifier!, + resolvedGenericMap + ) + if (!resolved) return [] + + return referenceExtraction({ + ...paramNodeContext, + nodeFunctionId: node?.id!, + parameterIndex: index, + inputIndex: inputIndex, + inputTypeIdentifier: inputType.inputIdentifier! + }, resolved, dataTypeService) + }) + }) + }) + }) + }, [flow, nodeContexts, functionService, dataTypeService]) + + return [ + ...inputSuggestions, + ...flowInputSuggestions, + ...nodeSuggestions + ].flat() +} + +const referenceExtraction = (nodeContext: ReferenceValueContext, dataTypeIdentifier: DataTypeIdentifier, dataTypeService: DFlowDataTypeReactiveService): ExtendedReferenceValue[] => { + + const dataType: Maybe | undefined = dataTypeIdentifier.dataType ? dataTypeService.getDataType(dataTypeIdentifier) : dataTypeIdentifier.genericType?.dataType + if (!dataType) return [] + + const references = dataType.rules?.nodes?.map(rule => { + if (rule?.variant === "CONTAINS_KEY") { + if (!dataTypeIdentifier) return + return referenceExtraction({ + ...nodeContext, + referencePath: [ + ...(nodeContext.referencePath ?? []), + { + path: (rule.config as DataTypeRulesContainsKeyConfig).key!! + } + ] + }, (rule.config as DataTypeRulesContainsKeyConfig).dataTypeIdentifier!!, dataTypeService) + } + + return undefined + + }).flat().filter(ref => !!ref) ?? [] + + return [ + ...references, + { + ...nodeContext, + __typename: "ReferenceValue", + nodeFunctionId: nodeContext.nodeFunctionId, + dataTypeIdentifier + }] + +} + +const useNodeContext = ( + flowId: Flow['id'] +): ReferenceValueContext[] => { + const dataTypeService = useService(DFlowDataTypeReactiveService); + const flowService = useService(DFlowReactiveService); + const functionService = useService(DFlowFunctionReactiveService); + + const flowStore = useStore(DFlowReactiveService); + const functionStore = useStore(DFlowFunctionReactiveService); + const dataTypeStore = useStore(DFlowDataTypeReactiveService); + + const flow = React.useMemo(() => flowService.getById(flowId), [flowId, flowStore]); + + return React.useMemo(() => { + if (!dataTypeService || !flowService || !functionService) return undefined; + if (!flow?.startingNodeId) return undefined; + + let globalGroupId = 0; + const nextGroupId = () => ++globalGroupId; + + let globalNodeId = 0; + const nextNodeId = () => ++globalNodeId; + + const contexts: ReferenceValueContext[] = []; + + const traverse = ( + node: NodeFunctionIdWrapper | NodeFunction | undefined, + depth: number, + scopePath: number[] + ) => { + if (!node) return; + + let current: NodeFunction | undefined = + node.__typename === "NodeFunctionIdWrapper" + ? flowService.getNodeById(flowId, node.id) + : (node as NodeFunction); + + while (current) { + const def = functionService.getById(current.functionDefinition?.id!!); + if (!def) break; + + if (current.parameters && def.parameterDefinitions) { + for (const pDef of def.parameterDefinitions) { + const pType = dataTypeService.getDataType(pDef.dataTypeIdentifier!!); + const paramInstance = current.parameters?.nodes?.find((p) => p?.parameterDefinition?.id === pDef.id); + + if (pType?.variant === "NODE") { + if (paramInstance?.value && paramInstance.value.__typename === "NodeFunctionIdWrapper") { + const childScopePath = [...scopePath, nextGroupId()]; + traverse(paramInstance.value as NodeFunctionIdWrapper, depth + 1, childScopePath); + } + } else if (paramInstance?.value && paramInstance.value.__typename === "NodeFunctionIdWrapper") { + traverse(paramInstance.value as NodeFunctionIdWrapper, depth, scopePath); + } + } + } + + const nodeIndex = nextNodeId(); + contexts.push({node: nodeIndex, depth, scope: scopePath, nodeFunctionId: current.id}); + + current = flowService.getNodeById(flow.id, current.nextNodeId); + } + }; + + + traverse(flowService.getNodeById(flow.id, flow.startingNodeId), 0, [0]); + + return contexts + }, [dataTypeService, flow, flowId, flowService, functionService, dataTypeStore, flowStore, functionStore]) ?? []; +}; diff --git a/src/packages/ce/src/function/hooks/DFlowSuggestion.hook.tsx b/src/packages/ce/src/function/hooks/DFlowSuggestion.hook.tsx new file mode 100644 index 00000000..4a8a7c6e --- /dev/null +++ b/src/packages/ce/src/function/hooks/DFlowSuggestion.hook.tsx @@ -0,0 +1,49 @@ +import {useService, useStore} from "../../utils"; +import {DFlowSuggestion} from "./DFlowSuggestion.view"; +import {DFlowFunctionReactiveService} from "../d-flow-function"; +import {DFlowReactiveService} from "../d-flow"; +import React from "react"; +import type {Flow, NodeFunction, NodeParameter,} from "@code0-tech/sagittarius-graphql-types"; +import {useValueSuggestions} from "./DFlowValueSuggestions.hook"; +import {useReferenceSuggestions} from "./DFlowReferenceSuggestions.hook"; +import {useFunctionSuggestions} from "./DFlowFunctionSuggestions.hook"; +import {useDataTypeSuggestions} from "./DFlowDataTypeSuggestions.hook"; + +//TODO: deep type search +//TODO: calculate FUNCTION_COMBINATION deepness max 2 + +export const useSuggestions = ( + flowId: Flow['id'], + nodeId?: NodeFunction['id'], + parameterId?: NodeParameter['id'] +): DFlowSuggestion[] => { + + const functionService = useService(DFlowFunctionReactiveService) + const functionStore = useStore(DFlowFunctionReactiveService) + const flowService = useService(DFlowReactiveService) + const flowStore = useStore(DFlowReactiveService) + + const node = React.useMemo(() => (flowService.getNodeById(flowId, nodeId)), [flowId, flowStore, nodeId]) + const functionDefinition = React.useMemo(() => (node?.functionDefinition?.id ? functionService.getById(node.functionDefinition.id) : undefined), [functionStore, node?.functionDefinition?.id]) + const parameterDefinition = React.useMemo(() => (functionDefinition?.parameterDefinitions?.find(definition => { + const parameterDefinitionId = node?.parameters?.nodes?.find(parameter => parameter?.id === parameterId)?.parameterDefinition?.id + return definition.id === parameterDefinitionId + })), [functionDefinition?.parameterDefinitions, node]) + + const dataTypeIdentifier = parameterDefinition?.dataTypeIdentifier! + const genericKeys = functionDefinition?.genericKeys ?? [] + + const valueSuggestions = useValueSuggestions(dataTypeIdentifier) + const dataTypeSuggestions = useDataTypeSuggestions(dataTypeIdentifier) + const refObjectSuggestions = useReferenceSuggestions(flowId, nodeId, dataTypeIdentifier, genericKeys) + const functionSuggestions = useFunctionSuggestions(dataTypeIdentifier, genericKeys) + + return React.useMemo(() => { + return [ + ...valueSuggestions, + ...dataTypeSuggestions, + ...refObjectSuggestions, + ...functionSuggestions + ].sort() + }, [flowId, nodeId, parameterId, dataTypeSuggestions, refObjectSuggestions, functionSuggestions]) +} diff --git a/src/packages/ce/src/function/hooks/DFlowValueSuggestions.hook.tsx b/src/packages/ce/src/function/hooks/DFlowValueSuggestions.hook.tsx new file mode 100644 index 00000000..458f3ce1 --- /dev/null +++ b/src/packages/ce/src/function/hooks/DFlowValueSuggestions.hook.tsx @@ -0,0 +1,57 @@ +import React from "react"; +import {useService, useStore} from "../../utils"; +import {DFlowDataTypeReactiveService} from "../d-flow-data-type"; +import {DFlowSuggestion, DFlowSuggestionType} from "./DFlowSuggestion.view"; +import type { + DataTypeIdentifier, + DataTypeRulesItemOfCollectionConfig, + DataTypeRulesNumberRangeConfig +} from "@code0-tech/sagittarius-graphql-types"; + +export const useValueSuggestions = (dataTypeIdentifier?: DataTypeIdentifier): DFlowSuggestion[] => { + const dataTypeService = useService(DFlowDataTypeReactiveService) + const dataTypeStore = useStore(DFlowDataTypeReactiveService) + + const dataType = React.useMemo(() => ( + dataTypeIdentifier ? dataTypeService?.getDataType(dataTypeIdentifier) : undefined + ), [dataTypeIdentifier, dataTypeService, dataTypeStore]) + + return React.useMemo(() => { + if (!dataType) return [] + + const suggestions: DFlowSuggestion[] = [] + dataType.rules?.nodes?.forEach(rule => { + if (rule?.variant === "ITEM_OF_COLLECTION") { + (rule.config as DataTypeRulesItemOfCollectionConfig)!!.items?.forEach(value => { + suggestions.push({ + path: [], + type: DFlowSuggestionType.VALUE, + displayText: [value.toString()], + value: { + __typename: "LiteralValue", + value: value + }, + }) + }) + } else if (rule?.variant === "NUMBER_RANGE") { + const config: DataTypeRulesNumberRangeConfig = rule.config as DataTypeRulesNumberRangeConfig + if (config.from === null || config.from === undefined) return + if (config.to === null || config.to === undefined) return + + for (let i = config.from; i <= config.to; i += ((config.steps ?? 1) <= 0 ? 1 : (config.steps ?? 1))) { + suggestions.push({ + path: [], + type: DFlowSuggestionType.VALUE, + displayText: [i.toString() ?? ""], + value: { + __typename: "LiteralValue", + value: String(i) + }, + }) + } + } + }) + + return suggestions + }, [dataType]) +} From e900373881b1ef88db4b3ee13ee30a27b0a86bfb Mon Sep 17 00:00:00 2001 From: nicosammito Date: Thu, 5 Mar 2026 19:37:18 +0100 Subject: [PATCH 02/35] feat: rename FlowBuilder and related components to FlowBuilderComponent for consistency --- .../ce/src/datatype/services/DataType.view.ts | 2 +- ...e.scss => FlowBuilderComponent.style.scss} | 0 ...owBuilder.tsx => FlowBuilderComponent.tsx} | 88 +++++++++---------- ...r.util.ts => FlowBuilderComponent.util.ts} | 0 ...rEdge.tsx => FlowBuilderEdgeComponent.tsx} | 2 +- .../components/FlowCreateDialogComponent.tsx | 6 +- ...alog.tsx => FlowDeleteDialogComponent.tsx} | 27 ++++-- ...le.scss => FlowFolderComponent.style.scss} | 0 ...FlowFolder.tsx => FlowFolderComponent.tsx} | 70 ++++++++------- ...tsx => FlowFolderContextMenuComponent.tsx} | 23 +++-- .../components/FlowNameInputComponent.tsx | 5 +- ...trol.tsx => FlowPanelControlComponent.tsx} | 34 ++++--- ...ayout.tsx => FlowPanelLayoutComponent.tsx} | 15 ++-- ...nelSize.tsx => FlowPanelSizeComponent.tsx} | 9 +- ...pdate.tsx => FlowPanelUpdateComponent.tsx} | 38 +++++--- ...alog.tsx => FlowRenameDialogComponent.tsx} | 26 +++--- 16 files changed, 192 insertions(+), 153 deletions(-) rename src/packages/ce/src/flow/components/{FlowBuilder.style.scss => FlowBuilderComponent.style.scss} (100%) rename src/packages/ce/src/flow/components/{FlowBuilder.tsx => FlowBuilderComponent.tsx} (91%) rename src/packages/ce/src/flow/components/{FlowBuilder.util.ts => FlowBuilderComponent.util.ts} (100%) rename src/packages/ce/src/flow/components/{FlowBuilderEdge.tsx => FlowBuilderEdgeComponent.tsx} (96%) rename src/packages/ce/src/flow/components/{DFlowFolderDeleteDialog.tsx => FlowDeleteDialogComponent.tsx} (78%) rename src/packages/ce/src/flow/components/{DFlowFolder.style.scss => FlowFolderComponent.style.scss} (100%) rename src/packages/ce/src/flow/components/{DFlowFolder.tsx => FlowFolderComponent.tsx} (85%) rename src/packages/ce/src/flow/components/{DFlowFolderContextMenu.tsx => FlowFolderContextMenuComponent.tsx} (81%) rename src/packages/ce/src/flow/components/{FlowPanelControl.tsx => FlowPanelControlComponent.tsx} (80%) rename src/packages/ce/src/flow/components/{FlowPanelLayout.tsx => FlowPanelLayoutComponent.tsx} (84%) rename src/packages/ce/src/flow/components/{FlowPanelSize.tsx => FlowPanelSizeComponent.tsx} (84%) rename src/packages/ce/src/flow/components/{FlowPanelUpdate.tsx => FlowPanelUpdateComponent.tsx} (82%) rename src/packages/ce/src/flow/components/{DFlowFolderRenameDialog.tsx => FlowRenameDialogComponent.tsx} (77%) diff --git a/src/packages/ce/src/datatype/services/DataType.view.ts b/src/packages/ce/src/datatype/services/DataType.view.ts index 71a3c852..f60993d6 100644 --- a/src/packages/ce/src/datatype/services/DataType.view.ts +++ b/src/packages/ce/src/datatype/services/DataType.view.ts @@ -3,7 +3,7 @@ import { DataTypeRuleConnection, DataTypeVariant, Maybe, Runtime, Scalars, Translation, } from "@code0-tech/sagittarius-graphql-types"; -import {attachDataTypeIdentifiers, resolveDataTypeIdentifiers} from "@edition/flow/components/FlowBuilder.util"; +import {attachDataTypeIdentifiers, resolveDataTypeIdentifiers} from "@edition/flow/components/FlowBuilderComponent.util"; export class DataTypeView { diff --git a/src/packages/ce/src/flow/components/FlowBuilder.style.scss b/src/packages/ce/src/flow/components/FlowBuilderComponent.style.scss similarity index 100% rename from src/packages/ce/src/flow/components/FlowBuilder.style.scss rename to src/packages/ce/src/flow/components/FlowBuilderComponent.style.scss diff --git a/src/packages/ce/src/flow/components/FlowBuilder.tsx b/src/packages/ce/src/flow/components/FlowBuilderComponent.tsx similarity index 91% rename from src/packages/ce/src/flow/components/FlowBuilder.tsx rename to src/packages/ce/src/flow/components/FlowBuilderComponent.tsx index c7dbcbe7..a4688e64 100644 --- a/src/packages/ce/src/flow/components/FlowBuilder.tsx +++ b/src/packages/ce/src/flow/components/FlowBuilderComponent.tsx @@ -13,24 +13,21 @@ import { } from "@xyflow/react"; import React from "react"; import '@xyflow/react/dist/style.css'; -import "./FlowBuilder.style.scss" -import {FlowBuilderEdge} from "./FlowBuilderEdge"; +import "./FlowBuilderComponent.style.scss" +import {FlowBuilderEdgeComponent} from "./FlowBuilderEdgeComponent"; import {Flow, type Namespace, type NamespaceProject} from "@code0-tech/sagittarius-graphql-types"; import {LineWobble} from 'ldrs/react' import 'ldrs/react/LineWobble.css' import {useFlowNodes} from "@edition/flow/hooks/Flow.nodes.hook"; -import { - Code0ComponentProps, DFlowPanelControl, - DFlowPanelLayout, - DFlowPanelSize, DFlowValidation, - mergeCode0Props, - Spacing, Text -} from "@code0-tech/pictor"; +import {Code0ComponentProps, mergeCode0Props, Spacing, Text} from "@code0-tech/pictor"; import {DFlowNodeDefaultCard} from "@edition/function/components/DFlowNodeDefaultCard"; import {DFlowNodeGroupCard} from "@edition/function/components/DFlowNodeGroupCard"; import {DFlowNodeTriggerCard} from "@edition/function/components/DFlowNodeTriggerCard"; -import {DFlowPanelUpdate} from "@code0-tech/pictor/dist/components/d-flow-panel/DFlowPanelUpdate"; import {useEdges} from "@edition/flow/hooks/Flow.edges.hook"; +import {FlowPanelSizeComponent} from "@edition/flow/components/FlowPanelSizeComponent"; +import {FlowPanelLayoutComponent} from "@edition/flow/components/FlowPanelLayoutComponent"; +import {FlowPanelControlComponent} from "@edition/flow/components/FlowPanelControlComponent"; +import {FlowPanelUpdateComponent} from "@edition/flow/components/FlowPanelUpdateComponent"; /** * Dynamically layouts a tree of nodes and their parameter nodes for a flow-based editor. @@ -43,7 +40,7 @@ import {useEdges} from "@edition/flow/hooks/Flow.edges.hook"; * @returns An object containing the new positioned nodes and the unchanged edges. */ const getLayoutElements = (nodes: Node[], dirtyIds?: Set) => { - if (!dirtyIds || dirtyIds.size === 0) return { nodes } + if (!dirtyIds || dirtyIds.size === 0) return {nodes} /* Konstanten */ const V = 50; // vertical gap Node ↕ Node @@ -112,7 +109,7 @@ const getLayoutElements = (nodes: Node[], dirtyIds?: Set) => { const styleH = typeof n.style?.height === "number" ? (n.style.height as number) : undefined const mw = n.measured?.width && n.measured.width > 0 ? n.measured.width : undefined const mh = n.measured?.height && n.measured.height > 0 ? n.measured.height : undefined - baseSizes.set(n.id, { w: styleW ?? mw ?? 200, h: styleH ?? mh ?? 80 }) + baseSizes.set(n.id, {w: styleW ?? mw ?? 200, h: styleH ?? mh ?? 80}) } const getStyleW = (n: Node) => styleWH.get(n.id)?.width @@ -134,7 +131,7 @@ const getLayoutElements = (nodes: Node[], dirtyIds?: Set) => { const sw = getStyleW(n) const sh = getStyleH(n) if (sw !== undefined && sh !== undefined) { - const s = { w: sw, h: sh } + const s = {w: sw, h: sh} sizeCache.set(n.id, s) return s } @@ -153,7 +150,7 @@ const getLayoutElements = (nodes: Node[], dirtyIds?: Set) => { } stackH += V * Math.max(0, count - 1) - const g = { w: wMax + 2 * PAD, h: (count ? stackH : 0) + 2 * PAD } + const g = {w: wMax + 2 * PAD, h: (count ? stackH : 0) + 2 * PAD} sizeCache.set(n.id, g) return g } @@ -204,15 +201,15 @@ const getLayoutElements = (nodes: Node[], dirtyIds?: Set) => { bottom?: number } - const stack: Frame[] = [{ node: root, cx, cy, phase: 0 }] + const stack: Frame[] = [{node: root, cx, cy, phase: 0}] let returnBottom = 0 while (stack.length) { const f = stack[stack.length - 1] switch (f.phase) { case 0: { - relCenter.set(f.node.id, { x: f.cx, y: f.cy }) - const { w, h } = size(f.node) + relCenter.set(f.node.id, {x: f.cx, y: f.cy}) + const {w, h} = size(f.node) f.w = w f.h = h @@ -259,7 +256,7 @@ const getLayoutElements = (nodes: Node[], dirtyIds?: Set) => { f.childKey = key f.childPs = ps - stack.push({ node: p, cx: px, cy: pcy, phase: 0 }) + stack.push({node: p, cx: px, cy: pcy, phase: 0}) f.phase = 10 } else { f.bottom = Math.max(f.cy + f.h! / 2, f.rightBottom!) @@ -312,7 +309,7 @@ const getLayoutElements = (nodes: Node[], dirtyIds?: Set) => { const gcy = f.gy! + gs.h / 2 f.gx! += gs.w + H - stack.push({ node: g, cx: gcx, cy: gcy, phase: 0 }) + stack.push({node: g, cx: gcx, cy: gcy, phase: 0}) f.childPs = gs f.phase = 30 } else { @@ -353,7 +350,7 @@ const getLayoutElements = (nodes: Node[], dirtyIds?: Set) => { const ks = size(k) const ky = f.curY! + ks.h / 2 - stack.push({ node: k, cx: f.cx, cy: ky, phase: 0 }) + stack.push({node: k, cx: f.cx, cy: ky, phase: 0}) f.childPs = ks f.phase = 50 } else { @@ -399,9 +396,9 @@ const getLayoutElements = (nodes: Node[], dirtyIds?: Set) => { // rel (Center) → absTL_initial (global Top-Left) const absTL_initial = new Map() for (const n of nodes) { - const { w, h } = size(n) + const {w, h} = size(n) const c = relCenter.get(n.id)! - absTL_initial.set(n.id, { x: c.x - w / 2, y: c.y - h / 2 }) + absTL_initial.set(n.id, {x: c.x - w / 2, y: c.y - h / 2}) } // initial posTL setzen (in RF-Koordinaten, relativ zu Parent) @@ -418,7 +415,7 @@ const getLayoutElements = (nodes: Node[], dirtyIds?: Set) => { const prev = posTL.get(n.id) if (!prev || Math.abs(prev.x - px) > EPS || Math.abs(prev.y - py) > EPS) { - posTL.set(n.id, { x: px, y: py }) + posTL.set(n.id, {x: px, y: py}) changed = true } } @@ -443,7 +440,7 @@ const getLayoutElements = (nodes: Node[], dirtyIds?: Set) => { const sw = typeof n.style?.width === "number" ? (n.style.width as number) : undefined const sh = typeof n.style?.height === "number" ? (n.style.height as number) : undefined const s = baseSizes.get(n.id)! - return { w: sw ?? s.w, h: sh ?? s.h } + return {w: sw ?? s.w, h: sh ?? s.h} } for (const g of groups) { @@ -456,9 +453,9 @@ const getLayoutElements = (nodes: Node[], dirtyIds?: Set) => { // minimal group size const gw = getStyleW(g) ?? (typeof g.style?.width === "number" ? (g.style.width as number) : 2 * PAD) const gh = getStyleH(g) ?? (typeof g.style?.height === "number" ? (g.style.height as number) : 2 * PAD) - styleWH.set(g.id, { width: gw, height: gh }) - measuredWH.set(g.id, { width: gw, height: gh }) - baseSizes.set(g.id, { w: gw, h: gh }) + styleWH.set(g.id, {width: gw, height: gh}) + measuredWH.set(g.id, {width: gw, height: gh}) + baseSizes.set(g.id, {w: gw, h: gh}) continue } @@ -486,7 +483,7 @@ const getLayoutElements = (nodes: Node[], dirtyIds?: Set) => { const ny = p.y - dy if (Math.abs(p.x - nx) > EPS || Math.abs(p.y - ny) > EPS) { - posTL.set(k.id, { x: nx, y: ny }) + posTL.set(k.id, {x: nx, y: ny}) changed = true } } @@ -500,9 +497,9 @@ const getLayoutElements = (nodes: Node[], dirtyIds?: Set) => { if (Math.abs(newW - oldW) > EPS || Math.abs(newH - oldH) > EPS) changed = true - styleWH.set(g.id, { width: newW, height: newH }) - measuredWH.set(g.id, { width: newW, height: newH }) - baseSizes.set(g.id, { w: newW, h: newH }) + styleWH.set(g.id, {width: newW, height: newH}) + measuredWH.set(g.id, {width: newW, height: newH}) + baseSizes.set(g.id, {w: newW, h: newH}) } // Größen-Cache invalidieren (Group-Styles haben sich ggf. geändert) @@ -516,7 +513,7 @@ const getLayoutElements = (nodes: Node[], dirtyIds?: Set) => { const s = size(n) const c = relCenter.get(n.id)! absCenterAfter.set(n.id, c) - absTL_after.set(n.id, { x: c.x - s.w / 2, y: c.y - s.h / 2 }) + absTL_after.set(n.id, {x: c.x - s.w / 2, y: c.y - s.h / 2}) } // Param-Group-Row nach Bounding sauber zentrieren @@ -545,13 +542,13 @@ const getLayoutElements = (nodes: Node[], dirtyIds?: Set) => { for (let i = 0; i < ordered.length; i++) { const g = ordered[i] - const containerTL = g.parentId ? absTL_after.get(g.parentId)! : { x: 0, y: 0 } + const containerTL = g.parentId ? absTL_after.get(g.parentId)! : {x: 0, y: 0} const cur = posTL.get(g.id)! const nx = gx - containerTL.x const ny = cur.y if (Math.abs(cur.x - nx) > EPS || Math.abs(cur.y - ny) > EPS) { - posTL.set(g.id, { x: nx, y: ny }) + posTL.set(g.id, {x: nx, y: ny}) changed = true } gx += widths[i] + H @@ -571,12 +568,12 @@ const getLayoutElements = (nodes: Node[], dirtyIds?: Set) => { let isChanged = false if (nextP) { - const op = n.position ?? { x: 0, y: 0 } + const op = n.position ?? {x: 0, y: 0} if (Math.abs(op.x - nextP.x) > EPS || Math.abs(op.y - nextP.y) > EPS) isChanged = true } if (nextM) { - const om = n.measured ?? ({ width: 0, height: 0 } as any) + const om = n.measured ?? ({width: 0, height: 0} as any) if (Math.abs((om as any).width - nextM.width) > EPS || Math.abs((om as any).height - nextM.height) > EPS) isChanged = true } @@ -591,14 +588,14 @@ const getLayoutElements = (nodes: Node[], dirtyIds?: Set) => { return { ...n, position: nextP ?? n.position, - measured: nextM ? ({ ...(n.measured as any), width: nextM.width, height: nextM.height } as any) : n.measured, + measured: nextM ? ({...(n.measured as any), width: nextM.width, height: nextM.height} as any) : n.measured, style: nextS - ? ({ ...(n.style as any), width: nextS.width, height: nextS.height } as any) + ? ({...(n.style as any), width: nextS.width, height: nextS.height} as any) : n.style, } as Node }) - return { nodes: out } + return {nodes: out} } const getCachedLayoutElements = React.cache(getLayoutElements) @@ -609,7 +606,7 @@ export interface FlowBuilderProps extends Code0ComponentProps { projectId: NamespaceProject['id'] } -export const FlowBuilder: React.FC = (props) => { +export const FlowBuilderComponent: React.FC = (props) => { return @@ -625,7 +622,7 @@ const InternalFlowBuilder: React.FC = (props) => { }), []) const edgeTypes = React.useMemo(() => ({ - default: FlowBuilderEdge, + default: FlowBuilderEdgeComponent, }), []) const initialNodes = useFlowNodes(flowId, namespaceId, projectId) @@ -770,11 +767,10 @@ const InternalFlowBuilder: React.FC = (props) => { {showTree ? ( <> - - - - - + + + + ) : null} diff --git a/src/packages/ce/src/flow/components/FlowBuilder.util.ts b/src/packages/ce/src/flow/components/FlowBuilderComponent.util.ts similarity index 100% rename from src/packages/ce/src/flow/components/FlowBuilder.util.ts rename to src/packages/ce/src/flow/components/FlowBuilderComponent.util.ts diff --git a/src/packages/ce/src/flow/components/FlowBuilderEdge.tsx b/src/packages/ce/src/flow/components/FlowBuilderEdgeComponent.tsx similarity index 96% rename from src/packages/ce/src/flow/components/FlowBuilderEdge.tsx rename to src/packages/ce/src/flow/components/FlowBuilderEdgeComponent.tsx index 82455d89..0e2130c5 100644 --- a/src/packages/ce/src/flow/components/FlowBuilderEdge.tsx +++ b/src/packages/ce/src/flow/components/FlowBuilderEdgeComponent.tsx @@ -14,7 +14,7 @@ export interface FlowBuilderEdgeDataProps extends Code0Component // @ts-ignore export type FlowBuilderEdgeProps = EdgeProps> -export const FlowBuilderEdge: React.FC = memo((props) => { +export const FlowBuilderEdgeComponent: React.FC = memo((props) => { const { sourceX, sourceY, diff --git a/src/packages/ce/src/flow/components/FlowCreateDialogComponent.tsx b/src/packages/ce/src/flow/components/FlowCreateDialogComponent.tsx index 44f46e47..09c0872f 100644 --- a/src/packages/ce/src/flow/components/FlowCreateDialogComponent.tsx +++ b/src/packages/ce/src/flow/components/FlowCreateDialogComponent.tsx @@ -82,7 +82,11 @@ export const FlowCreateDialogComponent: React.FC ) const flowTypes = React.useMemo( - () => flowTypeService.values({runtimeId: primaryRuntime?.id, projectId: projectId, namespaceId: project?.namespace?.id}), + () => flowTypeService.values({ + runtimeId: primaryRuntime?.id, + projectId: projectId, + namespaceId: project?.namespace?.id + }), [flowTypeStore] ) diff --git a/src/packages/ce/src/flow/components/DFlowFolderDeleteDialog.tsx b/src/packages/ce/src/flow/components/FlowDeleteDialogComponent.tsx similarity index 78% rename from src/packages/ce/src/flow/components/DFlowFolderDeleteDialog.tsx rename to src/packages/ce/src/flow/components/FlowDeleteDialogComponent.tsx index e4f7f67a..84b1c83f 100644 --- a/src/packages/ce/src/flow/components/DFlowFolderDeleteDialog.tsx +++ b/src/packages/ce/src/flow/components/FlowDeleteDialogComponent.tsx @@ -1,20 +1,29 @@ -import {Dialog, DialogClose, DialogContent, DialogOverlay, DialogPortal} from "../dialog/Dialog"; -import {Text} from "../text/Text"; -import {Badge} from "../badge/Badge"; -import {Flex} from "../flex/Flex"; -import {Button} from "../button/Button"; import React from "react"; -import {DFlowFolderContextMenuGroupData, DFlowFolderContextMenuItemData} from "./DFlowFolderContextMenu"; +import { + FlowFolderContextMenuComponentGroupData, + FlowFolderContextMenuComponentItemData +} from "./FlowFolderContextMenuComponent"; import {Flow} from "@code0-tech/sagittarius-graphql-types"; +import { + Badge, + Button, + Dialog, + DialogClose, + DialogContent, + DialogOverlay, + DialogPortal, + Flex, + Text +} from "@code0-tech/pictor"; -export interface DFlowFolderDeleteDialogProps { +export interface FlowDeleteDialogComponentProps { open?: boolean onOpenChange?: (open: boolean) => void - contextData: DFlowFolderContextMenuGroupData | DFlowFolderContextMenuItemData + contextData: FlowFolderContextMenuComponentGroupData | FlowFolderContextMenuComponentItemData onDelete?: (flow: Flow) => void } -export const DFlowFolderDeleteDialog: React.FC = (props) => { +export const FlowDeleteDialogComponent: React.FC = (props) => { const {open} = props diff --git a/src/packages/ce/src/flow/components/DFlowFolder.style.scss b/src/packages/ce/src/flow/components/FlowFolderComponent.style.scss similarity index 100% rename from src/packages/ce/src/flow/components/DFlowFolder.style.scss rename to src/packages/ce/src/flow/components/FlowFolderComponent.style.scss diff --git a/src/packages/ce/src/flow/components/DFlowFolder.tsx b/src/packages/ce/src/flow/components/FlowFolderComponent.tsx similarity index 85% rename from src/packages/ce/src/flow/components/DFlowFolder.tsx rename to src/packages/ce/src/flow/components/FlowFolderComponent.tsx index 4736f576..d83aee0d 100644 --- a/src/packages/ce/src/flow/components/DFlowFolder.tsx +++ b/src/packages/ce/src/flow/components/FlowFolderComponent.tsx @@ -1,34 +1,42 @@ "use client" -import "./DFlowFolder.style.scss" +import "./FlowFolderComponent.style.scss" import React from "react" -import {Code0Component, mergeCode0Props, useService, useStore} from "../../utils" import {IconFile, IconFolderFilled, IconFolderOpen} from "@tabler/icons-react" import type {Flow, FlowType, Namespace, NamespaceProject, Scalars} from "@code0-tech/sagittarius-graphql-types" -import {DFlowReactiveService} from "../d-flow" -import {ScrollArea, ScrollAreaScrollbar, ScrollAreaThumb, ScrollAreaViewport} from "../scroll-area/ScrollArea" -import {Flex} from "../flex/Flex" -import {Text} from "../text/Text" import { - DFlowFolderContextMenu, - DFlowFolderContextMenuGroupData, - DFlowFolderContextMenuItemData -} from "./DFlowFolderContextMenu"; -import {hashToColor} from "../d-flow/DFlow.util"; + FlowFolderContextMenuComponent, + FlowFolderContextMenuComponentGroupData, + FlowFolderContextMenuComponentItemData +} from "./FlowFolderContextMenuComponent"; import {HoverCard, HoverCardContent, HoverCardPortal, HoverCardTrigger} from "@radix-ui/react-hover-card"; - - -export interface DFlowFolderProps { +import { + Code0Component, + Flex, + hashToColor, + mergeCode0Props, + ScrollArea, + ScrollAreaScrollbar, + ScrollAreaThumb, + ScrollAreaViewport, + Text, + useService, + useStore +} from "@code0-tech/pictor"; +import {FlowService} from "@edition/flow/services/Flow.service"; + + +export interface FlowFolderComponentProps { activeFlowId: Scalars["FlowID"]["output"] namespaceId: Namespace['id'] projectId: NamespaceProject['id'] - onRename?: (contextData: DFlowFolderContextMenuGroupData | DFlowFolderContextMenuItemData) => void - onDelete?: (contextData: DFlowFolderContextMenuGroupData | DFlowFolderContextMenuItemData) => void + onRename?: (contextData: FlowFolderContextMenuComponentGroupData | FlowFolderContextMenuComponentItemData) => void + onDelete?: (contextData: FlowFolderContextMenuComponentGroupData | FlowFolderContextMenuComponentItemData) => void onCreate?: (type: FlowType['id']) => void onSelect?: (flow: Flow) => void } -export type DFlowFolderHandle = { +export type FlowFolderComponentHandle = { openAll: () => void closeAll: () => void openActivePath: () => void @@ -36,26 +44,26 @@ export type DFlowFolderHandle = { type OpenMode = "default" | "allOpen" | "allClosed" | "activePath" -export interface DFlowFolderGroupProps extends DFlowFolderProps, Omit, "onSelect"> { +export interface FlowFolderComponentGroupProps extends FlowFolderComponentProps, Omit, "onSelect"> { name: string - children: React.ReactElement | React.ReactElement[] | React.ReactElement | React.ReactElement[] + children: React.ReactElement | React.ReactElement[] | React.ReactElement | React.ReactElement[] defaultOpen?: boolean flows: Flow[] } -export interface DFlowFolderItemProps extends DFlowFolderProps, Omit, "onSelect"> { +export interface FlowFolderComponentItemProps extends FlowFolderComponentProps, Omit, "onSelect"> { name: string path: string active?: boolean flow: Flow } -export const DFlowFolder = React.forwardRef((props, ref) => { +export const FlowFolderComponent = React.forwardRef((props, ref) => { const {activeFlowId, namespaceId, projectId} = props - const flowService = useService(DFlowReactiveService) - const flowStore = useStore(DFlowReactiveService) + const flowService = useService(FlowService) + const flowStore = useStore(FlowService) type TreeNode = { name: string @@ -215,11 +223,11 @@ export const DFlowFolder = React.forwardRef return ( - +
    {renderChildren(tree.children)}
    -
    +
    @@ -229,7 +237,7 @@ export const DFlowFolder = React.forwardRef }) -export const DFlowFolderGroup: React.FC = (props) => { +export const DFlowFolderGroup: React.FC = (props) => { const { name, @@ -248,7 +256,7 @@ export const DFlowFolderGroup: React.FC = (props) => { const contextMenuProps = {onCreate, onDelete, onRename, activeFlowId, namespaceId, projectId} return <> - = (props) => { {name} - +
    {open ? children : null}
    } -export const DFlowFolderItem: React.FC = (props) => { +export const DFlowFolderItem: React.FC = (props) => { const { name, @@ -304,7 +312,7 @@ export const DFlowFolderItem: React.FC = (props) => { const contextMenuProps = {onCreate, onDelete, onRename, activeFlowId, namespaceId, projectId} - return = (props) => { - + } diff --git a/src/packages/ce/src/flow/components/DFlowFolderContextMenu.tsx b/src/packages/ce/src/flow/components/FlowFolderContextMenuComponent.tsx similarity index 81% rename from src/packages/ce/src/flow/components/DFlowFolderContextMenu.tsx rename to src/packages/ce/src/flow/components/FlowFolderContextMenuComponent.tsx index 207343e4..07c65957 100644 --- a/src/packages/ce/src/flow/components/DFlowFolderContextMenu.tsx +++ b/src/packages/ce/src/flow/components/FlowFolderContextMenuComponent.tsx @@ -1,5 +1,7 @@ -import {DFlowFolderProps} from "./DFlowFolder"; +import {FlowFolderComponentProps} from "./FlowFolderComponent"; import React from "react"; +import {IconChevronRight, IconEdit, IconTrash} from "@tabler/icons-react"; +import {Flow} from "@code0-tech/sagittarius-graphql-types"; import { ContextMenu, ContextMenuContent, @@ -11,32 +13,27 @@ import { ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger -} from "../context-menu/ContextMenu"; -import {Flex} from "../flex/Flex"; -import {Text} from "../text/Text"; -import {IconChevronRight, IconEdit, IconTrash} from "@tabler/icons-react"; -import {useService, useStore} from "../../utils"; -import {DFlowTypeReactiveService} from "../d-flow-type"; -import {Flow} from "@code0-tech/sagittarius-graphql-types"; +} from "@code0-tech/pictor/dist/components/context-menu/ContextMenu"; +import {DFlowTypeReactiveService, Flex, Text, useService, useStore} from "@code0-tech/pictor"; -export interface DFlowFolderContextMenuGroupData { +export interface FlowFolderContextMenuComponentGroupData { name: string flow: Flow[] type: "folder" } -export interface DFlowFolderContextMenuItemData { +export interface FlowFolderContextMenuComponentItemData { name: string flow: Flow type: "item" } -export interface DFlowFolderContextMenuProps extends DFlowFolderProps { +export interface FlowFolderContextMenuComponentProps extends FlowFolderComponentProps { children: React.ReactNode - contextData?: DFlowFolderContextMenuGroupData | DFlowFolderContextMenuItemData + contextData?: FlowFolderContextMenuComponentGroupData | FlowFolderContextMenuComponentItemData } -export const DFlowFolderContextMenu: React.FC = (props) => { +export const FlowFolderContextMenuComponent: React.FC = (props) => { const {children} = props diff --git a/src/packages/ce/src/flow/components/FlowNameInputComponent.tsx b/src/packages/ce/src/flow/components/FlowNameInputComponent.tsx index 6aff18fc..914a0fcc 100644 --- a/src/packages/ce/src/flow/components/FlowNameInputComponent.tsx +++ b/src/packages/ce/src/flow/components/FlowNameInputComponent.tsx @@ -1,5 +1,5 @@ import React from "react"; -import {TextInputProps, InputSuggestion, InputSyntaxSegment, TextInput, Badge} from "@code0-tech/pictor" +import {Badge, InputSuggestion, InputSyntaxSegment, TextInput, TextInputProps} from "@code0-tech/pictor" export interface FlowNameInputComponentProps extends TextInputProps { @@ -35,7 +35,8 @@ export const FlowNameInputComponent: React.FC = (pr start: cursor, end: cursor + value.length, visualLength: splitValue.length - 1 !== index ? 1 : value.length, - content: splitValue.length - 1 !== index ? {value} : value, + content: splitValue.length - 1 !== index ? + {value} : value, } cursor += value.length return [segment] diff --git a/src/packages/ce/src/flow/components/FlowPanelControl.tsx b/src/packages/ce/src/flow/components/FlowPanelControlComponent.tsx similarity index 80% rename from src/packages/ce/src/flow/components/FlowPanelControl.tsx rename to src/packages/ce/src/flow/components/FlowPanelControlComponent.tsx index 9727f3b2..253f69fd 100644 --- a/src/packages/ce/src/flow/components/FlowPanelControl.tsx +++ b/src/packages/ce/src/flow/components/FlowPanelControlComponent.tsx @@ -1,21 +1,27 @@ import React from "react"; -import {ButtonGroup} from "../button-group/ButtonGroup"; -import {Button} from "../button/Button"; -import {Panel} from "@xyflow/react"; -import {useService, useStore} from "../../utils"; -import {FileTabsService} from "../file-tabs/FileTabs.service"; -import {DFlowReactiveService} from "../d-flow"; import {Flow, NodeFunction} from "@code0-tech/sagittarius-graphql-types"; -import {Tooltip, TooltipArrow, TooltipContent, TooltipPortal, TooltipTrigger} from "../tooltip/Tooltip"; -import {Text} from "../text/Text"; -import {DFlowSuggestionMenu} from "../d-flow-suggestion/DFlowSuggestionMenu"; -import {useSuggestions} from "../d-flow-suggestion/DFlowSuggestion.hook"; +import {FileTabsService} from "@code0-tech/pictor/dist/components/file-tabs/FileTabs.service"; +import { + Button, + Text, + Tooltip, + TooltipContent, + TooltipPortal, + TooltipTrigger, + useService, + useStore +} from "@code0-tech/pictor"; +import {useSuggestions} from "@edition/function/hooks/DFlowSuggestion.hook"; +import {Panel} from "@xyflow/react"; +import {ButtonGroup} from "@code0-tech/pictor/dist/components/button-group/ButtonGroup"; +import {DFlowSuggestionMenu} from "@edition/function/components/DFlowSuggestionMenu"; +import {FlowService} from "@edition/flow/services/Flow.service"; -export interface DFlowPanelControlProps { +export interface FlowPanelControlComponentProps { flowId: Flow['id'] } -export const FlowPanelControl: React.FC = (props) => { +export const FlowPanelControlComponent: React.FC = (props) => { //props const {flowId} = props @@ -23,8 +29,8 @@ export const FlowPanelControl: React.FC = (props) => { //services and stores const fileTabsService = useService(FileTabsService) const fileTabsStore = useStore(FileTabsService) - const flowService = useService(DFlowReactiveService) - const flowStore = useStore(DFlowReactiveService) + const flowService = useService(FlowService) + const flowStore = useStore(FlowService) const [, startTransition] = React.useTransition() //memoized values diff --git a/src/packages/ce/src/flow/components/FlowPanelLayout.tsx b/src/packages/ce/src/flow/components/FlowPanelLayoutComponent.tsx similarity index 84% rename from src/packages/ce/src/flow/components/FlowPanelLayout.tsx rename to src/packages/ce/src/flow/components/FlowPanelLayoutComponent.tsx index bf01d0e6..ca162f86 100644 --- a/src/packages/ce/src/flow/components/FlowPanelLayout.tsx +++ b/src/packages/ce/src/flow/components/FlowPanelLayoutComponent.tsx @@ -1,15 +1,20 @@ import React from "react"; -import {SegmentedControl, SegmentedControlItem} from "../segmented-control/SegmentedControl"; import {IconLayout, IconLayoutDistributeHorizontal, IconLayoutDistributeVertical} from "@tabler/icons-react"; import {Panel} from "@xyflow/react"; -import {Tooltip, TooltipArrow, TooltipContent, TooltipPortal, TooltipTrigger} from "../tooltip/Tooltip"; -import {Text} from "../text/Text"; +import { + SegmentedControl, + SegmentedControlItem, Text, + Tooltip, + TooltipContent, + TooltipPortal, + TooltipTrigger +} from "@code0-tech/pictor"; -export interface DFlowPanelLayoutProps { +export interface FlowPanelLayoutComponentProps { } -export const FlowPanelLayout: React.FC = (props) => { +export const FlowPanelLayoutComponent: React.FC = (props) => { const {} = props diff --git a/src/packages/ce/src/flow/components/FlowPanelSize.tsx b/src/packages/ce/src/flow/components/FlowPanelSizeComponent.tsx similarity index 84% rename from src/packages/ce/src/flow/components/FlowPanelSize.tsx rename to src/packages/ce/src/flow/components/FlowPanelSizeComponent.tsx index 90ec7e68..e134364c 100644 --- a/src/packages/ce/src/flow/components/FlowPanelSize.tsx +++ b/src/packages/ce/src/flow/components/FlowPanelSizeComponent.tsx @@ -1,13 +1,10 @@ import React from "react"; import {Panel, useReactFlow, useViewport} from "@xyflow/react"; -import {ButtonGroup} from "../button-group/ButtonGroup"; -import {Button} from "../button/Button"; import {IconFocusCentered, IconMinus, IconPlus} from "@tabler/icons-react"; -import {Badge} from "../badge/Badge"; -import {Flex} from "../flex/Flex"; -import {Text} from "../text/Text"; +import {Badge, Button, Flex, Text} from "@code0-tech/pictor"; +import {ButtonGroup} from "@code0-tech/pictor/dist/components/button-group/ButtonGroup"; -export const FlowPanelSize: React.FC = () => { +export const FlowPanelSizeComponent: React.FC = () => { const viewport = useViewport(); const reactFlow = useReactFlow(); diff --git a/src/packages/ce/src/flow/components/FlowPanelUpdate.tsx b/src/packages/ce/src/flow/components/FlowPanelUpdateComponent.tsx similarity index 82% rename from src/packages/ce/src/flow/components/FlowPanelUpdate.tsx rename to src/packages/ce/src/flow/components/FlowPanelUpdateComponent.tsx index 40e67d92..a8081064 100644 --- a/src/packages/ce/src/flow/components/FlowPanelUpdate.tsx +++ b/src/packages/ce/src/flow/components/FlowPanelUpdateComponent.tsx @@ -1,24 +1,31 @@ import React from "react"; import {IconCloudCheck, IconCloudUpload} from "@tabler/icons-react"; import {Panel} from "@xyflow/react"; -import {Tooltip, TooltipArrow, TooltipContent, TooltipPortal, TooltipTrigger} from "../tooltip/Tooltip"; -import {Text} from "../text/Text"; import {Flow} from "@code0-tech/sagittarius-graphql-types"; -import {useService, useStore} from "../../utils"; -import {DFlowReactiveService} from "../d-flow"; -import {Badge} from "../badge/Badge"; -import {Button} from "../button/Button"; - -export interface DFlowPanelUpdateProps { +import { + Badge, + Button, + Text, + Tooltip, + TooltipArrow, + TooltipContent, + TooltipPortal, + TooltipTrigger, + useService, + useStore +} from "@code0-tech/pictor"; +import {FlowService} from "@edition/flow/services/Flow.service"; + +export interface FlowPanelUpdateComponentProps { flowId: Flow['id'] } -export const FlowPanelUpdate: React.FC = (props) => { +export const FlowPanelUpdateComponent: React.FC = (props) => { const {flowId} = props - const flowService = useService(DFlowReactiveService) - const flowStore = useStore(DFlowReactiveService) + const flowService = useService(FlowService) + const flowStore = useStore(FlowService) const [loading, startTransition] = React.useTransition() const flow = React.useMemo(() => flowService.getById(flowId), [flowId, flowStore]) @@ -106,9 +113,12 @@ const formatTimeAgo = (input: Date | number | string) => { const DAY = 24 * HOUR const YEAR = 365 * DAY - const years = Math.floor(diff / YEAR); diff %= YEAR - const days = Math.floor(diff / DAY); diff %= DAY - const hours = Math.floor(diff / HOUR); diff %= HOUR + const years = Math.floor(diff / YEAR); + diff %= YEAR + const days = Math.floor(diff / DAY); + diff %= DAY + const hours = Math.floor(diff / HOUR); + diff %= HOUR const minutes = Math.floor(diff / MIN) const parts = [ diff --git a/src/packages/ce/src/flow/components/DFlowFolderRenameDialog.tsx b/src/packages/ce/src/flow/components/FlowRenameDialogComponent.tsx similarity index 77% rename from src/packages/ce/src/flow/components/DFlowFolderRenameDialog.tsx rename to src/packages/ce/src/flow/components/FlowRenameDialogComponent.tsx index 3b07233c..b4abbb6f 100644 --- a/src/packages/ce/src/flow/components/DFlowFolderRenameDialog.tsx +++ b/src/packages/ce/src/flow/components/FlowRenameDialogComponent.tsx @@ -1,20 +1,26 @@ import React from "react"; -import {Dialog, DialogClose, DialogContent, DialogOverlay, DialogPortal} from "../dialog/Dialog"; -import {DFlowFolderItemPathInput} from "./DFlowFolderItemPathInput"; -import {Flex} from "../flex/Flex"; -import {Button} from "../button/Button"; -import {DFlowFolderContextMenuGroupData, DFlowFolderContextMenuItemData} from "./DFlowFolderContextMenu"; -import {useForm} from "../form"; +import {FlowFolderContextMenuComponentGroupData, FlowFolderContextMenuComponentItemData} from "./FlowFolderContextMenuComponent"; import {Flow} from "@code0-tech/sagittarius-graphql-types"; +import { + Button, + Dialog, + DialogClose, + DialogContent, + DialogOverlay, + DialogPortal, + Flex, + useForm +} from "@code0-tech/pictor"; +import {FlowNameInputComponent} from "@edition/flow/components/FlowNameInputComponent"; -export interface DFlowFolderRenameDialogProps { - contextData: DFlowFolderContextMenuGroupData | DFlowFolderContextMenuItemData +export interface FlowRenameDialogComponentProps { + contextData: FlowFolderContextMenuComponentGroupData | FlowFolderContextMenuComponentItemData open?: boolean onOpenChange?: (open: boolean) => void onRename?: (flow: Flow, newName: string) => void } -export const DFlowFolderRenameDialog: React.FC = (props) => { +export const FlowRenameDialogComponent: React.FC = (props) => { const {open} = props const [renameDialogOpen, setRenameDialogOpen] = React.useState(open) @@ -53,7 +59,7 @@ export const DFlowFolderRenameDialog: React.FC = (
    - From ee284404735d306c99bbd45d2933ca3a2ad90b8f Mon Sep 17 00:00:00 2001 From: nicosammito Date: Thu, 5 Mar 2026 23:49:38 +0100 Subject: [PATCH 03/35] feat: rename DFlow components and hooks to Function for consistency --- .../[projectId]/flow/[flowId]/page.tsx | 3 +- .../datatype/components/DataTypeJSONInput.tsx | 10 +-- .../datatype/components/DataTypeTextInput.tsx | 10 +-- .../services/rules/DataTypeReturnTypeRule.ts | 2 +- .../flow/components/FlowBuilderComponent.tsx | 12 ++-- .../components/FlowPanelControlComponent.tsx | 10 +-- .../ce/src/flow/hooks/NodeValidation.hook.ts | 4 +- src/packages/ce/src/flow/utils/generics.ts | 2 +- .../DFlowSuggestionMenuSearchBar.tsx | 18 ----- ...TabDefault.tsx => FunctionFileDefault.tsx} | 27 ++++---- ...TabTrigger.tsx => FunctionFileTrigger.tsx} | 36 +++++----- .../{DFlowTabs.tsx => FunctionFiles.tsx} | 43 +++++++----- ...ode.style.scss => FunctionNode.style.scss} | 0 ...{DFlowNode.ts => FunctionNodeComponent.ts} | 4 +- ...efaultCard.tsx => FunctionNodeDefault.tsx} | 44 ++++++------- ...odeGroupCard.tsx => FunctionNodeGroup.tsx} | 8 +-- ...riggerCard.tsx => FunctionNodeTrigger.tsx} | 28 ++++---- ...ion.view.ts => FunctionSuggestion.view.ts} | 0 ...ionMenu.tsx => FunctionSuggestionMenu.tsx} | 32 ++++----- ...il.tsx => FunctionSuggestionMenu.util.tsx} | 5 +- ...r.tsx => FunctionSuggestionMenuFooter.tsx} | 7 +- .../FunctionSuggestionSearchBar.tsx | 18 +++++ ... FunctionSuggestionSearchInput.style.scss} | 0 ....tsx => FunctionSuggestionSearchInput.tsx} | 12 ++-- ...return.hook.ts => Function.return.hook.ts} | 12 ++-- ...x => FunctionDataTypeSuggestions.hook.tsx} | 10 +-- ...rn.hook.ts => FunctionNode.return.hook.ts} | 24 +++---- ...s => FunctionNodeReference.return.hook.ts} | 22 +++---- ...k.tsx => FunctionNodeSuggestions.hook.tsx} | 18 ++--- ... => FunctionReferenceSuggestions.hook.tsx} | 66 +++++++++---------- ...n.hook.tsx => FunctionSuggestion.hook.tsx} | 24 +++---- ....tsx => FunctionValueSuggestions.hook.tsx} | 5 +- 32 files changed, 257 insertions(+), 259 deletions(-) delete mode 100644 src/packages/ce/src/function/components/DFlowSuggestionMenuSearchBar.tsx rename src/packages/ce/src/function/components/{DFlowTabDefault.tsx => FunctionFileDefault.tsx} (87%) rename src/packages/ce/src/function/components/{DFlowTabTrigger.tsx => FunctionFileTrigger.tsx} (77%) rename src/packages/ce/src/function/components/{DFlowTabs.tsx => FunctionFiles.tsx} (85%) rename src/packages/ce/src/function/components/{DFlowNode.style.scss => FunctionNode.style.scss} (100%) rename src/packages/ce/src/function/components/{DFlowNode.ts => FunctionNodeComponent.ts} (60%) rename src/packages/ce/src/function/components/{DFlowNodeDefaultCard.tsx => FunctionNodeDefault.tsx} (82%) rename src/packages/ce/src/function/components/{DFlowNodeGroupCard.tsx => FunctionNodeGroup.tsx} (90%) rename src/packages/ce/src/function/components/{DFlowNodeTriggerCard.tsx => FunctionNodeTrigger.tsx} (74%) rename src/packages/ce/src/function/components/{DFlowSuggestion.view.ts => FunctionSuggestion.view.ts} (100%) rename src/packages/ce/src/function/components/{DFlowSuggestionMenu.tsx => FunctionSuggestionMenu.tsx} (71%) rename src/packages/ce/src/function/components/{DFlowSuggestionMenu.util.tsx => FunctionSuggestionMenu.util.tsx} (93%) rename src/packages/ce/src/function/components/{DFlowSuggestionMenuFooter.tsx => FunctionSuggestionMenuFooter.tsx} (89%) create mode 100644 src/packages/ce/src/function/components/FunctionSuggestionSearchBar.tsx rename src/packages/ce/src/function/components/{DFlowSuggestionSearchInput.style.scss => FunctionSuggestionSearchInput.style.scss} (100%) rename src/packages/ce/src/function/components/{DFlowSuggestionSearchInput.tsx => FunctionSuggestionSearchInput.tsx} (59%) rename src/packages/ce/src/function/hooks/{DFlowFunction.return.hook.ts => Function.return.hook.ts} (53%) rename src/packages/ce/src/function/hooks/{DFlowDataTypeSuggestions.hook.tsx => FunctionDataTypeSuggestions.hook.tsx} (70%) rename src/packages/ce/src/function/hooks/{DFlowNode.return.hook.ts => FunctionNode.return.hook.ts} (62%) rename src/packages/ce/src/function/hooks/{DFlowNodeReference.return.hook.ts => FunctionNodeReference.return.hook.ts} (93%) rename src/packages/ce/src/function/hooks/{DFlowFunctionSuggestions.hook.tsx => FunctionNodeSuggestions.hook.tsx} (84%) rename src/packages/ce/src/function/hooks/{DFlowReferenceSuggestions.hook.tsx => FunctionReferenceSuggestions.hook.tsx} (88%) rename src/packages/ce/src/function/hooks/{DFlowSuggestion.hook.tsx => FunctionSuggestion.hook.tsx} (70%) rename src/packages/ce/src/function/hooks/{DFlowValueSuggestions.hook.tsx => FunctionValueSuggestions.hook.tsx} (92%) diff --git a/src/app/(flow)/namespace/[namespaceId]/project/[projectId]/flow/[flowId]/page.tsx b/src/app/(flow)/namespace/[namespaceId]/project/[projectId]/flow/[flowId]/page.tsx index 3ac5b622..5457b613 100644 --- a/src/app/(flow)/namespace/[namespaceId]/project/[projectId]/flow/[flowId]/page.tsx +++ b/src/app/(flow)/namespace/[namespaceId]/project/[projectId]/flow/[flowId]/page.tsx @@ -15,6 +15,7 @@ import React from "react"; import {IconDatabase, IconFile, IconMessageChatbot} from "@tabler/icons-react"; import {useParams} from "next/navigation"; import {Flow} from "@code0-tech/sagittarius-graphql-types"; +import {FlowBuilderComponent} from "@edition/flow/components/FlowBuilderComponent"; export default function Page() { @@ -42,7 +43,7 @@ export default function Page() { }> - + {show && ( <> diff --git a/src/packages/ce/src/datatype/components/DataTypeJSONInput.tsx b/src/packages/ce/src/datatype/components/DataTypeJSONInput.tsx index 22c84d68..1ed3e878 100644 --- a/src/packages/ce/src/datatype/components/DataTypeJSONInput.tsx +++ b/src/packages/ce/src/datatype/components/DataTypeJSONInput.tsx @@ -6,7 +6,7 @@ import {DataTypeInputProps} from "./DataTypeInput"; import {LiteralValue, NodeFunction, NodeParameterValue} from "@code0-tech/sagittarius-graphql-types"; import {DataTypeInputNodeBadge} from "./DataTypeInputNodeBadge"; import {DataTypeInputReferenceBadge} from "./DataTypeInputReferenceBadge"; -import {useSuggestions} from "@edition/function/hooks/DFlowSuggestion.hook"; +import {useSuggestions} from "@edition/function/hooks/FunctionSuggestion.hook"; import { Button, Card, @@ -17,7 +17,7 @@ import { useStore } from "@code0-tech/pictor"; import {ButtonGroup} from "@code0-tech/pictor/dist/components/button-group/ButtonGroup"; -import {DFlowSuggestionMenu} from "@edition/function/components/DFlowSuggestionMenu"; +import {FunctionSuggestionMenu} from "@edition/function/components/FunctionSuggestionMenu"; import {DataTypeJSONInputEditDialog} from "@edition/datatype/components/DataTypeJSONInputEditDialog"; export interface EditableJSONEntry { @@ -114,9 +114,9 @@ export const DataTypeJSONInput: React.FC = (props) => { {"Object"} - setValue(suggestion.value)} - triggerContent={}/> + color="secondary" + onClick={() => setEditDialogOpen(true)}> + + }/> - - @@ -52,18 +55,19 @@ export const ApplicationTabView: React.FC = () => { }, [currentUser, runtimeStore, organizationStore]) return - - - - - - - - {adminLinks} - - + + + + + + + + {adminLinks} + + } \ No newline at end of file diff --git a/src/packages/ce/src/namespace/pages/NamespaceOverviewPage.tsx b/src/packages/ce/src/namespace/pages/NamespaceOverviewPage.tsx index c4f8bdf4..7e999e24 100644 --- a/src/packages/ce/src/namespace/pages/NamespaceOverviewPage.tsx +++ b/src/packages/ce/src/namespace/pages/NamespaceOverviewPage.tsx @@ -2,11 +2,12 @@ import React from "react"; import {ProjectsView} from "@edition/project/views/ProjectsView"; -import {DLayout, useService, useStore, useUserSession, ScrollArea, ScrollAreaScrollbar, ScrollAreaThumb, ScrollAreaViewport} from "@code0-tech/pictor"; +import {DLayout, useService, useStore, ScrollArea, ScrollAreaScrollbar, ScrollAreaThumb, ScrollAreaViewport} from "@code0-tech/pictor"; import {NamespaceOverviewPersonalLeftView} from "@edition/namespace/views/NamespaceOverviewPersonalLeftView"; import {useParams} from "next/navigation"; import {UserService} from "@edition/user/services/User.service"; import {NamespaceOverviewOrganizationLeftView} from "@edition/namespace/views/NamespaceOverviewOrganizationLeftView"; +import {useUserSession} from "@edition/user/hooks/User.session.hook"; export const NamespaceOverviewPage: React.FC = () => { diff --git a/src/packages/ce/src/namespace/views/NamespaceOverviewPersonalLeftView.tsx b/src/packages/ce/src/namespace/views/NamespaceOverviewPersonalLeftView.tsx index 13775728..fbaba351 100644 --- a/src/packages/ce/src/namespace/views/NamespaceOverviewPersonalLeftView.tsx +++ b/src/packages/ce/src/namespace/views/NamespaceOverviewPersonalLeftView.tsx @@ -17,8 +17,7 @@ import { TooltipPortal, TooltipTrigger, useService, - useStore, - useUserSession + useStore } from "@code0-tech/pictor"; import {UserService} from "@edition/user/services/User.service"; import {IconMail, IconSparkles, IconUser, IconUserCog} from "@tabler/icons-react"; @@ -26,6 +25,7 @@ import {OrganizationService} from "@edition/organization/services/Organization.s import Link from "next/link"; import {useParams} from "next/navigation"; import {MemberService} from "@edition/member/services/Member.service"; +import {useUserSession} from "@edition/user/hooks/User.session.hook"; export const NamespaceOverviewPersonalLeftView: React.FC = () => { diff --git a/src/packages/ce/src/organization/components/OrganizationDataTableRowComponent.tsx b/src/packages/ce/src/organization/components/OrganizationDataTableRowComponent.tsx index f51c8553..b35d1ef6 100644 --- a/src/packages/ce/src/organization/components/OrganizationDataTableRowComponent.tsx +++ b/src/packages/ce/src/organization/components/OrganizationDataTableRowComponent.tsx @@ -1,23 +1,14 @@ import React from "react"; import {Organization} from "@code0-tech/sagittarius-graphql-types"; -import { - Avatar, - Button, - DataTableColumn, - DOrganizationView, - Flex, - hashToColor, - Text, - useService, - useStore, - useUserSession -} from "@code0-tech/pictor"; +import {Avatar, Button, DataTableColumn, Flex, hashToColor, Text, useService, useStore} from "@code0-tech/pictor"; import {IconLogout} from "@tabler/icons-react"; import {OrganizationService} from "@edition/organization/services/Organization.service"; import {NamespaceService} from "@edition/namespace/services/Namespace.service"; import {MemberService} from "@edition/member/services/Member.service"; import {UserService} from "@edition/user/services/User.service"; import {formatDistanceToNow} from "date-fns"; +import {useUserSession} from "@edition/user/hooks/User.session.hook"; +import {DOrganizationView} from "@edition/organization/services/Organization.view"; export interface OrganizationDataTableRowComponentProps { organizationId: Organization['id'] diff --git a/src/packages/ce/src/project/views/PersonalProjectsView.tsx b/src/packages/ce/src/project/views/PersonalProjectsView.tsx index 57b96325..b4b719f8 100644 --- a/src/packages/ce/src/project/views/PersonalProjectsView.tsx +++ b/src/packages/ce/src/project/views/PersonalProjectsView.tsx @@ -10,8 +10,7 @@ import { Spacing, Text, useService, - useStore, - useUserSession + useStore } from "@code0-tech/pictor"; import {UserService} from "@edition/user/services/User.service"; import Link from "next/link"; @@ -21,6 +20,7 @@ import {DataTableFilterProps, DataTableSortProps} from "@code0-tech/pictor/dist/ import {ProjectDataTableFilterInputComponent} from "@edition/project/components/ProjectDataTableFilterInputComponent"; import {ButtonGroup} from "@code0-tech/pictor/dist/components/button-group/ButtonGroup"; import {IconMinus, IconSortAscending, IconSortDescending} from "@tabler/icons-react"; +import {useUserSession} from "@edition/user/hooks/User.session.hook"; export const PersonalProjectsView: React.FC = () => { diff --git a/src/packages/ce/src/runtime/pages/RuntimeCreatePage.tsx b/src/packages/ce/src/runtime/pages/RuntimeCreatePage.tsx index f56077b1..89732673 100644 --- a/src/packages/ce/src/runtime/pages/RuntimeCreatePage.tsx +++ b/src/packages/ce/src/runtime/pages/RuntimeCreatePage.tsx @@ -11,14 +11,14 @@ import { toast, useForm, useService, - useStore, - useUserSession + useStore } from "@code0-tech/pictor"; import Link from "next/link"; import {notFound, useParams, useRouter} from "next/navigation"; import {RuntimeService} from "@edition/runtime/services/Runtime.service"; import {UserService} from "@edition/user/services/User.service"; import {NamespaceService} from "@edition/namespace/services/Namespace.service"; +import {useUserSession} from "@edition/user/hooks/User.session.hook"; export const RuntimeCreatePage: React.FC = () => { diff --git a/src/packages/ce/src/runtime/pages/RuntimesPage.tsx b/src/packages/ce/src/runtime/pages/RuntimesPage.tsx index 49591f71..9625208d 100644 --- a/src/packages/ce/src/runtime/pages/RuntimesPage.tsx +++ b/src/packages/ce/src/runtime/pages/RuntimesPage.tsx @@ -12,8 +12,7 @@ import { Spacing, Text, useService, - useStore, - useUserSession + useStore } from "@code0-tech/pictor"; import Link from "next/link"; import {notFound, useParams, useRouter} from "next/navigation"; @@ -23,6 +22,7 @@ import {DataTableFilterProps, DataTableSortProps} from "@code0-tech/pictor/dist/ import {RuntimeDataTableFilterInputComponent} from "@edition/runtime/components/RuntimeDataTableFilterInputComponent"; import {IconMinus, IconSortAscending, IconSortDescending} from "@tabler/icons-react"; import {ButtonGroup} from "@code0-tech/pictor/dist/components/button-group/ButtonGroup"; +import {useUserSession} from "@edition/user/hooks/User.session.hook"; export const RuntimesPage: React.FC = () => { diff --git a/src/packages/ce/src/user/hooks/User.session.hook.tsx b/src/packages/ce/src/user/hooks/User.session.hook.tsx new file mode 100644 index 00000000..c069da3f --- /dev/null +++ b/src/packages/ce/src/user/hooks/User.session.hook.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import type {UserSession} from "@code0-tech/sagittarius-graphql-types"; + +export const useUserSession = () => { + const [session, setSession] = React.useState(undefined) + + React.useEffect(() => { + const userSession = JSON.parse(localStorage.getItem("ide_code-zero_session")!!) as UserSession + if (userSession && userSession.token) setSession(userSession) + else setSession(null) + }, []) + + return session +} + +export const setUserSession = (userSession: UserSession) => { + localStorage.setItem("ide_code-zero_session", JSON.stringify(userSession)) +} \ No newline at end of file diff --git a/src/packages/ce/src/user/pages/UsersPage.tsx b/src/packages/ce/src/user/pages/UsersPage.tsx index 9321eba3..0fa46588 100644 --- a/src/packages/ce/src/user/pages/UsersPage.tsx +++ b/src/packages/ce/src/user/pages/UsersPage.tsx @@ -1,14 +1,27 @@ "use client" import React from "react"; -import {Button, ButtonGroup, Flex, Spacing, Text, useService, useStore, useUserSession, Menu, MenuTrigger, MenuPortal, MenuContent, MenuCheckboxItem} from "@code0-tech/pictor"; +import { + Button, + ButtonGroup, + Flex, + Menu, + MenuCheckboxItem, + MenuContent, + MenuPortal, + MenuTrigger, + Spacing, + Text, + useService, + useStore +} from "@code0-tech/pictor"; import {UserService} from "@edition/user/services/User.service"; import {notFound} from "next/navigation"; import {UserDataTableComponent} from "@edition/user/components/UserDataTableComponent"; import {DataTableFilterProps, DataTableSortProps} from "@code0-tech/pictor/dist/components/data-table/DataTable"; import {UserDataTableFilterInputComponent} from "@edition/user/components/UserDataTableFilterInputComponent"; -import Link from "next/link"; import {IconMinus, IconSortAscending, IconSortDescending} from "@tabler/icons-react"; +import {useUserSession} from "@edition/user/hooks/User.session.hook"; export const UsersPage: React.FC = () => { From d22af7853e468aca3d099f55826ad6087046972d Mon Sep 17 00:00:00 2001 From: nicosammito Date: Sat, 7 Mar 2026 17:11:56 +0100 Subject: [PATCH 26/35] feat: enhance ProjectDataTableComponent and add ProjectMenuComponent for improved project management --- .../components/ProjectDataTableComponent.tsx | 6 +- .../ProjectDataTableRowComponent.tsx | 4 +- .../components/ProjectMenuComponent.tsx | 102 ++++++++++++++++++ .../ce/src/role/services/Role.service.ts | 2 +- .../role/views/RoleGeneralAdjustmentView.tsx | 7 +- .../ce/src/role/views/RolePermissionView.tsx | 6 +- .../ce/src/role/views/RoleProjectView.tsx | 59 +++++----- 7 files changed, 146 insertions(+), 40 deletions(-) create mode 100644 src/packages/ce/src/project/components/ProjectMenuComponent.tsx diff --git a/src/packages/ce/src/project/components/ProjectDataTableComponent.tsx b/src/packages/ce/src/project/components/ProjectDataTableComponent.tsx index 78ce332b..5e29b581 100644 --- a/src/packages/ce/src/project/components/ProjectDataTableComponent.tsx +++ b/src/packages/ce/src/project/components/ProjectDataTableComponent.tsx @@ -11,11 +11,13 @@ export interface ProjectDataTableComponentProps { filter?: DataTableFilterProps preFilter?: (project: NamespaceProject, index: number) => boolean onSelect?: (item: NamespaceProject | undefined) => void + additionalColumns?: (project: NamespaceProject, index: number) => React.ReactNode[] + } export const ProjectDataTableComponent: React.FC = (props) => { - const {namespaceId, sort, filter, preFilter = () => true, onSelect} = props + const {namespaceId, sort, filter, preFilter = () => true, onSelect, additionalColumns = () => []} = props const projectService = useService(ProjectService) const projectStore = useStore(ProjectService) @@ -31,7 +33,7 @@ export const ProjectDataTableComponent: React.FC onSelect={(item) => item && onSelect?.(item)} data={projects.map(p => p.json()).filter(preFilter)}> {(project, index) => { - return + return }} diff --git a/src/packages/ce/src/project/components/ProjectDataTableRowComponent.tsx b/src/packages/ce/src/project/components/ProjectDataTableRowComponent.tsx index a2ef403b..cf8945b0 100644 --- a/src/packages/ce/src/project/components/ProjectDataTableRowComponent.tsx +++ b/src/packages/ce/src/project/components/ProjectDataTableRowComponent.tsx @@ -7,11 +7,12 @@ import {formatDistanceToNow} from "date-fns"; export interface ProjectDataTableRowComponentProps { projectId: NamespaceProject['id'] + additionalColumns?: React.ReactNode[] } export const ProjectDataTableRowComponent: React.FC = (props) => { - const {projectId} = props + const {projectId, additionalColumns} = props const projectService = useService(ProjectService) const projectStore = useStore(ProjectService) @@ -74,5 +75,6 @@ export const ProjectDataTableRowComponent: React.FC + {additionalColumns} } \ No newline at end of file diff --git a/src/packages/ce/src/project/components/ProjectMenuComponent.tsx b/src/packages/ce/src/project/components/ProjectMenuComponent.tsx new file mode 100644 index 00000000..08a8c504 --- /dev/null +++ b/src/packages/ce/src/project/components/ProjectMenuComponent.tsx @@ -0,0 +1,102 @@ +"use client" + +import React from "react" +import {IconArrowDown, IconArrowUp, IconCornerDownLeft} from "@tabler/icons-react"; +import {DNamespaceProjectView} from "@edition/project/services/Project.view"; +import { + Avatar, + Badge, + Button, + Card, + Flex, hashToColor, + Menu, + MenuContent, + MenuItem, + MenuLabel, + MenuPortal, + MenuProps, + MenuSeparator, + MenuTrigger, + Spacing, Text, + useService +} from "@code0-tech/pictor"; +import {Namespace, Scalars} from "@code0-tech/sagittarius-graphql-types"; +import {ProjectService} from "@edition/project/services/Project.service"; + +export interface ProjectMenuComponentProps extends MenuProps { + onProjectSelect: (project: DNamespaceProjectView) => void + namespaceId: Namespace["id"] + filter?: (project: DNamespaceProjectView, index: number) => boolean + projectId?: Scalars['NamespaceProjectID']['output'] + children?: React.ReactNode +} + +export const ProjectMenuComponent: React.FC = props => { + + const {onProjectSelect, namespaceId, filter = () => true, projectId, children} = props + + const projectService = useService(ProjectService) + const projectStore = useService(ProjectService) + const currentProject = projectService.getById(projectId) + const projects = React.useMemo(() => projectService.values({namespaceId: namespaceId}).filter(filter), [projectStore, namespaceId]) + + return React.useMemo(() => { + return ( + + + {children ? children : ( + + )} + + + + + {projects.map((project, index) => ( + <> + onProjectSelect(project)} + > + + + + + {project?.name} + + + {project?.description} + + + + + + {index < projects.length - 1 && } + + ))} + + + + + + + + + move + + + + + add + + + + + + + ) + }, [projectStore]) +} \ No newline at end of file diff --git a/src/packages/ce/src/role/services/Role.service.ts b/src/packages/ce/src/role/services/Role.service.ts index 41d6b10a..fda1420d 100644 --- a/src/packages/ce/src/role/services/Role.service.ts +++ b/src/packages/ce/src/role/services/Role.service.ts @@ -77,7 +77,7 @@ export class RoleService extends ReactiveArrayService role && role.id === id); + return this.values(dependencies!).find(role => role && role.id === id); } async roleAssignAbilities(payload: NamespacesRolesAssignAbilitiesInput): Promise { diff --git a/src/packages/ce/src/role/views/RoleGeneralAdjustmentView.tsx b/src/packages/ce/src/role/views/RoleGeneralAdjustmentView.tsx index 4efafb52..04ce5625 100644 --- a/src/packages/ce/src/role/views/RoleGeneralAdjustmentView.tsx +++ b/src/packages/ce/src/role/views/RoleGeneralAdjustmentView.tsx @@ -20,13 +20,16 @@ export const RoleGeneralAdjustmentView: React.FC = () => { const namespaceId: Namespace['id'] = `gid://sagittarius/Namespace/${namespaceIndex}` const roleId: NamespaceRole['id'] = `gid://sagittarius/NamespaceRole/${roleIndex}` - const role = React.useMemo(() => roleService.getById(roleId, {namespaceId: namespaceId}), [roleStore, roleId, namespaceId]) + const role = React.useMemo( + () => roleService.getById(roleId, {namespaceId: namespaceId}), + [roleStore, roleId, namespaceId] + ) const initialValues = React.useMemo(() => { return { name: role?.name, } - }, []) + }, [role]) const [inputs, validate] = useForm({ initialValues: initialValues, diff --git a/src/packages/ce/src/role/views/RolePermissionView.tsx b/src/packages/ce/src/role/views/RolePermissionView.tsx index a24121f8..bf425e41 100644 --- a/src/packages/ce/src/role/views/RolePermissionView.tsx +++ b/src/packages/ce/src/role/views/RolePermissionView.tsx @@ -183,7 +183,11 @@ export const RolePermissionView: React.FC = () => { const namespaceId: Namespace['id'] = `gid://sagittarius/Namespace/${namespaceIndex}` const roleId: NamespaceRole['id'] = `gid://sagittarius/NamespaceRole/${roleIndex}` - const role = React.useMemo(() => roleService.getById(roleId, {namespaceId: namespaceId}), [roleStore, roleId, namespaceId]) + const role = React.useMemo( + () => roleService.getById(roleId, {namespaceId: namespaceId}), + [roleStore, roleId, namespaceId] + ) + const roleAbilities = React.useMemo(() => { return { ...permissions.reduce((acc, group) => { diff --git a/src/packages/ce/src/role/views/RoleProjectView.tsx b/src/packages/ce/src/role/views/RoleProjectView.tsx index 36e40da6..e257c78d 100644 --- a/src/packages/ce/src/role/views/RoleProjectView.tsx +++ b/src/packages/ce/src/role/views/RoleProjectView.tsx @@ -1,26 +1,15 @@ "use client" -import { - Alert, - Button, - Card, - DNamespaceProjectView, - Flex, - Spacing, - Text, - toast, - useService, - useStore -} from "@code0-tech/pictor"; -import DNamespaceProjectMenu from "@code0-tech/pictor/dist/components/d-project/DNamespaceProjectMenu"; -import CardSection from "@code0-tech/pictor/dist/components/card/CardSection"; -import {DNamespaceProjectContent} from "@code0-tech/pictor/dist/components/d-project/DNamespaceProjectContent"; -import {IconTrash} from "@tabler/icons-react"; +import {Alert, Button, DataTableColumn, Flex, Spacing, Text, toast, useService, useStore} from "@code0-tech/pictor"; import {TabContent} from "@code0-tech/pictor/dist/components/tab/Tab"; import React from "react"; import {useParams} from "next/navigation"; import {RoleService} from "@edition/role/services/Role.service"; -import type {Namespace, NamespaceRole, Scalars} from "@code0-tech/sagittarius-graphql-types"; +import type {Namespace, NamespaceProject, NamespaceRole, Scalars} from "@code0-tech/sagittarius-graphql-types"; +import {ProjectDataTableComponent} from "@edition/project/components/ProjectDataTableComponent"; +import {ProjectMenuComponent} from "@edition/project/components/ProjectMenuComponent"; +import {DNamespaceProjectView} from "@edition/project/services/Project.view"; +import {IconTrash} from "@tabler/icons-react"; export const RoleProjectView: React.FC = () => { @@ -71,10 +60,14 @@ export const RoleProjectView: React.FC = () => { }) } - const filterProjects = React.useCallback((project: DNamespaceProjectView) => { + const filterNotAssignedProjects = React.useCallback((project: DNamespaceProjectView) => { return !assignedProjectIds.find(projectId => projectId == project.id!!) }, [assignedProjectIds]) + const filterAssignedProjects = React.useCallback((project: NamespaceProject) => { + return !!assignedProjectIds.find(projectId => projectId == project.id!!) + }, [assignedProjectIds]) + return @@ -82,12 +75,12 @@ export const RoleProjectView: React.FC = () => { - addAssignedProject(project.id!!)}> + addAssignedProject(project.id!!)}> - + @@ -100,18 +93,18 @@ export const RoleProjectView: React.FC = () => { ) : ( - - {assignedProjectIds.map(projectId => { - return - - - - - - })} - + + ] + }} namespaceId={namespaceId} preFilter={filterAssignedProjects}/> + )} } \ No newline at end of file From d6ca5a881bcffd4d68f37068e21e8824c8edecbd Mon Sep 17 00:00:00 2001 From: nicosammito Date: Sat, 7 Mar 2026 17:23:53 +0100 Subject: [PATCH 27/35] feat: restore FlowTypeView and FunctionDefinitionView imports in DataTypeInputNodeBadge for proper functionality --- .../ce/src/datatype/components/DataTypeInputNodeBadge.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/packages/ce/src/datatype/components/DataTypeInputNodeBadge.tsx b/src/packages/ce/src/datatype/components/DataTypeInputNodeBadge.tsx index a708eee0..bad8926b 100644 --- a/src/packages/ce/src/datatype/components/DataTypeInputNodeBadge.tsx +++ b/src/packages/ce/src/datatype/components/DataTypeInputNodeBadge.tsx @@ -4,8 +4,6 @@ import {IconBolt, IconNote} from "@tabler/icons-react"; import { Badge, BadgeType, - FlowTypeView, - FunctionDefinitionView, hashToColor, Text, useService, useStore @@ -13,6 +11,8 @@ import { import {FunctionService} from "@edition/function/services/Function.service"; import {FlowService} from "@edition/flow/services/Flow.service"; import {FlowTypeService} from "@edition/flowtype/services/FlowType.service"; +import {FunctionDefinitionView} from "@edition/function/services/Function.view"; +import {FlowTypeView} from "@edition/flowtype/services/FlowType.view"; export interface DataTypeInputNodeBadgeProps extends Omit { value: NodeFunction | NodeFunctionIdWrapper From a2e6ff1c43c64e18a96e485defcbc86b7d594abb Mon Sep 17 00:00:00 2001 From: nicosammito Date: Sat, 7 Mar 2026 22:30:43 +0100 Subject: [PATCH 28/35] feat: replace DResizable components with Resizable components for improved layout handling in various pages --- src/app/(auth)/layout.tsx | 8 ++-- src/app/(dashboard)/layout.tsx | 32 +++++++-------- .../project/[projectId]/default.tsx | 6 +-- src/app/(flow)/layout.tsx | 18 ++++----- .../[projectId]/flow/[flowId]/page.tsx | 35 ++++++++--------- .../project/[projectId]/flow/layout.tsx | 16 +++++--- .../project/[projectId]/flow/page.tsx | 6 +-- .../pages/ApplicationSettingsPage.tsx | 22 ++++++----- .../DataTypeInputReferenceBadge.tsx | 4 +- .../DataTypeJSONInputEditDialog.tsx | 25 +++++++----- .../DataTypeTypeInputEditDialog.tsx | 28 ++++++------- .../components/FlowCreateDialogComponent.tsx | 3 +- .../ce/src/flow/hooks/Flow.nodes.hook.ts | 4 +- src/packages/ce/src/flow/utils/generics.ts | 2 +- .../ce/src/flow/views/FlowFolderView.tsx | 6 +-- .../components/FunctionFileDefault.tsx | 4 +- .../src/function/components/FunctionFiles.tsx | 6 +-- .../function/hooks/Function.return.hook.ts | 2 +- .../FunctionNodeReference.return.hook.ts | 2 +- .../MemberDataTableFilterInputComponent.tsx | 6 +-- .../namespace/pages/NamespaceOverviewPage.tsx | 7 ++-- .../src/namespace/views/NamespaceTabView.tsx | 7 +--- .../pages/OrganizationSettingsPage.tsx | 39 +++++++------------ .../src/project/pages/ProjectSettingsPage.tsx | 21 ++++++---- .../ce/src/role/pages/RoleSettingsPage.tsx | 22 ++++++----- .../RuntimeDataTableFilterInputComponent.tsx | 6 +-- .../src/runtime/pages/RuntimeSettingsPage.tsx | 22 ++++++----- .../user/pages/UserEmailVerificationPage.tsx | 3 +- .../src/user/pages/UserForgotPasswordPage.tsx | 1 - .../ce/src/user/pages/UserLoginPage.tsx | 3 +- .../src/user/pages/UserRegistrationPage.tsx | 6 +-- .../src/user/pages/UserResetPasswordPage.tsx | 4 +- 32 files changed, 190 insertions(+), 186 deletions(-) diff --git a/src/app/(auth)/layout.tsx b/src/app/(auth)/layout.tsx index f77ffc93..bcc9f049 100644 --- a/src/app/(auth)/layout.tsx +++ b/src/app/(auth)/layout.tsx @@ -5,8 +5,6 @@ import { Col, Container, ContextStoreProvider, - DFullScreen, - DUserView, Flex, Spacing, Text } from "@code0-tech/pictor"; @@ -16,6 +14,8 @@ import {GraphqlClient} from "@core/util/graphql-client"; import Image from "next/image"; import React from "react"; import {usePersistentReactiveArrayService} from "@/hooks/usePersistentReactiveArrayService"; +import {DUserView} from "@edition/user/services/User.view"; +import {FullScreen} from "@code0-tech/pictor/dist/components/fullscreen/FullScreen"; export default function AuthLayout({children}: Readonly<{ children: React.ReactNode }>) { @@ -23,7 +23,7 @@ export default function AuthLayout({children}: Readonly<{ children: React.ReactN const [store, service] = usePersistentReactiveArrayService("auth-users", (store) => new UserService(new GraphqlClient(client), store)) return ( - + @@ -61,6 +61,6 @@ export default function AuthLayout({children}: Readonly<{ children: React.ReactN
    - + ); } diff --git a/src/app/(dashboard)/layout.tsx b/src/app/(dashboard)/layout.tsx index 0bb2d45c..0c4085bd 100644 --- a/src/app/(dashboard)/layout.tsx +++ b/src/app/(dashboard)/layout.tsx @@ -5,16 +5,7 @@ import {useApolloClient} from "@apollo/client/react"; import { AuroraBackground, ContextStoreProvider, - DLayout, - DNamespaceMemberView, - DNamespaceProjectView, - DNamespaceRoleView, - DNamespaceView, - DOrganizationView, - DRuntimeView, - DUserView, - Flex, - useUserSession + Flex } from "@code0-tech/pictor"; import {UserService} from "@edition/user/services/User.service"; import {GraphqlClient} from "@core/util/graphql-client"; @@ -28,6 +19,15 @@ import {ProjectService} from "@edition/project/services/Project.service"; import {RoleService} from "@edition/role/services/Role.service"; import Image from "next/image"; import {Application, ApplicationService} from "@edition/application/services/Application.service"; +import {useUserSession} from "@edition/user/hooks/User.session.hook"; +import {DUserView} from "@edition/user/services/User.view"; +import {DOrganizationView} from "@edition/organization/services/Organization.view"; +import {DNamespaceMemberView} from "@edition/member/services/Member.view"; +import {DNamespaceView} from "@edition/namespace/services/Namespace.view"; +import {DRuntimeView} from "@edition/runtime/services/Runtime.view"; +import {DNamespaceProjectView} from "@edition/project/services/Project.view"; +import {DNamespaceRoleView} from "@edition/role/services/Role.view"; +import {Layout} from "@code0-tech/pictor/dist/components/layout/Layout"; interface ApplicationLayoutProps { children: React.ReactNode @@ -55,7 +55,7 @@ const ApplicationLayout: React.FC = ({children, bar, tab if (currentSession === null) router.push("/login") return -
    = ({children, bar, tab {tab} }> - {bar}}> - + {bar}}> + <>{children} - - - + + + } diff --git a/src/app/(flow)/@tab/namespace/[namespaceId]/project/[projectId]/default.tsx b/src/app/(flow)/@tab/namespace/[namespaceId]/project/[projectId]/default.tsx index d2a8b584..d50ac4aa 100644 --- a/src/app/(flow)/@tab/namespace/[namespaceId]/project/[projectId]/default.tsx +++ b/src/app/(flow)/@tab/namespace/[namespaceId]/project/[projectId]/default.tsx @@ -2,10 +2,9 @@ import {Tab, TabList, TabTrigger} from "@code0-tech/pictor/dist/components/tab/Tab"; import {Button, Text, Tooltip, TooltipContent, TooltipPortal, TooltipTrigger} from "@code0-tech/pictor"; -import {IconBuilding, IconHome, IconSettings} from "@tabler/icons-react"; +import {IconHome, IconSettings} from "@tabler/icons-react"; import React from "react"; import {useParams, usePathname, useRouter} from "next/navigation"; -import {hashToColor} from "@code0-tech/pictor/dist/components/d-flow/DFlow.util"; export default function Page() { @@ -45,7 +44,8 @@ export default function Page() { - diff --git a/src/app/(flow)/layout.tsx b/src/app/(flow)/layout.tsx index c1642f06..0b764ace 100644 --- a/src/app/(flow)/layout.tsx +++ b/src/app/(flow)/layout.tsx @@ -5,9 +5,7 @@ import {useParams, useRouter} from "next/navigation"; import { AuroraBackground, ContextStoreProvider, - DLayout, Flex, - useUserSession } from "@code0-tech/pictor"; import React from "react"; import {GraphqlClient} from "@core/util/graphql-client"; @@ -37,8 +35,10 @@ import {DNamespaceRoleView} from "@edition/role/services/Role.view"; import {FunctionDefinitionView} from "@edition/function/services/Function.view"; import {DataTypeView} from "@edition/datatype/services/DataType.view"; import {FlowTypeView} from "@edition/flowtype/services/FlowType.view"; +import {useUserSession} from "@edition/user/hooks/User.session.hook"; +import {Layout} from "@code0-tech/pictor/dist/components/layout/Layout"; -export default function Layout({bar, tab, children}: { +export default function FlowLayout({bar, tab, children}: { bar: React.ReactNode, tab: React.ReactNode, children: React.ReactNode @@ -86,7 +86,7 @@ export default function Layout({bar, tab, children}: { return -
    }> - {bar}}> - + {bar}}> + <> {children} - - - + + + } \ No newline at end of file diff --git a/src/app/(flow)/namespace/[namespaceId]/project/[projectId]/flow/[flowId]/page.tsx b/src/app/(flow)/namespace/[namespaceId]/project/[projectId]/flow/[flowId]/page.tsx index e101593e..dabc21e5 100644 --- a/src/app/(flow)/namespace/[namespaceId]/project/[projectId]/flow/[flowId]/page.tsx +++ b/src/app/(flow)/namespace/[namespaceId]/project/[projectId]/flow/[flowId]/page.tsx @@ -2,14 +2,7 @@ import { Button, - DFlow, - DFlowTabs, - DLayout, - DResizableHandle, - DResizablePanel, - DResizablePanelGroup, Flex, - Text } from "@code0-tech/pictor"; import React from "react"; import {IconDatabase, IconFile, IconMessageChatbot} from "@tabler/icons-react"; @@ -17,6 +10,12 @@ import {useParams} from "next/navigation"; import {Flow} from "@code0-tech/sagittarius-graphql-types"; import {FlowBuilderComponent} from "@edition/flow/components/FlowBuilderComponent"; import {FunctionFiles} from "@edition/function/components/FunctionFiles"; +import { + ResizableHandle, + ResizablePanel, + ResizablePanelGroup +} from "@code0-tech/pictor/dist/components/resizable/Resizable"; +import {Layout} from "@code0-tech/pictor/dist/components/layout/Layout"; export default function Page() { @@ -27,8 +26,8 @@ export default function Page() { const [show, setShow] = React.useState(false); - return - + }> - - + + - + {show && ( <> - - + + - + )} - - - + + + } \ No newline at end of file diff --git a/src/app/(flow)/namespace/[namespaceId]/project/[projectId]/flow/layout.tsx b/src/app/(flow)/namespace/[namespaceId]/project/[projectId]/flow/layout.tsx index ae24ca0d..70ce7476 100644 --- a/src/app/(flow)/namespace/[namespaceId]/project/[projectId]/flow/layout.tsx +++ b/src/app/(flow)/namespace/[namespaceId]/project/[projectId]/flow/layout.tsx @@ -1,8 +1,12 @@ "use client" import React from "react"; -import {DResizableHandle, DResizablePanel, DResizablePanelGroup} from "@code0-tech/pictor"; import {FlowFolderView} from "@edition/flow/views/FlowFolderView"; +import { + ResizableHandle, + ResizablePanel, + ResizablePanelGroup +} from "@code0-tech/pictor/dist/components/resizable/Resizable"; interface ApplicationLayoutProps { children: React.ReactNode @@ -10,13 +14,13 @@ interface ApplicationLayoutProps { const ApplicationLayout: React.FC = ({children}) => { - return - + return + - - + + {children} - + } export default ApplicationLayout diff --git a/src/app/(flow)/namespace/[namespaceId]/project/[projectId]/flow/page.tsx b/src/app/(flow)/namespace/[namespaceId]/project/[projectId]/flow/page.tsx index 2022222c..4c316f74 100644 --- a/src/app/(flow)/namespace/[namespaceId]/project/[projectId]/flow/page.tsx +++ b/src/app/(flow)/namespace/[namespaceId]/project/[projectId]/flow/page.tsx @@ -3,7 +3,6 @@ import { Button, Col, - DResizablePanel, Flex, ScrollArea, ScrollAreaScrollbar, @@ -15,6 +14,7 @@ import { import React from "react"; import Link from "next/link"; import {useParams} from "next/navigation"; +import {ResizablePanel} from "@code0-tech/pictor/dist/components/resizable/Resizable"; export default function Page() { @@ -22,7 +22,7 @@ export default function Page() { const namespaceIndex = params?.namespaceId as any as number - return @@ -67,5 +67,5 @@ export default function Page() { left: 0, zIndex: -1, }}/> - + } \ No newline at end of file diff --git a/src/packages/ce/src/application/pages/ApplicationSettingsPage.tsx b/src/packages/ce/src/application/pages/ApplicationSettingsPage.tsx index 7ec8247c..05a463ca 100644 --- a/src/packages/ce/src/application/pages/ApplicationSettingsPage.tsx +++ b/src/packages/ce/src/application/pages/ApplicationSettingsPage.tsx @@ -5,9 +5,6 @@ import { Badge, Button, Card, - DResizableHandle, - DResizablePanel, - DResizablePanelGroup, Flex, Spacing, SwitchInput, @@ -24,6 +21,11 @@ import {IconLayoutSidebar} from "@tabler/icons-react"; import CardSection from "@code0-tech/pictor/dist/components/card/CardSection"; import {ApplicationService} from "@edition/application/services/Application.service"; import {useUserSession} from "@edition/user/hooks/User.session.hook"; +import { + ResizableHandle, + ResizablePanel, + ResizablePanelGroup +} from "@code0-tech/pictor/dist/components/resizable/Resizable"; export const ApplicationSettingsPage: React.FC = () => { @@ -86,8 +88,8 @@ export const ApplicationSettingsPage: React.FC = () => { }) return - - + @@ -114,9 +116,9 @@ export const ApplicationSettingsPage: React.FC = () => { - - - + + + <> General @@ -231,7 +233,7 @@ export const ApplicationSettingsPage: React.FC = () => { - - + + } \ No newline at end of file diff --git a/src/packages/ce/src/datatype/components/DataTypeInputReferenceBadge.tsx b/src/packages/ce/src/datatype/components/DataTypeInputReferenceBadge.tsx index 3a77c57e..f207ca66 100644 --- a/src/packages/ce/src/datatype/components/DataTypeInputReferenceBadge.tsx +++ b/src/packages/ce/src/datatype/components/DataTypeInputReferenceBadge.tsx @@ -2,7 +2,9 @@ import {Flow, ReferenceValue} from "@code0-tech/sagittarius-graphql-types"; import React from "react"; import {DataTypeInputNodeBadge} from "./DataTypeInputNodeBadge"; import {IconVariable} from "@tabler/icons-react"; -import {Badge, BadgeType, Flex, FlowTypeView, FunctionDefinitionView, Text} from "@code0-tech/pictor"; +import {Badge, BadgeType, Flex, Text} from "@code0-tech/pictor"; +import {FunctionDefinitionView} from "@edition/function/services/Function.view"; +import {FlowTypeView} from "@edition/flowtype/services/FlowType.view"; export interface DataTypeInputReferenceBadgeProps extends Omit { value: ReferenceValue diff --git a/src/packages/ce/src/datatype/components/DataTypeJSONInputEditDialog.tsx b/src/packages/ce/src/datatype/components/DataTypeJSONInputEditDialog.tsx index 36a07ce5..b8f55f90 100644 --- a/src/packages/ce/src/datatype/components/DataTypeJSONInputEditDialog.tsx +++ b/src/packages/ce/src/datatype/components/DataTypeJSONInputEditDialog.tsx @@ -9,11 +9,16 @@ import { DialogContent, DialogOverlay, DialogPortal, - DLayout, DResizableHandle, DResizablePanel, DResizablePanelGroup, Flex, ScrollArea, ScrollAreaScrollbar, ScrollAreaThumb, ScrollAreaViewport, Spacing, Text } from "@code0-tech/pictor"; import {IconX} from "@tabler/icons-react"; import {Editor} from "@code0-tech/pictor/dist/components/editor/Editor"; +import {Layout} from "@code0-tech/pictor/dist/components/layout/Layout"; +import { + ResizableHandle, + ResizablePanel, + ResizablePanelGroup +} from "@code0-tech/pictor/dist/components/resizable/Resizable"; export interface DataTypeJSONInputEditDialogProps { open: boolean @@ -128,7 +133,7 @@ export const DataTypeJSONInputEditDialog: React.FC - {entry?.key ?? "Edit Object"} @@ -139,8 +144,8 @@ export const DataTypeJSONInputEditDialog: React.FC }> - - + + @@ -161,9 +166,9 @@ export const DataTypeJSONInputEditDialog: React.FC - - - + + + - - - + + + diff --git a/src/packages/ce/src/datatype/components/DataTypeTypeInputEditDialog.tsx b/src/packages/ce/src/datatype/components/DataTypeTypeInputEditDialog.tsx index 8252dd35..1d0a2c47 100644 --- a/src/packages/ce/src/datatype/components/DataTypeTypeInputEditDialog.tsx +++ b/src/packages/ce/src/datatype/components/DataTypeTypeInputEditDialog.tsx @@ -13,10 +13,6 @@ import { DialogOverlay, DialogPortal, DialogTitle, - DLayout, - DResizableHandle, - DResizablePanel, - DResizablePanelGroup, Flex, hashToColor, ScrollArea, @@ -30,6 +26,12 @@ import { } from "@code0-tech/pictor"; import {DatatypeService} from "@edition/datatype/services/Datatype.service"; import {Editor, EditorTokenHighlights} from "@code0-tech/pictor/dist/components/editor/Editor"; +import {Layout} from "@code0-tech/pictor/dist/components/layout/Layout"; +import { + ResizableHandle, + ResizablePanel, + ResizablePanelGroup +} from "@code0-tech/pictor/dist/components/resizable/Resizable"; export interface DataTypeTypeInputEditDialogProps { dataTypeIdentifier: DataTypeIdentifier @@ -128,7 +130,7 @@ export const DataTypeTypeInputEditDialog: React.FC - @@ -140,8 +142,8 @@ export const DataTypeTypeInputEditDialog: React.FC }> - - + + @@ -155,9 +157,9 @@ export const DataTypeTypeInputEditDialog: React.FC - - - + + + { const dataTypeIdentifier = dataTypeService.getTypeFromValue({ @@ -167,9 +169,9 @@ export const DataTypeTypeInputEditDialog: React.FC - - - + + + diff --git a/src/packages/ce/src/flow/components/FlowCreateDialogComponent.tsx b/src/packages/ce/src/flow/components/FlowCreateDialogComponent.tsx index c31fccb0..46042424 100644 --- a/src/packages/ce/src/flow/components/FlowCreateDialogComponent.tsx +++ b/src/packages/ce/src/flow/components/FlowCreateDialogComponent.tsx @@ -10,7 +10,7 @@ import { DialogContent, DialogOverlay, DialogPortal, - Flex, + Flex, hashToColor, InputDescription, InputLabel, InputMessage, @@ -27,7 +27,6 @@ import { useService, useStore } from "@code0-tech/pictor"; -import {hashToColor} from "@code0-tech/pictor/dist/components/d-flow/DFlow.util"; import {FlowTypeService} from "@edition/flowtype/services/FlowType.service"; import {FlowNameInputComponent} from "@edition/flow/components/FlowNameInputComponent"; import {ButtonGroup} from "@code0-tech/pictor/dist/components/button-group/ButtonGroup"; diff --git a/src/packages/ce/src/flow/hooks/Flow.nodes.hook.ts b/src/packages/ce/src/flow/hooks/Flow.nodes.hook.ts index b69e091e..5db99832 100644 --- a/src/packages/ce/src/flow/hooks/Flow.nodes.hook.ts +++ b/src/packages/ce/src/flow/hooks/Flow.nodes.hook.ts @@ -5,7 +5,7 @@ import {hashToColor, useService, useStore} from "@code0-tech/pictor"; import {FlowService} from "@edition/flow/services/Flow.service"; import {FunctionService} from "@edition/function/services/Function.service"; import {DatatypeService} from "@edition/datatype/services/Datatype.service"; -import {DFlowNodeProps} from "@code0-tech/pictor/dist/components/d-flow-node/DFlowNode"; +import {FunctionNodeProps} from "@edition/function/components/FunctionNodeComponent"; const packageNodes = new Map([ ['std', 'default'], @@ -108,7 +108,7 @@ export const useFlowNodes = (flowId: Flow["id"], namespaceId?: Namespace["id"], if (functionStore.length <= 0) return [] if (dataTypeStore.length <= 0) return [] - const nodes: Node[] = []; + const nodes: Node[] = []; const visited = new Set(); let groupCounter = 0; diff --git a/src/packages/ce/src/flow/utils/generics.ts b/src/packages/ce/src/flow/utils/generics.ts index 36628752..1bd4654a 100644 --- a/src/packages/ce/src/flow/utils/generics.ts +++ b/src/packages/ce/src/flow/utils/generics.ts @@ -12,9 +12,9 @@ import type { NodeParameterValue } from "@code0-tech/sagittarius-graphql-types"; import {useReturnType} from "@edition/function/hooks/Function.return.hook"; -import {FunctionDefinitionView} from "@code0-tech/pictor"; import {DatatypeService} from "@edition/datatype/services/Datatype.service"; import {FunctionService} from "@edition/function/services/Function.service"; +import {FunctionDefinitionView} from "@edition/function/services/Function.view"; const GENERIC_PLACEHOLDER = "GENERIC"; diff --git a/src/packages/ce/src/flow/views/FlowFolderView.tsx b/src/packages/ce/src/flow/views/FlowFolderView.tsx index 52f6526c..255070dd 100644 --- a/src/packages/ce/src/flow/views/FlowFolderView.tsx +++ b/src/packages/ce/src/flow/views/FlowFolderView.tsx @@ -3,7 +3,6 @@ import React from "react"; import { Button, - DLayout, Flex, Text, toast, @@ -26,6 +25,7 @@ import { FlowFolderContextMenuComponentGroupData, FlowFolderContextMenuComponentItemData } from "@edition/flow/components/FlowFolderContextMenuComponent"; +import {Layout} from "@code0-tech/pictor/dist/components/layout/Layout"; export const FlowFolderView: React.FC = () => { @@ -80,7 +80,7 @@ export const FlowFolderView: React.FC = () => { contextData={contextData} onDelete={deleteFlow}/> - Explorer @@ -167,6 +167,6 @@ export const FlowFolderView: React.FC = () => { }} namespaceId={`gid://sagittarius/Namespace/${namespaceIndex}`} projectId={`gid://sagittarius/NamespaceProject/${projectIndex}`}/> - + } \ No newline at end of file diff --git a/src/packages/ce/src/function/components/FunctionFileDefault.tsx b/src/packages/ce/src/function/components/FunctionFileDefault.tsx index 9adf7e12..bc6969fe 100644 --- a/src/packages/ce/src/function/components/FunctionFileDefault.tsx +++ b/src/packages/ce/src/function/components/FunctionFileDefault.tsx @@ -1,5 +1,5 @@ import React from "react"; -import {Flex, InputSyntaxSegment, ParameterDefinitionView, useForm, useService, useStore} from "@code0-tech/pictor"; +import {Flex, InputSyntaxSegment, useForm, useService, useStore} from "@code0-tech/pictor"; import { Flow, LiteralValue, @@ -10,10 +10,10 @@ import { } from "@code0-tech/sagittarius-graphql-types"; import {FileTabsService} from "@code0-tech/pictor/dist/components/file-tabs/FileTabs.service"; import {useNodeValidation} from "@edition/flow/hooks/NodeValidation.hook"; -import {DFlowInput} from "@code0-tech/pictor/dist/components/d-flow-input/DFlowInput"; import {FunctionService} from "@edition/function/services/Function.service"; import {FlowService} from "@edition/flow/services/Flow.service"; import {DataTypeInput} from "@edition/datatype/components/DataTypeInput"; +import {ParameterDefinitionView} from "@edition/function/services/Function.view"; export interface FunctionFileDefaultProps { node: NodeFunction diff --git a/src/packages/ce/src/function/components/FunctionFiles.tsx b/src/packages/ce/src/function/components/FunctionFiles.tsx index fe974b79..69c050df 100644 --- a/src/packages/ce/src/function/components/FunctionFiles.tsx +++ b/src/packages/ce/src/function/components/FunctionFiles.tsx @@ -2,7 +2,6 @@ import React from "react"; import {Flow, Namespace, NamespaceProject} from "@code0-tech/sagittarius-graphql-types"; import { Button, - DLayout, Menu, MenuContent, MenuItem, MenuLabel, MenuPortal, MenuSeparator, MenuTrigger, useService, @@ -20,6 +19,7 @@ import {ButtonGroup} from "@code0-tech/pictor/dist/components/button-group/Butto import {IconDotsVertical, IconPlus} from "@tabler/icons-react"; import {FlowService} from "@edition/flow/services/Flow.service"; import {FlowTypeService} from "@edition/flowtype/services/FlowType.service"; +import {Layout} from "@code0-tech/pictor/dist/components/layout/Layout"; export interface FunctionFilesProps { flowId: Flow['id'] @@ -83,7 +83,7 @@ export const FunctionFiles: React.FC = (props) => { fileTabsService.activateTab(value); }} > - @@ -165,7 +165,7 @@ export const FunctionFiles: React.FC = (props) => { ))} - + ); diff --git a/src/packages/ce/src/function/hooks/Function.return.hook.ts b/src/packages/ce/src/function/hooks/Function.return.hook.ts index 54f4ee31..1e327525 100644 --- a/src/packages/ce/src/function/hooks/Function.return.hook.ts +++ b/src/packages/ce/src/function/hooks/Function.return.hook.ts @@ -1,8 +1,8 @@ import type {DataTypeIdentifier, NodeParameterValue} from "@code0-tech/sagittarius-graphql-types"; -import {FunctionDefinitionView} from "@code0-tech/pictor"; import {DatatypeService} from "@edition/datatype/services/Datatype.service"; import {FunctionService} from "@edition/function/services/Function.service"; import {replaceGenericKeysInType, resolveGenericKeys} from "@edition/flow/utils/generics"; +import {FunctionDefinitionView} from "@edition/function/services/Function.view"; export const useReturnType = ( func: FunctionDefinitionView, diff --git a/src/packages/ce/src/function/hooks/FunctionNodeReference.return.hook.ts b/src/packages/ce/src/function/hooks/FunctionNodeReference.return.hook.ts index 0cc52c17..dc3e3b19 100644 --- a/src/packages/ce/src/function/hooks/FunctionNodeReference.return.hook.ts +++ b/src/packages/ce/src/function/hooks/FunctionNodeReference.return.hook.ts @@ -1,13 +1,13 @@ import {DataTypeIdentifier, FlowType, NodeFunction, ReferenceValue} from "@code0-tech/sagittarius-graphql-types"; import {DatatypeService} from "@edition/datatype/services/Datatype.service"; import {FunctionService} from "@edition/function/services/Function.service"; -import {FunctionDefinitionView} from "@code0-tech/pictor"; import { replaceGenericKeysInType, resolveGenericKeys, resolveType, targetForGenericKey } from "@edition/flow/utils/generics"; +import {FunctionDefinitionView} from "@edition/function/services/Function.view"; export const getReferenceType = ( reference: ReferenceValue, diff --git a/src/packages/ce/src/member/components/MemberDataTableFilterInputComponent.tsx b/src/packages/ce/src/member/components/MemberDataTableFilterInputComponent.tsx index d62858f3..5a921b30 100644 --- a/src/packages/ce/src/member/components/MemberDataTableFilterInputComponent.tsx +++ b/src/packages/ce/src/member/components/MemberDataTableFilterInputComponent.tsx @@ -1,17 +1,15 @@ import React from "react"; import { DataTableFilterInput, - DataTableFilterSuggestionMenu, DRuntimeView, + DataTableFilterSuggestionMenu, MenuCheckboxItem, useService, useStore } from "@code0-tech/pictor"; import {IconCheck} from "@tabler/icons-react"; -import {Namespace, Runtime} from "@code0-tech/sagittarius-graphql-types"; +import {Namespace} from "@code0-tech/sagittarius-graphql-types"; import {DataTableFilterProps} from "@code0-tech/pictor/dist/components/data-table/DataTable"; import {RoleService} from "@edition/role/services/Role.service"; -import {RuntimeService} from "@edition/runtime/services/Runtime.service"; -import {UserService} from "@edition/user/services/User.service"; import {MemberService} from "@edition/member/services/Member.service"; export interface MemberDataTableFilterInputComponentProps { diff --git a/src/packages/ce/src/namespace/pages/NamespaceOverviewPage.tsx b/src/packages/ce/src/namespace/pages/NamespaceOverviewPage.tsx index 7e999e24..011fc4ce 100644 --- a/src/packages/ce/src/namespace/pages/NamespaceOverviewPage.tsx +++ b/src/packages/ce/src/namespace/pages/NamespaceOverviewPage.tsx @@ -2,12 +2,13 @@ import React from "react"; import {ProjectsView} from "@edition/project/views/ProjectsView"; -import {DLayout, useService, useStore, ScrollArea, ScrollAreaScrollbar, ScrollAreaThumb, ScrollAreaViewport} from "@code0-tech/pictor"; +import {useService, useStore, ScrollArea, ScrollAreaScrollbar, ScrollAreaThumb, ScrollAreaViewport} from "@code0-tech/pictor"; import {NamespaceOverviewPersonalLeftView} from "@edition/namespace/views/NamespaceOverviewPersonalLeftView"; import {useParams} from "next/navigation"; import {UserService} from "@edition/user/services/User.service"; import {NamespaceOverviewOrganizationLeftView} from "@edition/namespace/views/NamespaceOverviewOrganizationLeftView"; import {useUserSession} from "@edition/user/hooks/User.session.hook"; +import {Layout} from "@code0-tech/pictor/dist/components/layout/Layout"; export const NamespaceOverviewPage: React.FC = () => { @@ -20,7 +21,7 @@ export const NamespaceOverviewPage: React.FC = () => { const namespaceIndexCurrentUser = currentUser?.namespace?.id?.match(/Namespace\/(\d+)$/)?.[1] const namespaceId = params.namespaceId as any as string - return : }>
    {
    -
    + } \ No newline at end of file diff --git a/src/packages/ce/src/namespace/views/NamespaceTabView.tsx b/src/packages/ce/src/namespace/views/NamespaceTabView.tsx index 7233599f..a70dc755 100644 --- a/src/packages/ce/src/namespace/views/NamespaceTabView.tsx +++ b/src/packages/ce/src/namespace/views/NamespaceTabView.tsx @@ -1,17 +1,12 @@ "use client" import React from "react"; -import {Badge, Button, useService, useStore} from "@code0-tech/pictor"; +import {Button, useService, useStore} from "@code0-tech/pictor"; import {Tab, TabList, TabTrigger} from "@code0-tech/pictor/dist/components/tab/Tab"; import {IconFolders, IconHome, IconServer, IconSettings, IconUserCog, IconUsers} from "@tabler/icons-react"; import {useParams, usePathname, useRouter} from "next/navigation"; import {NamespaceService} from "@edition/namespace/services/Namespace.service"; import {OrganizationService} from "@edition/organization/services/Organization.service"; -import {ProjectService} from "@edition/project/services/Project.service"; -import {RoleService} from "@edition/role/services/Role.service"; -import {MemberService} from "@edition/member/services/Member.service"; -import {RuntimeService} from "@edition/runtime/services/Runtime.service"; -import {hashToColor} from "@code0-tech/pictor/dist/components/d-flow/DFlow.util"; export const NamespaceTabView: React.FC = () => { diff --git a/src/packages/ce/src/organization/pages/OrganizationSettingsPage.tsx b/src/packages/ce/src/organization/pages/OrganizationSettingsPage.tsx index 063e30a3..349a3b65 100644 --- a/src/packages/ce/src/organization/pages/OrganizationSettingsPage.tsx +++ b/src/packages/ce/src/organization/pages/OrganizationSettingsPage.tsx @@ -1,37 +1,27 @@ "use client" import React from "react"; -import {useParams} from "next/navigation"; -import {Namespace} from "@code0-tech/sagittarius-graphql-types"; -import { - AuroraBackground, - Badge, - Button, - DResizableHandle, - DResizablePanel, - DResizablePanelGroup, - Flex, - Text, - useService, - useStore -} from "@code0-tech/pictor"; -import {NamespaceService} from "@edition/namespace/services/Namespace.service"; -import {OrganizationService} from "@edition/organization/services/Organization.service"; +import {AuroraBackground, Button, Flex, Text} from "@code0-tech/pictor"; import {IconLayoutSidebar} from "@tabler/icons-react"; import {Tab, TabContent, TabList, TabTrigger} from "@code0-tech/pictor/dist/components/tab/Tab"; import {UsageView} from "@edition/runtime/views/UsageView"; import {OrganizationUpgradeView} from "@edition/organization/views/OrganizationUpgradeView"; import {OrganizationDeleteView} from "@edition/organization/views/OrganizationDeleteView"; import {OrganizationGeneralSettingsView} from "@edition/organization/views/OrganizationGeneralSettingsView"; +import { + ResizableHandle, + ResizablePanel, + ResizablePanelGroup +} from "@code0-tech/pictor/dist/components/resizable/Resizable"; export const OrganizationSettingsPage: React.FC = () => { //TODO: add ability check for organization settings access for every settings tab return - - + + Organization settings @@ -71,9 +61,10 @@ export const OrganizationSettingsPage: React.FC = () => { - - - + + + <> @@ -88,7 +79,7 @@ export const OrganizationSettingsPage: React.FC = () => { - - + + } \ No newline at end of file diff --git a/src/packages/ce/src/project/pages/ProjectSettingsPage.tsx b/src/packages/ce/src/project/pages/ProjectSettingsPage.tsx index c9988b40..d7715b75 100644 --- a/src/packages/ce/src/project/pages/ProjectSettingsPage.tsx +++ b/src/packages/ce/src/project/pages/ProjectSettingsPage.tsx @@ -2,16 +2,21 @@ import {Tab, TabList, TabTrigger} from "@code0-tech/pictor/dist/components/tab/Tab"; import React from "react"; -import {Button, DResizableHandle, DResizablePanel, DResizablePanelGroup, Flex, Text} from "@code0-tech/pictor"; +import {Button, Flex, Text} from "@code0-tech/pictor"; import {IconLayoutSidebar} from "@tabler/icons-react"; import {ProjectSettingsGeneralView} from "@edition/project/views/ProjectSettingsGeneralView"; import {ProjectSettingsRuntimesView} from "@edition/project/views/ProjectSettingsRuntimesView"; import {ProjectSettingsDeleteView} from "@edition/project/views/ProjectSettingsDeleteView"; +import { + ResizableHandle, + ResizablePanel, + ResizablePanelGroup +} from "@code0-tech/pictor/dist/components/resizable/Resizable"; export const ProjectSettingsPage: React.FC = () => { return - - + @@ -43,16 +48,16 @@ export const ProjectSettingsPage: React.FC = () => { - - - + + <> - - + + } \ No newline at end of file diff --git a/src/packages/ce/src/role/pages/RoleSettingsPage.tsx b/src/packages/ce/src/role/pages/RoleSettingsPage.tsx index feb1f344..2a5cf3fb 100644 --- a/src/packages/ce/src/role/pages/RoleSettingsPage.tsx +++ b/src/packages/ce/src/role/pages/RoleSettingsPage.tsx @@ -3,9 +3,6 @@ import React from "react"; import { Button, - DResizableHandle, - DResizablePanel, - DResizablePanelGroup, Flex, ScrollArea, ScrollAreaScrollbar, @@ -19,6 +16,11 @@ import {RolePermissionView} from "@edition/role/views/RolePermissionView"; import {RoleGeneralAdjustmentView} from "@edition/role/views/RoleGeneralAdjustmentView"; import {RoleDeleteView} from "@edition/role/views/RoleDeleteView"; import {IconLayoutSidebar} from "@tabler/icons-react"; +import { + ResizableHandle, + ResizablePanel, + ResizablePanelGroup +} from "@code0-tech/pictor/dist/components/resizable/Resizable"; export const RoleSettingsPage: React.FC = () => { @@ -26,8 +28,8 @@ export const RoleSettingsPage: React.FC = () => { return <> - - + @@ -65,9 +67,9 @@ export const RoleSettingsPage: React.FC = () => { - - - + + @@ -80,8 +82,8 @@ export const RoleSettingsPage: React.FC = () => { - - + + } \ No newline at end of file diff --git a/src/packages/ce/src/runtime/components/RuntimeDataTableFilterInputComponent.tsx b/src/packages/ce/src/runtime/components/RuntimeDataTableFilterInputComponent.tsx index 6747d11b..8dfc7c1f 100644 --- a/src/packages/ce/src/runtime/components/RuntimeDataTableFilterInputComponent.tsx +++ b/src/packages/ce/src/runtime/components/RuntimeDataTableFilterInputComponent.tsx @@ -1,16 +1,16 @@ import React from "react"; import { DataTableFilterInput, - DataTableFilterSuggestionMenu, DRuntimeView, + DataTableFilterSuggestionMenu, MenuCheckboxItem, useService, useStore } from "@code0-tech/pictor"; import {IconCheck} from "@tabler/icons-react"; -import {Namespace, Runtime} from "@code0-tech/sagittarius-graphql-types"; +import {Namespace} from "@code0-tech/sagittarius-graphql-types"; import {DataTableFilterProps} from "@code0-tech/pictor/dist/components/data-table/DataTable"; -import {RoleService} from "@edition/role/services/Role.service"; import {RuntimeService} from "@edition/runtime/services/Runtime.service"; +import {DRuntimeView} from "@edition/runtime/services/Runtime.view"; export interface RoleDataTableFilterInputComponentProps { namespaceId?: Namespace['id'] diff --git a/src/packages/ce/src/runtime/pages/RuntimeSettingsPage.tsx b/src/packages/ce/src/runtime/pages/RuntimeSettingsPage.tsx index 447e047f..bc886729 100644 --- a/src/packages/ce/src/runtime/pages/RuntimeSettingsPage.tsx +++ b/src/packages/ce/src/runtime/pages/RuntimeSettingsPage.tsx @@ -4,9 +4,6 @@ import React from "react"; import { Button, Card, - DResizableHandle, - DResizablePanel, - DResizablePanelGroup, Flex, Spacing, Text, @@ -21,6 +18,11 @@ import {notFound, useParams, useRouter} from "next/navigation"; import {Tab, TabContent, TabList, TabTrigger} from "@code0-tech/pictor/dist/components/tab/Tab"; import CardSection from "@code0-tech/pictor/dist/components/card/CardSection"; import {IconLayoutSidebar} from "@tabler/icons-react"; +import { + ResizableHandle, + ResizablePanel, + ResizablePanelGroup +} from "@code0-tech/pictor/dist/components/resizable/Resizable"; export const RuntimeSettingsPage: React.FC = () => { @@ -116,8 +118,8 @@ export const RuntimeSettingsPage: React.FC = () => { return - - + @@ -149,9 +151,9 @@ export const RuntimeSettingsPage: React.FC = () => { - - - + + <> @@ -221,7 +223,7 @@ export const RuntimeSettingsPage: React.FC = () => { - - + + } \ No newline at end of file diff --git a/src/packages/ce/src/user/pages/UserEmailVerificationPage.tsx b/src/packages/ce/src/user/pages/UserEmailVerificationPage.tsx index 23811696..9528cee8 100644 --- a/src/packages/ce/src/user/pages/UserEmailVerificationPage.tsx +++ b/src/packages/ce/src/user/pages/UserEmailVerificationPage.tsx @@ -1,10 +1,9 @@ "use client"; import React from "react"; -import {Button, setUserSession, Text, TextInput, useForm, useService} from "@code0-tech/pictor"; +import {Button, Text, TextInput, useForm, useService} from "@code0-tech/pictor"; import {UserService} from "@edition/user/services/User.service"; import Link from "next/link"; -import Image from "next/image"; import {useRouter} from "next/navigation"; export const UserEmailVerificationPage: React.FC = () => { diff --git a/src/packages/ce/src/user/pages/UserForgotPasswordPage.tsx b/src/packages/ce/src/user/pages/UserForgotPasswordPage.tsx index 9afe6012..85ef9d6b 100644 --- a/src/packages/ce/src/user/pages/UserForgotPasswordPage.tsx +++ b/src/packages/ce/src/user/pages/UserForgotPasswordPage.tsx @@ -4,7 +4,6 @@ import React from "react"; import {Button, EmailInput, emailValidation, Text, useForm, useService} from "@code0-tech/pictor"; import {UserService} from "@edition/user/services/User.service"; import Link from "next/link"; -import Image from "next/image"; import {useRouter} from "next/navigation"; export const UserForgotPasswordPage: React.FC = () => { diff --git a/src/packages/ce/src/user/pages/UserLoginPage.tsx b/src/packages/ce/src/user/pages/UserLoginPage.tsx index 7690e008..663ca003 100644 --- a/src/packages/ce/src/user/pages/UserLoginPage.tsx +++ b/src/packages/ce/src/user/pages/UserLoginPage.tsx @@ -7,16 +7,15 @@ import { EmailInput, emailValidation, PasswordInput, - setUserSession, Spacing, Text, useForm, useService } from "@code0-tech/pictor"; import {UserService} from "@edition/user/services/User.service"; -import Image from "next/image"; import Link from "next/link"; import {useRouter, useSearchParams} from "next/navigation"; +import {setUserSession} from "@edition/user/hooks/User.session.hook"; export const UserLoginPage: React.FC = () => { diff --git a/src/packages/ce/src/user/pages/UserRegistrationPage.tsx b/src/packages/ce/src/user/pages/UserRegistrationPage.tsx index 9f1306ac..c46f3a12 100644 --- a/src/packages/ce/src/user/pages/UserRegistrationPage.tsx +++ b/src/packages/ce/src/user/pages/UserRegistrationPage.tsx @@ -3,19 +3,19 @@ import React from "react"; import { Button, - EmailInput, emailValidation, + EmailInput, + emailValidation, Flex, PasswordInput, - setUserSession, Text, TextInput, useForm, useService } from "@code0-tech/pictor"; -import Image from "next/image"; import Link from "next/link"; import {UserService} from "@edition/user/services/User.service"; import {useRouter} from "next/navigation"; +import {setUserSession} from "@edition/user/hooks/User.session.hook"; export const UserRegistrationPage: React.FC = () => { diff --git a/src/packages/ce/src/user/pages/UserResetPasswordPage.tsx b/src/packages/ce/src/user/pages/UserResetPasswordPage.tsx index 159583b7..90485fa7 100644 --- a/src/packages/ce/src/user/pages/UserResetPasswordPage.tsx +++ b/src/packages/ce/src/user/pages/UserResetPasswordPage.tsx @@ -4,7 +4,6 @@ import React from "react"; import {Alert, Button, PasswordInput, Spacing, Text, TextInput, useForm, useService} from "@code0-tech/pictor"; import {UserService} from "@edition/user/services/User.service"; import Link from "next/link"; -import Image from "next/image"; import {useRouter, useSearchParams} from "next/navigation"; export const UserResetPasswordPage: React.FC = () => { @@ -59,7 +58,8 @@ export const UserResetPasswordPage: React.FC = () => { {query.has("passwordReset") ? ( <> - If your email address exists, you received an email with a token to reset your password. + If your email address exists, you received an email with a token to reset your + password. ) : null} From 0bc9dea3b5c6b748667cdb48819e8fc97340d5fb Mon Sep 17 00:00:00 2001 From: nicosammito Date: Sat, 7 Mar 2026 22:30:48 +0100 Subject: [PATCH 29/35] feat: update NEXT_PUBLIC_PICTOR_VERSION to 0.0.0-mvp.47 for versioning consistency --- .env.local | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env.local b/.env.local index c2bce7d0..9893bacf 100644 --- a/.env.local +++ b/.env.local @@ -1,4 +1,4 @@ EDITION=ce SAGITTARIUS_GRAPHQL_URL=http://localhost:3010/graphql NEXT_PUBLIC_SCULPTOR_VERSION=0.0.0 -NEXT_PUBLIC_PICTOR_VERSION=0.0.0-mvp.46 \ No newline at end of file +NEXT_PUBLIC_PICTOR_VERSION=0.0.0-mvp.47 \ No newline at end of file From 04630acd280dedff7cc4c5059e25138a4dbfb388 Mon Sep 17 00:00:00 2001 From: nicosammito Date: Sat, 7 Mar 2026 22:30:53 +0100 Subject: [PATCH 30/35] feat: update @code0-tech/pictor to version 0.0.0-mvp.47 and @tabler/icons-react to version 3.37.1 for dependency consistency --- package-lock.json | 22 +++++++++++----------- package.json | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4eca6798..e1e76542 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.0", "dependencies": { "@apollo/client": "^4.0.9", - "@code0-tech/pictor": "^0.0.0-mvp.46", + "@code0-tech/pictor": "^0.0.0-mvp.47", "date-fns": "^4.1.0", "graphql": "^16.12.0", "graphql-tag": "^2.12.6", @@ -361,9 +361,9 @@ } }, "node_modules/@code0-tech/pictor": { - "version": "0.0.0-mvp.46", - "resolved": "https://registry.npmjs.org/@code0-tech/pictor/-/pictor-0.0.0-mvp.46.tgz", - "integrity": "sha512-TtKCKfFuH9rcJIA2kiojd+d2+BGf2g0T8HyFfs1jJwlwlmls9UQ8/4aOEqZPUaa6S/SljVE1TOB86YpaqqUe0g==", + "version": "0.0.0-mvp.47", + "resolved": "https://registry.npmjs.org/@code0-tech/pictor/-/pictor-0.0.0-mvp.47.tgz", + "integrity": "sha512-BjgROUri4N/RykcGFrDy1ZoQAtSG+MRyzcW+cao6R7lw+3n9ADD+7e8L5dCYvqhX3vlUyUWNRNiymwnCNr4Z2A==", "peerDependencies": { "@ariakit/react": "^0.4.17", "@code0-tech/sagittarius-graphql-types": "0.0.0-experimental-2342308809-931efb40b4bf3245999c53abbdd9164cea82e82d", @@ -385,7 +385,7 @@ "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-toggle-group": "^1.1.11", "@radix-ui/react-tooltip": "^1.2.8", - "@tabler/icons-react": "3.36.1", + "@tabler/icons-react": "3.37.1", "@uiw/codemirror-themes": "^4.25.4", "@uiw/react-codemirror": "^4.25.4", "@xyflow/react": "^12.10.0", @@ -2861,9 +2861,9 @@ } }, "node_modules/@tabler/icons": { - "version": "3.36.1", - "resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.36.1.tgz", - "integrity": "sha512-f4Jg3Fof/Vru5ioix/UO4GX+sdDsF9wQo47FbtvG+utIYYVQ/QVAC0QYgcBbAjQGfbdOh2CCf0BgiFOF9Ixtjw==", + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.40.0.tgz", + "integrity": "sha512-V/Q4VgNPKubRTiLdmWjV/zscYcj5IIk+euicUtaVVqF6luSC9rDngYWgST5/yh3Mrg/mYUwRv1YVTk71Jp0twQ==", "license": "MIT", "funding": { "type": "github", @@ -2871,9 +2871,9 @@ } }, "node_modules/@tabler/icons-react": { - "version": "3.36.1", - "resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-3.36.1.tgz", - "integrity": "sha512-/8nOXeNeMoze9xY/QyEKG65wuvRhkT3q9aytaur6Gj8bYU2A98YVJyLc9MRmc5nVvpy+bRlrrwK/Ykr8WGyUWg==", + "version": "3.37.1", + "resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-3.37.1.tgz", + "integrity": "sha512-R7UE71Jji7i4Su56Y9zU1uYEBakUejuDJvyuYVmBuUoqp/x3Pn4cv2huarexR3P0GJ2eHg4rUj9l5zccqS6K/Q==", "license": "MIT", "peer": true, "dependencies": { diff --git a/package.json b/package.json index f15e8853..c4837267 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ }, "dependencies": { "@apollo/client": "^4.0.9", - "@code0-tech/pictor": "^0.0.0-mvp.46", + "@code0-tech/pictor": "^0.0.0-mvp.47", "date-fns": "^4.1.0", "graphql": "^16.12.0", "graphql-tag": "^2.12.6", From 8df44a96db34ec7d06bb6d91e72baccdfb22de58 Mon Sep 17 00:00:00 2001 From: nicosammito Date: Sat, 7 Mar 2026 22:46:20 +0100 Subject: [PATCH 31/35] feat: rename DataTypeInput and related components for consistency and clarity --- .../LiteralBadgeComponent.tsx} | 4 +-- .../NodeBadgeComponent.tsx} | 4 +-- .../ReferenceBadgeComponent.tsx} | 8 ++--- .../DataTypeInputComponent.tsx} | 12 ++++---- .../json/DataTypeJSONInputComponent.tsx} | 24 +++++++-------- .../DataTypeJSONInputEditDialogComponent.tsx} | 10 +++---- .../json/DataTypeJSONInputTreeComponent.tsx} | 8 ++--- .../text/DataTypeTextInputComponent.tsx} | 16 +++++----- .../DataTypeTypeInputComponent.style.scss} | 0 .../type/DataTypeTypeInputComponent.tsx} | 26 ++++++++-------- .../DataTypeTypeInputEditDialogComponent.tsx} | 10 +++---- .../components/FunctionFileDefault.tsx | 18 +++++------ .../components/FunctionFileTrigger.tsx | 30 +++++++++---------- .../components/FunctionNodeDefault.tsx | 12 ++++---- 14 files changed, 91 insertions(+), 91 deletions(-) rename src/packages/ce/src/datatype/components/{DataTypeInputLiteralBadge.tsx => badges/LiteralBadgeComponent.tsx} (69%) rename src/packages/ce/src/datatype/components/{DataTypeInputNodeBadge.tsx => badges/NodeBadgeComponent.tsx} (93%) rename src/packages/ce/src/datatype/components/{DataTypeInputReferenceBadge.tsx => badges/ReferenceBadgeComponent.tsx} (80%) rename src/packages/ce/src/datatype/components/{DataTypeInput.tsx => inputs/DataTypeInputComponent.tsx} (82%) rename src/packages/ce/src/datatype/components/{DataTypeJSONInput.tsx => inputs/json/DataTypeJSONInputComponent.tsx} (87%) rename src/packages/ce/src/datatype/components/{DataTypeJSONInputEditDialog.tsx => inputs/json/DataTypeJSONInputEditDialogComponent.tsx} (94%) rename src/packages/ce/src/datatype/components/{DataTypeJSONInputTree.tsx => inputs/json/DataTypeJSONInputTreeComponent.tsx} (96%) rename src/packages/ce/src/datatype/components/{DataTypeTextInput.tsx => inputs/text/DataTypeTextInputComponent.tsx} (89%) rename src/packages/ce/src/datatype/components/{DataTypeTypeInput.style.scss => inputs/type/DataTypeTypeInputComponent.style.scss} (100%) rename src/packages/ce/src/datatype/components/{DataTypeTypeInput.tsx => inputs/type/DataTypeTypeInputComponent.tsx} (86%) rename src/packages/ce/src/datatype/components/{DataTypeTypeInputEditDialog.tsx => inputs/type/DataTypeTypeInputEditDialogComponent.tsx} (94%) diff --git a/src/packages/ce/src/datatype/components/DataTypeInputLiteralBadge.tsx b/src/packages/ce/src/datatype/components/badges/LiteralBadgeComponent.tsx similarity index 69% rename from src/packages/ce/src/datatype/components/DataTypeInputLiteralBadge.tsx rename to src/packages/ce/src/datatype/components/badges/LiteralBadgeComponent.tsx index 6745023d..d47af860 100644 --- a/src/packages/ce/src/datatype/components/DataTypeInputLiteralBadge.tsx +++ b/src/packages/ce/src/datatype/components/badges/LiteralBadgeComponent.tsx @@ -2,11 +2,11 @@ import React from "react"; import {LiteralValue} from "@code0-tech/sagittarius-graphql-types"; import {Badge, BadgeType, Text} from "@code0-tech/pictor"; -export interface DataTypeInputLiteralBadgeProps extends Omit { +export interface LiteralBadgeComponentProps extends Omit { value: LiteralValue } -export const DataTypeInputLiteralBadge: React.FC = (props) => { +export const LiteralBadgeComponent: React.FC = (props) => { const {value, ...rest} = props diff --git a/src/packages/ce/src/datatype/components/DataTypeInputNodeBadge.tsx b/src/packages/ce/src/datatype/components/badges/NodeBadgeComponent.tsx similarity index 93% rename from src/packages/ce/src/datatype/components/DataTypeInputNodeBadge.tsx rename to src/packages/ce/src/datatype/components/badges/NodeBadgeComponent.tsx index bad8926b..bb26b583 100644 --- a/src/packages/ce/src/datatype/components/DataTypeInputNodeBadge.tsx +++ b/src/packages/ce/src/datatype/components/badges/NodeBadgeComponent.tsx @@ -14,13 +14,13 @@ import {FlowTypeService} from "@edition/flowtype/services/FlowType.service"; import {FunctionDefinitionView} from "@edition/function/services/Function.view"; import {FlowTypeView} from "@edition/flowtype/services/FlowType.view"; -export interface DataTypeInputNodeBadgeProps extends Omit { +export interface NodeBadgeComponentProps extends Omit { value: NodeFunction | NodeFunctionIdWrapper flowId: Flow['id'] definition?: FunctionDefinitionView | FlowTypeView } -export const DataTypeInputNodeBadge: React.FC = (props) => { +export const NodeBadgeComponent: React.FC = (props) => { const {value, flowId, definition, ...rest} = props diff --git a/src/packages/ce/src/datatype/components/DataTypeInputReferenceBadge.tsx b/src/packages/ce/src/datatype/components/badges/ReferenceBadgeComponent.tsx similarity index 80% rename from src/packages/ce/src/datatype/components/DataTypeInputReferenceBadge.tsx rename to src/packages/ce/src/datatype/components/badges/ReferenceBadgeComponent.tsx index f207ca66..99e3f43e 100644 --- a/src/packages/ce/src/datatype/components/DataTypeInputReferenceBadge.tsx +++ b/src/packages/ce/src/datatype/components/badges/ReferenceBadgeComponent.tsx @@ -1,24 +1,24 @@ import {Flow, ReferenceValue} from "@code0-tech/sagittarius-graphql-types"; import React from "react"; -import {DataTypeInputNodeBadge} from "./DataTypeInputNodeBadge"; +import {NodeBadgeComponent} from "./NodeBadgeComponent"; import {IconVariable} from "@tabler/icons-react"; import {Badge, BadgeType, Flex, Text} from "@code0-tech/pictor"; import {FunctionDefinitionView} from "@edition/function/services/Function.view"; import {FlowTypeView} from "@edition/flowtype/services/FlowType.view"; -export interface DataTypeInputReferenceBadgeProps extends Omit { +export interface ReferenceBadgeComponentProps extends Omit { value: ReferenceValue flowId: Flow['id'] definition?: FunctionDefinitionView | FlowTypeView } -export const DataTypeInputReferenceBadge: React.FC = (props) => { +export const ReferenceBadgeComponent: React.FC = (props) => { const {value, flowId, definition, ...rest} = props const content = React.useMemo(() => { if (flowId) { return - diff --git a/src/packages/ce/src/datatype/components/DataTypeInput.tsx b/src/packages/ce/src/datatype/components/inputs/DataTypeInputComponent.tsx similarity index 82% rename from src/packages/ce/src/datatype/components/DataTypeInput.tsx rename to src/packages/ce/src/datatype/components/inputs/DataTypeInputComponent.tsx index e51538ca..6c8394b4 100644 --- a/src/packages/ce/src/datatype/components/DataTypeInput.tsx +++ b/src/packages/ce/src/datatype/components/inputs/DataTypeInputComponent.tsx @@ -1,13 +1,13 @@ import {Flow, NodeFunction, NodeParameter} from "@code0-tech/sagittarius-graphql-types"; import React from "react"; -import {DataTypeTextInput} from "./DataTypeTextInput"; -import {DataTypeJSONInput} from "./DataTypeJSONInput"; +import {DataTypeTextInputComponent} from "./text/DataTypeTextInputComponent"; +import {DataTypeJSONInputComponent} from "./json/DataTypeJSONInputComponent"; import {InputProps, useService, useStore} from "@code0-tech/pictor"; import {FlowService} from "@edition/flow/services/Flow.service"; import {DatatypeService} from "@edition/datatype/services/Datatype.service"; import {FunctionService} from "@edition/function/services/Function.service"; -export interface DataTypeInputProps extends Omit, "wrapperComponent" | "type"> { +export interface DataTypeInputComponentProps extends Omit, "wrapperComponent" | "type"> { flowId: Flow['id'] nodeId: NodeFunction['id'] parameterId: NodeParameter['id'] @@ -15,7 +15,7 @@ export interface DataTypeInputProps extends Omit, "wrappe onClear?: (event: React.MouseEvent) => void } -export const DataTypeInput: React.FC = (props) => { +export const DataTypeInputComponent: React.FC = (props) => { const {flowId, nodeId, parameterId, ...rest} = props @@ -54,14 +54,14 @@ export const DataTypeInput: React.FC = (props) => { switch (dataType?.variant) { case "ARRAY": case "OBJECT": - return default: - return = (props) => { +export const DataTypeJSONInputComponent: React.FC = (props) => { const {flowId, nodeId, parameterId, title, description, formValidation, onChange} = props @@ -102,7 +102,7 @@ export const DataTypeJSONInput: React.FC = (props) => { return ( <> {value?.__typename === "LiteralValue" && ( - = (props) => { {value?.__typename === "NodeFunction" || value?.__typename === "NodeFunctionIdWrapper" ? ( - + ) : value?.__typename === "ReferenceValue" ? ( - + ) : ( - = (props) => { +export const DataTypeJSONInputEditDialogComponent: React.FC = (props) => { const { open, entry, @@ -149,7 +149,7 @@ export const DataTypeJSONInputEditDialog: React.FC - = (props) => { +export const DataTypeJSONInputTreeComponent: React.FC = (props) => { const { object, parentKey, @@ -126,7 +126,7 @@ export const DataTypeJSONInputTree: React.FC = (prop ) const childTree = isCollapsable && !isCollapsed ? ( - { const result: (string | Record)[] = [] @@ -93,7 +93,7 @@ export const splitTextAndObjects = (input: string) => { return result } -export const DataTypeTextInput: React.FC = (props) => { +export const DataTypeTextInputComponent: React.FC = (props) => { const {flowId, nodeId, parameterId, ...rest} = props @@ -147,8 +147,8 @@ export const DataTypeTextInput: React.FC = (props) => { if (value?.__typename === "NodeFunctionIdWrapper" || value?.__typename === "NodeFunction") { const node = value?.__typename === "NodeFunction" ? value : flowService.getNodeById(flowId, value.id) return buildBlockSegment( - , + , value ) } @@ -156,7 +156,7 @@ export const DataTypeTextInput: React.FC = (props) => { if (value?.__typename === "ReferenceValue") { const node = (value as ReferenceValue).nodeFunctionId === "gid://sagittarius/NodeFunction/-1" ? flowTypeService.getById(flow?.type?.id) : functionService.getById(flowService.getNodeById(flowId, (value as ReferenceValue).nodeFunctionId)?.functionDefinition?.id) return buildBlockSegment( - , + , value ) } diff --git a/src/packages/ce/src/datatype/components/DataTypeTypeInput.style.scss b/src/packages/ce/src/datatype/components/inputs/type/DataTypeTypeInputComponent.style.scss similarity index 100% rename from src/packages/ce/src/datatype/components/DataTypeTypeInput.style.scss rename to src/packages/ce/src/datatype/components/inputs/type/DataTypeTypeInputComponent.style.scss diff --git a/src/packages/ce/src/datatype/components/DataTypeTypeInput.tsx b/src/packages/ce/src/datatype/components/inputs/type/DataTypeTypeInputComponent.tsx similarity index 86% rename from src/packages/ce/src/datatype/components/DataTypeTypeInput.tsx rename to src/packages/ce/src/datatype/components/inputs/type/DataTypeTypeInputComponent.tsx index cf3a6e68..66629c6b 100644 --- a/src/packages/ce/src/datatype/components/DataTypeTypeInput.tsx +++ b/src/packages/ce/src/datatype/components/inputs/type/DataTypeTypeInputComponent.tsx @@ -6,8 +6,8 @@ import { } from "@code0-tech/sagittarius-graphql-types"; import React from "react"; import {IconEdit} from "@tabler/icons-react"; -import "./DataTypeTypeInput.style.scss" -import {DataTypeTypeInputEditDialog} from "./DataTypeTypeInputEditDialog"; +import "./DataTypeTypeInputComponent.style.scss" +import {DataTypeTypeInputEditDialogComponent} from "./DataTypeTypeInputEditDialogComponent"; import { Badge, Button, @@ -23,19 +23,19 @@ import { import {DatatypeService} from "@edition/datatype/services/Datatype.service"; import CardSection from "@code0-tech/pictor/dist/components/card/CardSection"; -export interface DataTypeTypeInputProps extends ValidationProps { +export interface DataTypeTypeInputComponentProps extends ValidationProps { onChange?: (value: DataTypeIdentifier | null) => void description?: string label?: string } -export interface DataTypeTypeInputRuleTreeProps { +export interface DataTypeTypeInputRuleTreeComponentProps { dataTypeIdentifier: DataTypeIdentifier parentRule?: Maybe isRoot?: boolean } -export const DataTypeTypeInput: React.FC = (props) => { +export const DataTypeTypeInputComponent: React.FC = (props) => { const {initialValue, defaultValue, value, label, description} = props const initValue = value ?? initialValue ?? defaultValue ?? null @@ -50,10 +50,10 @@ export const DataTypeTypeInput: React.FC = (props) => { }, [dataTypeStore, initValue]) return
    - setEditOpen(open)}/> + setEditOpen(open)}/> {label} {description} @@ -70,13 +70,13 @@ export const DataTypeTypeInput: React.FC = (props) => { - +
    } -export const DFlowInputDataTypeRuleTree: React.FC = (props) => { +export const DataTypeTypeInputRuleTreeComponent: React.FC = (props) => { const {dataTypeIdentifier, parentRule, isRoot = !parentRule} = props const dataTypeService = useService(DatatypeService) @@ -114,7 +114,7 @@ export const DFlowInputDataTypeRuleTree: React.FC + const childTree = if (isRoot) return {label} {childTree} diff --git a/src/packages/ce/src/datatype/components/DataTypeTypeInputEditDialog.tsx b/src/packages/ce/src/datatype/components/inputs/type/DataTypeTypeInputEditDialogComponent.tsx similarity index 94% rename from src/packages/ce/src/datatype/components/DataTypeTypeInputEditDialog.tsx rename to src/packages/ce/src/datatype/components/inputs/type/DataTypeTypeInputEditDialogComponent.tsx index 1d0a2c47..9e401521 100644 --- a/src/packages/ce/src/datatype/components/DataTypeTypeInputEditDialog.tsx +++ b/src/packages/ce/src/datatype/components/inputs/type/DataTypeTypeInputEditDialogComponent.tsx @@ -1,6 +1,6 @@ import React from "react"; import {DataTypeIdentifier, LiteralValue} from "@code0-tech/sagittarius-graphql-types"; -import {DFlowInputDataTypeRuleTree} from "./DataTypeTypeInput"; +import {DataTypeTypeInputRuleTreeComponent} from "./DataTypeTypeInputComponent"; import {CompletionContext, CompletionResult} from "@codemirror/autocomplete"; import {syntaxTree} from "@codemirror/language"; import {IconX} from "@tabler/icons-react"; @@ -33,14 +33,14 @@ import { ResizablePanelGroup } from "@code0-tech/pictor/dist/components/resizable/Resizable"; -export interface DataTypeTypeInputEditDialogProps { +export interface DataTypeTypeInputEditDialogComponentProps { dataTypeIdentifier: DataTypeIdentifier open?: boolean onOpenChange?: (open: boolean) => void onDataTypeChange?: (dataTypeIdentifier: DataTypeIdentifier) => void } -export const DataTypeTypeInputEditDialog: React.FC = (props) => { +export const DataTypeTypeInputEditDialogComponent: React.FC = (props) => { const {open, onOpenChange, onDataTypeChange} = props @@ -120,7 +120,7 @@ export const DataTypeTypeInputEditDialog: React.FC onOpenChange?.(open)}> - { + { const target = e.target as HTMLElement; if (target.closest("[data-slot=resizable-handle]") || target.closest("[data-slot=resizable-panel]")) { @@ -147,7 +147,7 @@ export const DataTypeTypeInputEditDialog: React.FC - + diff --git a/src/packages/ce/src/function/components/FunctionFileDefault.tsx b/src/packages/ce/src/function/components/FunctionFileDefault.tsx index bc6969fe..6f495100 100644 --- a/src/packages/ce/src/function/components/FunctionFileDefault.tsx +++ b/src/packages/ce/src/function/components/FunctionFileDefault.tsx @@ -12,7 +12,7 @@ import {FileTabsService} from "@code0-tech/pictor/dist/components/file-tabs/File import {useNodeValidation} from "@edition/flow/hooks/NodeValidation.hook"; import {FunctionService} from "@edition/function/services/Function.service"; import {FlowService} from "@edition/flow/services/Flow.service"; -import {DataTypeInput} from "@edition/datatype/components/DataTypeInput"; +import {DataTypeInputComponent} from "@edition/datatype/components/inputs/DataTypeInputComponent"; import {ParameterDefinitionView} from "@edition/function/services/Function.view"; export interface FunctionFileDefaultProps { @@ -136,17 +136,17 @@ export const FunctionFileDefault: React.FC = (props) = return
    {/*@ts-ignore*/} - { + { changedParameters.current.add(parameter.id!!) validate() }} - {...inputs.getInputProps(parameter.id!!)} + {...inputs.getInputProps(parameter.id!!)} />
    })} diff --git a/src/packages/ce/src/function/components/FunctionFileTrigger.tsx b/src/packages/ce/src/function/components/FunctionFileTrigger.tsx index 4ce2dfd3..9fd4ca67 100644 --- a/src/packages/ce/src/function/components/FunctionFileTrigger.tsx +++ b/src/packages/ce/src/function/components/FunctionFileTrigger.tsx @@ -11,8 +11,8 @@ import {toInputSuggestions} from "@edition/function/components/FunctionSuggestio import {FlowTypeService} from "@edition/flowtype/services/FlowType.service"; import {FlowService} from "@edition/flow/services/Flow.service"; import {DatatypeService} from "@edition/datatype/services/Datatype.service"; -import {DataTypeTextInput} from "@edition/datatype/components/DataTypeTextInput"; -import {DataTypeTypeInput} from "@edition/datatype/components/DataTypeTypeInput"; +import {DataTypeTextInputComponent} from "@edition/datatype/components/inputs/text/DataTypeTextInputComponent"; +import {DataTypeTypeInputComponent} from "@edition/datatype/components/inputs/type/DataTypeTypeInputComponent"; export interface FunctionFileTriggerProps { instance: Flow @@ -60,7 +60,7 @@ export const FunctionFileTrigger: React.FC = (props) = }) return - {definition?.inputType ? = (props) = } return
    - { + { submitValue(suggestion.value) }} - suggestions={toInputSuggestions(result)} + suggestions={toInputSuggestions(result)} />
    })} diff --git a/src/packages/ce/src/function/components/FunctionNodeDefault.tsx b/src/packages/ce/src/function/components/FunctionNodeDefault.tsx index c3781f18..739bf1f7 100644 --- a/src/packages/ce/src/function/components/FunctionNodeDefault.tsx +++ b/src/packages/ce/src/function/components/FunctionNodeDefault.tsx @@ -13,9 +13,9 @@ import {useNodeValidation} from "@edition/flow/hooks/NodeValidation.hook"; import {IconNote} from "@tabler/icons-react"; import {FlowService} from "@edition/flow/services/Flow.service"; import {FunctionService} from "@edition/function/services/Function.service"; -import {DataTypeInputLiteralBadge} from "@edition/datatype/components/DataTypeInputLiteralBadge"; -import {DataTypeInputReferenceBadge} from "@edition/datatype/components/DataTypeInputReferenceBadge"; -import {DataTypeInputNodeBadge} from "@edition/datatype/components/DataTypeInputNodeBadge"; +import {LiteralBadgeComponent} from "@edition/datatype/components/badges/LiteralBadgeComponent"; +import {ReferenceBadgeComponent} from "@edition/datatype/components/badges/ReferenceBadgeComponent"; +import {NodeBadgeComponent} from "@edition/datatype/components/badges/NodeBadgeComponent"; import {FunctionFileDefault} from "@edition/function/components/FunctionFileDefault"; export type FunctionNodeDefaultProps = NodeProps> @@ -81,15 +81,15 @@ export const FunctionNodeDefault: React.FC = memo((pro switch (param?.value?.__typename) { case "LiteralValue": return
    - +
    case "ReferenceValue": return
    - +
    case "NodeFunctionIdWrapper": return
    - + Date: Sat, 7 Mar 2026 22:49:31 +0100 Subject: [PATCH 32/35] feat: reorganize FlowBuilderComponent imports for improved structure and clarity --- .../project/[projectId]/flow/[flowId]/page.tsx | 2 +- src/packages/ce/src/datatype/services/DataType.view.ts | 2 +- .../ce/src/flow/components/FlowDeleteDialogComponent.tsx | 2 +- .../ce/src/flow/components/FlowRenameDialogComponent.tsx | 2 +- .../{ => builder}/FlowBuilderComponent.style.scss | 0 .../components/{ => builder}/FlowBuilderComponent.tsx | 8 ++++---- .../components/{ => builder}/FlowBuilderComponent.util.ts | 0 .../components/{ => builder}/FlowBuilderEdgeComponent.tsx | 0 .../{ => folder}/FlowFolderComponent.style.scss | 0 .../flow/components/{ => folder}/FlowFolderComponent.tsx | 0 .../{ => folder}/FlowFolderContextMenuComponent.tsx | 0 .../components/{ => panels}/FlowPanelControlComponent.tsx | 0 .../components/{ => panels}/FlowPanelLayoutComponent.tsx | 0 .../components/{ => panels}/FlowPanelSizeComponent.tsx | 0 .../components/{ => panels}/FlowPanelUpdateComponent.tsx | 0 src/packages/ce/src/flow/views/FlowFolderView.tsx | 4 ++-- src/packages/ce/src/function/services/Function.view.ts | 2 +- 17 files changed, 11 insertions(+), 11 deletions(-) rename src/packages/ce/src/flow/components/{ => builder}/FlowBuilderComponent.style.scss (100%) rename src/packages/ce/src/flow/components/{ => builder}/FlowBuilderComponent.tsx (99%) rename src/packages/ce/src/flow/components/{ => builder}/FlowBuilderComponent.util.ts (100%) rename src/packages/ce/src/flow/components/{ => builder}/FlowBuilderEdgeComponent.tsx (100%) rename src/packages/ce/src/flow/components/{ => folder}/FlowFolderComponent.style.scss (100%) rename src/packages/ce/src/flow/components/{ => folder}/FlowFolderComponent.tsx (100%) rename src/packages/ce/src/flow/components/{ => folder}/FlowFolderContextMenuComponent.tsx (100%) rename src/packages/ce/src/flow/components/{ => panels}/FlowPanelControlComponent.tsx (100%) rename src/packages/ce/src/flow/components/{ => panels}/FlowPanelLayoutComponent.tsx (100%) rename src/packages/ce/src/flow/components/{ => panels}/FlowPanelSizeComponent.tsx (100%) rename src/packages/ce/src/flow/components/{ => panels}/FlowPanelUpdateComponent.tsx (100%) diff --git a/src/app/(flow)/namespace/[namespaceId]/project/[projectId]/flow/[flowId]/page.tsx b/src/app/(flow)/namespace/[namespaceId]/project/[projectId]/flow/[flowId]/page.tsx index dabc21e5..eaff610e 100644 --- a/src/app/(flow)/namespace/[namespaceId]/project/[projectId]/flow/[flowId]/page.tsx +++ b/src/app/(flow)/namespace/[namespaceId]/project/[projectId]/flow/[flowId]/page.tsx @@ -8,7 +8,7 @@ import React from "react"; import {IconDatabase, IconFile, IconMessageChatbot} from "@tabler/icons-react"; import {useParams} from "next/navigation"; import {Flow} from "@code0-tech/sagittarius-graphql-types"; -import {FlowBuilderComponent} from "@edition/flow/components/FlowBuilderComponent"; +import {FlowBuilderComponent} from "@edition/flow/components/builder/FlowBuilderComponent"; import {FunctionFiles} from "@edition/function/components/FunctionFiles"; import { ResizableHandle, diff --git a/src/packages/ce/src/datatype/services/DataType.view.ts b/src/packages/ce/src/datatype/services/DataType.view.ts index f60993d6..112cf4e8 100644 --- a/src/packages/ce/src/datatype/services/DataType.view.ts +++ b/src/packages/ce/src/datatype/services/DataType.view.ts @@ -3,7 +3,7 @@ import { DataTypeRuleConnection, DataTypeVariant, Maybe, Runtime, Scalars, Translation, } from "@code0-tech/sagittarius-graphql-types"; -import {attachDataTypeIdentifiers, resolveDataTypeIdentifiers} from "@edition/flow/components/FlowBuilderComponent.util"; +import {attachDataTypeIdentifiers, resolveDataTypeIdentifiers} from "@edition/flow/components/builder/FlowBuilderComponent.util"; export class DataTypeView { diff --git a/src/packages/ce/src/flow/components/FlowDeleteDialogComponent.tsx b/src/packages/ce/src/flow/components/FlowDeleteDialogComponent.tsx index 84b1c83f..1fb98969 100644 --- a/src/packages/ce/src/flow/components/FlowDeleteDialogComponent.tsx +++ b/src/packages/ce/src/flow/components/FlowDeleteDialogComponent.tsx @@ -2,7 +2,7 @@ import React from "react"; import { FlowFolderContextMenuComponentGroupData, FlowFolderContextMenuComponentItemData -} from "./FlowFolderContextMenuComponent"; +} from "./folder/FlowFolderContextMenuComponent"; import {Flow} from "@code0-tech/sagittarius-graphql-types"; import { Badge, diff --git a/src/packages/ce/src/flow/components/FlowRenameDialogComponent.tsx b/src/packages/ce/src/flow/components/FlowRenameDialogComponent.tsx index b4abbb6f..54667c79 100644 --- a/src/packages/ce/src/flow/components/FlowRenameDialogComponent.tsx +++ b/src/packages/ce/src/flow/components/FlowRenameDialogComponent.tsx @@ -1,5 +1,5 @@ import React from "react"; -import {FlowFolderContextMenuComponentGroupData, FlowFolderContextMenuComponentItemData} from "./FlowFolderContextMenuComponent"; +import {FlowFolderContextMenuComponentGroupData, FlowFolderContextMenuComponentItemData} from "./folder/FlowFolderContextMenuComponent"; import {Flow} from "@code0-tech/sagittarius-graphql-types"; import { Button, diff --git a/src/packages/ce/src/flow/components/FlowBuilderComponent.style.scss b/src/packages/ce/src/flow/components/builder/FlowBuilderComponent.style.scss similarity index 100% rename from src/packages/ce/src/flow/components/FlowBuilderComponent.style.scss rename to src/packages/ce/src/flow/components/builder/FlowBuilderComponent.style.scss diff --git a/src/packages/ce/src/flow/components/FlowBuilderComponent.tsx b/src/packages/ce/src/flow/components/builder/FlowBuilderComponent.tsx similarity index 99% rename from src/packages/ce/src/flow/components/FlowBuilderComponent.tsx rename to src/packages/ce/src/flow/components/builder/FlowBuilderComponent.tsx index 9ab9603b..65208e1a 100644 --- a/src/packages/ce/src/flow/components/FlowBuilderComponent.tsx +++ b/src/packages/ce/src/flow/components/builder/FlowBuilderComponent.tsx @@ -24,10 +24,10 @@ import {FunctionNodeDefault} from "@edition/function/components/FunctionNodeDefa import {FunctionNodeGroup} from "@edition/function/components/FunctionNodeGroup"; import {FunctionNodeTrigger} from "@edition/function/components/FunctionNodeTrigger"; import {useEdges} from "@edition/flow/hooks/Flow.edges.hook"; -import {FlowPanelSizeComponent} from "@edition/flow/components/FlowPanelSizeComponent"; -import {FlowPanelLayoutComponent} from "@edition/flow/components/FlowPanelLayoutComponent"; -import {FlowPanelControlComponent} from "@edition/flow/components/FlowPanelControlComponent"; -import {FlowPanelUpdateComponent} from "@edition/flow/components/FlowPanelUpdateComponent"; +import {FlowPanelSizeComponent} from "@edition/flow/components/panels/FlowPanelSizeComponent"; +import {FlowPanelLayoutComponent} from "@edition/flow/components/panels/FlowPanelLayoutComponent"; +import {FlowPanelControlComponent} from "@edition/flow/components/panels/FlowPanelControlComponent"; +import {FlowPanelUpdateComponent} from "@edition/flow/components/panels/FlowPanelUpdateComponent"; /** * Dynamically layouts a tree of nodes and their parameter nodes for a flow-based editor. diff --git a/src/packages/ce/src/flow/components/FlowBuilderComponent.util.ts b/src/packages/ce/src/flow/components/builder/FlowBuilderComponent.util.ts similarity index 100% rename from src/packages/ce/src/flow/components/FlowBuilderComponent.util.ts rename to src/packages/ce/src/flow/components/builder/FlowBuilderComponent.util.ts diff --git a/src/packages/ce/src/flow/components/FlowBuilderEdgeComponent.tsx b/src/packages/ce/src/flow/components/builder/FlowBuilderEdgeComponent.tsx similarity index 100% rename from src/packages/ce/src/flow/components/FlowBuilderEdgeComponent.tsx rename to src/packages/ce/src/flow/components/builder/FlowBuilderEdgeComponent.tsx diff --git a/src/packages/ce/src/flow/components/FlowFolderComponent.style.scss b/src/packages/ce/src/flow/components/folder/FlowFolderComponent.style.scss similarity index 100% rename from src/packages/ce/src/flow/components/FlowFolderComponent.style.scss rename to src/packages/ce/src/flow/components/folder/FlowFolderComponent.style.scss diff --git a/src/packages/ce/src/flow/components/FlowFolderComponent.tsx b/src/packages/ce/src/flow/components/folder/FlowFolderComponent.tsx similarity index 100% rename from src/packages/ce/src/flow/components/FlowFolderComponent.tsx rename to src/packages/ce/src/flow/components/folder/FlowFolderComponent.tsx diff --git a/src/packages/ce/src/flow/components/FlowFolderContextMenuComponent.tsx b/src/packages/ce/src/flow/components/folder/FlowFolderContextMenuComponent.tsx similarity index 100% rename from src/packages/ce/src/flow/components/FlowFolderContextMenuComponent.tsx rename to src/packages/ce/src/flow/components/folder/FlowFolderContextMenuComponent.tsx diff --git a/src/packages/ce/src/flow/components/FlowPanelControlComponent.tsx b/src/packages/ce/src/flow/components/panels/FlowPanelControlComponent.tsx similarity index 100% rename from src/packages/ce/src/flow/components/FlowPanelControlComponent.tsx rename to src/packages/ce/src/flow/components/panels/FlowPanelControlComponent.tsx diff --git a/src/packages/ce/src/flow/components/FlowPanelLayoutComponent.tsx b/src/packages/ce/src/flow/components/panels/FlowPanelLayoutComponent.tsx similarity index 100% rename from src/packages/ce/src/flow/components/FlowPanelLayoutComponent.tsx rename to src/packages/ce/src/flow/components/panels/FlowPanelLayoutComponent.tsx diff --git a/src/packages/ce/src/flow/components/FlowPanelSizeComponent.tsx b/src/packages/ce/src/flow/components/panels/FlowPanelSizeComponent.tsx similarity index 100% rename from src/packages/ce/src/flow/components/FlowPanelSizeComponent.tsx rename to src/packages/ce/src/flow/components/panels/FlowPanelSizeComponent.tsx diff --git a/src/packages/ce/src/flow/components/FlowPanelUpdateComponent.tsx b/src/packages/ce/src/flow/components/panels/FlowPanelUpdateComponent.tsx similarity index 100% rename from src/packages/ce/src/flow/components/FlowPanelUpdateComponent.tsx rename to src/packages/ce/src/flow/components/panels/FlowPanelUpdateComponent.tsx diff --git a/src/packages/ce/src/flow/views/FlowFolderView.tsx b/src/packages/ce/src/flow/views/FlowFolderView.tsx index 255070dd..de2844dd 100644 --- a/src/packages/ce/src/flow/views/FlowFolderView.tsx +++ b/src/packages/ce/src/flow/views/FlowFolderView.tsx @@ -19,12 +19,12 @@ import {Flow, FlowType} from "@code0-tech/sagittarius-graphql-types"; import {FlowService} from "@edition/flow/services/Flow.service"; import {ButtonGroup} from "@code0-tech/pictor/dist/components/button-group/ButtonGroup"; import {FlowCreateDialogComponent} from "@edition/flow/components/FlowCreateDialogComponent"; -import {FlowFolderComponent, FlowFolderComponentHandle} from "@edition/flow/components/FlowFolderComponent"; +import {FlowFolderComponent, FlowFolderComponentHandle} from "@edition/flow/components/folder/FlowFolderComponent"; import {FlowDeleteDialogComponent} from "@edition/flow/components/FlowDeleteDialogComponent"; import { FlowFolderContextMenuComponentGroupData, FlowFolderContextMenuComponentItemData -} from "@edition/flow/components/FlowFolderContextMenuComponent"; +} from "@edition/flow/components/folder/FlowFolderContextMenuComponent"; import {Layout} from "@code0-tech/pictor/dist/components/layout/Layout"; export const FlowFolderView: React.FC = () => { diff --git a/src/packages/ce/src/function/services/Function.view.ts b/src/packages/ce/src/function/services/Function.view.ts index b8886e7d..eed47a8a 100644 --- a/src/packages/ce/src/function/services/Function.view.ts +++ b/src/packages/ce/src/function/services/Function.view.ts @@ -8,7 +8,7 @@ import { import { attachDataTypeIdentifiers, resolveDataTypeIdentifiers -} from "@edition/flow/components/FlowBuilderComponent.util"; +} from "@edition/flow/components/builder/FlowBuilderComponent.util"; export class FunctionDefinitionView { From 598347bfe74c9acbe8e0bfdaf1fa4717676738d2 Mon Sep 17 00:00:00 2001 From: nicosammito Date: Sat, 7 Mar 2026 23:31:18 +0100 Subject: [PATCH 33/35] feat: rename components for consistency and clarity across the project --- .../project/[projectId]/flow/[flowId]/page.tsx | 6 +++--- .../inputs/json/DataTypeJSONInputComponent.tsx | 8 ++++---- .../inputs/text/DataTypeTextInputComponent.tsx | 8 ++++---- .../builder/FlowBuilderComponent.tsx | 12 ++++++------ .../panels/FlowPanelControlComponent.tsx | 8 ++++---- .../ce/src/flow/hooks/Flow.nodes.hook.ts | 4 ++-- .../components/FunctionSuggestionSearchBar.tsx | 18 ------------------ .../FunctionFileDefaultComponent.tsx} | 4 ++-- .../FunctionFileTriggerComponent.tsx} | 8 ++++---- .../FunctionFilesComponent.tsx} | 4 ++-- .../FunctionNodeComponent.style.scss} | 0 .../{ => nodes}/FunctionNodeComponent.ts | 2 +- .../FunctionNodeDefaultComponent.tsx} | 12 ++++++------ .../FunctionNodeGroupComponent.tsx} | 6 +++--- .../FunctionNodeTriggerComponent.tsx} | 10 +++++----- .../FunctionSuggestionComponent.view.ts} | 0 .../FunctionSuggestionMenuComponent.tsx} | 10 +++++----- .../FunctionSuggestionMenuComponent.util.tsx} | 2 +- .../FunctionSuggestionMenuFooterComponent.tsx} | 2 +- .../FunctionSuggestionSearchBarComponent.tsx | 18 ++++++++++++++++++ .../FunctionSuggestionSearchInput.style.scss | 0 ...FunctionSuggestionSearchInputComponent.tsx} | 4 ++-- .../hooks/FunctionDataTypeSuggestions.hook.tsx | 2 +- .../hooks/FunctionNodeSuggestions.hook.tsx | 2 +- .../FunctionReferenceSuggestions.hook.tsx | 2 +- .../function/hooks/FunctionSuggestion.hook.tsx | 2 +- .../hooks/FunctionValueSuggestions.hook.tsx | 2 +- 27 files changed, 78 insertions(+), 78 deletions(-) delete mode 100644 src/packages/ce/src/function/components/FunctionSuggestionSearchBar.tsx rename src/packages/ce/src/function/components/{FunctionFileDefault.tsx => files/FunctionFileDefaultComponent.tsx} (97%) rename src/packages/ce/src/function/components/{FunctionFileTrigger.tsx => files/FunctionFileTriggerComponent.tsx} (93%) rename src/packages/ce/src/function/components/{FunctionFiles.tsx => files/FunctionFilesComponent.tsx} (98%) rename src/packages/ce/src/function/components/{FunctionNode.style.scss => nodes/FunctionNodeComponent.style.scss} (100%) rename src/packages/ce/src/function/components/{ => nodes}/FunctionNodeComponent.ts (71%) rename src/packages/ce/src/function/components/{FunctionNodeDefault.tsx => nodes/FunctionNodeDefaultComponent.tsx} (93%) rename src/packages/ce/src/function/components/{FunctionNodeGroup.tsx => nodes/FunctionNodeGroupComponent.tsx} (90%) rename src/packages/ce/src/function/components/{FunctionNodeTrigger.tsx => nodes/FunctionNodeTriggerComponent.tsx} (86%) rename src/packages/ce/src/function/components/{FunctionSuggestion.view.ts => suggestion/FunctionSuggestionComponent.view.ts} (100%) rename src/packages/ce/src/function/components/{FunctionSuggestionMenu.tsx => suggestion/FunctionSuggestionMenuComponent.tsx} (85%) rename src/packages/ce/src/function/components/{FunctionSuggestionMenu.util.tsx => suggestion/FunctionSuggestionMenuComponent.util.tsx} (98%) rename src/packages/ce/src/function/components/{FunctionSuggestionMenuFooter.tsx => suggestion/FunctionSuggestionMenuFooterComponent.tsx} (97%) create mode 100644 src/packages/ce/src/function/components/suggestion/FunctionSuggestionSearchBarComponent.tsx rename src/packages/ce/src/function/components/{ => suggestion}/FunctionSuggestionSearchInput.style.scss (100%) rename src/packages/ce/src/function/components/{FunctionSuggestionSearchInput.tsx => suggestion/FunctionSuggestionSearchInputComponent.tsx} (74%) diff --git a/src/app/(flow)/namespace/[namespaceId]/project/[projectId]/flow/[flowId]/page.tsx b/src/app/(flow)/namespace/[namespaceId]/project/[projectId]/flow/[flowId]/page.tsx index eaff610e..8bf57d53 100644 --- a/src/app/(flow)/namespace/[namespaceId]/project/[projectId]/flow/[flowId]/page.tsx +++ b/src/app/(flow)/namespace/[namespaceId]/project/[projectId]/flow/[flowId]/page.tsx @@ -9,7 +9,7 @@ import {IconDatabase, IconFile, IconMessageChatbot} from "@tabler/icons-react"; import {useParams} from "next/navigation"; import {Flow} from "@code0-tech/sagittarius-graphql-types"; import {FlowBuilderComponent} from "@edition/flow/components/builder/FlowBuilderComponent"; -import {FunctionFiles} from "@edition/function/components/FunctionFiles"; +import {FunctionFilesComponent} from "@edition/function/components/files/FunctionFilesComponent"; import { ResizableHandle, ResizablePanel, @@ -49,8 +49,8 @@ export default function Page() { <> - + )} diff --git a/src/packages/ce/src/datatype/components/inputs/json/DataTypeJSONInputComponent.tsx b/src/packages/ce/src/datatype/components/inputs/json/DataTypeJSONInputComponent.tsx index 6081e020..8cac2bf1 100644 --- a/src/packages/ce/src/datatype/components/inputs/json/DataTypeJSONInputComponent.tsx +++ b/src/packages/ce/src/datatype/components/inputs/json/DataTypeJSONInputComponent.tsx @@ -19,7 +19,7 @@ import { useStore } from "@code0-tech/pictor"; import {ButtonGroup} from "@code0-tech/pictor/dist/components/button-group/ButtonGroup"; -import {FunctionSuggestionMenu} from "@edition/function/components/FunctionSuggestionMenu"; +import {FunctionSuggestionMenuComponent} from "@edition/function/components/suggestion/FunctionSuggestionMenuComponent"; import {DataTypeJSONInputEditDialogComponent} from "@edition/datatype/components/inputs/json/DataTypeJSONInputEditDialogComponent"; import {FlowService} from "@edition/flow/services/Flow.service"; import {DatatypeService} from "@edition/datatype/services/Datatype.service"; @@ -119,9 +119,9 @@ export const DataTypeJSONInputComponent: React.FC{"Object"} - setValue(suggestion.value)} - triggerContent={