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 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", diff --git a/src/app/(auth)/layout.tsx b/src/app/(auth)/layout.tsx index f77ffc93..ad0ed6c1 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,14 +14,16 @@ import {GraphqlClient} from "@core/util/graphql-client"; import Image from "next/image"; import React from "react"; import {usePersistentReactiveArrayService} from "@/hooks/usePersistentReactiveArrayService"; +import {UserView} 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 }>) { const client = useApolloClient() - const [store, service] = usePersistentReactiveArrayService("auth-users", (store) => new UserService(new GraphqlClient(client), store)) + 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..58a68aa1 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 {UserView} from "@edition/user/services/User.view"; +import {OrganizationView} from "@edition/organization/services/Organization.view"; +import {MemberView} from "@edition/member/services/Member.view"; +import {NamespaceView} from "@edition/namespace/services/Namespace.view"; +import {RuntimeView} from "@edition/runtime/services/Runtime.view"; +import {ProjectView} from "@edition/project/services/Project.view"; +import {RoleView} from "@edition/role/services/Role.view"; +import {Layout} from "@code0-tech/pictor/dist/components/layout/Layout"; interface ApplicationLayoutProps { children: React.ReactNode @@ -43,19 +43,19 @@ const ApplicationLayout: React.FC = ({children, bar, tab const graphqlClient = React.useMemo(() => new GraphqlClient(client), [client]) - const user = usePersistentReactiveArrayService(`dashboard::users::${currentSession?.id}`, (store) => new UserService(graphqlClient, store)) - const organization = usePersistentReactiveArrayService(`dashboard::organizations::${currentSession?.id}`, (store) => new OrganizationService(graphqlClient, store)) - const member = usePersistentReactiveArrayService(`dashboard::members::${currentSession?.id}`, (store) => new MemberService(graphqlClient, store)) - const namespace = usePersistentReactiveArrayService(`dashboard::namespaces::${currentSession?.id}`, (store) => new NamespaceService(graphqlClient, store)) - const runtime = usePersistentReactiveArrayService(`dashboard::global_runtimes::${currentSession?.id}`, (store) => new RuntimeService(graphqlClient, store)) - const project = usePersistentReactiveArrayService(`dashboard::projects::${currentSession?.id}`, (store) => new ProjectService(graphqlClient, store)) - const role = usePersistentReactiveArrayService(`dashboard::roles::${currentSession?.id}`, (store) => new RoleService(graphqlClient, store)) + const user = usePersistentReactiveArrayService(`dashboard::users::${currentSession?.id}`, (store) => new UserService(graphqlClient, store)) + const organization = usePersistentReactiveArrayService(`dashboard::organizations::${currentSession?.id}`, (store) => new OrganizationService(graphqlClient, store)) + const member = usePersistentReactiveArrayService(`dashboard::members::${currentSession?.id}`, (store) => new MemberService(graphqlClient, store)) + const namespace = usePersistentReactiveArrayService(`dashboard::namespaces::${currentSession?.id}`, (store) => new NamespaceService(graphqlClient, store)) + const runtime = usePersistentReactiveArrayService(`dashboard::global_runtimes::${currentSession?.id}`, (store) => new RuntimeService(graphqlClient, store)) + const project = usePersistentReactiveArrayService(`dashboard::projects::${currentSession?.id}`, (store) => new ProjectService(graphqlClient, store)) + const role = usePersistentReactiveArrayService(`dashboard::roles::${currentSession?.id}`, (store) => new RoleService(graphqlClient, store)) const application = usePersistentReactiveArrayService(`dashboard::application::${currentSession?.id}`, (store) => new ApplicationService(graphqlClient, store)) 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 a2d853ff..5dcfe105 100644 --- a/src/app/(flow)/layout.tsx +++ b/src/app/(flow)/layout.tsx @@ -5,19 +5,7 @@ import {useParams, useRouter} from "next/navigation"; import { AuroraBackground, ContextStoreProvider, - DataTypeView, - DLayout, - DNamespaceMemberView, - DNamespaceProjectView, - DNamespaceRoleView, - DNamespaceView, - DOrganizationView, - DRuntimeView, - DUserView, Flex, - FlowTypeView, - FunctionDefinitionView, - useUserSession } from "@code0-tech/pictor"; import React from "react"; import {GraphqlClient} from "@core/util/graphql-client"; @@ -33,12 +21,24 @@ import {RoleService} from "@edition/role/services/Role.service"; 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 {FlowTypeService} from "@edition/flowtype/services/FlowTypeService"; +import {FlowTypeService} from "@edition/flowtype/services/FlowType.service"; import {FileTabsView} from "@code0-tech/pictor/dist/components/file-tabs/FileTabs.view"; import {FileTabsService} from "@code0-tech/pictor/dist/components/file-tabs/FileTabs.service"; import Image from "next/image"; +import {UserView} from "@edition/user/services/User.view"; +import {OrganizationView} from "@edition/organization/services/Organization.view"; +import {MemberView} from "@edition/member/services/Member.view"; +import {NamespaceView} from "@edition/namespace/services/Namespace.view"; +import {RuntimeView} from "@edition/runtime/services/Runtime.view"; +import {ProjectView} from "@edition/project/services/Project.view"; +import {RoleView} from "@edition/role/services/Role.view"; +import {FunctionView} 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 @@ -59,15 +59,15 @@ export default function Layout({bar, tab, children}: { if (currentSession === null) router.push("/login") - const user = usePersistentReactiveArrayService(`dashboard::users::${currentSession?.id}`, (store) => new UserService(graphqlClient, store)) - const organization = usePersistentReactiveArrayService(`dashboard::organizations::${currentSession?.id}`, (store) => new OrganizationService(graphqlClient, store)) - const member = usePersistentReactiveArrayService(`dashboard::members::${currentSession?.id}`, (store) => new MemberService(graphqlClient, store)) - const namespace = usePersistentReactiveArrayService(`dashboard::namespaces::${currentSession?.id}`, (store) => new NamespaceService(graphqlClient, store)) - const runtime = usePersistentReactiveArrayService(`dashboard::global_runtimes::${currentSession?.id}`, (store) => new RuntimeService(graphqlClient, store)) - const project = usePersistentReactiveArrayService(`dashboard::projects::${currentSession?.id}`, (store) => new ProjectService(graphqlClient, store)) - const role = usePersistentReactiveArrayService(`dashboard::roles::${currentSession?.id}`, (store) => new RoleService(graphqlClient, store)) + const user = usePersistentReactiveArrayService(`dashboard::users::${currentSession?.id}`, (store) => new UserService(graphqlClient, store)) + const organization = usePersistentReactiveArrayService(`dashboard::organizations::${currentSession?.id}`, (store) => new OrganizationService(graphqlClient, store)) + const member = usePersistentReactiveArrayService(`dashboard::members::${currentSession?.id}`, (store) => new MemberService(graphqlClient, store)) + const namespace = usePersistentReactiveArrayService(`dashboard::namespaces::${currentSession?.id}`, (store) => new NamespaceService(graphqlClient, store)) + const runtime = usePersistentReactiveArrayService(`dashboard::global_runtimes::${currentSession?.id}`, (store) => new RuntimeService(graphqlClient, store)) + const project = usePersistentReactiveArrayService(`dashboard::projects::${currentSession?.id}`, (store) => new ProjectService(graphqlClient, store)) + const role = usePersistentReactiveArrayService(`dashboard::roles::${currentSession?.id}`, (store) => new RoleService(graphqlClient, store)) const flow = usePersistentReactiveArrayService(`dashboard::flows::${currentSession?.id}`, (store) => new FlowService(graphqlClient, store)) - const functions = usePersistentReactiveArrayService(`dashboard::functions::${currentSession?.id}`, (store) => new FunctionService(graphqlClient, store)) + const functions = usePersistentReactiveArrayService(`dashboard::functions::${currentSession?.id}`, (store) => new FunctionService(graphqlClient, store)) const datatype = usePersistentReactiveArrayService(`dashboard::datatypes::${currentSession?.id}`, (store) => new DatatypeService(graphqlClient, store)) const flowtype = usePersistentReactiveArrayService(`dashboard::flowtypes::${currentSession?.id}`, (store) => new FlowTypeService(graphqlClient, store)) const file = usePersistentReactiveArrayService(`dashboard::files::${flowId}`, FileTabsService, []) @@ -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 3ac5b622..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 @@ -2,19 +2,20 @@ 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"; import {useParams} from "next/navigation"; import {Flow} from "@code0-tech/sagittarius-graphql-types"; +import {FlowBuilderComponent} from "@edition/flow/components/builder/FlowBuilderComponent"; +import {FunctionFilesComponent} from "@edition/function/components/files/FunctionFilesComponent"; +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() { @@ -25,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/ApplicationPage.tsx b/src/packages/ce/src/application/pages/ApplicationPage.tsx index 4b2dc5f0..0a3e3d2e 100644 --- a/src/packages/ce/src/application/pages/ApplicationPage.tsx +++ b/src/packages/ce/src/application/pages/ApplicationPage.tsx @@ -15,12 +15,12 @@ import { Spacing, Text, useService, - useStore, - useUserSession + useStore } from "@code0-tech/pictor"; import {UserService} from "@edition/user/services/User.service"; import React from "react"; import {OrganizationsTopView} from "@edition/organization/views/OrganizationsTopView"; +import {useUserSession} from "@edition/user/hooks/User.session.hook"; export const ApplicationPage = () => { diff --git a/src/packages/ce/src/application/pages/ApplicationSettingsPage.tsx b/src/packages/ce/src/application/pages/ApplicationSettingsPage.tsx index fa6fd741..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, @@ -15,8 +12,7 @@ import { toast, useForm, useService, - useStore, - useUserSession + useStore } from "@code0-tech/pictor"; import {UserService} from "@edition/user/services/User.service"; import {notFound} from "next/navigation"; @@ -24,6 +20,12 @@ import {Tab, TabContent, TabList, TabTrigger} from "@code0-tech/pictor/dist/comp 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/application/views/ApplicationBarView.tsx b/src/packages/ce/src/application/views/ApplicationBarView.tsx index 9390a835..95fbae55 100644 --- a/src/packages/ce/src/application/views/ApplicationBarView.tsx +++ b/src/packages/ce/src/application/views/ApplicationBarView.tsx @@ -1,24 +1,14 @@ "use client" -import { - Avatar, - Badge, - Button, - Flex, - MenuItem, - MenuSeparator, - TextInput, - useService, - useStore, - useUserSession -} from "@code0-tech/pictor"; +import {Button, Flex, MenuItem, MenuSeparator, useService, useStore} from "@code0-tech/pictor"; import {UserService} from "@edition/user/services/User.service"; import {useRouter} from "next/navigation"; import React from "react"; -import DUserMenu from "@code0-tech/pictor/dist/components/d-user/DUserMenu"; import Link from "next/link"; import {IconBuilding, IconFolders, IconInbox, IconLogout, IconSearch} from "@tabler/icons-react"; import {ApplicationBreadcrumbView} from "@edition/application/views/ApplicationBreadcrumbView"; +import UserMenuComponent from "@edition/user/components/UserMenuComponent"; +import {useUserSession} from "@edition/user/hooks/User.session.hook"; export const ApplicationBarView: React.FC = () => { @@ -48,7 +38,7 @@ export const ApplicationBarView: React.FC = () => { }) } - return + return Organizations @@ -63,7 +53,7 @@ export const ApplicationBarView: React.FC = () => { Logout - + }, [currentUser, currentSession, namespaceIndex]) return diff --git a/src/packages/ce/src/application/views/ApplicationTabView.tsx b/src/packages/ce/src/application/views/ApplicationTabView.tsx index d0ab27c2..fde1514b 100644 --- a/src/packages/ce/src/application/views/ApplicationTabView.tsx +++ b/src/packages/ce/src/application/views/ApplicationTabView.tsx @@ -2,13 +2,13 @@ import React from "react"; import {Tab, TabList, TabTrigger} from "@code0-tech/pictor/dist/components/tab/Tab"; -import {Badge, Button, Container, useService, useStore, useUserSession} from "@code0-tech/pictor"; +import {Button, useService, useStore} from "@code0-tech/pictor"; import {IconBuilding, IconHome, IconServer, IconSettings, IconUser} from "@tabler/icons-react"; import {usePathname, useRouter} from "next/navigation"; import {UserService} from "@edition/user/services/User.service"; import {RuntimeService} from "@edition/runtime/services/Runtime.service"; import {OrganizationService} from "@edition/organization/services/Organization.service"; -import {hashToColor} from "@code0-tech/pictor/dist/components/d-flow/DFlow.util"; +import {useUserSession} from "@edition/user/hooks/User.session.hook"; export const ApplicationTabView: React.FC = () => { @@ -38,12 +38,14 @@ export const ApplicationTabView: React.FC = () => { - - @@ -53,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/datatype/components/badges/LiteralBadgeComponent.tsx b/src/packages/ce/src/datatype/components/badges/LiteralBadgeComponent.tsx new file mode 100644 index 00000000..d47af860 --- /dev/null +++ b/src/packages/ce/src/datatype/components/badges/LiteralBadgeComponent.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 LiteralBadgeComponentProps extends Omit { + value: LiteralValue +} + +export const LiteralBadgeComponent: 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/badges/NodeBadgeComponent.tsx b/src/packages/ce/src/datatype/components/badges/NodeBadgeComponent.tsx new file mode 100644 index 00000000..e1324fc2 --- /dev/null +++ b/src/packages/ce/src/datatype/components/badges/NodeBadgeComponent.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, + 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/FlowType.service"; +import {FunctionView} from "@edition/function/services/Function.view"; +import {FlowTypeView} from "@edition/flowtype/services/FlowType.view"; + +export interface NodeBadgeComponentProps extends Omit { + value: NodeFunction | NodeFunctionIdWrapper + flowId: Flow['id'] + definition?: FunctionView | FlowTypeView +} + +export const NodeBadgeComponent: 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/badges/ReferenceBadgeComponent.tsx b/src/packages/ce/src/datatype/components/badges/ReferenceBadgeComponent.tsx new file mode 100644 index 00000000..7f59e3fe --- /dev/null +++ b/src/packages/ce/src/datatype/components/badges/ReferenceBadgeComponent.tsx @@ -0,0 +1,42 @@ +import {Flow, ReferenceValue} from "@code0-tech/sagittarius-graphql-types"; +import React from "react"; +import {NodeBadgeComponent} from "./NodeBadgeComponent"; +import {IconVariable} from "@tabler/icons-react"; +import {Badge, BadgeType, Flex, Text} from "@code0-tech/pictor"; +import {FunctionView} from "@edition/function/services/Function.view"; +import {FlowTypeView} from "@edition/flowtype/services/FlowType.view"; + +export interface ReferenceBadgeComponentProps extends Omit { + value: ReferenceValue + flowId: Flow['id'] + definition?: FunctionView | FlowTypeView +} + +export const ReferenceBadgeComponent: 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/inputs/DataTypeInputComponent.tsx b/src/packages/ce/src/datatype/components/inputs/DataTypeInputComponent.tsx new file mode 100644 index 00000000..6c8394b4 --- /dev/null +++ b/src/packages/ce/src/datatype/components/inputs/DataTypeInputComponent.tsx @@ -0,0 +1,71 @@ +import {Flow, NodeFunction, NodeParameter} from "@code0-tech/sagittarius-graphql-types"; +import React from "react"; +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 DataTypeInputComponentProps extends Omit, "wrapperComponent" | "type"> { + flowId: Flow['id'] + nodeId: NodeFunction['id'] + parameterId: NodeParameter['id'] + clearable?: boolean + onClear?: (event: React.MouseEvent) => void +} + +export const DataTypeInputComponent: 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/inputs/json/DataTypeJSONInputComponent.tsx b/src/packages/ce/src/datatype/components/inputs/json/DataTypeJSONInputComponent.tsx new file mode 100644 index 00000000..8cac2bf1 --- /dev/null +++ b/src/packages/ce/src/datatype/components/inputs/json/DataTypeJSONInputComponent.tsx @@ -0,0 +1,159 @@ +import React from "react" +import {IconAlignLeft, IconEdit, IconX} from "@tabler/icons-react" +import "../type/DataTypeTypeInputComponent.style.scss" +import {DataTypeJSONInputTreeComponent} from "./DataTypeJSONInputTreeComponent"; +import {DataTypeInputComponentProps} from "../DataTypeInputComponent"; +import {LiteralValue, NodeFunction, NodeParameterValue} from "@code0-tech/sagittarius-graphql-types"; +import {NodeBadgeComponent} from "../../badges/NodeBadgeComponent"; +import {ReferenceBadgeComponent} from "../../badges/ReferenceBadgeComponent"; +import {useSuggestions} from "@edition/function/hooks/FunctionSuggestion.hook"; +import { + Button, + Card, + Flex, + InputDescription, + InputLabel, + InputMessage, + Text, + useService, + useStore +} from "@code0-tech/pictor"; +import {ButtonGroup} from "@code0-tech/pictor/dist/components/button-group/ButtonGroup"; +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"; +import {FunctionService} from "@edition/function/services/Function.service"; + +export interface EditableJSONEntry { + key: string + value: LiteralValue | null + path: string[] +} + +export type DataTypeJSONInputComponentProps = DataTypeInputComponentProps + +export const DataTypeJSONInputComponent: React.FC = (props) => { + + + const {flowId, nodeId, parameterId, title, description, formValidation, onChange} = 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 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/inputs/json/DataTypeJSONInputEditDialogComponent.tsx b/src/packages/ce/src/datatype/components/inputs/json/DataTypeJSONInputEditDialogComponent.tsx new file mode 100644 index 00000000..441040e5 --- /dev/null +++ b/src/packages/ce/src/datatype/components/inputs/json/DataTypeJSONInputEditDialogComponent.tsx @@ -0,0 +1,187 @@ +import React from "react" +import {DataTypeJSONInputTreeComponent} from "./DataTypeJSONInputTreeComponent"; +import {LiteralValue} from "@code0-tech/sagittarius-graphql-types"; +import {EditableJSONEntry} from "@edition/datatype/components/inputs/json/DataTypeJSONInputComponent"; +import { + Button, + Dialog, + DialogClose, + DialogContent, + DialogOverlay, + DialogPortal, + 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 DataTypeJSONInputEditDialogComponentProps { + 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 DataTypeJSONInputEditDialogComponent: 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/inputs/json/DataTypeJSONInputTreeComponent.tsx b/src/packages/ce/src/datatype/components/inputs/json/DataTypeJSONInputTreeComponent.tsx new file mode 100644 index 00000000..717d5d38 --- /dev/null +++ b/src/packages/ce/src/datatype/components/inputs/json/DataTypeJSONInputTreeComponent.tsx @@ -0,0 +1,166 @@ +import {EditableJSONEntry} from "./DataTypeJSONInputComponent" +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 DataTypeJSONInputTreeComponentProps { + 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 DataTypeJSONInputTreeComponent: 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/inputs/text/DataTypeTextInputComponent.tsx b/src/packages/ce/src/datatype/components/inputs/text/DataTypeTextInputComponent.tsx new file mode 100644 index 00000000..05d80dcf --- /dev/null +++ b/src/packages/ce/src/datatype/components/inputs/text/DataTypeTextInputComponent.tsx @@ -0,0 +1,182 @@ +import React from "react"; +import {ReferenceValue} from "@code0-tech/sagittarius-graphql-types"; +import {NodeBadgeComponent} from "../../badges/NodeBadgeComponent"; +import {ReferenceBadgeComponent} from "../../badges/ReferenceBadgeComponent"; +import {DataTypeInputComponentProps} from "../DataTypeInputComponent"; +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/FlowType.service"; +import {useSuggestions} from "@edition/function/hooks/FunctionSuggestion.hook"; +import {FunctionSuggestionMenuFooterComponent} from "@edition/function/components/suggestion/FunctionSuggestionMenuFooterComponent"; +import {toInputSuggestions} from "@edition/function/components/suggestion/FunctionSuggestionMenuComponent.util"; +import {FunctionSuggestion} from "@edition/function/components/suggestion/FunctionSuggestionComponent.view"; + +export type DataTypeTextInputComponentProps = DataTypeInputComponentProps + +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 DataTypeTextInputComponent: 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 FunctionSuggestion[])} + {...rest} + + /> +} diff --git a/src/packages/ce/src/datatype/components/inputs/type/DataTypeTypeInputComponent.style.scss b/src/packages/ce/src/datatype/components/inputs/type/DataTypeTypeInputComponent.style.scss new file mode 100644 index 00000000..358ee6ae --- /dev/null +++ b/src/packages/ce/src/datatype/components/inputs/type/DataTypeTypeInputComponent.style.scss @@ -0,0 +1,64 @@ +@use "@core/style/helpers"; +@use "@core/style/box"; +@use "@core/style/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/inputs/type/DataTypeTypeInputComponent.tsx b/src/packages/ce/src/datatype/components/inputs/type/DataTypeTypeInputComponent.tsx new file mode 100644 index 00000000..66629c6b --- /dev/null +++ b/src/packages/ce/src/datatype/components/inputs/type/DataTypeTypeInputComponent.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 "./DataTypeTypeInputComponent.style.scss" +import {DataTypeTypeInputEditDialogComponent} from "./DataTypeTypeInputEditDialogComponent"; +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 DataTypeTypeInputComponentProps extends ValidationProps { + onChange?: (value: DataTypeIdentifier | null) => void + description?: string + label?: string +} + +export interface DataTypeTypeInputRuleTreeComponentProps { + dataTypeIdentifier: DataTypeIdentifier + parentRule?: Maybe + isRoot?: boolean +} + +export const DataTypeTypeInputComponent: 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 DataTypeTypeInputRuleTreeComponent: 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/inputs/type/DataTypeTypeInputEditDialogComponent.tsx b/src/packages/ce/src/datatype/components/inputs/type/DataTypeTypeInputEditDialogComponent.tsx new file mode 100644 index 00000000..9e401521 --- /dev/null +++ b/src/packages/ce/src/datatype/components/inputs/type/DataTypeTypeInputEditDialogComponent.tsx @@ -0,0 +1,178 @@ +import React from "react"; +import {DataTypeIdentifier, LiteralValue} from "@code0-tech/sagittarius-graphql-types"; +import {DataTypeTypeInputRuleTreeComponent} from "./DataTypeTypeInputComponent"; +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, + 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"; +import {Layout} from "@code0-tech/pictor/dist/components/layout/Layout"; +import { + ResizableHandle, + ResizablePanel, + ResizablePanelGroup +} from "@code0-tech/pictor/dist/components/resizable/Resizable"; + +export interface DataTypeTypeInputEditDialogComponentProps { + dataTypeIdentifier: DataTypeIdentifier + open?: boolean + onOpenChange?: (open: boolean) => void + onDataTypeChange?: (dataTypeIdentifier: DataTypeIdentifier) => void +} + +export const DataTypeTypeInputEditDialogComponent: 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..112cf4e8 --- /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/builder/FlowBuilderComponent.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..0f393e61 100644 --- a/src/packages/ce/src/datatype/services/Datatype.service.ts +++ b/src/packages/ce/src/datatype/services/Datatype.service.ts @@ -1,14 +1,35 @@ -import { - DataTypeView, DFlowDataTypeDependencies, - DFlowDataTypeReactiveService, - ReactiveArrayStore -} from "@code0-tech/pictor"; +import {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, Namespace, NamespaceProject, + NodeFunctionIdWrapper, + NodeParameterValue, + Query, Runtime +} 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 type DataTypeDependencies = { + namespaceId: Namespace['id'] + projectId: NamespaceProject['id'] + runtimeId: Runtime['id'] +} -export class DatatypeService extends DFlowDataTypeReactiveService { +export class DatatypeService extends ReactiveArrayService { private readonly client: GraphqlClient private i = 0 @@ -18,7 +39,7 @@ export class DatatypeService extends DFlowDataTypeReactiveService { this.client = client } - values(dependencies?: DFlowDataTypeDependencies): DataTypeView[] { + values(dependencies?: DataTypeDependencies): DataTypeView[] { const dataTypes = super.values() if (!dependencies?.namespaceId || !dependencies.projectId || !dependencies.runtimeId) return dataTypes @@ -61,4 +82,264 @@ export class DatatypeService extends DFlowDataTypeReactiveService { return dataType !== undefined } + getDataType(type: DataTypeIdentifier, dependencies?: DataTypeDependencies): 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?: DataTypeDependencies): 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?: DataTypeDependencies): 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?: DataTypeDependencies): 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?: DataTypeDependencies): 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..ac43dc40 --- /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/Function.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/FlowCreateDialogComponent.tsx b/src/packages/ce/src/flow/components/FlowCreateDialogComponent.tsx index 44f46e47..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,8 +27,7 @@ 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/FlowTypeService"; +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"; import {useParams, useRouter} from "next/navigation"; @@ -82,7 +81,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/FlowDeleteDialogComponent.tsx b/src/packages/ce/src/flow/components/FlowDeleteDialogComponent.tsx new file mode 100644 index 00000000..1fb98969 --- /dev/null +++ b/src/packages/ce/src/flow/components/FlowDeleteDialogComponent.tsx @@ -0,0 +1,68 @@ +import React from "react"; +import { + FlowFolderContextMenuComponentGroupData, + FlowFolderContextMenuComponentItemData +} from "./folder/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 FlowDeleteDialogComponentProps { + open?: boolean + onOpenChange?: (open: boolean) => void + contextData: FlowFolderContextMenuComponentGroupData | FlowFolderContextMenuComponentItemData + onDelete?: (flow: Flow) => void +} + +export const FlowDeleteDialogComponent: 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/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/FlowRenameDialogComponent.tsx b/src/packages/ce/src/flow/components/FlowRenameDialogComponent.tsx new file mode 100644 index 00000000..54667c79 --- /dev/null +++ b/src/packages/ce/src/flow/components/FlowRenameDialogComponent.tsx @@ -0,0 +1,78 @@ +import React from "react"; +import {FlowFolderContextMenuComponentGroupData, FlowFolderContextMenuComponentItemData} from "./folder/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 FlowRenameDialogComponentProps { + contextData: FlowFolderContextMenuComponentGroupData | FlowFolderContextMenuComponentItemData + open?: boolean + onOpenChange?: (open: boolean) => void + onRename?: (flow: Flow, newName: string) => void +} + +export const FlowRenameDialogComponent: 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/builder/FlowBuilderComponent.style.scss b/src/packages/ce/src/flow/components/builder/FlowBuilderComponent.style.scss new file mode 100644 index 00000000..23cc1e64 --- /dev/null +++ b/src/packages/ce/src/flow/components/builder/FlowBuilderComponent.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/builder/FlowBuilderComponent.tsx b/src/packages/ce/src/flow/components/builder/FlowBuilderComponent.tsx new file mode 100644 index 00000000..5a3523fd --- /dev/null +++ b/src/packages/ce/src/flow/components/builder/FlowBuilderComponent.tsx @@ -0,0 +1,803 @@ +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 "./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, mergeCode0Props, Spacing, Text} from "@code0-tech/pictor"; +import {FunctionNodeDefaultComponent} from "@edition/function/components/nodes/FunctionNodeDefaultComponent"; +import {FunctionNodeGroupComponent} from "@edition/function/components/nodes/FunctionNodeGroupComponent"; +import {FunctionNodeTriggerComponent} from "@edition/function/components/nodes/FunctionNodeTriggerComponent"; +import {useEdges} from "@edition/flow/hooks/Flow.edges.hook"; +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. + * - 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 FlowBuilderComponent: React.FC = (props) => { + return + + +} + +const InternalFlowBuilder: React.FC = (props) => { + const {flowId, namespaceId, projectId, ...rest} = props + + const nodeTypes = React.useMemo(() => ({ + default: FunctionNodeDefaultComponent, + group: FunctionNodeGroupComponent, + trigger: FunctionNodeTriggerComponent, + }), []) + + const edgeTypes = React.useMemo(() => ({ + default: FlowBuilderEdgeComponent, + }), []) + + 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/builder/FlowBuilderComponent.util.ts b/src/packages/ce/src/flow/components/builder/FlowBuilderComponent.util.ts new file mode 100644 index 00000000..eaa2e760 --- /dev/null +++ b/src/packages/ce/src/flow/components/builder/FlowBuilderComponent.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/builder/FlowBuilderEdgeComponent.tsx b/src/packages/ce/src/flow/components/builder/FlowBuilderEdgeComponent.tsx new file mode 100644 index 00000000..0e2130c5 --- /dev/null +++ b/src/packages/ce/src/flow/components/builder/FlowBuilderEdgeComponent.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 FlowBuilderEdgeComponent: 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/folder/FlowFolderComponent.style.scss b/src/packages/ce/src/flow/components/folder/FlowFolderComponent.style.scss new file mode 100644 index 00000000..2544d69b --- /dev/null +++ b/src/packages/ce/src/flow/components/folder/FlowFolderComponent.style.scss @@ -0,0 +1,99 @@ +@use "@core/style/helpers"; +@use "@core/style/box"; +@use "@core/style/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/folder/FlowFolderComponent.tsx b/src/packages/ce/src/flow/components/folder/FlowFolderComponent.tsx new file mode 100644 index 00000000..d83aee0d --- /dev/null +++ b/src/packages/ce/src/flow/components/folder/FlowFolderComponent.tsx @@ -0,0 +1,397 @@ +"use client" + +import "./FlowFolderComponent.style.scss" +import React from "react" +import {IconFile, IconFolderFilled, IconFolderOpen} from "@tabler/icons-react" +import type {Flow, FlowType, Namespace, NamespaceProject, Scalars} from "@code0-tech/sagittarius-graphql-types" +import { + FlowFolderContextMenuComponent, + FlowFolderContextMenuComponentGroupData, + FlowFolderContextMenuComponentItemData +} from "./FlowFolderContextMenuComponent"; +import {HoverCard, HoverCardContent, HoverCardPortal, HoverCardTrigger} from "@radix-ui/react-hover-card"; +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: FlowFolderContextMenuComponentGroupData | FlowFolderContextMenuComponentItemData) => void + onDelete?: (contextData: FlowFolderContextMenuComponentGroupData | FlowFolderContextMenuComponentItemData) => void + onCreate?: (type: FlowType['id']) => void + onSelect?: (flow: Flow) => void +} + +export type FlowFolderComponentHandle = { + openAll: () => void + closeAll: () => void + openActivePath: () => void +} + +type OpenMode = "default" | "allOpen" | "allClosed" | "activePath" + +export interface FlowFolderComponentGroupProps extends FlowFolderComponentProps, Omit, "onSelect"> { + name: string + children: React.ReactElement | React.ReactElement[] | React.ReactElement | React.ReactElement[] + defaultOpen?: boolean + flows: Flow[] +} + +export interface FlowFolderComponentItemProps extends FlowFolderComponentProps, Omit, "onSelect"> { + name: string + path: string + active?: boolean + flow: Flow +} + +export const FlowFolderComponent = React.forwardRef((props, ref) => { + + const {activeFlowId, namespaceId, projectId} = props + + const flowService = useService(FlowService) + const flowStore = useStore(FlowService) + + 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/folder/FlowFolderContextMenuComponent.tsx b/src/packages/ce/src/flow/components/folder/FlowFolderContextMenuComponent.tsx new file mode 100644 index 00000000..a447655b --- /dev/null +++ b/src/packages/ce/src/flow/components/folder/FlowFolderContextMenuComponent.tsx @@ -0,0 +1,88 @@ +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, + ContextMenuItem, + ContextMenuLabel, + ContextMenuPortal, + ContextMenuSeparator, + ContextMenuSub, + ContextMenuSubContent, + ContextMenuSubTrigger, + ContextMenuTrigger +} from "@code0-tech/pictor/dist/components/context-menu/ContextMenu"; +import {Flex, Text, useService, useStore} from "@code0-tech/pictor"; +import {FlowTypeService} from "@edition/flowtype/services/FlowType.service"; + +export interface FlowFolderContextMenuComponentGroupData { + name: string + flow: Flow[] + type: "folder" +} + +export interface FlowFolderContextMenuComponentItemData { + name: string + flow: Flow + type: "item" +} + +export interface FlowFolderContextMenuComponentProps extends FlowFolderComponentProps { + children: React.ReactNode + contextData?: FlowFolderContextMenuComponentGroupData | FlowFolderContextMenuComponentItemData +} + +export const FlowFolderContextMenuComponent: React.FC = (props) => { + + const {children} = props + + const flowTypeService = useService(FlowTypeService) + const flowTypeStore = useStore(FlowTypeService) + + 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/panels/FlowPanelControlComponent.tsx b/src/packages/ce/src/flow/components/panels/FlowPanelControlComponent.tsx new file mode 100644 index 00000000..7b29f471 --- /dev/null +++ b/src/packages/ce/src/flow/components/panels/FlowPanelControlComponent.tsx @@ -0,0 +1,101 @@ +import React from "react"; +import {Flow, NodeFunction} from "@code0-tech/sagittarius-graphql-types"; +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/FunctionSuggestion.hook"; +import {Panel} from "@xyflow/react"; +import {ButtonGroup} from "@code0-tech/pictor/dist/components/button-group/ButtonGroup"; +import {FunctionSuggestionMenuComponent} from "@edition/function/components/suggestion/FunctionSuggestionMenuComponent"; +import {FlowService} from "@edition/flow/services/Flow.service"; + +export interface FlowPanelControlComponentProps { + flowId: Flow['id'] +} + +export const FlowPanelControlComponent: React.FC = (props) => { + + //props + const {flowId} = props + + //services and stores + const fileTabsService = useService(FileTabsService) + const fileTabsStore = useStore(FileTabsService) + const flowService = useService(FlowService) + const flowStore = useStore(FlowService) + 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/panels/FlowPanelLayoutComponent.tsx b/src/packages/ce/src/flow/components/panels/FlowPanelLayoutComponent.tsx new file mode 100644 index 00000000..ca162f86 --- /dev/null +++ b/src/packages/ce/src/flow/components/panels/FlowPanelLayoutComponent.tsx @@ -0,0 +1,61 @@ +import React from "react"; +import {IconLayout, IconLayoutDistributeHorizontal, IconLayoutDistributeVertical} from "@tabler/icons-react"; +import {Panel} from "@xyflow/react"; +import { + SegmentedControl, + SegmentedControlItem, Text, + Tooltip, + TooltipContent, + TooltipPortal, + TooltipTrigger +} from "@code0-tech/pictor"; + +export interface FlowPanelLayoutComponentProps { + +} + +export const FlowPanelLayoutComponent: 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/panels/FlowPanelSizeComponent.tsx b/src/packages/ce/src/flow/components/panels/FlowPanelSizeComponent.tsx new file mode 100644 index 00000000..e134364c --- /dev/null +++ b/src/packages/ce/src/flow/components/panels/FlowPanelSizeComponent.tsx @@ -0,0 +1,44 @@ +import React from "react"; +import {Panel, useReactFlow, useViewport} from "@xyflow/react"; +import {IconFocusCentered, IconMinus, IconPlus} from "@tabler/icons-react"; +import {Badge, Button, Flex, Text} from "@code0-tech/pictor"; +import {ButtonGroup} from "@code0-tech/pictor/dist/components/button-group/ButtonGroup"; + +export const FlowPanelSizeComponent: 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/panels/FlowPanelUpdateComponent.tsx b/src/packages/ce/src/flow/components/panels/FlowPanelUpdateComponent.tsx new file mode 100644 index 00000000..a8081064 --- /dev/null +++ b/src/packages/ce/src/flow/components/panels/FlowPanelUpdateComponent.tsx @@ -0,0 +1,132 @@ +import React from "react"; +import {IconCloudCheck, IconCloudUpload} from "@tabler/icons-react"; +import {Panel} from "@xyflow/react"; +import {Flow} from "@code0-tech/sagittarius-graphql-types"; +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 FlowPanelUpdateComponent: React.FC = (props) => { + + const {flowId} = props + + const flowService = useService(FlowService) + const flowStore = useStore(FlowService) + 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..9355aaba --- /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 {FunctionNodeComponentProps} from "@edition/function/components/nodes/FunctionNodeComponent"; + +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..f4c64380 --- /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/Function.return.hook"; +import {getReferenceType} from "@edition/function/hooks/FunctionNodeReference.return.hook"; +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 {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/services/Flow.service.ts b/src/packages/ce/src/flow/services/Flow.service.ts index f7403d0a..98ad330f 100644 --- a/src/packages/ce/src/flow/services/Flow.service.ts +++ b/src/packages/ce/src/flow/services/Flow.service.ts @@ -1,15 +1,24 @@ -import {DFlowDependencies, DFlowReactiveService, ReactiveArrayStore} from "@code0-tech/pictor"; +import {ReactiveArrayService, ReactiveArrayStore} from "@code0-tech/pictor"; import { - Flow, LiteralValue, - Mutation, + FlowInput, + FlowSetting, + LiteralValue, + Maybe, + Mutation, Namespace, NamespaceProject, NamespacesProjectsFlowsCreateInput, NamespacesProjectsFlowsCreatePayload, NamespacesProjectsFlowsDeleteInput, NamespacesProjectsFlowsDeletePayload, NamespacesProjectsFlowsUpdateInput, NamespacesProjectsFlowsUpdatePayload, - NodeFunction, NodeParameter, - Query, ReferenceValue + NodeFunction, + NodeFunctionIdWrapper, + NodeParameter, + NodeParameterValueInput, + Query, + ReferencePathInput, + ReferenceValue, + Scalars } from "@code0-tech/sagittarius-graphql-types"; import {GraphqlClient} from "@core/util/graphql-client"; import flowsQuery from "@edition/flow/services/queries/Flows.query.graphql"; @@ -17,22 +26,27 @@ import flowCreateMutation from "@edition/flow/services/mutations/Flow.create.mut import flowDeleteMutation from "@edition/flow/services/mutations/Flow.delete.mutation.graphql"; import flowUpdateMutation from "@edition/flow/services/mutations/Flow.update.mutation.graphql"; import {View} from "@code0-tech/pictor/dist/utils/view"; +import {FlowView} from "@edition/flow/services/Flow.view"; +export type FlowDependencies = { + namespaceId: Namespace['id'] + projectId: NamespaceProject['id'] +} -export class FlowService extends DFlowReactiveService { +export class FlowService extends ReactiveArrayService { private readonly client: GraphqlClient - private flowUpdateQueue: Array + private flowUpdateQueue: Array private i - constructor(client: GraphqlClient, store: ReactiveArrayStore>) { + constructor(client: GraphqlClient, store: ReactiveArrayStore>) { super(store) this.client = client this.flowUpdateQueue = [] this.i = 0 } - values(dependencies?: DFlowDependencies): Flow[] { + values(dependencies?: FlowDependencies): FlowView[] { const flows = super.values() if (!dependencies?.namespaceId || !dependencies.projectId) return flows @@ -82,30 +96,241 @@ export class FlowService extends DFlowReactiveService { return filtered } - hasById(id: Flow["id"]): boolean { + hasById(id: FlowView["id"]): boolean { const flow = super.values().find(f => f.id === id) return flow !== undefined } + getById(id: FlowView['id'], dependencies?: FlowDependencies): FlowView | undefined { + return this.values(dependencies).find(value => value.id === id); + } + + protected removeParameterNode(flow: FlowView, parameter: NodeParameter): void { + if (parameter?.value?.__typename === "NodeFunctionIdWrapper") { + const parameterNode = flow?.nodes?.nodes?.find(n => n?.id === (parameter.value as NodeFunction)?.id) + if (parameterNode) { + flow!.nodes!.nodes = flow!.nodes!.nodes!.filter(n => n?.id !== (parameter.value as NodeFunction)?.id) + let nextNodeId = parameterNode.nextNodeId + while (nextNodeId) { + const nextNode = flow!.nodes!.nodes!.find(n => n?.id === nextNodeId) + if (nextNode) { + flow!.nodes!.nodes = flow!.nodes!.nodes!.filter(n => n?.id !== nextNodeId) + nextNodeId = nextNode.nextNodeId + } else { + nextNodeId = null + } + } + parameterNode.parameters?.nodes?.forEach(p => { + this.removeParameterNode(flow, p!!) + }) + } + } + } + + getLinkedNodesById(flowId: FlowView['id'], nodeId: NodeFunction['id']): NodeFunction[] { + const parentNode = this.getNodeById(flowId, nodeId) + const nextNodes = parentNode ? this.getLinkedNodesById(flowId, parentNode.nextNodeId) : [] + const parameterNodes: NodeFunction[] = [] + parentNode?.parameters?.nodes?.forEach(p => { + if (p?.value?.__typename === "NodeFunctionIdWrapper") { + const parameterNode = this.getNodeById(flowId, (p.value as NodeFunctionIdWrapper)?.id!!) + if (parameterNode) { + parameterNodes.push(parameterNode) + parameterNodes.push(...(parameterNode ? this.getLinkedNodesById(flowId, parameterNode.nextNodeId) : [])) + } + } + }) + return [...(parentNode ? [parentNode] : []), ...parameterNodes, ...nextNodes] + } + + getNodeById(flowId: FlowView['id'], nodeId: NodeFunction['id']): NodeFunction | undefined { + return this.getById(flowId)?.nodes?.nodes?.find(node => node?.id === nodeId)!! + } + + getPayloadById(flowId: FlowView['id']): FlowInput { + const flow = this.getById(flowId) + + return { + name: flow?.name!, + type: flow?.type?.id!, + settings: flow?.settings?.nodes?.map(setting => { + return { + flowSettingIdentifier: setting?.flowSettingIdentifier!, + value: setting?.value!, + } + }) ?? [], + nodes: (flow?.nodes?.nodes ?? []).map(node => ({ + id: node?.id!, + nextNodeId: node?.nextNodeId!, + functionDefinitionId: node?.functionDefinition?.id!, + parameters: (node?.parameters?.nodes ?? []).map(parameter => { + let value: NodeParameterValueInput + + switch (parameter?.value?.__typename) { + case "NodeFunctionIdWrapper": + value = {nodeFunctionId: parameter.value.id!} + break + + case "LiteralValue": + value = {literalValue: parameter.value.value!} + break + + case "ReferenceValue": { + const v = parameter.value as ReferenceValue + value = { + referenceValue: { + ...(v.nodeFunctionId ? {nodeFunctionId: v.nodeFunctionId} : {}), + ...(v.parameterIndex && v.inputIndex ? + { + parameterIndex: v.parameterIndex, + inputIndex: v.inputIndex + } : {}), + referencePath: v.referencePath?.map(referencePath => { + const reference: ReferencePathInput = { + path: referencePath.path + } + return reference + }) ?? [], + }, + } + break + } + + default: + value = {literalValue: null} + } + + return { + parameterDefinitionId: parameter?.parameterDefinition?.id!, + value, + } + }), + })), + startingNodeId: flow?.startingNodeId!, + } + } + + async deleteNodeById(flowId: FlowView['id'], nodeId: NodeFunction['id']): Promise { + const flow = this.getById(flowId) + const node = this.getNodeById(flowId, nodeId) + const parentNode = flow?.nodes?.nodes?.find(node => node?.parameters?.nodes?.find(p => p?.value?.__typename === "NodeFunctionIdWrapper" && (p.value as NodeFunction)?.id === nodeId)) + const previousNodes = flow?.nodes?.nodes?.find(n => n?.nextNodeId === nodeId) + const index = this.values().findIndex(f => f.id === flowId) + if (!flow || !node) return + + flow.nodes!.nodes = flow.nodes!.nodes!.filter(n => n?.id !== nodeId) + node.parameters?.nodes?.forEach(p => this.removeParameterNode(flow, p!!)) + + + if (previousNodes) { + previousNodes.nextNodeId = node.nextNodeId + } else { + if (!parentNode) flow.startingNodeId = node.nextNodeId ?? undefined + } + + if (parentNode) { + const parameter = parentNode.parameters?.nodes?.find(p => p?.value?.__typename === "NodeFunctionIdWrapper" && (p.value as NodeFunction)?.id === nodeId) + if (parameter) { + parameter.value = undefined + } + } - async addNextNodeById(flowId: Flow["id"], parentNodeId: NodeFunction["id"] | null, nextNode: NodeFunction): Promise { - await super.addNextNodeById(flowId, parentNodeId, nextNode) + flow.editedAt = new Date().toISOString() + + this.set(index, new View(flow)) await this.syncFlow(flowId) } + async addNextNodeById(flowId: FlowView['id'], parentNodeId: NodeFunction['id'] | null, nextNode: NodeFunction): Promise { + + const flow = this.getById(flowId) + const index = this.values().findIndex(f => f.id === flowId) + const parentNode = parentNodeId ? this.getNodeById(flowId, parentNodeId) : undefined + + if (!flow || (parentNodeId && !parentNode)) return + + const nextNodeIndex: number = Math.max(0, ...flow.nodes?.nodes?.map(node => Number(node?.id?.match(/NodeFunction\/(\d+)$/)?.[1] ?? 0)) ?? [0]) + const nextNodeId: NodeFunction['id'] = `gid://sagittarius/NodeFunction/${nextNodeIndex + 1}` + const addingNode: NodeFunction = { + ...JSON.parse(JSON.stringify(nextNode)), + id: nextNodeId, + } + + if (parentNode && parentNode.nextNodeId) { + addingNode.nextNodeId = parentNode.nextNodeId + } else if (!parentNode && flow.startingNodeId) { + addingNode.nextNodeId = flow.startingNodeId + } + + flow.nodes?.nodes?.push(addingNode) + + if (parentNode) { + parentNode.nextNodeId = addingNode.id + } else { + flow.startingNodeId = addingNode.id + } - async deleteNodeById(flowId: Flow["id"], nodeId: NodeFunction["id"]): Promise { - await super.deleteNodeById(flowId, nodeId) + flow.editedAt = new Date().toISOString() + + this.set(index, new View(flow)) await this.syncFlow(flowId) } + async setSettingValue(flowId: FlowView['id'], settingIdentifier: Maybe, value: FlowSetting['value']): Promise { + const flow = this.getById(flowId) + const index = this.values().findIndex(f => f.id === flowId) + if (!flow) return + + flow.editedAt = new Date().toISOString() + + const setting: Maybe | undefined = flow.settings?.nodes?.find(s => s?.flowSettingIdentifier === settingIdentifier) + + if (!setting) { + const localSetting = { + flowSettingIdentifier: settingIdentifier, + value: null + } + localSetting.value = value + flow.settings!.nodes!.push(localSetting) + } else { + setting.value = value + } + + this.set(index, new View(flow)) + await this.syncFlow(flowId) + } + + async setParameterValue(flowId: FlowView['id'], nodeId: NodeFunction['id'], parameterId: NodeParameter['id'], value?: LiteralValue | ReferenceValue | NodeFunction): Promise { + const flow = this.getById(flowId) + const index = this.values().findIndex(f => f.id === flowId) + if (!flow) return + const node = this.getNodeById(flowId, nodeId) + if (!node) return + const parameter = node.parameters?.nodes?.find(p => p?.id === parameterId) + if (!parameter) return + this.removeParameterNode(flow, parameter) + if (value?.__typename === "NodeFunction") { + const nextNodeIndex: number = Math.max(0, ...flow.nodes?.nodes?.map(node => Number(node?.id?.match(/NodeFunction\/(\d+)$/)?.[1] ?? 0)) ?? [0]) + const addingIdValue: NodeFunction = { + ...value, + id: `gid://sagittarius/NodeFunction/${nextNodeIndex + 1}` + } + flow.nodes?.nodes?.push(addingIdValue) + parameter.value = { + id: `gid://sagittarius/NodeFunction/${nextNodeIndex + 1}`, + __typename: "NodeFunctionIdWrapper" + } as NodeFunctionIdWrapper + } else { + parameter.value = value as LiteralValue | ReferenceValue + } + + flow.editedAt = new Date().toISOString() - async setParameterValue(flowId: Flow["id"], nodeId: NodeFunction["id"], parameterId: NodeParameter["id"], value?: LiteralValue | ReferenceValue | NodeFunction): Promise { - await super.setParameterValue(flowId, nodeId, parameterId, value) + this.set(index, new View(flow)) await this.syncFlow(flowId) } - private async syncFlow(flowId: Flow["id"]) { + private async syncFlow(flowId: FlowView["id"]) { const alreadyQueued = this.flowUpdateQueue.includes(flowId) if (alreadyQueued) return Promise.resolve() @@ -124,7 +349,7 @@ export class FlowService extends DFlowReactiveService { }) this.flowUpdateQueue.splice(this.flowUpdateQueue.indexOf(flowId), 1) - }, 1000*60) // 1 min + }, 1000 * 60) // 1 min } async flowCreate(payload: NamespacesProjectsFlowsCreateInput): Promise { diff --git a/src/packages/ce/src/flow/services/Flow.view.ts b/src/packages/ce/src/flow/services/Flow.view.ts new file mode 100644 index 00000000..67b190b2 --- /dev/null +++ b/src/packages/ce/src/flow/services/Flow.view.ts @@ -0,0 +1,5 @@ +import {Flow as SagittariusFlow, Maybe, Scalars} from "@code0-tech/sagittarius-graphql-types"; + +export interface FlowView extends SagittariusFlow { + editedAt?: Maybe; +} \ 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..99e2f336 --- /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/Function.return.hook"; +import {DatatypeService} from "@edition/datatype/services/Datatype.service"; +import {FunctionService} from "@edition/function/services/Function.service"; +import {FunctionView} from "@edition/function/services/Function.view"; + +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: FunctionView, + 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: FunctionView, 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/flow/views/FlowFolderView.tsx b/src/packages/ce/src/flow/views/FlowFolderView.tsx index 47f6638d..de2844dd 100644 --- a/src/packages/ce/src/flow/views/FlowFolderView.tsx +++ b/src/packages/ce/src/flow/views/FlowFolderView.tsx @@ -3,10 +3,6 @@ import React from "react"; import { Button, - DFlowFolder, - DFlowFolderDeleteDialog, - DFlowFolderHandle, - DLayout, Flex, Text, toast, @@ -21,12 +17,15 @@ import {IconArrowsMaximize, IconArrowsMinimize, IconCircleDot, IconLayoutSidebar import {useParams, useRouter} from "next/navigation"; import {Flow, FlowType} from "@code0-tech/sagittarius-graphql-types"; import {FlowService} from "@edition/flow/services/Flow.service"; -import { - DFlowFolderContextMenuGroupData, - DFlowFolderContextMenuItemData -} from "@code0-tech/pictor/dist/components/d-flow-folder/DFlowFolderContextMenu"; 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/folder/FlowFolderComponent"; +import {FlowDeleteDialogComponent} from "@edition/flow/components/FlowDeleteDialogComponent"; +import { + FlowFolderContextMenuComponentGroupData, + FlowFolderContextMenuComponentItemData +} from "@edition/flow/components/folder/FlowFolderContextMenuComponent"; +import {Layout} from "@code0-tech/pictor/dist/components/layout/Layout"; export const FlowFolderView: React.FC = () => { @@ -40,13 +39,13 @@ export const FlowFolderView: React.FC = () => { const flowId: Flow['id'] = `gid://sagittarius/Flow/${flowIndex}` const [, startTransition] = React.useTransition() - const ref = React.useRef(null) + const ref = React.useRef(null) const [createDialogOpen, setCreateDialogOpen] = React.useState(false) const [flowTypeId, setFlowTypeId] = React.useState(undefined) const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false) - const [contextData, setContextData] = React.useState({ + const [contextData, setContextData] = React.useState({ flow: [], name: "", type: "folder" @@ -76,12 +75,12 @@ export const FlowFolderView: React.FC = () => { onOpenChange={(open) => setCreateDialogOpen(open)} flowTypeId={flowTypeId}/> - setDeleteDialogOpen(open)} - contextData={contextData} - onDelete={deleteFlow}/> + setDeleteDialogOpen(open)} + contextData={contextData} + onDelete={deleteFlow}/> - Explorer @@ -153,21 +152,21 @@ export const FlowFolderView: React.FC = () => { }> - { - const number = flow.id?.match(/Flow\/(\d+)$/)?.[1] - router.push(`/namespace/${namespaceIndex}/project/${projectIndex}/flow/${number}`) - }} - onCreate={flowTypeId => { - setCreateDialogOpen(true) - setFlowTypeId(flowTypeId) - }} - onDelete={contextData => { - setDeleteDialogOpen(true) - setContextData(contextData) - }} - namespaceId={`gid://sagittarius/Namespace/${namespaceIndex}`} - projectId={`gid://sagittarius/NamespaceProject/${projectIndex}`}/> - + { + const number = flow.id?.match(/Flow\/(\d+)$/)?.[1] + router.push(`/namespace/${namespaceIndex}/project/${projectIndex}/flow/${number}`) + }} + onCreate={flowTypeId => { + setCreateDialogOpen(true) + setFlowTypeId(flowTypeId) + }} + onDelete={contextData => { + setDeleteDialogOpen(true) + setContextData(contextData) + }} + namespaceId={`gid://sagittarius/Namespace/${namespaceIndex}`} + projectId={`gid://sagittarius/NamespaceProject/${projectIndex}`}/> + } \ No newline at end of file diff --git a/src/packages/ce/src/flowtype/services/FlowTypeService.ts b/src/packages/ce/src/flowtype/services/FlowType.service.ts similarity index 73% rename from src/packages/ce/src/flowtype/services/FlowTypeService.ts rename to src/packages/ce/src/flowtype/services/FlowType.service.ts index 15824221..66bf58d2 100644 --- a/src/packages/ce/src/flowtype/services/FlowTypeService.ts +++ b/src/packages/ce/src/flowtype/services/FlowType.service.ts @@ -1,10 +1,20 @@ -import {DFlowTypeDependencies, DFlowTypeReactiveService, FlowTypeView, ReactiveArrayStore} from "@code0-tech/pictor"; +import { + ReactiveArrayService, + ReactiveArrayStore +} from "@code0-tech/pictor"; import {GraphqlClient} from "@core/util/graphql-client"; -import {FlowType, Query} from "@code0-tech/sagittarius-graphql-types"; +import {FlowType, Namespace, NamespaceProject, Query, Runtime} from "@code0-tech/sagittarius-graphql-types"; import flowTypesQuery from "@edition/flowtype/services/queries/FlowTypes.query.graphql" import {View} from "@code0-tech/pictor/dist/utils/view"; +import {FlowTypeView} from "@edition/flowtype/services/FlowType.view"; -export class FlowTypeService extends DFlowTypeReactiveService { +export type FlowTypeDependencies = { + namespaceId?: Namespace['id'] + projectId?: NamespaceProject['id'] + runtimeId?: Runtime['id'] +} + +export class FlowTypeService extends ReactiveArrayService { private readonly client: GraphqlClient private i = 0 @@ -14,7 +24,7 @@ export class FlowTypeService extends DFlowTypeReactiveService { this.client = client } - values(dependencies?: DFlowTypeDependencies): FlowTypeView[] { + values(dependencies?: FlowTypeDependencies): FlowTypeView[] { const functions = super.values() if (!dependencies?.runtimeId) return functions @@ -61,4 +71,8 @@ export class FlowTypeService extends DFlowTypeReactiveService { return flowType !== undefined } + getById(id: FlowType['id'], dependencies?: FlowTypeDependencies): FlowTypeView | undefined { + return this.values(dependencies).find(value => value.id === id); + } + } \ No newline at end of file diff --git a/src/packages/ce/src/flowtype/services/FlowType.view.ts b/src/packages/ce/src/flowtype/services/FlowType.view.ts new file mode 100644 index 00000000..59960ef8 --- /dev/null +++ b/src/packages/ce/src/flowtype/services/FlowType.view.ts @@ -0,0 +1,126 @@ +import { + DataType, + FlowType, + FlowTypeSetting, + Maybe, Runtime, + Scalars, Translation, +} from "@code0-tech/sagittarius-graphql-types"; +import {DataTypeView} from "@edition/datatype/services/DataType.view"; + + +export class FlowTypeView { + + /** Name of the function */ + private readonly _aliases?: Maybe>; + /** Time when this FlowType was created */ + private readonly _createdAt?: Maybe; + /** Descriptions of the flow type */ + private readonly _descriptions?: Maybe>; + /** Display message of the function */ + private readonly _displayMessages?: Maybe>; + /** Editable status of the flow type */ + private readonly _editable?: Maybe; + /** Flow type settings of the flow type */ + private readonly _flowTypeSettings?: Maybe>; + /** Global ID of this FlowType */ + private readonly _id?: Maybe; + /** Identifier of the flow type */ + private readonly _identifier?: Maybe; + /** Input type of the flow type */ + private readonly _inputType?: Maybe; + /** Names of the flow type */ + private readonly _names?: Maybe>; + /** Return type of the flow type */ + private readonly _returnType?: Maybe; + /** Runtime of the flow type */ + private readonly _runtime?: Maybe; + /** Time when this FlowType was last updated */ + private readonly _updatedAt?: Maybe; + + + constructor(flowType: FlowType) { + this._aliases = flowType.aliases; + this._createdAt = flowType.createdAt; + this._descriptions = flowType.descriptions; + this._displayMessages = flowType.displayMessages; + this._editable = flowType.editable; + this._flowTypeSettings = flowType.flowTypeSettings; + this._id = flowType.id; + this._identifier = flowType.identifier; + this._inputType = flowType.inputType ? new DataTypeView(flowType.inputType).json : undefined; + this._names = flowType.names; + this._returnType = flowType.returnType ? new DataTypeView(flowType.returnType).json : undefined; + this._runtime = flowType.runtime; + this._updatedAt = flowType.updatedAt; + } + + get aliases(): Maybe> | undefined { + return this._aliases; + } + + get createdAt(): Maybe | undefined { + return this._createdAt; + } + + get descriptions(): Maybe> | undefined { + return this._descriptions; + } + + get displayMessages(): Maybe> | undefined { + return this._displayMessages; + } + + get editable(): Maybe | undefined { + return this._editable; + } + + get flowTypeSettings(): Maybe> | undefined { + return this._flowTypeSettings; + } + + get id(): Maybe | undefined { + return this._id; + } + + get identifier(): Maybe | undefined { + return this._identifier; + } + + get inputType(): Maybe | undefined { + return this._inputType; + } + + get names(): Maybe> | undefined { + return this._names; + } + + get returnType(): Maybe | undefined { + return this._returnType; + } + + get runtime(): Maybe | undefined { + return this._runtime; + } + + get updatedAt(): Maybe | undefined { + return this._updatedAt; + } + + json(): FlowType { + return { + aliases: this._aliases, + createdAt: this._createdAt, + descriptions: this._descriptions, + displayMessages: this._displayMessages, + editable: this._editable, + flowTypeSettings: this._flowTypeSettings, + id: this._id, + identifier: this._identifier, + inputType: this._inputType, + names: this._names, + returnType: this._returnType, + runtime: this._runtime, + updatedAt: this._updatedAt + } + } +} \ No newline at end of file diff --git a/src/packages/ce/src/function/components/files/FunctionFileDefaultComponent.tsx b/src/packages/ce/src/function/components/files/FunctionFileDefaultComponent.tsx new file mode 100644 index 00000000..a52f3047 --- /dev/null +++ b/src/packages/ce/src/function/components/files/FunctionFileDefaultComponent.tsx @@ -0,0 +1,154 @@ +import React from "react"; +import {Flex, InputSyntaxSegment, useForm, useService, useStore} from "@code0-tech/pictor"; +import { + Flow, + LiteralValue, + NodeFunction, + NodeParameterValue, + ReferenceValue, + Scalars +} 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 {FunctionService} from "@edition/function/services/Function.service"; +import {FlowService} from "@edition/flow/services/Flow.service"; +import {DataTypeInputComponent} from "@edition/datatype/components/inputs/DataTypeInputComponent"; +import {ParameterView} from "@edition/function/services/Function.view"; + +export interface FunctionFileDefaultComponentProps { + node: NodeFunction + flowId: Flow['id'] +} + +export const FunctionFileDefaultComponent: React.FC = (props) => { + + const {node, flowId} = props + + const functionService = useService(FunctionService) + const functionStore = useStore(FunctionService) + const flowService = useService(FlowService) + const flowStore = useStore(FlowService) + 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/files/FunctionFileTriggerComponent.tsx b/src/packages/ce/src/function/components/files/FunctionFileTriggerComponent.tsx new file mode 100644 index 00000000..ac5eb50a --- /dev/null +++ b/src/packages/ce/src/function/components/files/FunctionFileTriggerComponent.tsx @@ -0,0 +1,123 @@ +import React from "react"; +import { + Flex, + useService +} from "@code0-tech/pictor"; +import {Flow, LiteralValue, NodeParameterValue, Scalars} from "@code0-tech/sagittarius-graphql-types"; +import {FunctionSuggestion} from "@edition/function/components/suggestion/FunctionSuggestionComponent.view"; +import {useValueSuggestions} from "@edition/function/hooks/FunctionValueSuggestions.hook"; +import {useDataTypeSuggestions} from "@edition/function/hooks/FunctionDataTypeSuggestions.hook"; +import {toInputSuggestions} from "@edition/function/components/suggestion/FunctionSuggestionMenuComponent.util"; +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 {DataTypeTextInputComponent} from "@edition/datatype/components/inputs/text/DataTypeTextInputComponent"; +import {DataTypeTypeInputComponent} from "@edition/datatype/components/inputs/type/DataTypeTypeInputComponent"; + +export interface FunctionFileTriggerComponentProps { + instance: Flow +} + +export const FunctionFileTriggerComponent: React.FC = (props) => { + + const {instance} = props + + const flowTypeService = useService(FlowTypeService) + const flowService = useService(FlowService) + const dataTypeService = useService(DatatypeService) + 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/files/FunctionFilesComponent.tsx b/src/packages/ce/src/function/components/files/FunctionFilesComponent.tsx new file mode 100644 index 00000000..a1608fe8 --- /dev/null +++ b/src/packages/ce/src/function/components/files/FunctionFilesComponent.tsx @@ -0,0 +1,172 @@ +import React from "react"; +import {Flow, Namespace, NamespaceProject} from "@code0-tech/sagittarius-graphql-types"; +import { + Button, + Menu, MenuContent, MenuItem, MenuLabel, MenuPortal, MenuSeparator, + MenuTrigger, + useService, + useStore +} from "@code0-tech/pictor"; +import {FileTabsService} from "@code0-tech/pictor/dist/components/file-tabs/FileTabs.service"; +import {FileTabsView} from "@code0-tech/pictor/dist/components/file-tabs/FileTabs.view"; +import { + FileTabs, + FileTabsContent, + FileTabsList, + FileTabsTrigger +} from "@code0-tech/pictor/dist/components/file-tabs/FileTabs"; +import {ButtonGroup} from "@code0-tech/pictor/dist/components/button-group/ButtonGroup"; +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 FunctionFilesComponentProps { + flowId: Flow['id'] + namespaceId: Namespace['id'] + projectId: NamespaceProject['id'] +} + +export const FunctionFilesComponent: React.FC = (props) => { + + const {flowId, namespaceId, projectId} = props + + const fileTabsService = useService(FileTabsService) + const fileTabsStore = useStore(FileTabsService) + const flowService = useService(FlowService) + const flowStore = useStore(FlowService) + const flowTypeService = useService(FlowTypeService) + const flowTypeStore = useStore(FlowTypeService) + 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/components/nodes/FunctionNodeComponent.style.scss b/src/packages/ce/src/function/components/nodes/FunctionNodeComponent.style.scss new file mode 100644 index 00000000..763509d5 --- /dev/null +++ b/src/packages/ce/src/function/components/nodes/FunctionNodeComponent.style.scss @@ -0,0 +1,32 @@ +@use "@core/style/helpers"; +@use "@core/style/box"; +@use "@core/style/variables"; + +.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/nodes/FunctionNodeComponent.ts b/src/packages/ce/src/function/components/nodes/FunctionNodeComponent.ts new file mode 100644 index 00000000..bfc1bb97 --- /dev/null +++ b/src/packages/ce/src/function/components/nodes/FunctionNodeComponent.ts @@ -0,0 +1,11 @@ +import {Flow, NodeFunction} from "@code0-tech/sagittarius-graphql-types"; +import {Code0Component} from "@code0-tech/pictor"; + +export interface FunctionNodeComponentProps 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/nodes/FunctionNodeDefaultComponent.tsx b/src/packages/ce/src/function/components/nodes/FunctionNodeDefaultComponent.tsx new file mode 100644 index 00000000..7dc1d40e --- /dev/null +++ b/src/packages/ce/src/function/components/nodes/FunctionNodeDefaultComponent.tsx @@ -0,0 +1,170 @@ +import {Handle, Node, NodeProps, Position, useReactFlow, useStore} from "@xyflow/react"; +import React, {CSSProperties, memo} from "react"; +import "./FunctionNodeComponent.style.scss"; +import {FunctionNodeComponentProps} from "./FunctionNodeComponent"; +import {FileTabsService} from "@code0-tech/pictor/dist/components/file-tabs/FileTabs.service"; +import { + Badge, Card, + Flex, Text, underlineBySeverity, + useService, + useStore as usePictorStore +} from "@code0-tech/pictor"; +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 {LiteralBadgeComponent} from "@edition/datatype/components/badges/LiteralBadgeComponent"; +import {ReferenceBadgeComponent} from "@edition/datatype/components/badges/ReferenceBadgeComponent"; +import {NodeBadgeComponent} from "@edition/datatype/components/badges/NodeBadgeComponent"; +import {FunctionFileDefaultComponent} from "@edition/function/components/files/FunctionFileDefaultComponent"; + +export type FunctionNodeDefaultComponentProps = NodeProps> + +export const FunctionNodeDefaultComponent: 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(FlowService) + const flowStore = usePictorStore(FlowService) + const functionService = useService(FunctionService) + const functionStore = usePictorStore(FunctionService) + + 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/nodes/FunctionNodeGroupComponent.tsx b/src/packages/ce/src/function/components/nodes/FunctionNodeGroupComponent.tsx new file mode 100644 index 00000000..643cbddf --- /dev/null +++ b/src/packages/ce/src/function/components/nodes/FunctionNodeGroupComponent.tsx @@ -0,0 +1,86 @@ +import React, {memo} from "react"; +import {Handle, Node, NodeProps, Position} from "@xyflow/react"; +import {FunctionNodeComponentProps} from "./FunctionNodeComponent"; +import {Card} from "@code0-tech/pictor"; + +export type FunctionNodeGroupComponentProps = NodeProps> + +export const FunctionNodeGroupComponent: 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/nodes/FunctionNodeTriggerComponent.tsx b/src/packages/ce/src/function/components/nodes/FunctionNodeTriggerComponent.tsx new file mode 100644 index 00000000..1b27023e --- /dev/null +++ b/src/packages/ce/src/function/components/nodes/FunctionNodeTriggerComponent.tsx @@ -0,0 +1,89 @@ +import React, {memo} from "react"; +import {Handle, Node, NodeProps, Position, useReactFlow, useStore} from "@xyflow/react"; +import {Badge, Card, Flex, Text, useService, useStore as usePictorStore} from "@code0-tech/pictor"; +import {FunctionNodeComponentProps} from "@edition/function/components/nodes/FunctionNodeComponent"; +import {FileTabsService} from "@code0-tech/pictor/dist/components/file-tabs/FileTabs.service"; +import {FlowTypeService} from "@edition/flowtype/services/FlowType.service"; +import {FlowService} from "@edition/flow/services/Flow.service"; +import {IconBolt} from "@tabler/icons-react"; +import {FunctionFileTriggerComponent} from "@edition/function/components/files/FunctionFileTriggerComponent"; + + +export type FunctionNodeTriggerComponentProps = NodeProps> + +export const FunctionNodeTriggerComponent: React.FC = memo((props) => { + + const {data, id} = props + const fileTabsService = useService(FileTabsService) + const flowInstance = useReactFlow() + const flowTypeService = useService(FlowTypeService) + const flowTypeStore = usePictorStore(FlowTypeService) + const flowService = useService(FlowService) + const flowStore = usePictorStore(FlowService) + + 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/suggestion/FunctionSuggestionComponent.view.ts b/src/packages/ce/src/function/components/suggestion/FunctionSuggestionComponent.view.ts new file mode 100644 index 00000000..58ae6998 --- /dev/null +++ b/src/packages/ce/src/function/components/suggestion/FunctionSuggestionComponent.view.ts @@ -0,0 +1,21 @@ +import type { + LiteralValue, + NodeFunction, + ReferenceValue +} from "@code0-tech/sagittarius-graphql-types"; + +export enum FunctionSuggestionType { + REF_OBJECT, + VALUE, + FUNCTION, + FUNCTION_COMBINATION, + DATA_TYPE, +} + +export interface FunctionSuggestion { + + displayText: string[] + path: number[] + value: LiteralValue | ReferenceValue | NodeFunction + type: FunctionSuggestionType +} \ No newline at end of file diff --git a/src/packages/ce/src/function/components/suggestion/FunctionSuggestionMenuComponent.tsx b/src/packages/ce/src/function/components/suggestion/FunctionSuggestionMenuComponent.tsx new file mode 100644 index 00000000..b314ba03 --- /dev/null +++ b/src/packages/ce/src/function/components/suggestion/FunctionSuggestionMenuComponent.tsx @@ -0,0 +1,75 @@ +import React from "react"; +import {FunctionSuggestion} from "./FunctionSuggestionComponent.view"; +import {toInputSuggestions} from "./FunctionSuggestionMenuComponent.util"; +import {FunctionSuggestionSearchBarComponent} from "./FunctionSuggestionSearchBarComponent"; +import { + Card, + InputSuggestionMenuContent, InputSuggestionMenuContentItems, + InputSuggestionMenuContentItemsHandle, + Menu, + MenuPortal, + MenuTrigger +} from "@code0-tech/pictor"; + +export interface FunctionSuggestionMenuComponentProps { + triggerContent: React.ReactNode + suggestions?: FunctionSuggestion[] + onSuggestionSelect?: (suggestion: FunctionSuggestion) => void +} + +export const FunctionSuggestionMenuComponent: 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 FunctionSuggestion) + }} + /> + + + + + + +} \ No newline at end of file diff --git a/src/packages/ce/src/function/components/suggestion/FunctionSuggestionMenuComponent.util.tsx b/src/packages/ce/src/function/components/suggestion/FunctionSuggestionMenuComponent.util.tsx new file mode 100644 index 00000000..ddaed372 --- /dev/null +++ b/src/packages/ce/src/function/components/suggestion/FunctionSuggestionMenuComponent.util.tsx @@ -0,0 +1,58 @@ +import {FunctionSuggestion, FunctionSuggestionType} from "./FunctionSuggestionComponent.view"; +import React from "react"; +import {IconCircleDot, IconCirclesRelation, IconFileFunctionFilled} from "@tabler/icons-react"; +import {InputSuggestion, Text} from "@code0-tech/pictor"; + +export const toInputSuggestions = (suggestions: FunctionSuggestion[]): InputSuggestion[] => { + + const staticGroupLabels: Partial> = { + [FunctionSuggestionType.VALUE]: "Values", + [FunctionSuggestionType.REF_OBJECT]: "Variables", + [FunctionSuggestionType.DATA_TYPE]: "Datatypes", + } + + return suggestions.map(suggestion => { + + const iconMap: Record = { + [FunctionSuggestionType.FUNCTION]: , + [FunctionSuggestionType.FUNCTION_COMBINATION]: , + [FunctionSuggestionType.REF_OBJECT]: , + [FunctionSuggestionType.VALUE]: , + [FunctionSuggestionType.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 === FunctionSuggestionType.FUNCTION || suggestion.type === FunctionSuggestionType.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/suggestion/FunctionSuggestionMenuFooterComponent.tsx b/src/packages/ce/src/function/components/suggestion/FunctionSuggestionMenuFooterComponent.tsx new file mode 100644 index 00000000..41e6f8be --- /dev/null +++ b/src/packages/ce/src/function/components/suggestion/FunctionSuggestionMenuFooterComponent.tsx @@ -0,0 +1,50 @@ +"use client"; + +import React from "react"; +import { + IconArrowsShuffle, + IconBulb, + IconCircleDot, + IconCirclesRelation, + IconCornerDownLeft, + IconFileFunctionFilled +} from "@tabler/icons-react"; +import {Flex, MenuLabel, Text, Tooltip, TooltipContent, TooltipPortal, TooltipTrigger} from "@code0-tech/pictor"; + +export const FunctionSuggestionMenuFooterComponent: 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/suggestion/FunctionSuggestionSearchBarComponent.tsx b/src/packages/ce/src/function/components/suggestion/FunctionSuggestionSearchBarComponent.tsx new file mode 100644 index 00000000..bc641edd --- /dev/null +++ b/src/packages/ce/src/function/components/suggestion/FunctionSuggestionSearchBarComponent.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import {FunctionSuggestionSearchInputComponent} from "./FunctionSuggestionSearchInputComponent"; +import {IconSearch} from "@tabler/icons-react"; +import {Code0Component} from "@code0-tech/pictor"; + +export interface FunctionSuggestionSearchBarProps extends Code0Component { + onType: (event: React.KeyboardEvent) => void +} + +export const FunctionSuggestionSearchBarComponent: 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/suggestion/FunctionSuggestionSearchInput.style.scss b/src/packages/ce/src/function/components/suggestion/FunctionSuggestionSearchInput.style.scss new file mode 100644 index 00000000..46def7e6 --- /dev/null +++ b/src/packages/ce/src/function/components/suggestion/FunctionSuggestionSearchInput.style.scss @@ -0,0 +1,18 @@ +@use "@core/style/helpers"; +@use "@core/style/box"; +@use "@core/style/variables"; + +.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/suggestion/FunctionSuggestionSearchInputComponent.tsx b/src/packages/ce/src/function/components/suggestion/FunctionSuggestionSearchInputComponent.tsx new file mode 100644 index 00000000..a51548e9 --- /dev/null +++ b/src/packages/ce/src/function/components/suggestion/FunctionSuggestionSearchInputComponent.tsx @@ -0,0 +1,39 @@ +import React, {RefObject} from "react"; +import {IconX} from "@tabler/icons-react"; +import "./FunctionSuggestionSearchInput.style.scss" +import {clearInputElement} from "@code0-tech/pictor/dist/components/form/Input.utils"; +import {Button, Input, InputProps} from "@code0-tech/pictor"; + +interface FunctionSuggestionSearchInputComponentProps extends Omit, "wrapperComponent" | "type"> { + //defaults to false + clearable?: boolean +} + +//@ts-ignore +export const FunctionSuggestionSearchInputComponent: 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/hooks/Function.return.hook.ts b/src/packages/ce/src/function/hooks/Function.return.hook.ts new file mode 100644 index 00000000..832b3a4f --- /dev/null +++ b/src/packages/ce/src/function/hooks/Function.return.hook.ts @@ -0,0 +1,19 @@ +import type {DataTypeIdentifier, NodeParameterValue} from "@code0-tech/sagittarius-graphql-types"; +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 {FunctionView} from "@edition/function/services/Function.view"; + +export const useReturnType = ( + func: FunctionView, + values: NodeParameterValue[], + dataTypeService: DatatypeService, + functionService: FunctionService, +): 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/FunctionDataTypeSuggestions.hook.tsx b/src/packages/ce/src/function/hooks/FunctionDataTypeSuggestions.hook.tsx new file mode 100644 index 00000000..a072b467 --- /dev/null +++ b/src/packages/ce/src/function/hooks/FunctionDataTypeSuggestions.hook.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import type {DataTypeIdentifier} from "@code0-tech/sagittarius-graphql-types"; +import {useService, useStore} from "@code0-tech/pictor"; +import {FunctionSuggestion, FunctionSuggestionType} from "@edition/function/components/suggestion/FunctionSuggestionComponent.view"; +import {DatatypeService} from "@edition/datatype/services/Datatype.service"; + +export const useDataTypeSuggestions = (dataTypeIdentifier?: DataTypeIdentifier): FunctionSuggestion[] => { + const dataTypeService = useService(DatatypeService) + const dataTypeStore = useStore(DatatypeService) + + 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: FunctionSuggestionType.DATA_TYPE, + displayText: [nextDataType.name!![0]?.content!], + /*@ts-ignore*/ + value: nextDataType.json, + })) + }, [dataType, dataTypeService, dataTypeStore]) +} diff --git a/src/packages/ce/src/function/hooks/FunctionNode.return.hook.ts b/src/packages/ce/src/function/hooks/FunctionNode.return.hook.ts new file mode 100644 index 00000000..2d5ef091 --- /dev/null +++ b/src/packages/ce/src/function/hooks/FunctionNode.return.hook.ts @@ -0,0 +1,43 @@ +import type {DataTypeIdentifier, Flow, NodeFunction} from "@code0-tech/sagittarius-graphql-types"; +import React from "react"; +import {useService, useStore} from "@code0-tech/pictor"; +import {replaceGenericKeysInType, resolveGenericKeys} from "@edition/flow/utils/generics"; +import {FlowService} from "@edition/flow/services/Flow.service"; +import {FunctionService} from "@edition/function/services/Function.service"; +import {DatatypeService} from "@edition/datatype/services/Datatype.service"; + +export const useReturnTypes = ( + flowId: Flow['id'] +): Map => { + + const flowService = useService(FlowService) + const flowStore = useStore(FlowService) + const functionService = useService(FunctionService) + const functionStore = useStore(FunctionService) + const dataTypeService = useService(DatatypeService) + + return React.useMemo(() => { + const flow = flowService.getById(flowId) + return getReturnTypesForFlow(flow!, functionService, dataTypeService) + }, [flowId, flowStore, functionStore, dataTypeService]) + +} + +export function getReturnTypesForFlow( + flow: Flow, + functionService: FunctionService, + dataTypeService: DatatypeService +): 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/FunctionNodeReference.return.hook.ts b/src/packages/ce/src/function/hooks/FunctionNodeReference.return.hook.ts new file mode 100644 index 00000000..969e975d --- /dev/null +++ b/src/packages/ce/src/function/hooks/FunctionNodeReference.return.hook.ts @@ -0,0 +1,143 @@ +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 { + replaceGenericKeysInType, + resolveGenericKeys, + resolveType, + targetForGenericKey +} from "@edition/flow/utils/generics"; +import {FunctionView} from "@edition/function/services/Function.view"; + +export const getReferenceType = ( + reference: ReferenceValue, + dataTypeService: DatatypeService, + functionService: FunctionService, + functionDefinition?: FunctionView, + 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/FunctionNodeSuggestions.hook.tsx b/src/packages/ce/src/function/hooks/FunctionNodeSuggestions.hook.tsx new file mode 100644 index 00000000..01884a2c --- /dev/null +++ b/src/packages/ce/src/function/hooks/FunctionNodeSuggestions.hook.tsx @@ -0,0 +1,81 @@ +import React from "react"; +import type { + DataTypeIdentifier, + LiteralValue, + NodeFunction, + ReferenceValue +} from "@code0-tech/sagittarius-graphql-types"; +import {FunctionSuggestion, FunctionSuggestionType} from "@edition/function/components/suggestion/FunctionSuggestionComponent.view"; +import {useService, useStore} from "@code0-tech/pictor"; +import {isMatchingType, replaceGenericsAndSortType, resolveType} from "@edition/flow/utils/generics"; +import {DatatypeService} from "@edition/datatype/services/Datatype.service"; +import {FunctionService} from "@edition/function/services/Function.service"; + +export const useFunctionSuggestions = ( + dataTypeIdentifier?: DataTypeIdentifier, + genericKeys: string[] = [] +): FunctionSuggestion[] => { + const dataTypeService = useService(DatatypeService) + const functionService = useService(FunctionService) + const dataTypeStore = useStore(DatatypeService) + const functionStore = useStore(FunctionService) + + 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: FunctionSuggestionType.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/FunctionReferenceSuggestions.hook.tsx b/src/packages/ce/src/function/hooks/FunctionReferenceSuggestions.hook.tsx new file mode 100644 index 00000000..2dd48578 --- /dev/null +++ b/src/packages/ce/src/function/hooks/FunctionReferenceSuggestions.hook.tsx @@ -0,0 +1,337 @@ +import React from "react"; +import { + DataType, + DataTypeIdentifier, + DataTypeRulesContainsKeyConfig, + DataTypeRulesInputTypesConfig, + Flow, + Maybe, + NodeFunction, + NodeFunctionIdWrapper, + NodeParameterValue, + ReferenceValue +} from "@code0-tech/sagittarius-graphql-types"; +import {useService, useStore} from "@code0-tech/pictor"; +import {FunctionSuggestion, FunctionSuggestionType} from "@edition/function/components/suggestion/FunctionSuggestionComponent.view"; +import { + isMatchingType, + replaceGenericKeysInType, + replaceGenericsAndSortType, + resolveGenericKeys, + resolveType, + targetForGenericKey +} from "@edition/flow/utils/generics"; +import {useReturnType} from "@edition/function/hooks/Function.return.hook"; +import {DatatypeService} from "@edition/datatype/services/Datatype.service"; +import {FlowService} from "@edition/flow/services/Flow.service"; +import {FunctionService} from "@edition/function/services/Function.service"; +import {FlowTypeService} from "@edition/flowtype/services/FlowType.service"; + +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[] = [] +): FunctionSuggestion[] => { + const dataTypeService = useService(DatatypeService) + const dataTypeStore = useStore(DatatypeService) + const flowService = useService(FlowService) + const flowStore = useStore(FlowService) + + 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: FunctionSuggestionType.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(DatatypeService) + const dataTypeStore = useStore(DatatypeService) + const functionService = useService(FunctionService) + const flowService = useService(FlowService) + const flowStore = useStore(FlowService) + const flowTypeService = useService(FlowTypeService) + const flowTypeStore = useStore(FlowTypeService) + + 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: DatatypeService): 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(DatatypeService); + const flowService = useService(FlowService); + const functionService = useService(FunctionService); + + const flowStore = useStore(FlowService); + const functionStore = useStore(FunctionService); + const dataTypeStore = useStore(DatatypeService); + + 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/FunctionSuggestion.hook.tsx b/src/packages/ce/src/function/hooks/FunctionSuggestion.hook.tsx new file mode 100644 index 00000000..7816a292 --- /dev/null +++ b/src/packages/ce/src/function/hooks/FunctionSuggestion.hook.tsx @@ -0,0 +1,49 @@ +import React from "react"; +import type {Flow, NodeFunction, NodeParameter,} from "@code0-tech/sagittarius-graphql-types"; +import {useValueSuggestions} from "./FunctionValueSuggestions.hook"; +import {useReferenceSuggestions} from "./FunctionReferenceSuggestions.hook"; +import {useFunctionSuggestions} from "./FunctionNodeSuggestions.hook"; +import {useDataTypeSuggestions} from "./FunctionDataTypeSuggestions.hook"; +import {useService, useStore} from "@code0-tech/pictor"; +import {FunctionSuggestion} from "@edition/function/components/suggestion/FunctionSuggestionComponent.view"; +import {FunctionService} from "@edition/function/services/Function.service"; +import {FlowService} from "@edition/flow/services/Flow.service"; + +//TODO: deep type search +//TODO: calculate FUNCTION_COMBINATION deepness max 2 + +export const useSuggestions = ( + flowId: Flow['id'], + nodeId?: NodeFunction['id'], + parameterId?: NodeParameter['id'] +): FunctionSuggestion[] => { + + const functionService = useService(FunctionService) + const functionStore = useStore(FunctionService) + const flowService = useService(FlowService) + const flowStore = useStore(FlowService) + + 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/FunctionValueSuggestions.hook.tsx b/src/packages/ce/src/function/hooks/FunctionValueSuggestions.hook.tsx new file mode 100644 index 00000000..19effafd --- /dev/null +++ b/src/packages/ce/src/function/hooks/FunctionValueSuggestions.hook.tsx @@ -0,0 +1,57 @@ +import React from "react"; +import type { + DataTypeIdentifier, + DataTypeRulesItemOfCollectionConfig, + DataTypeRulesNumberRangeConfig +} from "@code0-tech/sagittarius-graphql-types"; +import {FunctionSuggestion, FunctionSuggestionType} from "@edition/function/components/suggestion/FunctionSuggestionComponent.view"; +import {useService, useStore} from "@code0-tech/pictor"; +import {DatatypeService} from "@edition/datatype/services/Datatype.service"; + +export const useValueSuggestions = (dataTypeIdentifier?: DataTypeIdentifier): FunctionSuggestion[] => { + const dataTypeService = useService(DatatypeService) + const dataTypeStore = useStore(DatatypeService) + + const dataType = React.useMemo(() => ( + dataTypeIdentifier ? dataTypeService?.getDataType(dataTypeIdentifier) : undefined + ), [dataTypeIdentifier, dataTypeService, dataTypeStore]) + + return React.useMemo(() => { + if (!dataType) return [] + + const suggestions: FunctionSuggestion[] = [] + dataType.rules?.nodes?.forEach(rule => { + if (rule?.variant === "ITEM_OF_COLLECTION") { + (rule.config as DataTypeRulesItemOfCollectionConfig)!!.items?.forEach(value => { + suggestions.push({ + path: [], + type: FunctionSuggestionType.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: FunctionSuggestionType.VALUE, + displayText: [i.toString() ?? ""], + value: { + __typename: "LiteralValue", + value: String(i) + }, + }) + } + } + }) + + return suggestions + }, [dataType]) +} diff --git a/src/packages/ce/src/function/services/Function.service.ts b/src/packages/ce/src/function/services/Function.service.ts index c88beb79..94038fd4 100644 --- a/src/packages/ce/src/function/services/Function.service.ts +++ b/src/packages/ce/src/function/services/Function.service.ts @@ -1,25 +1,30 @@ import { - DFlowFunctionDependencies, - DFlowFunctionReactiveService, - FunctionDefinitionView, + ReactiveArrayService, ReactiveArrayStore } from "@code0-tech/pictor"; import {GraphqlClient} from "@core/util/graphql-client"; -import {FunctionDefinition, Query} from "@code0-tech/sagittarius-graphql-types"; +import {FunctionDefinition, Namespace, NamespaceProject, Query, Runtime} from "@code0-tech/sagittarius-graphql-types"; import functionsQuery from "@edition/function/services/queries/Functions.query.graphql"; import {View} from "@code0-tech/pictor/dist/utils/view"; +import {FunctionView} from "@edition/function/services/Function.view"; -export class FunctionService extends DFlowFunctionReactiveService { +export type FunctionDependencies = { + namespaceId: Namespace['id'] + projectId: NamespaceProject['id'] + runtimeId: Runtime['id'] +} + +export class FunctionService extends ReactiveArrayService { private readonly client: GraphqlClient private i = 0 - constructor(client: GraphqlClient, store: ReactiveArrayStore>) { + constructor(client: GraphqlClient, store: ReactiveArrayStore>) { super(store) this.client = client } - values(dependencies?: DFlowFunctionDependencies): FunctionDefinitionView[] { + values(dependencies?: FunctionDependencies): FunctionView[] { const functions = super.values() if (!dependencies?.namespaceId || !dependencies.projectId || !dependencies.runtimeId) return functions @@ -48,7 +53,7 @@ export class FunctionService extends DFlowFunctionReactiveService { const nodes = res.data?.namespace?.project?.primaryRuntime?.functionDefinitions?.nodes ?? [] nodes.forEach(functionD => { if (functionD && !this.hasById(functionD.id)) { - this.set(this.i++, new View(new FunctionDefinitionView(functionD))) + this.set(this.i++, new View(new FunctionView(functionD))) } }) }) @@ -62,4 +67,8 @@ export class FunctionService extends DFlowFunctionReactiveService { return functionD !== undefined } + getById(id: FunctionDefinition['id'], dependencies?: FunctionDependencies): FunctionView | undefined { + return this.values(dependencies).find(functionDefinition => functionDefinition.id === id) + } + } \ No newline at end of file diff --git a/src/packages/ce/src/function/services/Function.view.ts b/src/packages/ce/src/function/services/Function.view.ts new file mode 100644 index 00000000..d82a15da --- /dev/null +++ b/src/packages/ce/src/function/services/Function.view.ts @@ -0,0 +1,234 @@ +import { + DataTypeIdentifier, DataTypeIdentifierConnection, + FunctionDefinition, + Maybe, ParameterDefinition, + RuntimeFunctionDefinition, + Scalars, Translation, +} from "@code0-tech/sagittarius-graphql-types"; +import { + attachDataTypeIdentifiers, + resolveDataTypeIdentifiers +} from "@edition/flow/components/builder/FlowBuilderComponent.util"; + +export class FunctionView { + + /** Name of the function */ + private readonly _aliases?: Maybe>; + /** Time when this FunctionDefinition was created */ + private readonly _createdAt?: Maybe; + /** All data type identifiers used within this Node Function */ + private readonly _dataTypeIdentifiers?: Maybe; + /** Deprecation message of the function */ + private readonly _deprecationMessages?: Maybe>; + /** Description of the function */ + private readonly _descriptions?: Maybe>; + /** Display message of the function */ + private readonly _displayMessages?: Maybe>; + /** Documentation of the function */ + private readonly _documentations?: Maybe>; + /** Generic keys of the function */ + private readonly _genericKeys?: Maybe>; + /** Global ID of this FunctionDefinition */ + private readonly _id?: Maybe; + /** Identifier of the function */ + private readonly _identifier?: Maybe; + /** Name of the function */ + private readonly _names?: Maybe>; + /** Parameters of the function */ + private readonly _parameterDefinitions?: Maybe; + /** Return type of the function */ + private readonly _returnType?: Maybe; + /** Runtime function definition */ + private readonly _runtimeFunctionDefinition?: Maybe; + /** Indicates if the function can throw an error */ + private readonly _throwsError?: Maybe; + /** Time when this FunctionDefinition was last updated */ + private readonly _updatedAt?: Maybe; + + constructor(object: FunctionDefinition) { + + const dataTypeIdentifiers = resolveDataTypeIdentifiers((object.dataTypeIdentifiers?.nodes ?? []) as DataTypeIdentifier[]) + + this._aliases = object.aliases; + this._createdAt = object.createdAt; + this._dataTypeIdentifiers = object.dataTypeIdentifiers; + this._deprecationMessages = object.deprecationMessages; + this._descriptions = object.descriptions; + this._displayMessages = object.displayMessages; + this._documentations = object.documentations; + this._genericKeys = object.genericKeys; + this._id = object.id; + this._identifier = object.identifier; + this._names = object.names; + this._parameterDefinitions = object.parameterDefinitions?.nodes?.map(definition => new ParameterView(definition!!, dataTypeIdentifiers)) ?? undefined; + this._returnType = attachDataTypeIdentifiers(dataTypeIdentifiers, object.returnType); + this._runtimeFunctionDefinition = object.runtimeFunctionDefinition; + this._throwsError = object.throwsError; + this._updatedAt = object.updatedAt; + } + + + get aliases(): Maybe> | undefined { + return this._aliases; + } + + get createdAt(): Maybe | undefined { + return this._createdAt; + } + + get dataTypeIdentifiers(): Maybe | undefined { + return this._dataTypeIdentifiers; + } + + get deprecationMessages(): Maybe> | undefined { + return this._deprecationMessages; + } + + get descriptions(): Maybe> | undefined { + return this._descriptions; + } + + get displayMessages(): Maybe> | undefined { + return this._displayMessages; + } + + get documentations(): Maybe> | undefined { + return this._documentations; + } + + get genericKeys(): Maybe> | undefined { + return this._genericKeys; + } + + get id(): Maybe | undefined { + return this._id; + } + + get identifier(): Maybe | undefined { + return this._identifier; + } + + get names(): Maybe> | undefined { + return this._names; + } + + get parameterDefinitions(): Maybe | undefined { + return this._parameterDefinitions; + } + + get returnType(): Maybe | undefined { + return this._returnType; + } + + get runtimeFunctionDefinition(): Maybe | undefined { + return this._runtimeFunctionDefinition; + } + + get throwsError(): Maybe | undefined { + return this._throwsError; + } + + get updatedAt(): Maybe | undefined { + return this._updatedAt; + } + + json(): FunctionDefinition { + return { + aliases: this._aliases, + createdAt: this._createdAt, + deprecationMessages: this._deprecationMessages, + displayMessages: this._displayMessages, + descriptions: this._descriptions, + documentations: this._documentations, + genericKeys: this._genericKeys, + id: this._id, + identifier: this._identifier, + names: this._names, + parameterDefinitions: this._parameterDefinitions ? { + nodes: this._parameterDefinitions.map(definitionView => definitionView.json()!!) + } : undefined, + returnType: this._returnType, + runtimeFunctionDefinition: this._runtimeFunctionDefinition, + throwsError: this._throwsError, + updatedAt: this._updatedAt, + dataTypeIdentifiers: this._dataTypeIdentifiers + } + } +} + +export class ParameterView { + + /** Time when this ParameterDefinition was created */ + private readonly _createdAt?: Maybe; + /** Data type of the parameter */ + private readonly _dataTypeIdentifier?: Maybe; + /** Description of the parameter */ + private readonly _descriptions?: Maybe>; + /** Documentation of the parameter */ + private readonly _documentations?: Maybe>; + /** Global ID of this ParameterDefinition */ + private readonly _id?: Maybe; + /** Identifier of the parameter */ + private readonly _identifier?: Maybe; + /** Name of the parameter */ + private readonly _names?: Maybe>; + /** Time when this ParameterDefinition was last updated */ + private readonly _updatedAt?: Maybe; + + constructor(object: ParameterDefinition, dataTypeIdentifiers: DataTypeIdentifier[]) { + this._createdAt = object.createdAt; + this._dataTypeIdentifier = attachDataTypeIdentifiers(dataTypeIdentifiers, object.dataTypeIdentifier); + this._descriptions = object.descriptions; + this._documentations = object.documentations; + this._id = object.id; + this._identifier = object.identifier; + this._names = object.names; + this._updatedAt = object.updatedAt; + } + + + get createdAt(): Maybe | undefined { + return this._createdAt; + } + + get dataTypeIdentifier(): Maybe | undefined { + return this._dataTypeIdentifier; + } + + get descriptions(): Maybe> | undefined { + return this._descriptions; + } + + get documentations(): Maybe> | undefined { + return this._documentations; + } + + get id(): Maybe | undefined { + return this._id; + } + + get identifier(): Maybe | undefined { + return this._identifier; + } + + get names(): Maybe> | undefined { + return this._names; + } + + get updatedAt(): Maybe | undefined { + return this._updatedAt; + } + + json(): ParameterDefinition { + return { + createdAt: this._createdAt, + dataTypeIdentifier: this._dataTypeIdentifier, + descriptions: this._descriptions, + documentations: this._documentations, + id: this._id, + names: this._names, + updatedAt: this._updatedAt + } + } +} + 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/member/pages/MemberAddPage.tsx b/src/packages/ce/src/member/pages/MemberAddPage.tsx index 980136ea..a1ffa1be 100644 --- a/src/packages/ce/src/member/pages/MemberAddPage.tsx +++ b/src/packages/ce/src/member/pages/MemberAddPage.tsx @@ -1,24 +1,15 @@ "use client" import React from "react"; -import { - Button, - Col, - DUserInput, - DUserView, - Flex, - Spacing, - Text, - useForm, - useService, - useStore -} from "@code0-tech/pictor"; +import {Button, Col, Flex, Spacing, Text, useForm, useService, useStore} from "@code0-tech/pictor"; import {MemberService} from "@edition/member/services/Member.service"; import {useParams, useRouter} from "next/navigation"; import {Namespace} from "@code0-tech/sagittarius-graphql-types"; import Link from "next/link"; import {UserService} from "@edition/user/services/User.service"; import {InputSyntaxSegment} from "@code0-tech/pictor/dist/components/form/Input.syntax.hook"; +import {UserInputComponent} from "@edition/user/components/UserInputComponent"; +import {UserView} from "@edition/user/services/User.view"; export const MemberAddPage: React.FC = () => { @@ -36,7 +27,7 @@ export const MemberAddPage: React.FC = () => { const members = React.useMemo(() => memberService.values({namespaceId: namespaceId}), [memberStore, userStore]) const formInitialValues = React.useMemo(() => ({users: null}), []) const filteredUsers = React.useMemo(() => { - return (user: DUserView) => { + return (user: UserView) => { return !members.find(m => m.user?.id === user.id) } }, [members]) @@ -83,11 +74,11 @@ export const MemberAddPage: React.FC = () => { {/*@ts-ignore*/} - + diff --git a/src/packages/ce/src/member/services/Member.service.ts b/src/packages/ce/src/member/services/Member.service.ts index 4838c579..fd679445 100644 --- a/src/packages/ce/src/member/services/Member.service.ts +++ b/src/packages/ce/src/member/services/Member.service.ts @@ -1,11 +1,9 @@ import { - DMemberDependencies, - DNamespaceMemberReactiveService, - DNamespaceMemberView, + ReactiveArrayService, ReactiveArrayStore } from "@code0-tech/pictor" import { - Mutation, + Mutation, Namespace, NamespaceMember, NamespacesMembersAssignRolesInput, NamespacesMembersAssignRolesPayload, @@ -13,7 +11,7 @@ import { NamespacesMembersDeletePayload, NamespacesMembersInviteInput, NamespacesMembersInvitePayload, - Query + Query, User } from "@code0-tech/sagittarius-graphql-types" import {GraphqlClient} from "@core/util/graphql-client" import membersQuery from "./queries/Members.query.graphql" @@ -21,18 +19,23 @@ import memberAssignRoleMutation from "./mutations/Member.assignRoles.mutation.gr import memberDeleteMutation from "./mutations/Member.delete.mutation.graphql" import memberInviteMutation from "./mutations/Member.invite.mutation.graphql" import {View} from "@code0-tech/pictor/dist/utils/view"; +import {MemberView} from "@edition/member/services/Member.view"; -export class MemberService extends DNamespaceMemberReactiveService { +export type MemberDependencies = { + namespaceId: Namespace['id'] +} + +export class MemberService extends ReactiveArrayService { private readonly client: GraphqlClient private i = 0 - constructor(client: GraphqlClient, store: ReactiveArrayStore>) { + constructor(client: GraphqlClient, store: ReactiveArrayStore>) { super(store) this.client = client } - values(dependencies?: DMemberDependencies): DNamespaceMemberView[] { + values(dependencies?: MemberDependencies): MemberView[] { const members = super.values() if (!dependencies?.namespaceId) return members @@ -53,7 +56,7 @@ export class MemberService extends DNamespaceMemberReactiveService { const nodes = res.data?.namespace?.members?.nodes ?? [] nodes.forEach(member => { if (member && !this.hasById(member.id)) { - this.set(this.i++, new View(new DNamespaceMemberView(member))) + this.set(this.i++, new View(new MemberView(member))) } }) }) @@ -67,6 +70,14 @@ export class MemberService extends DNamespaceMemberReactiveService { return member !== undefined } + getById(id: NamespaceMember['id'], dependencies?: MemberDependencies): MemberView | undefined { + return this.values(dependencies).find(member => member && member.id === id); + } + + getByNamespaceIdAndUserId(namespaceId: Namespace['id'], userId: User['id']): MemberView | undefined { + return this.values({namespaceId: namespaceId}).find(member => member.namespace?.id === namespaceId && member.user?.id === userId) + } + async memberAssignRoles(payload: NamespacesMembersAssignRolesInput): Promise { const result = await this.client.mutate({ mutation: memberAssignRoleMutation, @@ -80,7 +91,7 @@ export class MemberService extends DNamespaceMemberReactiveService { const currentMember = this.getById(payload.memberId) const index = super.values().findIndex(m => m.id === payload.memberId) - const newMember = new DNamespaceMemberView({ + const newMember = new MemberView({ ...currentMember?.json(), roles: { count: payload.roleIds.length, @@ -121,7 +132,7 @@ export class MemberService extends DNamespaceMemberReactiveService { if (result.data && result.data.namespacesMembersInvite && result.data.namespacesMembersInvite.namespaceMember) { const member = result.data.namespacesMembersInvite.namespaceMember - this.set(this.i++, new View(new DNamespaceMemberView(member))) + this.set(this.i++, new View(new MemberView(member))) } return result.data?.namespacesMembersInvite ?? undefined diff --git a/src/packages/ce/src/member/services/Member.view.ts b/src/packages/ce/src/member/services/Member.view.ts new file mode 100644 index 00000000..0515472b --- /dev/null +++ b/src/packages/ce/src/member/services/Member.view.ts @@ -0,0 +1,85 @@ +import { + Maybe, + Namespace, + NamespaceMember, + NamespaceMemberRoleConnection, NamespaceMemberUserAbilities, NamespaceRoleConnection, + Scalars, + User +} from "@code0-tech/sagittarius-graphql-types"; + +export class MemberView { + /** Time when this NamespaceMember was created */ + private readonly _createdAt?: Maybe; + /** Global ID of this NamespaceMember */ + private readonly _id?: Maybe; + /** Memberroles of the member */ + private readonly _memberRoles?: Maybe; + /** Namespace this member belongs to */ + private readonly _namespace?: Maybe; + /** Roles of the member */ + private readonly _roles?: Maybe; + /** Time when this NamespaceMember was last updated */ + private readonly _updatedAt?: Maybe; + /** User this member belongs to */ + private readonly _user?: Maybe; + /** Abilities for the current user on this NamespaceMember */ + private readonly _userAbilities?: Maybe; + + constructor(payload: NamespaceMember) { + this._createdAt = payload.createdAt; + this._id = payload.id; + this._memberRoles = payload.memberRoles; + this._namespace = payload.namespace; + this._roles = payload.roles; + this._updatedAt = payload.updatedAt; + this._user = payload.user; + this._userAbilities = payload.userAbilities; + } + + + get createdAt(): Maybe | undefined { + return this._createdAt; + } + + get id(): Maybe | undefined { + return this._id; + } + + get memberRoles(): Maybe | undefined { + return this._memberRoles; + } + + get namespace(): Maybe | undefined { + return this._namespace; + } + + get roles(): Maybe | undefined { + return this._roles; + } + + get updatedAt(): Maybe | undefined { + return this._updatedAt; + } + + get user(): Maybe | undefined { + return this._user; + } + + get userAbilities(): Maybe | undefined { + return this._userAbilities; + } + + json(): NamespaceMember { + return { + __typename: "NamespaceMember", + createdAt: this._createdAt, + id: this._id, + memberRoles: this._memberRoles, + namespace: this._namespace, + roles: this._roles, + updatedAt: this._updatedAt, + user: this._user, + userAbilities: this._userAbilities, + }; + } +} \ 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..011fc4ce 100644 --- a/src/packages/ce/src/namespace/pages/NamespaceOverviewPage.tsx +++ b/src/packages/ce/src/namespace/pages/NamespaceOverviewPage.tsx @@ -2,11 +2,13 @@ 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 {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 = () => { @@ -19,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/services/Namespace.service.ts b/src/packages/ce/src/namespace/services/Namespace.service.ts index 532587df..0038be48 100644 --- a/src/packages/ce/src/namespace/services/Namespace.service.ts +++ b/src/packages/ce/src/namespace/services/Namespace.service.ts @@ -1,20 +1,21 @@ -import {DNamespaceReactiveService, DNamespaceView, ReactiveArrayStore} from "@code0-tech/pictor"; +import {ReactiveArrayService, ReactiveArrayStore} from "@code0-tech/pictor"; import {GraphqlClient} from "@core/util/graphql-client"; import {Namespace, Query} from "@code0-tech/sagittarius-graphql-types"; import namespaceQuery from "./queries/Namespace.query.graphql"; import {View} from "@code0-tech/pictor/dist/utils/view"; +import {NamespaceView} from "@edition/namespace/services/Namespace.view"; -export class NamespaceService extends DNamespaceReactiveService { +export class NamespaceService extends ReactiveArrayService { private readonly client: GraphqlClient - constructor(client: GraphqlClient, store: ReactiveArrayStore>) { + constructor(client: GraphqlClient, store: ReactiveArrayStore>) { super(store); this.client = client } - getById(id: Namespace["id"]): DNamespaceView | undefined { - if (super.getById(id)) return super.getById(id); + getById(id: Namespace["id"]): NamespaceView | undefined { + if (this.values().find(namespace => namespace && namespace.id === id)) return this.values().find(namespace => namespace && namespace.id === id) this.client.query({ query: namespaceQuery, variables: { @@ -25,11 +26,11 @@ export class NamespaceService extends DNamespaceReactiveService { if (!data) return if (data.namespace) { - this.add(new View(new DNamespaceView(data.namespace))) + this.add(new View(new NamespaceView(data.namespace))) } }) - return super.getById(id); + return this.values().find(namespace => namespace && namespace.id === id) } } \ No newline at end of file diff --git a/src/packages/ce/src/namespace/services/Namespace.view.ts b/src/packages/ce/src/namespace/services/Namespace.view.ts new file mode 100644 index 00000000..6ec3127d --- /dev/null +++ b/src/packages/ce/src/namespace/services/Namespace.view.ts @@ -0,0 +1,90 @@ +import { + Maybe, Namespace, + NamespaceLicenseConnection, + NamespaceMemberConnection, NamespaceParent, NamespaceProjectConnection, NamespaceRoleConnection, RuntimeConnection, + Scalars +} from "@code0-tech/sagittarius-graphql-types"; + +export class NamespaceView { + /** Time when this Namespace was created */ + private readonly _createdAt?: Maybe; + /** Global ID of this Namespace */ + private readonly _id?: Maybe; + /** Members of the namespace */ + private readonly _members?: Maybe; + /** Licenses of the namespace */ + private readonly _namespaceLicenses?: Maybe; + /** Parent of this namespace */ + private readonly _parent?: Maybe; + /** Projects of the namespace */ + private readonly _projects?: Maybe; + /** Roles of the namespace */ + private readonly _roles?: Maybe; + /** Runtime of the namespace */ + private readonly _runtimes?: Maybe; + /** Time when this Namespace was last updated */ + private readonly _updatedAt?: Maybe; + + constructor(payload: Namespace) { + this._createdAt = payload.createdAt; + this._id = payload.id; + this._members = payload.members; + this._namespaceLicenses = payload.namespaceLicenses; + this._parent = payload.parent; + this._projects = payload.projects; + this._roles = payload.roles; + this._runtimes = payload.runtimes; + this._updatedAt = payload.updatedAt; + } + + + get createdAt(): Maybe | undefined { + return this._createdAt; + } + + get id(): Maybe | undefined { + return this._id; + } + + get members(): Maybe | undefined { + return this._members; + } + + get namespaceLicenses(): Maybe | undefined { + return this._namespaceLicenses; + } + + get parent(): Maybe | undefined { + return this._parent; + } + + get projects(): Maybe | undefined { + return this._projects; + } + + get roles(): Maybe | undefined { + return this._roles; + } + + get runtimes(): Maybe | undefined { + return this._runtimes; + } + + get updatedAt(): Maybe | undefined { + return this._updatedAt; + } + + json(): Namespace { + return { + createdAt: this._createdAt, + id: this._id, + members: this._members, + namespaceLicenses: this._namespaceLicenses, + parent: this._parent, + projects: this._projects, + roles: this._roles, + runtimes: this._runtimes, + updatedAt: this._updatedAt + }; + } +} \ No newline at end of file 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/namespace/views/NamespaceOverviewRightView.tsx b/src/packages/ce/src/namespace/views/NamespaceOverviewRightView.tsx index 445f145a..cf65ee05 100644 --- a/src/packages/ce/src/namespace/views/NamespaceOverviewRightView.tsx +++ b/src/packages/ce/src/namespace/views/NamespaceOverviewRightView.tsx @@ -6,12 +6,10 @@ import { Avatar, Badge, Button, - DRuntimeList, Flex, Spacing, Text, Tooltip, - TooltipArrow, TooltipContent, TooltipPortal, TooltipTrigger, @@ -23,6 +21,7 @@ import {useParams} from "next/navigation"; import {NamespaceService} from "@edition/namespace/services/Namespace.service"; import {UserService} from "@edition/user/services/User.service"; import Link from "next/link"; +import {RuntimeDataTableComponent} from "@edition/runtime/components/RuntimeDataTableComponent"; export const NamespaceOverviewRightView: React.FC = () => { @@ -77,7 +76,7 @@ export const NamespaceOverviewRightView: React.FC = () => { Runtimes - +
    } \ 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/components/OrganizationDataTableRowComponent.tsx b/src/packages/ce/src/organization/components/OrganizationDataTableRowComponent.tsx index f51c8553..3311d315 100644 --- a/src/packages/ce/src/organization/components/OrganizationDataTableRowComponent.tsx +++ b/src/packages/ce/src/organization/components/OrganizationDataTableRowComponent.tsx @@ -1,27 +1,18 @@ 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 {OrganizationView} from "@edition/organization/services/Organization.view"; export interface OrganizationDataTableRowComponentProps { organizationId: Organization['id'] - onLeave?: (organization: DOrganizationView) => void + onLeave?: (organization: OrganizationView) => void minimized?: boolean } 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/organization/services/Organization.service.ts b/src/packages/ce/src/organization/services/Organization.service.ts index 3247504d..1ca8d04b 100644 --- a/src/packages/ce/src/organization/services/Organization.service.ts +++ b/src/packages/ce/src/organization/services/Organization.service.ts @@ -1,4 +1,4 @@ -import {DOrganizationReactiveService, DOrganizationView, ReactiveArrayStore} from "@code0-tech/pictor"; +import {ReactiveArrayService, ReactiveArrayStore} from "@code0-tech/pictor"; import { Mutation, Organization, @@ -16,18 +16,19 @@ import updateOrganizationMutation from "./mutations/Organization.update.mutation import deleteOrganizationMutation from "./mutations/Organization.delete.mutation.graphql"; import organizationQuery from "./queries/Organization.query.graphql"; import {View} from "@code0-tech/pictor/dist/utils/view"; +import {OrganizationView} from "@edition/organization/services/Organization.view"; -export class OrganizationService extends DOrganizationReactiveService { +export class OrganizationService extends ReactiveArrayService { private readonly client: GraphqlClient private i = 0; - constructor(client: GraphqlClient, store: ReactiveArrayStore>) { + constructor(client: GraphqlClient, store: ReactiveArrayStore>) { super(store); this.client = client } - values(): DOrganizationView[] { + values(): OrganizationView[] { if (super.values().length > 0) return super.values(); this.client.query({ query: organizationQuery @@ -37,7 +38,7 @@ export class OrganizationService extends DOrganizationReactiveService { if (data.organizations && data.organizations.nodes) { data.organizations.nodes.forEach((organization) => { - if (organization && !this.hasById(organization.id)) this.set(this.i++, new View(new DOrganizationView(organization))) + if (organization && !this.hasById(organization.id)) this.set(this.i++, new View(new OrganizationView(organization))) }) } }) @@ -49,6 +50,10 @@ export class OrganizationService extends DOrganizationReactiveService { return organization !== undefined } + getById(id: Organization["id"]): OrganizationView | undefined { + return this.values().find(organization => organization && organization.id === id) + } + async organizationCreate(payload: OrganizationsCreateInput): Promise { const result = await this.client.mutate({ mutation: createOrganizationMutation, @@ -60,7 +65,7 @@ export class OrganizationService extends DOrganizationReactiveService { if (result.data && result.data.organizationsCreate && result.data.organizationsCreate.organization) { const organization = result.data.organizationsCreate.organization if (!this.hasById(organization.id)) { - this.add(new View(new DOrganizationView(organization))) + this.add(new View(new OrganizationView(organization))) } } @@ -96,7 +101,7 @@ export class OrganizationService extends DOrganizationReactiveService { if (result.data && result.data.organizationsUpdate && result.data.organizationsUpdate.organization) { const organization = result.data.organizationsUpdate.organization const index = this.values().findIndex(o => o.id === organization.id) - this.set(index, new View(new DOrganizationView(organization))) + this.set(index, new View(new OrganizationView(organization))) } diff --git a/src/packages/ce/src/organization/services/Organization.view.ts b/src/packages/ce/src/organization/services/Organization.view.ts new file mode 100644 index 00000000..94122237 --- /dev/null +++ b/src/packages/ce/src/organization/services/Organization.view.ts @@ -0,0 +1,69 @@ +import { + Maybe, + Namespace, + Organization, + OrganizationUserAbilities, + Scalars +} from "@code0-tech/sagittarius-graphql-types"; + +export class OrganizationView { + + private readonly _createdAt?: Maybe; + /** Global ID of this Organization */ + private readonly _id?: Maybe; + /** Name of the organization */ + private _name?: Maybe; + /** Namespace of this organization */ + private readonly _namespace?: Maybe; + /** Time when this Organization was last updated */ + private readonly _updatedAt?: Maybe; + /** Abilities for the current user on this Organization */ + private readonly _userAbilities?: Maybe; + + constructor(payload: Organization) { + this._createdAt = payload.createdAt; + this._id = payload.id; + this._name = payload.name; + this._namespace = payload.namespace; + this._updatedAt = payload.updatedAt; + this._userAbilities = payload.userAbilities; + } + + get createdAt(): Maybe | undefined { + return this._createdAt; + } + + get id(): Maybe { + return this._id; + } + + get name(): Maybe | undefined { + return this._name; + } + + get namespace(): Maybe | undefined { + return this._namespace; + } + + get updatedAt(): Maybe | undefined { + return this._updatedAt; + } + + get userAbilities(): Maybe | undefined { + return this._userAbilities; + } + + set name(value: Maybe) { + this._name = value; + } + + json(): Organization { + return { + createdAt: this._createdAt, + id: this._id, + name: this._name, + namespace: this._namespace, + updatedAt: this._updatedAt + }; + } +} \ No newline at end of file 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..e31c24a5 --- /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 {ProjectView} 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: ProjectView) => void + namespaceId: Namespace["id"] + filter?: (project: ProjectView, 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/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/project/services/Project.service.ts b/src/packages/ce/src/project/services/Project.service.ts index a3cb0668..49e3b053 100644 --- a/src/packages/ce/src/project/services/Project.service.ts +++ b/src/packages/ce/src/project/services/Project.service.ts @@ -1,11 +1,9 @@ import { - DNamespaceProjectReactiveService, - DNamespaceProjectView, - DProjectDependencies, + ReactiveArrayService, ReactiveArrayStore } from "@code0-tech/pictor"; import { - Mutation, + Mutation, Namespace, NamespaceProject, NamespacesProjectsAssignRuntimesInput, NamespacesProjectsAssignRuntimesPayload, @@ -24,18 +22,23 @@ import projectDeleteMutation from "./mutations/Project.delete.mutation.graphql" import projectUpdateMutation from "./mutations/Project.update.mutation.graphql" import projectAssignRuntimesMutation from "./mutations/Project.assignRuntimes.mutation.graphql" import {View} from "@code0-tech/pictor/dist/utils/view"; +import {ProjectView} from "@edition/project/services/Project.view"; -export class ProjectService extends DNamespaceProjectReactiveService { +export type ProjectDependencies = { + namespaceId: Namespace['id'] +} + +export class ProjectService extends ReactiveArrayService { private readonly client: GraphqlClient private i = 0; - constructor(client: GraphqlClient, store: ReactiveArrayStore>) { + constructor(client: GraphqlClient, store: ReactiveArrayStore>) { super(store); this.client = client } - values(dependencies: DProjectDependencies): DNamespaceProjectView[] { + values(dependencies: ProjectDependencies): ProjectView[] { const projects = super.values() if (!dependencies?.namespaceId) return projects @@ -58,7 +61,7 @@ export class ProjectService extends DNamespaceProjectReactiveService { project.namespace?.id === namespaceId && !this.hasById(project.id) ) { - this.set(this.i++, new View(new DNamespaceProjectView(project))) + this.set(this.i++, new View(new ProjectView(project))) } }) }) @@ -72,6 +75,10 @@ export class ProjectService extends DNamespaceProjectReactiveService { return project !== undefined } + getById(id: NamespaceProject['id'], dependencies?: ProjectDependencies): ProjectView | undefined { + return this.values(dependencies!).find(project => project && project.id === id) + } + async projectAssignRuntimes(payload: NamespacesProjectsAssignRuntimesInput): Promise { const result = await this.client.mutate({ mutation: projectAssignRuntimesMutation, @@ -84,7 +91,7 @@ export class ProjectService extends DNamespaceProjectReactiveService { const project = result.data.namespacesProjectsAssignRuntimes.namespaceProject const index = this.values({namespaceId: project?.namespace?.id}).findIndex(o => o.id === project.id) const projectStored = this.values({namespaceId: project?.namespace?.id}).find(o => o.id === project.id) - this.set(index, new View(new DNamespaceProjectView({ + this.set(index, new View(new ProjectView({ ...projectStored?.json(), runtimes: { count: payload.runtimeIds.length, @@ -108,7 +115,7 @@ export class ProjectService extends DNamespaceProjectReactiveService { if (result.data && result.data.namespacesProjectsCreate && result.data.namespacesProjectsCreate.namespaceProject) { const project = result.data.namespacesProjectsCreate.namespaceProject if (!this.hasById(project.id)) { - this.add(new View(new DNamespaceProjectView(project))) + this.add(new View(new ProjectView(project))) } } @@ -145,7 +152,7 @@ export class ProjectService extends DNamespaceProjectReactiveService { const project = result.data.namespacesProjectsUpdate.namespaceProject const index = this.values({namespaceId: project?.namespace?.id}).findIndex(o => o.id === project.id) const projectStored = this.values({namespaceId: project?.namespace?.id}).find(o => o.id === project.id) - this.set(index, new View(new DNamespaceProjectView({ + this.set(index, new View(new ProjectView({ ...projectStored?.json(), ...(payload.name ? {name: payload.name} : {}), ...(payload.description ? {description: payload.description} : {}), diff --git a/src/packages/ce/src/project/services/Project.view.ts b/src/packages/ce/src/project/services/Project.view.ts new file mode 100644 index 00000000..7518ea5a --- /dev/null +++ b/src/packages/ce/src/project/services/Project.view.ts @@ -0,0 +1,122 @@ +import { + Flow, + FlowConnection, + Maybe, + Namespace, NamespaceProject, NamespaceProjectUserAbilities, NamespaceRoleConnection, + Runtime, + RuntimeConnection, + Scalars +} from "@code0-tech/sagittarius-graphql-types"; + +export class ProjectView { + /** Time when this NamespaceProject was created */ + private readonly _createdAt?: Maybe; + /** Description of the project */ + private readonly _description?: Maybe; + /** Fetches an flow given by its ID */ + private readonly _flow?: Maybe; + /** Fetches all flows in this project */ + private readonly _flows?: Maybe; + /** Global ID of this NamespaceProject */ + private readonly _id?: Maybe; + /** Name of the project */ + private readonly _name?: Maybe; + /** The namespace where this project belongs to */ + private readonly _namespace?: Maybe; + /** The primary runtime for the project */ + private readonly _primaryRuntime?: Maybe; + /** Roles assigned to this project */ + private readonly _roles?: Maybe; + /** Runtimes assigned to this project */ + private readonly _runtimes?: Maybe; + /** Slug of the project used in URLs to identify flows */ + private readonly _slug?: Maybe; + /** Time when this NamespaceProject was last updated */ + private readonly _updatedAt?: Maybe; + /** Abilities for the current user on this NamespaceProject */ + private readonly _userAbilities?: Maybe; + + constructor(payload: NamespaceProject) { + this._createdAt = payload.createdAt; + this._description = payload.description; + this._flow = payload.flow; + this._flows = payload.flows; + this._id = payload.id; + this._name = payload.name; + this._namespace = payload.namespace; + this._primaryRuntime = payload.primaryRuntime; + this._roles = payload.roles; + this._runtimes = payload.runtimes; + this._slug = payload.slug; + this._updatedAt = payload.updatedAt; + this._userAbilities = payload.userAbilities; + } + + + get createdAt(): Maybe | undefined { + return this._createdAt; + } + + get description(): Maybe | undefined { + return this._description; + } + + get flow(): Maybe | undefined { + return this._flow; + } + + get flows(): Maybe | undefined { + return this._flows; + } + + get id(): Maybe | undefined { + return this._id; + } + + get name(): Maybe | undefined { + return this._name; + } + + get namespace(): Maybe | undefined { + return this._namespace; + } + + get primaryRuntime(): Maybe | undefined { + return this._primaryRuntime; + } + + get roles(): Maybe | undefined { + return this._roles; + } + + get runtimes(): Maybe | undefined { + return this._runtimes; + } + + get slug(): Maybe | undefined { + return this._slug; + } + + get updatedAt(): Maybe | undefined { + return this._updatedAt; + } + + get userAbilities(): Maybe | undefined { + return this._userAbilities; + } + + json(): NamespaceProject { + return { + createdAt: this._createdAt, + description: this._description, + flow: this._flow, + flows: this._flows, + id: this._id, + name: this._name, + namespace: this._namespace, + primaryRuntime: this._primaryRuntime, + runtimes: this._runtimes, + updatedAt: this._updatedAt + }; + } +} \ No newline at end of file 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/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/role/services/Role.service.ts b/src/packages/ce/src/role/services/Role.service.ts index 581c94ba..680fff2f 100644 --- a/src/packages/ce/src/role/services/Role.service.ts +++ b/src/packages/ce/src/role/services/Role.service.ts @@ -1,11 +1,6 @@ +import {ReactiveArrayService, ReactiveArrayStore} from "@code0-tech/pictor" import { - DNamespaceRoleReactiveService, - DNamespaceRoleView, - DRoleDependencies, - ReactiveArrayStore -} from "@code0-tech/pictor" -import { - Mutation, + Mutation, Namespace, NamespaceRole, NamespacesRolesAssignAbilitiesInput, NamespacesRolesAssignAbilitiesPayload, @@ -16,7 +11,7 @@ import { NamespacesRolesDeleteInput, NamespacesRolesDeletePayload, NamespacesRolesUpdateInput, - NamespacesRolesUpdatePayload, OrganizationsDeleteInput, + NamespacesRolesUpdatePayload, Query } from "@code0-tech/sagittarius-graphql-types" import {GraphqlClient} from "@core/util/graphql-client"; @@ -26,18 +21,23 @@ import roleDeleteMutation from "@edition/role/services/mutations/Role.delete.mut import roleAssignAbilitiesMutation from "@edition/role/services/mutations/Role.assignAbilities.mutation.graphql"; import roleAssignProjectsMutation from "@edition/role/services/mutations/Role.assignProjects.mutation.graphql"; import {View} from "@code0-tech/pictor/dist/utils/view"; +import {RoleView} from "@edition/role/services/Role.view"; + +export type RoleDependencies = { + namespaceId: Namespace['id'] +} -export class RoleService extends DNamespaceRoleReactiveService { +export class RoleService extends ReactiveArrayService { private readonly client: GraphqlClient private i = 0; - constructor(client: GraphqlClient, store: ReactiveArrayStore>) { + constructor(client: GraphqlClient, store: ReactiveArrayStore>) { super(store); this.client = client } - values(dependencies: DRoleDependencies): DNamespaceRoleView[] { + values(dependencies: RoleDependencies): RoleView[] { const roles = super.values() if (!dependencies?.namespaceId) return roles @@ -62,7 +62,7 @@ export class RoleService extends DNamespaceRoleReactiveService { role.namespace?.id === namespaceId && !this.hasById(role.id) ) { - this.set(this.i++, new View(new DNamespaceRoleView(role))) + this.set(this.i++, new View(new RoleView(role))) } }) }) @@ -76,6 +76,10 @@ export class RoleService extends DNamespaceRoleReactiveService { return role !== undefined } + getById(id: NamespaceRole['id'], dependencies?: RoleDependencies): RoleView | undefined { + return this.values(dependencies!).find(role => role && role.id === id); + } + async roleAssignAbilities(payload: NamespacesRolesAssignAbilitiesInput): Promise { const result = await this.client.mutate({ mutation: roleAssignAbilitiesMutation, @@ -89,7 +93,7 @@ export class RoleService extends DNamespaceRoleReactiveService { const currentRole = this.getById(payload.roleId) const index = super.values().findIndex(m => m.id === payload.roleId) - const newRole = new DNamespaceRoleView({ + const newRole = new RoleView({ ...currentRole?.json(), abilities: payload.abilities }) @@ -113,7 +117,7 @@ export class RoleService extends DNamespaceRoleReactiveService { const currentRole = this.getById(payload.roleId) const index = super.values().findIndex(m => m.id === payload.roleId) - const newRole = new DNamespaceRoleView({ + const newRole = new RoleView({ ...currentRole?.json(), assignedProjects: { count: payload.projectIds.length, @@ -142,7 +146,7 @@ export class RoleService extends DNamespaceRoleReactiveService { const currentRole = this.getById(payload.namespaceRoleId) const index = super.values().findIndex(m => m.id === payload.namespaceRoleId) - const newRole = new DNamespaceRoleView({ + const newRole = new RoleView({ ...currentRole?.json(), name: payload.name }) diff --git a/src/packages/ce/src/role/services/Role.view.ts b/src/packages/ce/src/role/services/Role.view.ts new file mode 100644 index 00000000..694f0c13 --- /dev/null +++ b/src/packages/ce/src/role/services/Role.view.ts @@ -0,0 +1,83 @@ +import { + Maybe, Namespace, + NamespaceProjectConnection, + NamespaceRole, + NamespaceRoleAbility, NamespaceRoleUserAbilities, Scalars +} from "@code0-tech/sagittarius-graphql-types"; + +export class RoleView { + + /** The abilities the role is granted */ + private readonly _abilities?: Maybe>; + /** The projects this role is assigned to */ + private readonly _assignedProjects?: Maybe; + /** Time when this NamespaceRole was created */ + private readonly _createdAt?: Maybe; + /** Global ID of this NamespaceRole */ + private readonly _id?: Maybe; + /** The name of this role */ + private readonly _name?: Maybe; + /** The namespace where this role belongs to */ + private readonly _namespace?: Maybe; + /** Time when this NamespaceRole was last updated */ + private readonly _updatedAt?: Maybe; + /** Abilities for the current user on this NamespaceRole */ + private readonly _userAbilities?: Maybe; + + constructor(payload: NamespaceRole) { + this._abilities = payload.abilities; + this._assignedProjects = payload.assignedProjects; + this._createdAt = payload.createdAt; + this._id = payload.id; + this._name = payload.name; + this._namespace = payload.namespace; + this._updatedAt = payload.updatedAt; + this._userAbilities = payload.userAbilities; + } + + + get abilities(): Maybe> | undefined { + return this._abilities; + } + + get assignedProjects(): Maybe | undefined { + return this._assignedProjects; + } + + get createdAt(): Maybe | undefined { + return this._createdAt; + } + + get id(): Maybe | undefined { + return this._id; + } + + get name(): Maybe | undefined { + return this._name; + } + + get namespace(): Maybe | undefined { + return this._namespace; + } + + get updatedAt(): Maybe | undefined { + return this._updatedAt; + } + + get userAbilities(): Maybe | undefined { + return this._userAbilities; + } + + json(): NamespaceRole { + return { + abilities: this._abilities, + assignedProjects: this._assignedProjects, + createdAt: this._createdAt, + id: this._id, + name: this._name, + namespace: this._namespace, + updatedAt: this._updatedAt, + userAbilities: this._userAbilities + }; + } +} \ No newline at end of file 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 2017bfcc..bf425e41 100644 --- a/src/packages/ce/src/role/views/RolePermissionView.tsx +++ b/src/packages/ce/src/role/views/RolePermissionView.tsx @@ -17,11 +17,11 @@ import { } from "@code0-tech/pictor"; import React from "react"; import type {Namespace, NamespaceRole, NamespaceRoleAbility} from "@code0-tech/sagittarius-graphql-types"; -import {DNamespaceRolePermissions} from "@code0-tech/pictor/dist/components/d-role/DNamespaceRolePermissions"; import CardSection from "@code0-tech/pictor/dist/components/card/CardSection"; import {TabContent} from "@code0-tech/pictor/dist/components/tab/Tab"; import {useParams} from "next/navigation"; import {RoleService} from "@edition/role/services/Role.service"; +import {RolePermissionComponent} from "@edition/role/components/RolePermissionComponent"; type Permission = { label: string @@ -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) => { @@ -257,7 +261,7 @@ export const RolePermissionView: React.FC = () => { {permissionTemplate.name} - enabled) .map(([ability, _]) => ability as NamespaceRoleAbility)}/> @@ -277,7 +281,7 @@ export const RolePermissionView: React.FC = () => { Current stored permissions - + {permissions.map(permissionGroup => { diff --git a/src/packages/ce/src/role/views/RoleProjectView.tsx b/src/packages/ce/src/role/views/RoleProjectView.tsx index 36e40da6..73b5b953 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 {ProjectView} 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: ProjectView) => { 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 diff --git a/src/packages/ce/src/runtime/components/RuntimeDataTableFilterInputComponent.tsx b/src/packages/ce/src/runtime/components/RuntimeDataTableFilterInputComponent.tsx index 6747d11b..6b69c5a1 100644 --- a/src/packages/ce/src/runtime/components/RuntimeDataTableFilterInputComponent.tsx +++ b/src/packages/ce/src/runtime/components/RuntimeDataTableFilterInputComponent.tsx @@ -1,21 +1,21 @@ 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 {RuntimeView} from "@edition/runtime/services/Runtime.view"; export interface RoleDataTableFilterInputComponentProps { namespaceId?: Namespace['id'] onChange: (filter: DataTableFilterProps) => void - preFilter?: (project: DRuntimeView, index: number) => boolean + preFilter?: (project: RuntimeView, index: number) => boolean } export const RuntimeDataTableFilterInputComponent: React.FC = (props) => { 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/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/runtime/pages/RuntimesPage.tsx b/src/packages/ce/src/runtime/pages/RuntimesPage.tsx index 0ca50b6a..9625208d 100644 --- a/src/packages/ce/src/runtime/pages/RuntimesPage.tsx +++ b/src/packages/ce/src/runtime/pages/RuntimesPage.tsx @@ -3,26 +3,26 @@ import React from "react"; import { Button, - DRuntimeList, Flex, - Menu, MenuCheckboxItem, MenuContent, MenuPortal, MenuTrigger, + Menu, + MenuCheckboxItem, + MenuContent, + MenuPortal, + MenuTrigger, Spacing, Text, useService, - useStore, - useUserSession + useStore } from "@code0-tech/pictor"; import Link from "next/link"; import {notFound, useParams, useRouter} from "next/navigation"; import {UserService} from "@edition/user/services/User.service"; import {RuntimeDataTableComponent} from "@edition/runtime/components/RuntimeDataTableComponent"; import {DataTableFilterProps, DataTableSortProps} from "@code0-tech/pictor/dist/components/data-table/DataTable"; -import { - OrganizationDataTableFilterInputComponent -} from "@edition/organization/components/OrganizationDataTableFilterInputComponent"; 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 = () => { @@ -128,13 +128,18 @@ export const RuntimesPage: React.FC = () => {
    - namespaceIndex ? true : !runtime?.namespace?.id} namespaceId={namespaceIndex ? `gid://sagittarius/Namespace/${namespaceIndex}` : undefined} onChange={filter => setFilter(filter)}/> + namespaceIndex ? true : !runtime?.namespace?.id} + namespaceId={namespaceIndex ? `gid://sagittarius/Namespace/${namespaceIndex}` : undefined} + onChange={filter => setFilter(filter)}/>
    - namespaceIndex ? true : !runtime?.namespace?.id} onSelect={(runtime) => { - const number = runtime?.id?.match(/Runtime\/(\d+)$/)?.[1] - router.push(namespaceIndex ? `/namespace/${namespaceIndex}/runtimes/${number}/settings` : `/runtimes/${number}/settings`) - }}/> + namespaceIndex ? true : !runtime?.namespace?.id} + onSelect={(runtime) => { + const number = runtime?.id?.match(/Runtime\/(\d+)$/)?.[1] + router.push(namespaceIndex ? `/namespace/${namespaceIndex}/runtimes/${number}/settings` : `/runtimes/${number}/settings`) + }}/>
    } \ No newline at end of file diff --git a/src/packages/ce/src/runtime/services/Runtime.service.ts b/src/packages/ce/src/runtime/services/Runtime.service.ts index ef77ce72..556b8b38 100644 --- a/src/packages/ce/src/runtime/services/Runtime.service.ts +++ b/src/packages/ce/src/runtime/services/Runtime.service.ts @@ -1,6 +1,6 @@ -import {DRuntimeDependencies, DRuntimeReactiveService, DRuntimeView, ReactiveArrayStore} from "@code0-tech/pictor"; +import {ReactiveArrayService, ReactiveArrayStore} from "@code0-tech/pictor"; import { - Mutation, + Mutation, Namespace, Query, Runtime, RuntimesCreateInput, @@ -20,13 +20,18 @@ import updateRuntimeMutation from "./mutations/Runtime.update.mutation.graphql" import deleteRuntimeMutation from "./mutations/Runtime.delete.mutation.graphql" import rotateTokenRuntimeMutation from "./mutations/Runtime.rotateToken.mutation.graphql" import {View} from "@code0-tech/pictor/dist/utils/view"; +import {RuntimeView} from "@edition/runtime/services/Runtime.view"; -export class RuntimeService extends DRuntimeReactiveService { +export type RuntimeDependencies = { + namespaceId: Namespace['id'] +} + +export class RuntimeService extends ReactiveArrayService { private readonly client: GraphqlClient private i = 0 - constructor(client: GraphqlClient, store: ReactiveArrayStore>) { + constructor(client: GraphqlClient, store: ReactiveArrayStore>) { super(store); this.client = client } @@ -36,9 +41,13 @@ export class RuntimeService extends DRuntimeReactiveService { return runtime !== undefined } + getById(id: Runtime['id']): RuntimeView | undefined { + return this.values().find(runtime => runtime && runtime.id === id); + } + //TODO: rework to be able to get all runtimes that you can access. If no namespace id is provided just get the global runtiimes // TODO: if namespace id is provided get the runtimes for this namespace and also the global runtimes - values(dependencies?: DRuntimeDependencies): DRuntimeView[] { + values(dependencies?: RuntimeDependencies): RuntimeView[] { const runtimes = super.values() if (!dependencies?.namespaceId) { @@ -53,7 +62,7 @@ export class RuntimeService extends DRuntimeReactiveService { const nodes = res.data?.globalRuntimes?.nodes ?? [] nodes.forEach(runtime => { if (runtime && !this.hasById(runtime.id)) { - this.set(this.i++, new View(new DRuntimeView(runtime))) + this.set(this.i++, new View(new RuntimeView(runtime))) } }) }) @@ -81,7 +90,7 @@ export class RuntimeService extends DRuntimeReactiveService { runtime.namespace?.id === namespaceId && !this.hasById(runtime.id) ) { - this.set(this.i++, new View(new DRuntimeView(runtime))) + this.set(this.i++, new View(new RuntimeView(runtime))) } }) }) @@ -101,7 +110,7 @@ export class RuntimeService extends DRuntimeReactiveService { if (result.data && result.data.runtimesCreate && result.data.runtimesCreate.runtime) { const runtime = result.data.runtimesCreate.runtime if (!this.hasById(runtime.id)) { - this.add(new View(new DRuntimeView(runtime))) + this.add(new View(new RuntimeView(runtime))) } } @@ -119,7 +128,7 @@ export class RuntimeService extends DRuntimeReactiveService { if (result.data && result.data.runtimesUpdate && result.data.runtimesUpdate.runtime) { const runtime = result.data.runtimesUpdate.runtime const index = this.values().findIndex(r => r.id === runtime.id) - this.set(index, new View(new DRuntimeView(runtime))) + this.set(index, new View(new RuntimeView(runtime))) } @@ -155,7 +164,7 @@ export class RuntimeService extends DRuntimeReactiveService { if (result.data && result.data.runtimesRotateToken && result.data.runtimesRotateToken.runtime) { const runtime = result.data.runtimesRotateToken.runtime const index = this.values().findIndex(r => r.id === runtime.id) - this.set(index, new View(new DRuntimeView(runtime))) + this.set(index, new View(new RuntimeView(runtime))) } diff --git a/src/packages/ce/src/runtime/services/Runtime.view.ts b/src/packages/ce/src/runtime/services/Runtime.view.ts new file mode 100644 index 00000000..af4ce9b7 --- /dev/null +++ b/src/packages/ce/src/runtime/services/Runtime.view.ts @@ -0,0 +1,117 @@ +import { + DataTypeConnection, + FlowTypeConnection, + Maybe, + Namespace, + NamespaceProjectConnection, + Runtime, + RuntimeStatusType, RuntimeUserAbilities, + Scalars +} from "@code0-tech/sagittarius-graphql-types"; + +export class RuntimeView { + + /** Time when this Runtime was created */ + private readonly _createdAt?: Maybe; + /** DataTypes of the runtime */ + private readonly _dataTypes?: Maybe; + /** The description for the runtime if present */ + private readonly _description?: Maybe; + /** FlowTypes of the runtime */ + private readonly _flowTypes?: Maybe; + /** Global ID of this Runtime */ + private readonly _id?: Maybe; + /** The name for the runtime */ + private readonly _name?: Maybe; + /** The parent namespace for the runtime */ + private readonly _namespace?: Maybe; + /** Projects associated with the runtime */ + private readonly _projects?: Maybe; + /** The status of the runtime */ + private readonly _status?: Maybe; + /** Token belonging to the runtime, only present on creation */ + private readonly _token?: Maybe; + /** Time when this Runtime was last updated */ + private readonly _updatedAt?: Maybe; + /** Abilities for the current user on this Runtime */ + private readonly _userAbilities?: Maybe; + + constructor(payload: Runtime) { + this._createdAt = payload.createdAt; + this._dataTypes = payload.dataTypes; + this._description = payload.description; + this._flowTypes = payload.flowTypes; + this._id = payload.id; + this._name = payload.name; + this._namespace = payload.namespace; + this._projects = payload.projects; + this._status = payload.status; + this._token = payload.token; + this._updatedAt = payload.updatedAt; + this._userAbilities = payload.userAbilities; + } + + get createdAt(): Maybe | undefined { + return this._createdAt; + } + + get dataTypes(): Maybe | undefined { + return this._dataTypes; + } + + get description(): Maybe | undefined { + return this._description; + } + + get flowTypes(): Maybe | undefined { + return this._flowTypes; + } + + get id(): Maybe | undefined { + return this._id; + } + + get name(): Maybe | undefined { + return this._name; + } + + get namespace(): Maybe | undefined { + return this._namespace; + } + + get projects(): Maybe | undefined { + return this._projects; + } + + get status(): Maybe | undefined { + return this._status; + } + + get token(): Maybe | undefined { + return this._token; + } + + get updatedAt(): Maybe | undefined { + return this._updatedAt; + } + + get userAbilities(): Maybe | undefined { + return this._userAbilities; + } + + json(): Runtime { + return { + createdAt: this._createdAt, + dataTypes: this._dataTypes, + description: this._description, + flowTypes: this._flowTypes, + id: this._id, + name: this._name, + namespace: this._namespace, + projects: this._projects, + status: this._status, + token: this._token, + updatedAt: this._updatedAt, + }; + } +} \ No newline at end of file diff --git a/src/packages/ce/src/user/components/UserInputComponent.tsx b/src/packages/ce/src/user/components/UserInputComponent.tsx new file mode 100644 index 00000000..339ebc14 --- /dev/null +++ b/src/packages/ce/src/user/components/UserInputComponent.tsx @@ -0,0 +1,112 @@ +import React from "react"; +import {IconArrowDown, IconArrowUp, IconCornerDownLeft} from "@tabler/icons-react"; +import {UserView} from "@edition/user/services/User.view"; +import { + Badge, + Flex, + InputSuggestion, + InputSyntaxSegment, + MenuItem, + MenuLabel, + Spacing, + Text, + TextInput, + TextInputProps, + useService, + useStore +} from "@code0-tech/pictor"; +import {UserService} from "@edition/user/services/User.service"; + +export interface UserInputComponentProps extends TextInputProps { + filter?: (user: UserView, index: number) => boolean +} + +export const UserInputComponent: React.FC = (props) => { + + const {filter = () => true, ...rest} = props + + const userService = useService(UserService) + const userStore = useStore(UserService) + const suggestions: InputSuggestion[] = React.useMemo(() => { + return userService.values().filter(filter).map(user => ({ + value: user.username || "", + children: + {user.username} + {user.email} + , + insertMode: "insert", + valueData: user, + groupBy: "Users" + })) + }, [userStore]) + + const transformSyntax = ( + _?: string | null, + appliedParts: (InputSuggestion | any)[] = [], + ): InputSyntaxSegment[] => { + + let cursor = 0 + + return appliedParts.map((part: string | InputSuggestion, index) => { + if (typeof part === "object") { + const segment = { + type: "block", + value: part.valueData, + start: cursor, + end: cursor + part.value.length, + visualLength: 1, + content: + + @{part.value} + + , + } + cursor += part.value.length + return segment + } + const textString = part ?? "" + if (!textString.length) return + + if (index == appliedParts.length - 1) { + const segment = { + type: "text", + value: textString, + start: cursor, + end: cursor + textString.length, + visualLength: textString.length, + content: textString, + } + cursor += textString.length + return segment + } + cursor += textString.length + return {} + }) as InputSyntaxSegment[] + } + + return No user found} + onLastTokenChange={token => { + userService.getByUsername(token) + }} + suggestionsFooter={ + + + + + + + move + + + + + insert + + + } + filterSuggestionsByLastToken + enforceUniqueSuggestions + transformSyntax={transformSyntax} {...rest} + suggestions={suggestions}/> +} diff --git a/src/packages/ce/src/user/components/UserMenuComponent.tsx b/src/packages/ce/src/user/components/UserMenuComponent.tsx new file mode 100644 index 00000000..f88b67dc --- /dev/null +++ b/src/packages/ce/src/user/components/UserMenuComponent.tsx @@ -0,0 +1,56 @@ +"use client" + +import React from "react" +import {Scalars} from "@code0-tech/sagittarius-graphql-types"; +import { + Avatar, + Flex, + Menu, + MenuContent, + MenuPortal, + MenuProps, + MenuTrigger, + Text, + useService, + useStore +} from "@code0-tech/pictor"; +import {UserService} from "@edition/user/services/User.service"; + +export interface UserMenuComponentProps extends MenuProps { + userId: Scalars['UserID']['output'] +} + +const UserMenuComponent: React.FC = props => { + const userService = useService(UserService) + const userStore = useStore(UserService) + const currentUser = React.useMemo(() => userService.getById(props.userId), [userStore, userService]) + + return React.useMemo(() => { + return ( + + + + + + + {currentUser?.username} + + + {currentUser?.email} + + + + + + + + {props.children} + + + + ) + }, [currentUser]) +} + +export default UserMenuComponent \ No newline at end of file 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/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} 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 = () => { diff --git a/src/packages/ce/src/user/services/User.service.ts b/src/packages/ce/src/user/services/User.service.ts index 89c3d4d5..64a6e903 100644 --- a/src/packages/ce/src/user/services/User.service.ts +++ b/src/packages/ce/src/user/services/User.service.ts @@ -1,4 +1,4 @@ -import {DUserReactiveService, DUserView, ReactiveArrayStore} from "@code0-tech/pictor"; +import {ReactiveArrayService, ReactiveArrayStore} from "@code0-tech/pictor"; import { Mutation, Query, @@ -41,18 +41,19 @@ import usersQuery from "./queries/Users.query.graphql"; import userByUsernameQuery from "./queries/User.byUsername.query.graphql"; import userByIdQuery from "./queries/User.byId.query.graphql"; import {View} from "@code0-tech/pictor/dist/utils/view"; +import {UserView} from "@edition/user/services/User.view"; -export class UserService extends DUserReactiveService { +export class UserService extends ReactiveArrayService { private readonly client: GraphqlClient private i = 0; - constructor(client: GraphqlClient, store: ReactiveArrayStore>) { + constructor(client: GraphqlClient, store: ReactiveArrayStore>) { super(store); this.client = client } - values(): DUserView[] { + values(): UserView[] { if (super.values().length > 0) return super.values(); this.client.query({ query: usersQuery @@ -60,18 +61,18 @@ export class UserService extends DUserReactiveService { const data = result.data if (!data) return - if (data && data.currentUser && !this.hasById(data.currentUser.id)) this.set(this.i++, new View(new DUserView(data.currentUser))) + if (data && data.currentUser && !this.hasById(data.currentUser.id)) this.set(this.i++, new View(new UserView(data.currentUser))) if (data.users && data.users.nodes) { data.users.nodes.forEach((user) => { - if (user && !(user.id === data.currentUser?.id) && !this.hasById(user.id)) this.set(this.i++, new View(new DUserView(user))) + if (user && !(user.id === data.currentUser?.id) && !this.hasById(user.id)) this.set(this.i++, new View(new UserView(user))) }) } }) return super.values(); } - getById(id: User["id"]): DUserView | undefined { - const user = super.getById(id) + getById(id: User["id"]): UserView | undefined { + const user = this.values().find(user => user && user.id === id) if (user) return user if (id) { @@ -84,15 +85,15 @@ export class UserService extends DUserReactiveService { const data = result.data if (!data) return - if (data && data.user && !this.hasById(data.user.id)) this.set(this.i++, new View(new DUserView(data.user))) + if (data && data.user && !this.hasById(data.user.id)) this.set(this.i++, new View(new UserView(data.user))) }) } - return super.getById(id) + return this.values().find(user => user && user.id === id) } - getByUsername(username: User["username"]): DUserView | undefined { - if (super.getByUsername(username)) return super.getByUsername(username) + getByUsername(username: User["username"]): UserView | undefined { + if (this.values().find(user => user && user.username === username)) return this.values().find(user => user && user.username === username) this.client.query({ query: userByUsernameQuery, @@ -103,10 +104,10 @@ export class UserService extends DUserReactiveService { const data = result.data if (!data) return - if (data && data.user && !this.hasById(data.user.id)) this.set(this.i++, new View(new DUserView(data.user))) + if (data && data.user && !this.hasById(data.user.id)) this.set(this.i++, new View(new UserView(data.user))) }) - return super.getByUsername(username) + return this.values().find(user => user && user.username === username) } deleteById(id: User["id"]): void { @@ -164,7 +165,7 @@ export class UserService extends DUserReactiveService { }) if (result.data && result.data.usersLogin && result.data.usersLogin.userSession?.user && !this.hasById(result.data.usersLogin.userSession?.user.id)) { - this.add(new View(new DUserView(result.data.usersLogin.userSession.user))) + this.add(new View(new UserView(result.data.usersLogin.userSession.user))) } return result.data?.usersLogin ?? undefined @@ -232,7 +233,7 @@ export class UserService extends DUserReactiveService { }) if (result.data && result.data.usersRegister && result.data.usersRegister.userSession?.user && !this.hasById(result.data.usersRegister.userSession?.user.id)) { - this.add(new View(new DUserView(result.data.usersRegister.userSession.user))) + this.add(new View(new UserView(result.data.usersRegister.userSession.user))) } return result.data?.usersRegister ?? undefined diff --git a/src/packages/ce/src/user/services/User.view.ts b/src/packages/ce/src/user/services/User.view.ts new file mode 100644 index 00000000..a24366eb --- /dev/null +++ b/src/packages/ce/src/user/services/User.view.ts @@ -0,0 +1,160 @@ +import { + Maybe, + Namespace, + NamespaceMemberConnection, + Scalars, + User, + UserIdentityConnection, UserSessionConnection, UserUserAbilities +} from "@code0-tech/sagittarius-graphql-types"; + +export class UserView { + + /** Global admin status of the user */ + private _admin?: Maybe; + /** The avatar if present of the user */ + private readonly _avatarPath?: Maybe; + /** Time when this User was created */ + private readonly _createdAt?: Maybe; + /** Email of the user */ + private _email?: Maybe; + /** Email verification date of the user if present */ + private readonly _emailVerifiedAt?: Maybe; + /** Firstname of the user */ + private _firstname?: Maybe; + /** Global ID of this User */ + private readonly _id?: Maybe; + /** Identities of this user */ + private readonly _identities?: Maybe; + /** Lastname of the user */ + private _lastname?: Maybe; + /** Namespace of this user */ + private readonly _namespace?: Maybe; + /** Namespace Memberships of this user */ + private readonly _namespaceMemberships?: Maybe; + /** Sessions of this user */ + private readonly _sessions?: Maybe; + /** Time when this User was last updated */ + private readonly _updatedAt?: Maybe; + /** Username of the user */ + private _username?: Maybe; + /** Abilities for the current user on this User */ + private readonly _userAbilities?: Maybe; + + constructor(user: User) { + this._admin = user.admin; + this._avatarPath = user.avatarPath; + this._createdAt = user.createdAt; + this._email = user.email; + this._emailVerifiedAt = user.emailVerifiedAt; + this._firstname = user.firstname; + this._id = user.id; + this._identities = user.identities; + this._lastname = user.lastname; + this._namespace = user.namespace; + this._namespaceMemberships = user.namespaceMemberships; + this._sessions = user.sessions; + this._updatedAt = user.updatedAt; + this._username = user.username; + this._userAbilities = user.userAbilities; + + } + + get admin(): Maybe | undefined { + return this._admin; + } + + get avatarPath(): Maybe | undefined { + return this._avatarPath; + } + + get createdAt(): Maybe | undefined { + return this._createdAt; + } + + get email(): Maybe | undefined { + return this._email; + } + + get emailVerifiedAt(): Maybe | undefined { + return this._emailVerifiedAt; + } + + get firstname(): Maybe | undefined { + return this._firstname; + } + + get id(): Maybe | undefined { + return this._id; + } + + get identities(): Maybe | undefined { + return this._identities; + } + + get lastname(): Maybe | undefined { + return this._lastname; + } + + get namespace(): Maybe | undefined { + return this._namespace; + } + + get namespaceMemberships(): Maybe | undefined { + return this._namespaceMemberships; + } + + get sessions(): Maybe | undefined { + return this._sessions; + } + + get updatedAt(): Maybe | undefined { + return this._updatedAt; + } + + get username(): Maybe | undefined { + return this._username; + } + + get userAbilities(): Maybe | undefined { + return this._userAbilities; + } + + set admin(value: Maybe) { + this._admin = value; + } + + set email(value: Maybe) { + this._email = value; + } + + set firstname(value: Maybe) { + this._firstname = value; + } + + set lastname(value: Maybe) { + this._lastname = value; + } + + set username(value: Maybe) { + this._username = value; + } + + json(): User { + return { + admin: this._admin, + avatarPath: this._avatarPath, + createdAt: this._createdAt, + email: this._email, + emailVerifiedAt: this._emailVerifiedAt, + firstname: this._firstname, + id: this._id, + identities: this._identities, + lastname: this._lastname, + namespace: this._namespace, + namespaceMemberships: this._namespaceMemberships, + sessions: this._sessions, + updatedAt: this._updatedAt, + username: this._username, + }; + } +} \ No newline at end of file diff --git a/src/packages/core/src/style/_box.scss b/src/packages/core/src/style/_box.scss new file mode 100644 index 00000000..d3176d7b --- /dev/null +++ b/src/packages/core/src/style/_box.scss @@ -0,0 +1,53 @@ +@use "variables"; +@use "sass:color"; +@use "helpers"; + +@mixin box( + $background: variables.$secondary, + $color: variables.$tertiary, + $borderColor: variables.$secondary +) { + + $mixedBorderWhite: color.mix($borderColor, variables.$white, 25%); + $mixedBorderBlack: color.mix($borderColor, variables.$black, 25%); + + background: helpers.backgroundColor($background); + box-shadow: inset 0 1px 1px helpers.borderColor($borderColor); + border: none; + color: helpers.color($color, variables.$hierarchySecondary); + position: relative; + box-sizing: border-box; + +} + +@mixin boxHover( + $background: variables.$secondary, + $color: variables.$secondary, + $borderColor: variables.$secondary +) { + &:hover { + background: helpers.backgroundColor($background, 1.5); + box-shadow: inset 0 1px 1px helpers.borderColor($borderColor); + } +} + +@mixin boxActiveStyle( + $background: variables.$secondary, + $color: variables.$secondary, + $borderColor: variables.$secondary +) { + background: helpers.backgroundColor($background, 2); + box-shadow: inset 0 1px 1px helpers.borderColor($borderColor); + outline: none; +} + +@mixin boxActive( + $background: variables.$secondary, + $color: variables.$secondary, + $borderColor: variables.$secondary +) { + + &:active, &:focus, &[aria-selected=true], &[data-state=open] { + @include boxActiveStyle($background, $color, $borderColor); + } +} \ No newline at end of file diff --git a/src/packages/core/src/style/_helpers.scss b/src/packages/core/src/style/_helpers.scss new file mode 100644 index 00000000..2fbac969 --- /dev/null +++ b/src/packages/core/src/style/_helpers.scss @@ -0,0 +1,51 @@ +@use "sass:math"; +@use "sass:color"; +@use "variables"; + +@mixin disabled() { + &:disabled, &[data-disabled], &[aria-disabled=true], &--disabled { + cursor: not-allowed; + opacity: 25%; + pointer-events: unset; + } +} + +@mixin borderRadius() { + border-radius: variables.$borderRadius; +} + +@mixin fontStyle() { + font-family: "Inter", sans-serif; + font-weight: 400; + letter-spacing: -0.5px; +} + +@function borderColor($color: variables.$secondary) { + @if ($color == variables.$primary) { + @return rgba(variables.$secondary, .1); + } + @return rgba($color, .1); +} + +@function color($color: variables.$white, $hierarchy: variables.$hierarchyTertiary) { + @if ($color == variables.$primary) { + @return rgba(variables.$secondary, $hierarchy); + } + @return rgba($color, $hierarchy); +} + +@function backgroundColor($color: variables.$secondary, $level: 1) { + @if($color == variables.$primary) { + @if($level == 1) { + @return variables.$primary + } + @return rgba(variables.$secondary, 10% * $level); + } + @return color.mix($color, variables.$primary, 10% * $level); +} + +@mixin noFocusStyle() { + &:focus { + outline: none; + } +} \ No newline at end of file diff --git a/src/packages/core/src/style/_media.scss b/src/packages/core/src/style/_media.scss new file mode 100644 index 00000000..79fa8244 --- /dev/null +++ b/src/packages/core/src/style/_media.scss @@ -0,0 +1,83 @@ +@use "sass:list"; +@use "sass:map"; +@use "sass:math"; +@use "sass:meta"; +$breakpoints: ( + xs: 0, + sm: 576px, + md: 768px, + lg: 992px, + xl: 1200px, + xxl: 1400px +); + +@mixin respond-above($breakpoint) { + @if map.has-key($breakpoints, $breakpoint) { + $breakpoint-value: map.get($breakpoints, $breakpoint); + @media (min-width: $breakpoint-value) { + @content; + } + } @else { + @warn 'Invalid breakpoint: #{$breakpoint}.'; + @if (is-number($breakpoint)) { + $breakpoint-value: $breakpoint+"px"; + @media (min-width: $breakpoint-value) { + @content; + } + } + } +} + +@mixin respond-below($breakpoint) { + @if map.has-key($breakpoints, $breakpoint) { + $breakpoint-value: map.get($breakpoints, $breakpoint); + @media (max-width: ($breakpoint-value - 1)) { + @content; + } + } @else { + @warn 'Invalid breakpoint: #{$breakpoint}.'; + @if (is-number($breakpoint)) { + $breakpoint-value: $breakpoint+"px"; + @media (max-width: ($breakpoint-value)) { + @content; + } + } + } +} + +@mixin respond-between($lower, $upper) { + @if map.has-key($breakpoints, $lower) and map.has-key($breakpoints, $upper) { + $lower-breakpoint: map.get($breakpoints, $lower); + $upper-breakpoint: map.get($breakpoints, $upper); + @media (min-width: $lower-breakpoint) and (max-width: ($upper-breakpoint - 1)) { + @content; + } + } @else { + @if (map.has-key($breakpoints, $lower) == false) { + @warn 'Your lower breakpoint was invalid: #{$lower}.'; + } + @if (map.has-key($breakpoints, $upper) == false) { + @warn 'Your upper breakpoint was invalid: #{$upper}.'; + } + @if (is-number($lower) and is-number($upper)) { + $lower-breakpoint: $lower+"px"; + $upper-breakpoint: $upper+"px"; + @media (min-width: $lower-breakpoint) and (max-width: ($upper-breakpoint)) { + @content; + } + } + @if (is-absolute-length-in-px($lower) and is-absolute-length-in-px($upper)) { + $lower-breakpoint: $lower; + $upper-breakpoint: $upper; + @media (min-width: $lower-breakpoint) and (max-width: ($upper-breakpoint)) { + @content; + } + } + } +} +@function is-number($value) { + @return meta.type-of($value) == 'number'; +} +@function is-absolute-length-in-px($value) { + @return is-number($value) and list.index('px', math.unit($value)) != null; +} \ No newline at end of file diff --git a/src/packages/core/src/style/_variables.scss b/src/packages/core/src/style/_variables.scss new file mode 100644 index 00000000..2eb3f076 --- /dev/null +++ b/src/packages/core/src/style/_variables.scss @@ -0,0 +1,52 @@ +//colors +$primary: #070514; +$secondary: #bfbfbf; +$tertiary: #ffffff; +$info: #70ffb2; +$success: #29BF12; +$warning: #FFBE0B; +$error: #D90429; +$black: #000000; +$white: #ffffff; +$bodyBg: $primary; +//(font-)sizes + +$xxs: 0.35rem; +$xs: 0.7rem; +$sm: 0.8rem; +$md: 1rem; +$lg: 1.2rem; +$xl: 1.3rem; + +$hierarchyPrimary: 1; +$hierarchySecondary: 0.75; +$hierarchyTertiary: 0.5; + +$sizes: ( + "xxs": $xxs, + "xs": $xs, + "sm": $sm, + "md": $md, + "lg": $lg, + "xl": $xl, +); + +$hierarchy: ( + "primary": $hierarchyPrimary, + "secondary": $hierarchySecondary, + "tertiary": $hierarchyTertiary, +); + + +//component styling +$borderRadius: $md; + +$colors: ( + "primary": $primary, + "secondary": $secondary, + "tertiary": $tertiary, + "success": $success, + "warning": $warning, + "error": $error, + "info": $info +)