diff --git a/backend/go-app/go.mod b/backend/go-app/go.mod index 831f0894..11241b88 100644 --- a/backend/go-app/go.mod +++ b/backend/go-app/go.mod @@ -22,12 +22,12 @@ require ( github.com/ghodss/yaml v1.0.0 github.com/go-co-op/gocron v1.37.0 github.com/go-git/go-billy/v5 v5.6.2 - github.com/go-git/go-git/v5 v5.16.1 + github.com/go-git/go-git/v5 v5.16.5 github.com/gorilla/mux v1.8.1 github.com/h2non/filetype v1.1.3 github.com/satori/go.uuid v1.2.0 - github.com/shuffle/shuffle-shared v0.9.83 - github.com/shuffle/singul v0.0.25 + github.com/shuffle/shuffle-shared v0.9.84 + github.com/shuffle/singul v0.0.26 golang.org/x/crypto v0.45.0 google.golang.org/api v0.236.0 google.golang.org/grpc v1.72.2 diff --git a/backend/go-app/go.sum b/backend/go-app/go.sum index 07881615..27d85f01 100644 --- a/backend/go-app/go.sum +++ b/backend/go-app/go.sum @@ -101,6 +101,8 @@ github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151X github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc= +github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= @@ -138,8 +140,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frikky/kin-openapi v0.42.0 h1:d5Z6vnuQ6RnCCPIxZaDL+TH2ODLxT8abytOt+Zh+Kd0= github.com/frikky/kin-openapi v0.42.0/go.mod h1:ev9OZAw7Bv5p0w93j91++6a1ElPzGcCofst+kmrWsj4= -github.com/frikky/schemaless v0.0.25 h1:qXjrKT54LWl3tzDFIUmevZ86F7FiusPpQKfa40xxNl8= -github.com/frikky/schemaless v0.0.25/go.mod h1:m9s+6gALXhA5ZERCrJw+jI2rRtTPNa8mkl4vav9sxnY= +github.com/frikky/schemaless v0.0.28 h1:gdurMqBwtvY4Y/5pcxn8bdGCJn/eolKGz+c5DcidLkI= +github.com/frikky/schemaless v0.0.28/go.mod h1:m9s+6gALXhA5ZERCrJw+jI2rRtTPNa8mkl4vav9sxnY= github.com/fsouza/go-dockerclient v1.12.1 h1:FMoLq+Zhv9Oz/rFmu6JWkImfr6CBgZOPcL+bHW4gS0o= github.com/fsouza/go-dockerclient v1.12.1/go.mod h1:OqsgJJcpCwqyM3JED7TdfM9QVWS5O7jSYwXxYKmOooY= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= @@ -156,12 +158,12 @@ github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UN github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.16.1 h1:TuxMBWNL7R05tXsUGi0kh1vi4tq0WfXNLlIrAkXG1k8= -github.com/go-git/go-git/v5 v5.16.1/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= +github.com/go-git/go-git/v5 v5.16.5 h1:mdkuqblwr57kVfXri5TTH+nMFLNUxIj9Z7F5ykFbw5s= +github.com/go-git/go-git/v5 v5.16.5/go.mod h1:QOMLpNf1qxuSY4StA/ArOdfFR2TrKEjJiye2kel2m+M= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= -github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -179,6 +181,8 @@ github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+Gr github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -346,10 +350,10 @@ github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/shuffle/opensearch-go/v4 v4.0.0 h1:Mh85CD1MwOgXiFFYlzS1llnvdqL3CztRdR1ZT/SLIjU= github.com/shuffle/opensearch-go/v4 v4.0.0/go.mod h1:gVLZKQE5khQWMb68XBtgKrhu78oLGL2zHwAGnFMDwC0= -github.com/shuffle/shuffle-shared v0.9.69 h1:5iWegL3EoK0zbhEyoa6aotWymKhj1GvHqjFWuh9k3PI= -github.com/shuffle/shuffle-shared v0.9.69/go.mod h1:yONCd7n4eWdxXBJ7Cwu0hIuc9vMwgdSVzEKDl+l/o78= -github.com/shuffle/singul v0.0.20 h1:Lz+K4l2GJQ5W6o8ePr/2qlIcGuZuMC7W8Wj/JFw1XDI= -github.com/shuffle/singul v0.0.20/go.mod h1:qY3ZmwaNwmRApLWMTqZaNEMXO6X8bjr/zY44xDU62hQ= +github.com/shuffle/shuffle-shared v0.9.84 h1:b77E386LUk63k9dTN735/hcRuH2XY1cNgTvxpx3qS/s= +github.com/shuffle/shuffle-shared v0.9.84/go.mod h1:AkXajlWWB16WfWjCw9K7y38L8JKABJzVxcX6KI/J1H4= +github.com/shuffle/singul v0.0.26 h1:P2uZ8YIYQUN4qNfQujoW3Lhod91X6TAfrRHGWiUPNI8= +github.com/shuffle/singul v0.0.26/go.mod h1:S8GszXL+fT2mTnh7j57V0r/tY5Y/mSW8QowQEnADs3k= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= diff --git a/frontend/public/icons/workflow-page/buildIcon-dark.svg b/frontend/public/icons/workflow-page/buildIcon-dark.svg new file mode 100644 index 00000000..425f56d7 --- /dev/null +++ b/frontend/public/icons/workflow-page/buildIcon-dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/icons/workflow-page/buildIcon.svg b/frontend/public/icons/workflow-page/buildIcon.svg new file mode 100644 index 00000000..850358fc --- /dev/null +++ b/frontend/public/icons/workflow-page/buildIcon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/icons/workflow-page/comment-dark.svg b/frontend/public/icons/workflow-page/comment-dark.svg new file mode 100644 index 00000000..f584a57b --- /dev/null +++ b/frontend/public/icons/workflow-page/comment-dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/icons/workflow-page/comment.svg b/frontend/public/icons/workflow-page/comment.svg new file mode 100644 index 00000000..861dc26f --- /dev/null +++ b/frontend/public/icons/workflow-page/comment.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/icons/workflow-page/delete-dark.svg b/frontend/public/icons/workflow-page/delete-dark.svg new file mode 100644 index 00000000..10693a6e --- /dev/null +++ b/frontend/public/icons/workflow-page/delete-dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/public/icons/workflow-page/deleteIcon.svg b/frontend/public/icons/workflow-page/deleteIcon.svg new file mode 100644 index 00000000..8c6a6864 --- /dev/null +++ b/frontend/public/icons/workflow-page/deleteIcon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/public/icons/workflow-page/download-dark.svg b/frontend/public/icons/workflow-page/download-dark.svg new file mode 100644 index 00000000..023896d9 --- /dev/null +++ b/frontend/public/icons/workflow-page/download-dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/public/icons/workflow-page/download.svg b/frontend/public/icons/workflow-page/download.svg new file mode 100644 index 00000000..5c8b413a --- /dev/null +++ b/frontend/public/icons/workflow-page/download.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/public/icons/workflow-page/fitToScreen-dark.svg b/frontend/public/icons/workflow-page/fitToScreen-dark.svg new file mode 100644 index 00000000..e45f3e24 --- /dev/null +++ b/frontend/public/icons/workflow-page/fitToScreen-dark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/public/icons/workflow-page/fitToScreen.svg b/frontend/public/icons/workflow-page/fitToScreen.svg new file mode 100644 index 00000000..a7c5dfda --- /dev/null +++ b/frontend/public/icons/workflow-page/fitToScreen.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/public/icons/workflow-page/historyClock-dark.svg b/frontend/public/icons/workflow-page/historyClock-dark.svg new file mode 100644 index 00000000..f8aa5c4d --- /dev/null +++ b/frontend/public/icons/workflow-page/historyClock-dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/icons/workflow-page/historyClock.svg b/frontend/public/icons/workflow-page/historyClock.svg new file mode 100644 index 00000000..2e915249 --- /dev/null +++ b/frontend/public/icons/workflow-page/historyClock.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/icons/workflow-page/playButton.svg b/frontend/public/icons/workflow-page/playButton.svg new file mode 100644 index 00000000..c89577e6 --- /dev/null +++ b/frontend/public/icons/workflow-page/playButton.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/public/icons/workflow-page/run-dark.svg b/frontend/public/icons/workflow-page/run-dark.svg new file mode 100644 index 00000000..d3f3a554 --- /dev/null +++ b/frontend/public/icons/workflow-page/run-dark.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/frontend/public/icons/workflow-page/runningMan.svg b/frontend/public/icons/workflow-page/runningMan.svg new file mode 100644 index 00000000..d40f7b28 --- /dev/null +++ b/frontend/public/icons/workflow-page/runningMan.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/frontend/public/icons/workflow-page/shuffle_agent.png b/frontend/public/icons/workflow-page/shuffle_agent.png new file mode 100644 index 00000000..8605011f Binary files /dev/null and b/frontend/public/icons/workflow-page/shuffle_agent.png differ diff --git a/frontend/public/icons/workflow-page/undo-dark.svg b/frontend/public/icons/workflow-page/undo-dark.svg new file mode 100644 index 00000000..cacb1f8d --- /dev/null +++ b/frontend/public/icons/workflow-page/undo-dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/public/icons/workflow-page/undo.svg b/frontend/public/icons/workflow-page/undo.svg new file mode 100644 index 00000000..ae37efe4 --- /dev/null +++ b/frontend/public/icons/workflow-page/undo.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/public/icons/workflow-page/yaml-dark.svg b/frontend/public/icons/workflow-page/yaml-dark.svg new file mode 100644 index 00000000..f213139e --- /dev/null +++ b/frontend/public/icons/workflow-page/yaml-dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/public/icons/workflow-page/yaml.svg b/frontend/public/icons/workflow-page/yaml.svg new file mode 100644 index 00000000..e6c8126d --- /dev/null +++ b/frontend/public/icons/workflow-page/yaml.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/src/components/AdminNavBar.jsx b/frontend/src/components/AdminNavBar.jsx index f32a284f..6a771afd 100644 --- a/frontend/src/components/AdminNavBar.jsx +++ b/frontend/src/components/AdminNavBar.jsx @@ -211,15 +211,7 @@ const AdminNavBar = (props) => { } else if (userdata && isOrgLoaded && isUserDataLoaded && userdata?.active_org?.role !== "admin" && !userdata?.support) { const queryParams = new URLSearchParams(location.search); const tabName = queryParams?.get('admin_tab')?.toLowerCase(); - if (tabName === "sso") { - toast.info("You are not allowed to access this tab. Please contact your admin for more information. Redirecting to Organization Configuration tab."); - setTimeout(() => { - setSelectedItem("Organization"); - navigate(`?admin_tab=org_config`, { replace: true }); - window.location.reload(); - } - , 3000); - } + // SSO tab is now accessible to non-admins (removed restriction) const params = new URLSearchParams(location.search); const tab = params?.get('tab')?.toLowerCase(); diff --git a/frontend/src/components/AppAuthTab.jsx b/frontend/src/components/AppAuthTab.jsx index c2544d1a..7c404e1c 100644 --- a/frontend/src/components/AppAuthTab.jsx +++ b/frontend/src/components/AppAuthTab.jsx @@ -1050,7 +1050,7 @@ const AppAuthTab = memo((props) => { - {["Valid", "Label", "App Name", "Workflows", "Fields", "Edited", "Actions", "Distribution"].map((header, index) => ( + {["Logo", "Valid", "Label", "App Name", "Fields", "Edited", "Actions", "Distribute"].map((header, index) => ( { } return ( + + + + + + + )} + style={{ display: "table-cell", verticalAlign: 'middle', minWidth: 60 }} + primaryTypographyProps={{ + style: { + padding: "8px 8px 8px 15px", + } + }} + onClick={() => { + const url = `/apps/${data.app.id}` + window.open(url, "_blank") + }} + /> + + {validIcon} @@ -1202,22 +1226,7 @@ const AppAuthTab = memo((props) => { }} style={{ marginLeft: 10, display: "table-cell", textAlign: 'center', verticalAlign: 'middle', padding: 8 }} /> - + { verticalAlign: 'middle' }} /> + { - const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, newSelectedApp, setNewSelectedApp, defaultSearch, showSearch, ConfiguredHits, userdata, cy, isCreatorPage, actionImageList, setActionImageList, setUserSpecialzedApp, inputHeight, } = props + const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, newSelectedApp, setNewSelectedApp, defaultSearch, showSearch, ConfiguredHits, userdata, cy, isCreatorPage, actionImageList, setActionImageList, setUserSpecialzedApp, inputHeight, apps, } = props const { themeMode } = useContext(Context) const theme = getTheme(themeMode) - const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (import.meta.env.VITE_IS_SSR === "true"); + const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; const rowHandler = maxRows === undefined || maxRows === null ? 50 : maxRows const xs = parsedXs === undefined || parsedXs === null ? 12 : parsedXs const [formMail, setFormMail] = React.useState(""); @@ -75,7 +75,7 @@ const Appsearch = props => { color="primary" defaultValue={defaultSearch} // placeholder={`Find ${defaultSearch} Apps...`} - placeholder= {defaultSearch ? `${defaultSearch}` : "Search Cases "} + placeholder= {defaultSearch ? `${defaultSearch}` : "Search Apps "} id="shuffle_workflow_search_field" onChange={(event) => { refine(event.currentTarget.value) @@ -92,6 +92,30 @@ const Appsearch = props => { const [mouseHoverIndex, setMouseHoverIndex] = useState(-1) var counted = 0 + if (hits?.length < 20 && apps !== undefined && apps !== null && apps?.length > 0) { + // Find the textbox html + const findId = "shuffle_workflow_search_field" + const searchField = document.getElementById(findId) + const searchValue = searchField === null || searchField === undefined ? "" : searchField?.value + if (searchValue?.length > 0) { + const parsedsearch = searchValue.toLowerCase().replaceAll(" ", "") + for (var appkey in apps) { + const app = apps[appkey] + const parsedAppname = app?.name?.toLowerCase().replaceAll(" ", "") + if (parsedAppname.includes(parsedsearch)) { + hits.push({ + objectID: app.id, + id: app.id, + name: app.name, + image_url: app.large_image, + }) + } + } + } else { + console.log("No value in textfield: ", searchValue) + } + } + return ( {hits.map((data, index) => { diff --git a/frontend/src/components/AuthenticationModal.jsx b/frontend/src/components/AuthenticationModal.jsx index 62f15e7f..77d9eb94 100644 --- a/frontend/src/components/AuthenticationModal.jsx +++ b/frontend/src/components/AuthenticationModal.jsx @@ -97,6 +97,12 @@ const AuthenticationModal = (props) => { } if (authenticationModalOpen === false) { + // Add 5px for every character over 12 + var addednumber = selectedAppData?.name?.length > 12 ? (selectedAppData?.name?.length - 12) * 5 : 0 + if (addednumber > 50) { + addednumber = 50 + } + return ( + + )} ); }; @@ -2143,14 +2402,14 @@ const Billing = memo((props) => { return ( -
+
{isCloud ? null : } {addDealModal} {clickedFromOrgTab ? - Billing & Licensing + Billing & Licensing : Billing & Licensing diff --git a/frontend/src/components/CacheView.jsx b/frontend/src/components/CacheView.jsx index 56bd3a12..26b751a0 100644 --- a/frontend/src/components/CacheView.jsx +++ b/frontend/src/components/CacheView.jsx @@ -5,7 +5,7 @@ import { toast } from 'react-toastify'; import ReactJson from "react-json-view-ssr"; import { validateJson, handleReactJsonClipboard, GetIconInfo, } from "../views/Workflows2.jsx"; -import { red } from "../views/AngularWorkflow.jsx"; +import { green, red } from "../views/AngularWorkflow.jsx"; import CollectIngestModal from "../components/CollectIngestModal.jsx"; import CorrelationGraph from "../components/CorrelationGraph.jsx"; import { useNavigate, Link, useParams } from "react-router-dom"; @@ -37,9 +37,12 @@ import { Pagination, PaginationItem, Avatar, + ListSubheader, } from "@mui/material"; -import { DataGrid } from '@mui/x-data-grid'; +import { + DataGrid, +} from '@mui/x-data-grid'; import { Link as LinkIcon, @@ -52,6 +55,7 @@ import { OpenInNew as OpenInNewIcon, CloudDownload as CloudDownloadIcon, Description as DescriptionIcon, + Polymer as PolymerIcon, CheckCircle as CheckCircleIcon, Close as CloseIcon, Apps as AppsIcon, @@ -59,6 +63,7 @@ import { Cached as CachedIcon, AccessibilityNew as AccessibilityNewIcon, Lock as LockIcon, + Eco as EcoIcon, Schedule as ScheduleIcon, Cloud as CloudIcon, Business as BusinessIcon, @@ -66,6 +71,7 @@ import { VisibilityOff as VisibilityOffIcon, Clear as ClearIcon, Add as AddIcon, + Remove as RemoveIcon, Rocket as RocketIcon, Webhook as WebhookIcon, Air as AirIcon, @@ -76,6 +82,8 @@ import { FilterAlt as FilterAltIcon, CompareArrows as CompareArrowsIcon, Hub as HubIcon, + Key as KeyIcon, + FlashOn as FlashOnIcon, } from "@mui/icons-material"; import { Context } from "../context/ContextApi.jsx"; @@ -133,6 +141,7 @@ const CacheView = memo((props) => { const [selectAllChecked, setSelectAllChecked] = React.useState(false) const [renderTextBox, setRenderTextBox] = React.useState(false); const [datastoreCategories, setDatastoreCategories] = React.useState(["default", "protected"]); + const [datastoreCategoryGroups, setDatastoreCategoryGroups] = React.useState([]); const [selectedCategory, setSelectedCategory] = React.useState("default"); const [selectedFileId, setSelectedFileId] = React.useState(""); const [updateToThisCategory, setUpdateToThisCategory] = useState("") @@ -163,6 +172,18 @@ const CacheView = memo((props) => { var to_be_copied = ""; const defaultAutomation = [ + { + "name": "Security Rules", + "description": "Describes security rules that are validated BEFORE an update occurs. This is in order for bad writes to be avoided. Control: allow, deny, merge, overwrite. Logic: if, or, and. Functions: same_shape, is_superset, has_deleted_field", + "options": [{ + "key": "rule", + "value": "", + }], + "icon": , + "beta": true, + "disabled": false, + "enabled": false, + }, { "name": "Run workflow", "description": "Runs one or more workflows with the updated value as runtime argument", @@ -200,14 +221,16 @@ const CacheView = memo((props) => { { "name": "Run AI Agent", - "description": "Runs an AI Agent to process the updated value. Uses built-in ShuffleAI configs. Learn more: https://shuffler.io/docs/AI", + "description": "Runs an AI Agent to process the updated value. Controllable based on your 'action' input, where the full updated value is added on the end. All actions run in order, meaning it needs to finish the previous one before the next is ran. Uses built-in ShuffleAI configs. Learn more: https://shuffler.io/docs/AI", + "type": "singul", "options": [{ - "key": "", + "key": "action-1", + "description": "Describe the action to perform on this item", "value": "", }], + "beta": true, "icon": , - "enabled": false, - "disabled": true, + "disabled": false, }, { @@ -218,22 +241,10 @@ const CacheView = memo((props) => { "key": "", "value": "", }], + "beta": true, "icon": "/images/logos/singul.svg", "disabled": false, }, - - { - "name": "Send message", - "description": "", - "type": "singul", - "options": [{ - "key": "app", - "value": "", - }], - "icon": , - "disabled": true, - "enabled": false, - }, ] const [categoryAutomations, setCategoryAutomations] = useState(defaultAutomation) @@ -317,8 +328,21 @@ const CacheView = memo((props) => { var chosenCategory = selectedCategory const urlParams = new URLSearchParams(window.location.search) - const categoryParam = urlParams.get("category") + var categoryParam = urlParams.get("category") if (categoryParam && categoryParam !== undefined && categoryParam !== "default" && categoryParam !== "") { + + // In order to make linking weird urls from workflow page work. + if (urlParams.get("src") == "workflow") { + if (categoryParam?.toLowerCase().startsWith("list")) { + const newParam = categoryParam.substring(5).replaceAll("%20", "_") + + // Replace the url param. There may be more params. + urlParams.set("category", newParam) + window.history.replaceState({}, '', `${window.location.pathname}?${urlParams.toString()}`) + categoryParam = newParam + } + } + chosenCategory = categoryParam setSelectedCategory(categoryParam) } @@ -447,7 +471,31 @@ const CacheView = memo((props) => { } } + var foundstartwords = {} + var categorygroups = [] + for (var key in newcategories) { + const category = newcategories[key] + if (!category.includes("_")) { + continue + } + + const startword = category.split("_")[0] + if (startword.length <= 2) { + continue + } + + if (!foundstartwords.hasOwnProperty(startword)) { + foundstartwords[startword] = 1 + } else { + foundstartwords[startword] += 1 + if (foundstartwords[startword] === 2) { + categorygroups.push(startword) + } + } + } + setDatastoreCategories(newcategories) + setDatastoreCategoryGroups(categorygroups) } if (responseJson?.category_config !== undefined && responseJson?.category_config !== null) { @@ -463,6 +511,8 @@ const CacheView = memo((props) => { //if (responseJson.category_config.automations[key].icon === undefined || responseJson.category_config.automations[key].icon === null || responseJson.category_config.automations[key].icon === "") { const foundItem = defaultAutomation.find((automation) => automation.name === responseJson.category_config.automations[key].name) if (foundItem) { + responseJson.category_config.automations[key].description = foundItem.description + responseJson.category_config.automations[key].beta = foundItem.beta responseJson.category_config.automations[key].disabled = foundItem.disabled responseJson.category_config.automations[key].icon = foundItem.icon responseJson.category_config.automations[key].type = foundItem?.type @@ -542,6 +592,7 @@ const CacheView = memo((props) => { key: dataValue.key, value: value, category: selectedCategory, + tags: dataValue?.tags || [], } if (dataValue?.category !== undefined && dataValue?.category !== "" && dataValue?.category !== "default") { @@ -558,7 +609,9 @@ const CacheView = memo((props) => { setCacheInput([entry]); - fetch(globalUrl + `/api/v1/orgs/${orgId}/set_cache`, { + //entry = [entry] + const parsedUrl = `${globalUrl}/api/v2/datastore` + fetch(parsedUrl, { method: "POST", headers: { @@ -780,6 +833,37 @@ const CacheView = memo((props) => { Category: {dataValue.category} : null} + {dataValue?.tags !== undefined && dataValue?.tags !== null && dataValue?.tags?.length > 0 ? +
+ + Tags + + + {dataValue.tags?.map((tag, index) => { + return ( + { + var newTags = dataValue.tags.filter((t) => t !== tag) + if (newTags.length === 0) { + newTags = ["none"] + } else if (newTags.length > 1) { + newTags = newTags.filter((t) => t !== "none") + } + + setDataValue({ + ...dataValue, + tags: newTags, + }) + + }} + /> + ) + })} +
+ : null}
: null} @@ -1065,7 +1149,6 @@ const CacheView = memo((props) => { saveAutomation(newAutomations) } - console.log("AUTO: ", automation) return ( @@ -1089,7 +1172,7 @@ const CacheView = memo((props) => { style={{ marginRight: 10, marginTop: -10, }} checked={automation.enabled} // Check if automation options have a value - disabled={automation.name !== "Enrich" && (automation?.disabled === true || automation.options.length === 0 || automation.options.some((option) => option.value === ""))} + disabled={automation.name !== "Enrich" && automation.name != "Run AI Agent" && (automation?.disabled === true || automation.options.length === 0 || automation.options.some((option) => option.value === ""))} onChange={(e) => { e.stopPropagation() e.preventDefault() @@ -1140,6 +1223,19 @@ const CacheView = memo((props) => { }}> {automation.name} + {automation?.beta !== true ? null : + + }
{automation?.disabled !== true ? @@ -1166,8 +1262,9 @@ const CacheView = memo((props) => { : null}
- {showOptions && ( - updatedAutomation.options.map((option, optionIndex) => { + {showOptions && +
+ {updatedAutomation.options.map((option, optionIndex) => { if (option?.key === "datastore_categories") { if (datastoreCategories === undefined || datastoreCategories === null || datastoreCategories.length <= 1) { return ( @@ -1446,6 +1543,87 @@ const CacheView = memo((props) => { }} /> ) + } else if (updatedAutomation?.name === "Run AI Agent") { + return ( + + { + if (e.target.value === "") { + updatedAutomation.enabled = false + } else { + updatedAutomation.enabled = true + } + + updatedAutomation.options[optionIndex].value = e.target.value; + setUpdatedAutomation(updatedAutomation) + setUpdated(true) + }} + /> + + {optionIndex === updatedAutomation.options.length - 1 ? + + : + + + + } + + + ) } return ( @@ -1478,8 +1656,9 @@ const CacheView = memo((props) => { }} /> ) - }) - )} + })} +
+ } ) @@ -1506,6 +1685,8 @@ const CacheView = memo((props) => { ) } + const urlParams = new URLSearchParams(window?.location?.search) + const highlightedKey = urlParams.get("key") || "" const columns = [ { field: 'key', @@ -1513,6 +1694,24 @@ const CacheView = memo((props) => { width: 200, filterable: true, sortable: true, + renderCell: (props) => { + return ( + + {props.row.key} + + ) + } }, { width: 540, @@ -1552,8 +1751,10 @@ const CacheView = memo((props) => { backgroundColor: theme.palette.platformColor, border: theme.palette.defaultBorder, padding: 5, + maxWidth: 500, minWidth: 500, maxHeight: 500, + overflowX: "auto", overflowY: "auto", }} collapsed={true} @@ -1735,6 +1936,7 @@ const CacheView = memo((props) => { "created": data.created, "workflow_id": data.workflow_id, "category": data.category, + "tags": data.tags, }) setValue(newvalue) setModalOpen(true) @@ -1914,7 +2116,7 @@ const CacheView = memo((props) => { }, ]; - + var previousgroup = "" const isAutomating = categoryAutomations?.find((automation) => automation.enabled) !== undefined return (
{ - - {datastoreCategories !== undefined && + + {datastoreCategories !== undefined && datastoreCategories !== null && datastoreCategories.length > 1 ? ( - - - Category - - + */} ) : null} @@ -2154,7 +2440,7 @@ const CacheView = memo((props) => { : - + @@ -2403,7 +2688,7 @@ const CacheView = memo((props) => { Subscribing - Enabling this feature will allow other organizations to subscribe to this category. This is NOT fully available yet. + Enabling subscriptions allows other organizations to subscribe to this category and receive updates when keys are added or modified. Makes the category searchable.
diff --git a/frontend/src/components/DiscordChat.jsx b/frontend/src/components/DiscordChat.jsx index 07394852..2bbc3b2c 100644 --- a/frontend/src/components/DiscordChat.jsx +++ b/frontend/src/components/DiscordChat.jsx @@ -223,14 +223,14 @@ const DiscordChat = props => { {formMessage} - + {/* Search by Algolia logo - + */} ); }; diff --git a/frontend/src/components/DocsGrid.jsx b/frontend/src/components/DocsGrid.jsx index ada42721..57616bd2 100644 --- a/frontend/src/components/DocsGrid.jsx +++ b/frontend/src/components/DocsGrid.jsx @@ -349,14 +349,14 @@ const DocsGrid = props => { : null } - + {/* Search by Algolia logo - + */} ) diff --git a/frontend/src/components/EnvironmentTab.jsx b/frontend/src/components/EnvironmentTab.jsx index cc4cc68f..58dd47d5 100644 --- a/frontend/src/components/EnvironmentTab.jsx +++ b/frontend/src/components/EnvironmentTab.jsx @@ -432,7 +432,7 @@ const EnvironmentTab = memo((props) => { -e SHUFFLE_SWARM_CONFIG=run \\ -e BASE_URL="${newUrl}" \\${addProxy ? ` -e HTTPS_PROXY=IP:PORT \\` : ""}${skipPipeline ? ` - -e SHUFFLE_PIPELINE_URL=http://tenzir-node:5160 \ \n -e SHUFFLE_PIPELINE_STANDALONE=true \\` : ""}${!showDetection ? ` + -e SHUFFLE_PIPELINE_URL=http://tenzir-node:5160 \\\n -e SHUFFLE_PIPELINE_STANDALONE=true \\` : ""}${!showDetection ? ` -v /tmp:/tmp \\` : ""} ghcr.io/shuffle/shuffle-orborus:latest `) @@ -1225,7 +1225,9 @@ const EnvironmentTab = memo((props) => { : (environment.running_ip === undefined || environment.running_ip === null || - environment.running_ip.length === 0) + environment.running_ip.length === 0) || + environment?.checkin === 0 || + Date.now() / 1000 - environment?.checkin > 180 ? + {params?.row?.filename}
+ {params?.row?.tags} + + } placement="left" arrow > @@ -1580,8 +1584,8 @@ const [filesLoaded, setFilesLoaded] = useState(false); columns={columns} checkboxSelection disableRowSelectionOnClick - selectionModel={selectedRows} - onSelectionModelChange={(newSelection) => { + rowSelectionModel={selectedRows} + onRowSelectionModelChange={(newSelection) => { setSelectedRows(newSelection); }} sx={{ diff --git a/frontend/src/components/LeftSideBar.jsx b/frontend/src/components/LeftSideBar.jsx index 5245c9bc..bf8fbd9b 100644 --- a/frontend/src/components/LeftSideBar.jsx +++ b/frontend/src/components/LeftSideBar.jsx @@ -2,14 +2,13 @@ import React, { useEffect, useRef, useState, useContext, useCallback, useMemo, m import { ExpandLess as ExpandLessIcon, ExpandMore as ExpandMoreIcon, - GridView as GridViewIcon, - ShieldOutlined as ShieldOutlinedIcon, - Add as AddIcon, - BorderColor, - Close as CloseIcon, - ConstructionOutlined as ConstructionOutlinedIcon, Toc as TocIcon, - Settings as SettingsIcon + Settings as SettingsIcon, + Business as BusinessIcon, + HelpOutline as HelpOutlineIcon, + MeetingRoom as MeetingRoomIcon, + Search as SearchIcon, + Add as AddIcon } from "@mui/icons-material"; import LightModeIcon from '@mui/icons-material/LightMode'; import DarkModeIcon from '@mui/icons-material/DarkMode'; @@ -44,23 +43,17 @@ import { useNavigate } from "react-router"; import { getTheme } from "../theme.jsx"; import { Link } from "react-router-dom"; -import { - Business as BusinessIcon, - Notifications as NotificationsIcon, - HelpOutline as HelpOutlineIcon, - MeetingRoom as MeetingRoomIcon, - Lightbulb as LightbulbIcon, - Search as SearchIcon -} from "@mui/icons-material"; import { toast } from "react-toastify"; import { Context } from "../context/ContextApi.jsx"; +import Licensed from "./Licensed.jsx"; const ShuffleLogo = "/images/Shuffle_logo.png"; const detectionIcon = "/icons/detection.svg"; const documentationIcon = "/icons/documentation.svg"; const ExpandMoreAndLessIcon = "/icons/expandMoreIcon.svg"; + const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => { const navigate = useNavigate(); @@ -206,7 +199,11 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => { setOpenAutocomplete(false) }} > - Add suborg + @@ -1926,72 +1923,48 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => { style={{ display: "flex", flexDirection: "column", - padding: 10, - marginLeft: 5, + padding: "0 8px", + marginBottom: 8, }} > - - {userdata?.licensed !== true && !userdata?.org_status?.includes("integration_partner") && expandLeftNav && !isProdStatusOn && -
- -
- } - - {!isCloud && activeOrgData?.old_org? ( -
{ - navigate("/admin?admin_tab=billingstats") - }} - > - + {userdata?.licensed !== true && !userdata?.org_status?.includes("integration_partner") && !isProdStatusOn && ( +
- ) : null} + onClick={() => { + if (isCloud) { + window.open("https://shuffler.io/contact?category=book_a_demo&ref=cloud", "_blank") + } else { + window.open("https://shuffler.io/contact?category=book_a_demo&ref=onprem", "_blank") + } + }} + > + Book a Demo + + )} + {!isCloud && (activeOrgData?.old_org || isProdStatusOn) && ( + + + )} + ) ) -}); +}); \ No newline at end of file diff --git a/frontend/src/components/LicencePopup.jsx b/frontend/src/components/LicencePopup.jsx index 322d0840..54611352 100644 --- a/frontend/src/components/LicencePopup.jsx +++ b/frontend/src/components/LicencePopup.jsx @@ -1319,8 +1319,14 @@ const LicencePopup = (props) => { > {`${ isPaidPlan ? "Next billing: " : "App runs resets on " - }${new Date( - (localSub.enddate || localSub.Enddate) * 1000 + }${( + isPaidPlan + ? new Date((localSub.enddate || localSub.Enddate) * 1000) + : new Date( + new Date().getFullYear(), + new Date().getMonth() + 1, + 1 + ) ).toLocaleDateString(undefined, { day: "2-digit", month: "short", diff --git a/frontend/src/components/Navbar.jsx b/frontend/src/components/Navbar.jsx index 63efe7a9..18dc748c 100644 --- a/frontend/src/components/Navbar.jsx +++ b/frontend/src/components/Navbar.jsx @@ -183,7 +183,7 @@ const menuData = { title: "Articles", icon: "/images/icons/articles.svg", hoverIcon: "/images/icons/articles_hover.svg", - link: "/articles/2.0_release", + link: "/articles", gaData: { category: "navbar", action: "resources_click", diff --git a/frontend/src/components/Oauth2Auth.jsx b/frontend/src/components/Oauth2Auth.jsx index f9db63b3..8e82ae62 100755 --- a/frontend/src/components/Oauth2Auth.jsx +++ b/frontend/src/components/Oauth2Auth.jsx @@ -535,7 +535,7 @@ const AuthenticationOauth2 = (props) => { } } - toast("Authentication successful!") + toast.info("Authentication window closed") // This is more a guess than anything // Should be handled in getAppAuthentication() diff --git a/frontend/src/components/OrgHeaderexpanded.jsx b/frontend/src/components/OrgHeaderexpanded.jsx index 059ad8ac..3b1c8808 100644 --- a/frontend/src/components/OrgHeaderexpanded.jsx +++ b/frontend/src/components/OrgHeaderexpanded.jsx @@ -343,7 +343,7 @@ console.log("defatult in handleEditOrg", defaults) const url = `${globalUrl}/api/v1/orgs/${selectedOrganization?.id}/change`; const data = { org_id: selectedOrganization?.id, - sso_test: true, + sso: true, }; fetch(url, { diff --git a/frontend/src/components/OrganizationTab.jsx b/frontend/src/components/OrganizationTab.jsx index be65b078..f66bcd32 100644 --- a/frontend/src/components/OrganizationTab.jsx +++ b/frontend/src/components/OrganizationTab.jsx @@ -62,11 +62,8 @@ const OrganizationTab = (props) => { if (isIntegrationPartner && isChildOrg && !isGlobalUser) { setVisibleTabs(items.filter((item) => item !== 'Branding' && item !== 'SSO')); } else { - if (userdata && userdata.active_org && userdata.active_org.role === 'admin') { - setVisibleTabs(items); - } else { - setVisibleTabs(items.filter((item) => item !== 'SSO')); - } + // Show SSO tab for all users now + setVisibleTabs(items); } //if (isCloud) { diff --git a/frontend/src/components/ParsedActionNew.jsx b/frontend/src/components/ParsedActionNew.jsx new file mode 100644 index 00000000..883506b2 --- /dev/null +++ b/frontend/src/components/ParsedActionNew.jsx @@ -0,0 +1,5693 @@ +import React, { useState, useEffect, useContext, useMemo } from "react"; +import { toast } from 'react-toastify'; +import { makeStyles, createStyles } from "@mui/styles"; +import {getTheme} from '../theme.jsx'; + +import { useNavigate, Link, useParams } from "react-router-dom"; +import { validateJson, GetIconInfo } from "../views/Workflows2.jsx"; +import { GetParsedPaths } from "../views/Apps.jsx"; +import { sortByKey, getActionState, setActionState } from "../views/AngularWorkflow.jsx"; +import { NestedMenuItem } from "mui-nested-menu"; +import { parsedDatatypeImages } from "../components/AppFramework.jsx"; +import { green, yellow, red } from "../views/AngularWorkflow.jsx" +//import { useAlert +import { Context } from "../context/ContextApi.jsx"; +import { + Chip, + ButtonGroup, + Popper, + TextField, + TextareaAutosize, + Drawer, + Button, + Paper, + Tabs, + InputAdornment, + Tab, + ButtonBase, + Tooltip, + Select, + MenuItem, + Divider, + Dialog, + Modal, + DialogActions, + DialogTitle, + InputLabel, + DialogContent, + FormControl, + IconButton, + Menu, + Input, + FormGroup, + FormControlLabel, + Typography, + Checkbox, + Breadcrumbs, + CircularProgress, + Switch, + Collapse, + Autocomplete, + Box, + ToggleButtonGroup, + ToggleButton, + Skeleton, +} from "@mui/material"; + +import { + HelpOutline as HelpOutlineIcon, + OpenInFull as OpenInFullIcon, + Description as DescriptionIcon, + GetApp as GetAppIcon, + Search as SearchIcon, + ArrowUpward as ArrowUpwardIcon, + Visibility as VisibilityIcon, + Done as DoneIcon, + Close as CloseIcon, + Error as ErrorIcon, + FindReplace as FindreplaceIcon, + ArrowLeft as ArrowLeftIcon, + Cached as CachedIcon, + DirectionsRun as DirectionsRunIcon, + Add as AddIcon, + Polymer as PolymerIcon, + FormatListNumbered as FormatListNumberedIcon, + Create as CreateIcon, + PlayArrow as PlayArrowIcon, + AspectRatio as AspectRatioIcon, + MoreVert as MoreVertIcon, + Apps as AppsIcon, + Schedule as ScheduleIcon, + FavoriteBorder as FavoriteBorderIcon, + Pause as PauseIcon, + Delete as DeleteIcon, + AddCircleOutline as AddCircleOutlineIcon, + Save as SaveIcon, + KeyboardArrowLeft as KeyboardArrowLeftIcon, + KeyboardArrowRight as KeyboardArrowRightIcon, + ArrowBack as ArrowBackIcon, + Settings as SettingsIcon, + LockOpen as LockOpenIcon, + ExpandMore as ExpandMoreIcon, + VpnKey as VpnKeyIcon, + AutoFixHigh as AutoFixHighIcon, + Circle as CircleIcon, + SquareFoot as SquareFootIcon, + Storage as StorageIcon, + Check as CheckIcon, + PriorityHigh as PriorityHighIcon, + Restore as RestoreIcon, +} from '@mui/icons-material'; + +export const useStyles = makeStyles({ + root: { + "& .MuiAutocomplete-listbox": { + border: "2px solid grey", + color: "white", + fontSize: 18, + "& li:nth-child(even)": { + backgroundColor: "#CCC", + }, + "& li:nth-child(odd)": { + backgroundColor: "#FFF", + }, + }, + }, + inputRoot: { + color: "white", + "&:hover .MuiOutlinedInput-notchedOutline": { + borderColor: "#f86a3e", + }, + }, +}); + +const openApiFieldDesc = "Generated by OpenAPI body example"; +const ParsedAction = (props) => { + const { + workflow, + files, + setWorkflow, + setAction, + setSelectedAction, + setUpdate, + appActionArguments, + selectedApp, + workflowExecutions, + setSelectedResult, + selectedAction, + setSelectedApp, + setSelectedTrigger, + setSelectedEdge, + setCurrentView, + cy, + setAuthenticationModalOpen, + setVariablesModalOpen, + setCodeModalOpen, + selectedNameChange, + rightsidebarStyle, + showEnvironment, + selectedActionEnvironment, + environments, + setNewSelectedAction, + appApiViewStyle, + globalUrl, + setSelectedActionEnvironment, + requiresAuthentication, + setRequiresAuthentication, + hideExtraTypes, + scrollConfig, + setScrollConfig, + authenticationType, + appAuthentication, + getAppAuthentication, + actionDelayChange, + getParents, + isCloud, + lastSaved, + setLastSaved, + setShowVideo, + toolsAppId, + aiSubmit, + + expansionModalOpen, + setExpansionModalOpen, + fixExample, + + listCache, + setActiveDialog, + authGroups, + apps, + setEditorData, + setcodedata, + setAiQueryModalOpen, + + suborgWorkflows, + originalWorkflow, + runFromHere, + } = props; + + let navigate = useNavigate() + const classes = useStyles() + + const [hideBody, setHideBody] = React.useState(false) + const [activateHidingBodyButton, setActivateHidingBodyButton] = React.useState(false) + const [appActionName, setAppActionName] = React.useState(selectedAction?.label); + const [delay, setDelay] = React.useState(selectedAction?.execution_delay || 0); + const [prevActionName, setPrevActionName] = React.useState(selectedAction?.label); + const [fieldCount, setFieldCount] = React.useState(0); + const [hiddenDescription, setHiddenDescription] = React.useState(true); + const [hiddenParameters, setHiddenParameters] = React.useState(true); + const [autoCompleting, setAutocompleting] = React.useState(false); + const [selectedActionParameters, setSelectedActionParameters] = React.useState(selectedAction?.parameters || []); + const [selectedVariableParameter, setSelectedVariableParameter] = React.useState(""); + const [paramUpdate, setParamUpdate] = React.useState(""); + const [actionlist, setActionlist] = React.useState([]); + const [jsonList, setJsonList] = React.useState([]); + const [showDropdown, setShowDropdown] = React.useState(false); + const [showDropdownNumber, setShowDropdownNumber] = React.useState(0); + const [showAutocomplete, setShowAutocomplete] = React.useState(false); + const [menuPosition, setMenuPosition] = useState(null); + const [uiBox, setUiBox] = useState(null); + const [parentAction, setParentAction] = useState(null); + const isIntegration = selectedAction.app_id === "integration" + const isAgent = selectedAction.app_id === "shuffle_agent" + const [distributeAuthToSuborgs, setDistributeAuthToSuborgs] = useState(selectedAction?.selectedAuthentication?.suborg_distributed || false) + const [activeMainTab, setActiveMainTab] = useState("setup") + const [optionalParamsOpen, setOptionalParamsOpen] = useState(false) + const [authSectionOpen, setAuthSectionOpen] = useState(true) + + useEffect(() => { + if (setLastSaved !== undefined) { + setLastSaved(false) + } + }, [expansionModalOpen]) + + const {themeMode, supportEmail} = useContext(Context) + const theme = getTheme(themeMode) + + const isSidebarLoading = + !selectedAction || + !selectedApp || + selectedActionParameters === undefined || + selectedActionParameters === null; + + /* + useEffect(() => { + // This will have the OLD selectedAction, not the new one huh? + // How do we map the fields correctly? + if (selectedAction === undefined || selectedAction === null) { + console.log("Selected action is undefined") + return + } + + console.log("Selected action: ", selectedAction?.name, selectedAction) + + }, [selectedAction]) + */ + + useEffect(() => { + // Changes the order of params to show in order: + // auth, required, optional + var changed = false + if (selectedActionParameters === undefined || selectedActionParameters === null || selectedActionParameters.length === 0) { + console.log("Returning because no params") + return + } + + if (selectedApp !== undefined && selectedApp !== null && selectedApp.generated !== true) { + + if (isAgent || isIntegration) { + } else { + return + } + } + + // Fixing required fields with a shitty structure :) + if (selectedApp !== undefined && selectedApp !== null && selectedApp.generated === true && selectedAction !== undefined && selectedAction !== null && selectedAction.name !== undefined && selectedAction.name !== null && selectedApp.actions !== undefined && selectedApp.actions !== null && selectedApp.actions.length > 0 && (selectedAction.required_body_fields === undefined || selectedAction.required_body_fields === null || selectedAction.required_body_fields.length === 0)) { + // Check for required fields + for (var actionkey in selectedApp.actions) { + var action = selectedApp.actions[actionkey] + if (action.name === selectedAction.name) { + selectedAction.required_body_fields = action.required_body_fields + break + } + } + } + + // Check if missing parameters? + var auth = [] + var required = [] + var optional = [] + + var bodyfield = [] + var special_optional = [] + var generated_optional = [] + + var keyorder = [] + + for (let paramkey in selectedActionParameters) { + var param = selectedActionParameters[paramkey] + keyorder.push(param.name) + + if (param?.configuration === true) { + auth.push(param) + continue + } + + if (param?.value === undefined || param?.value === null) { + console.log("Invalid value: ", param) + continue + } + + if (typeof param.value === "array") { + param.value = param.value.join(",") + } + + if (typeof param.value === "object") { + try { + // Check if it's an array or object + if (param.value.length !== undefined) { + param.value = param.value.join(",") + } else { + param.value = JSON.stringify(param.value) + } + } catch (e) { + console.log("Error parsing JSON for param value: ", param, e) + continue + } + } + + if (param?.value?.toLowerCase()?.includes("secret. replace")) { + param.value = "" + } + + if (selectedApp?.generated === true && param?.name === "body") { + param.required = true + bodyfield.push(param) + continue + } + + if (param?.required === false && param?.name?.startsWith("${") && param?.name?.endsWith("}")) { + // Check if it's a required param + param.autocompleted = true + if (selectedAction.required_body_fields !== undefined && selectedAction.required_body_fields !== null && selectedAction.required_body_fields.length > 0) { + + if (selectedAction.required_body_fields.includes(param.name)) { + param.required = true + } + } + } + + if (param.required) { + required.push(param) + continue + } + + if (param?.name === "headers" || param?.name === "queries") { + special_optional.push(param) + continue + } + + if (hideBody && param?.description?.includes("Generated")) { + continue + } + + if (param?.field_active === true) { + param.autocompleted = true + generated_optional.push(param) + continue + } + + optional.push(param) + } + + // Sort order: auth > body(used for simple/advanced) > required > optional + // Optional field order: + // 1. headers & queries + // 2. other fields + // 3. generated fields & all else + + + var newparams = auth + .concat(bodyfield) + .concat(required) + .concat(generated_optional) + .concat(special_optional) + .concat(optional) + + const dedupedParams = [] + for (var paramKey in newparams) { + const param = newparams[paramKey] + if (dedupedParams.find(item => item.name === param.name) === undefined) { + dedupedParams.push(param) + } + } + + newparams = dedupedParams + + var newkeyorder = [] + for (let paramkey in newparams) { + //console.log("Param: ", newparams[paramkey]) + + newkeyorder.push(newparams[paramkey].name) + } + + if (keyorder.join(",") !== newkeyorder.join(",")) { + //console.log("KEYORDER CHANGED! DID ACTION AS WELL?", keyorder, newkeyorder) + + setSelectedActionParameters(newparams) + selectedAction.parameters = newparams + setSelectedAction(selectedAction) + } + }, [selectedActionParameters]) + + useEffect(() => { + const shouldHide = localStorage.getItem("hideBody") + if (shouldHide !== null) { + const ishiding = shouldHide !== "true" + if (ishiding !== hideBody) { + setHideBody(ishiding) + } + } + + if (selectedActionEnvironment === undefined || selectedActionEnvironment === null || Object.keys(selectedActionEnvironment).length === 0) { + if (environments !== undefined && environments !== null && environments.length > 0) { + if (selectedAction.environment !== undefined && selectedAction.environment !== null) { + const foundenv = environments.find(env => env.id === selectedAction.environment || selectedAction.environment === env.Name) + if (foundenv !== undefined && foundenv !== null) { + setSelectedActionEnvironment(foundenv) + } else { + // Fallback: ensure we always have some environment selected + setSelectedActionEnvironment(environments[0]) + } + } else { + // No environment set on the action: default to first available + setSelectedActionEnvironment(environments[0]) + } + } + } + + // Fix apps with fewer actions + if (selectedApp !== undefined && selectedApp !== null && selectedApp.actions !== undefined && selectedApp.actions !== null && selectedApp.actions.length <= 1 && apps !== undefined && apps !== null && apps.length > 0) { + // 1. Check local storage (?) + // 2. Check the "apps" list + const foundApp = apps.find(app => app.id === selectedApp.id) + if (foundApp !== undefined && foundApp !== null && foundApp.actions !== undefined && foundApp.actions !== null && foundApp.actions.length > 1) { + setSelectedApp(foundApp) + } + + } + }, [selectedActionEnvironment, selectedAction, environments]) + + const keywords = [ + "len(", + "lower(", + "upper(", + "trim(", + "split(", + "length(", + "number(", + "parse(", + "join(", + ]; + + const getHttpMethodFromName = (actionName) => { + if (actionName === undefined || actionName === null) { + return "" + } + + const lowerName = actionName.toLowerCase() + if (lowerName.includes("get_")) { + return "GET" + } else if (lowerName.includes("post_")) { + return "POST" + } else if (lowerName.includes("put_")) { + return "PUT" + } else if (lowerName.includes("patch_")) { + return "PATCH" + } else if (lowerName.includes("delete_")) { + return "DELETE" + } else if (lowerName.includes("options_")) { + return "OPTIONS" + } else if (lowerName.includes("connect_")) { + return "CONNECT" + } + + return "" + } + + const getApp = (appId, setApp) => { + const url = `${globalUrl}/api/v1/apps/${appId}/config?openapi=false`; + fetch(url, { + headers: { + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status === 200) { + //toast("Successfully GOT app "+appId) + } else { + toast.error("Failed getting app. Please try again or contact support@shuffler.io"); + } + + return response.json(); + }) + .then((responseJson) => { + console.log("RESPONSE: ", responseJson); + + const parsedapp = + responseJson.app !== undefined && responseJson.app !== null + ? JSON.parse(atob(responseJson.app)) + : {}; + console.log("PARSED: ", parsedapp); + //data = parsedapp.body === undefined ? parsedapp : parsedapp.body + + if ( + setApp && + parsedapp.actions !== undefined && + parsedapp.actions !== null + ) { + console.log("Inside first if"); + if ( + selectedApp.versions !== undefined && + selectedApp.versions !== null + ) { + parsedapp.versions = selectedApp.versions; + } + + if ( + selectedApp.loop_versions !== undefined && + selectedApp.loop_versions !== null + ) { + parsedapp.loop_versions = selectedApp.loop_versions; + } + + // Find authentication, and if it works? + // If authentication has less OR more fields, it has to change + //console.log(selected + + console.log("Inside first if2"); + var foundAction = parsedapp.actions.find( + (action) => + action.name.toLowerCase() === selectedAction.name.toLowerCase() + ); + if (foundAction !== null && foundAction !== undefined) { + var foundparams = []; + for (let [paramkey, paramkeyval] in Object.entries(foundAction.parameters)) { + const param = foundAction.parameters[paramkey]; + + const foundParam = selectedAction.parameters.find( + (item) => item.name.toLowerCase() === param.name.toLowerCase() + ); + if (foundParam === undefined) { + console.log("COULDNT find Param: ", param); + } else { + foundAction.parameters[paramkey] = foundParam; + } + //foundparams.push(param.name) + } + } else { + toast("Couldn't find action " + selectedAction.name); + } + + selectedAction.errors = []; + selectedAction.is_valid = true; + + // Updating params for the new action + selectedAction.parameters = foundAction.parameters; + selectedAction.app_id = appId; + selectedAction.app_version = parsedapp.app_version; + + setSelectedAction(selectedAction); + setSelectedApp(parsedapp); + } + }) + .catch((error) => { + toast(error.toString()); + }); + }; + + const changeDistribution = (data) => { + editAuthenticationConfig(data.id, "suborg_distribute") + } + + const editAuthenticationConfig = (id, parentAction) => { + const data = { + id: id, + action: parentAction !== undefined && parentAction !== null ? parentAction : "assign_everywhere", + } + + const url = globalUrl + "/api/v1/apps/authentication/" + id + "/config"; + + fetch(url, { + mode: "cors", + method: "POST", + body: JSON.stringify(data), + credentials: "include", + crossDomain: true, + withCredentials: true, + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }) + .then((response) => + response.json().then((responseJson) => { + if (responseJson["success"] === false) { + toast("Failed overwriting appauth"); + } else { + if (distributeAuthToSuborgs) { + toast.success("Successfully updated auth"); + } else { + toast.success("Successfully distributed auth to suborgs"); + } + setTimeout(() => { + getAppAuthentication(); + setDistributeAuthToSuborgs(!distributeAuthToSuborgs) + }, 1000); + } + }) + ) + .catch((error) => { + toast("Err: " + error.toString()); + }); + }; + + const defineStartnode = () => { + if (cy === undefined) { + return; + } + + var oldstartnode = cy.getElementById(workflow.start); + if (oldstartnode.length > 0) { + oldstartnode[0].data("isStartNode", false); + var oldnodecnt = workflow.actions.findIndex( + (a) => a.id === workflow.start + ); + if (workflow.actions[oldnodecnt] !== undefined) { + workflow.actions[oldnodecnt].isStartNode = false; + } + } + + var newstartnode = cy.getElementById(selectedAction.id); + if (newstartnode.length > 0) { + newstartnode[0].data("isStartNode", true); + var newnodecnt = workflow.actions.findIndex( + (a) => a.id === selectedAction.id + ); + console.log("NEW NODE CNT: ", newnodecnt); + if (workflow.actions[newnodecnt] !== undefined) { + workflow.actions[newnodecnt].isStartNode = true; + console.log(workflow.actions[newnodecnt]); + } + } + + // Find branches with triggers as source nodes + // Move these targets to be the new node + // Set arrows pointing to new startnode with errors + //for (var key in workflow.branches) { + // var item = workflow.branches[key] + // if (item.destination_id === oldstartnode[0].data()["id"]) { + // var curbranch = cy.getElementById(item.id) + // if (curbranch.length > 0) { + // //console.log(curbranch[0].data()) + // //curbranch[0].data("target", selectedAction.id) + // //curbranch[0].data("hasErrors", true) + // //workflow.branches[key].destination_id = selectedAction.id + // //console.log(curbranch[0].data()) + // } + // } + //} + + setUpdate("start_node" + selectedAction.id); + workflow.start = selectedAction.id; + setWorkflow(workflow); + //setStartNode(selectedAction.id) + }; + + useEffect(() => { + if (selectedAction?.id) { + // Dynamically setting the active tab on Right Sidebar (Setup/Config) + const state = getActionState(selectedAction.id, workflow?.id) + + let tab = state.tab + // Only change tab automatically if no user preference exists (tab is empty/undefined) + // or if this is the initial load (tab is "setup" but no actual user selection made) + if (tab === undefined || tab === null || tab === "" || + (tab === "setup" && !state.userSelectedTab)) { + const hasAuth = (selectedAction.authentication && selectedAction.authentication.length > 0) || (selectedAction.authentication_id && selectedAction.authentication_id !== "") + if (!requiresAuthentication || hasAuth) { + tab = "configuration" + } else { + tab = "setup" + } + } + // If user has explicitly selected a tab, respect their choice + + setActiveMainTab(tab) + setOptionalParamsOpen(state.showOptionalParams) + + // If no required fields exist, open optional params by default + if (selectedAction?.parameters?.length > 0) { + const hasRequiredFields = selectedAction.parameters.some( + param => param.required === true || param.configuration === true + ) + if (!hasRequiredFields && !state.showOptionalParams) { + setOptionalParamsOpen(true) + } + } + } + + // Only set app action name if it has changed + if (selectedAction.label !== appActionName) { + setAppActionName(selectedAction.label); + + const shouldHide = localStorage.getItem("hideBody") + if (shouldHide !== null) { + const ishiding = shouldHide !== "true" + if (ishiding !== hideBody) { + setHideBody(ishiding) + } + } + } + + if (selectedAction.label !== prevActionName) { + setPrevActionName(selectedAction.label) + } + + // Only set delay if it has changed + const newDelay = selectedAction?.execution_delay || 0; + if (newDelay !== delay) { + setDelay(newDelay); + } + + // Only set selected action parameters if they have changed + if (selectedAction?.parameters?.length > 0 && selectedAction.label !== appActionName) { + //console.log("PARAMS CHANGED DURING APPCHANGE: ", selectedAction.parameters) + setSelectedActionParameters(selectedAction.parameters); + } + + // Only set selected variable parameter if it is null or undefined + if (!selectedVariableParameter && workflow.workflow_variables?.length > 0) { + setSelectedVariableParameter(workflow.workflow_variables[0].name); + } + + if (selectedAction?.parent_controlled === true && workflow?.parentorg_workflow?.length > 0 && originalWorkflow?.id !== undefined && originalWorkflow?.id !== null && originalWorkflow?.id !== workflow?.id && originalWorkflow?.actions !== undefined && originalWorkflow?.actions !== null && originalWorkflow?.actions.length > 0) { + // Due to ID remapping of actions not happening, this is easy + for (var key in originalWorkflow.actions) { + const curparentAction = originalWorkflow.actions[key] + + if (curparentAction.id === selectedAction.id) { + if (curparentAction.parameters === undefined || curparentAction.parameters === null || curparentAction.parameters.length === 0) { + console.log("Parent parameters missing!") + break + } + + if (curparentAction.id !== parentAction?.id) { + setParentAction(curparentAction) + } + + break + } + } + } + }, [selectedAction, selectedApp, setNewSelectedAction, workflow, workflowExecutions, getParents, requiresAuthentication]) + + useEffect(() => { + const newActionList = []; + const parentActionList = []; + + // Process workflowExecutions + if (workflowExecutions.length > 0) { + var appended = false + var foundvalue = "" + for (let execution of workflowExecutions) { + const execArg = execution.execution_argument; + if (execArg && execArg.length > 0) { + const valid = validateJson(execArg); + if (valid.valid) { + appended = true + newActionList.push({ + type: "Runtime Argument", + name: "Runtime Argument", + value: "$exec", + highlight: "exec", + autocomplete: "exec", + example: valid.result, + }) + + break + } else { + foundvalue = execArg + } + } + } + + if (!appended && foundvalue !== undefined && foundvalue !== "") { + newActionList.push({ + type: "Runtime Argument", + name: "Runtime Argument", + highlight: "exec", + autocomplete: "exec", + + value: foundvalue, + example: foundvalue, + }) + } + } + + // Add default Runtime Argument if none were added + if (newActionList.length === 0) { + newActionList.push({ + type: "Runtime Argument", + name: "Runtime Argument", + value: "$exec", + highlight: "exec", + autocomplete: "exec", + example: "", + }) + } + + // Look for cachekey + if (newActionList.find((item) => item.type === "Shuffle DB") === undefined) { + let cacheKey = { + type: "Shuffle DB", + name: "Shuffle Datastore", + value: "$shuffle_cache", + highlight: "shuffle_cache", + autocomplete: "shuffle_cache", + example: "", + }; + + if (listCache?.keys?.length > 0) { + cacheKey.example = {}; + for (let item of listCache.keys) { + if (item.key) { + let itemValue = item.value ?? ""; + if (itemValue.length > 10000) { + itemValue = ""; + } + cacheKey.example[item.key.split(" ").join("_")] = { value: itemValue }; + } + } + } + + newActionList.push(cacheKey); + } + + // Process workflow variables + if (workflow.workflow_variables?.length > 0) { + for (let variable of workflow.workflow_variables) { + newActionList.push({ + type: "workflow_variable", + name: variable.name, + value: variable.value, + id: variable.id, + autocomplete: variable.name.split(" ").join("_"), + example: variable.value, + }); + } + } + + // Process execution variables + if (workflow.execution_variables?.length > 0) { + for (let variable of workflow.execution_variables) { + let exampleOutput = ""; + for (let exec of workflowExecutions) { + const foundExec = exec.execution_variables?.find(exvar => exvar.name === variable.name); + if (foundExec?.value) { + exampleOutput = foundExec.value; + break; + } + } + newActionList.push({ + type: "execution_variable", + name: variable.name, + value: variable.value, + id: variable.id, + autocomplete: variable.name.split(" ").join("_"), + example: exampleOutput, + }); + } + } + + // Process parent actions if getParents is provided + if (getParents) { + const parents = getParents(selectedAction); + if (parents.length > 1) { + const labels = []; + for (let parentNode of parents) { + if (parentNode.label === "Runtime Argument" || labels.includes(parentNode.label)) { + continue + } + + labels.push(parentNode.label); + + var secondaryExample = "" + let exampleData = parentNode.example ?? ""; + if (parentNode?.app_name === "http") { + exampleData = "" + } + + if (workflowExecutions.length > 0) { + for (let exec of workflowExecutions) { + const foundResult = exec.results?.find(result => result?.action?.id === parentNode?.id); + if (foundResult) { + const valid = validateJson(foundResult.result); + if (valid.valid) { + + // Check if array, and if first item is object + success + if (Array.isArray(valid.result) && valid.result.length > 0 && typeof valid.result[0] === "object") { + if (valid.result[0].success !== false) { + exampleData = valid.result + break + } + } else { + if (valid.result.success !== false) { + exampleData = valid.result + break + } + } + } else { + secondaryExample = foundResult.result + } + } + } + } + + if (exampleData === "" && apps !== undefined && apps !== null && apps?.length > 0) { + // Check apps if it exists, then if it + const foundApp = apps?.find(app => app?.id === parentNode?.app_id) + if (foundApp !== undefined && foundApp !== null) { + if (foundApp?.generated === true || foundApp?.name === "http") { + const validationData = validateJson(`{ + "status": 200, + "body": { + "example": "json", + "values": "json" + }, + "url": "https://example.com", + "headers": { + "Content-Type": "application/json", + "Example-Header": "two" + }, + "cookies": { + "example": "session", + "__session": "sessionid" + }, + "success": true + }`) + + if (validationData.valid) { + exampleData = validationData.result + } + } + } + } + + if (exampleData === "" && secondaryExample !== "") { + exampleData = secondaryExample + } + + if (parentNode.label === undefined) { + parentNode.label = "" + } + + newActionList.push({ + type: "action", + id: parentNode.id, + name: parentNode.label, + autocomplete: parentNode.label.split(" ").join("_"), + example: exampleData, + }); + + parentActionList.push({ + type: "action", + id: parentNode.id, + name: parentNode.label, + autocomplete: parentNode.label.split(" ").join("_"), + example: exampleData, + }); + } + } + } + + let newParameters = selectedAction?.parameters?.map((param) => { + let paramvalue = param.value === undefined || param.value === null ? "" : param.value; + let errorVars = []; + + if (paramvalue.includes("$")) { + let actions = workflow.actions?.map((action) => { + return "$" + action.label?.toLowerCase().replaceAll(" ", "_"); + }) + + if (newActionList?.length > 0) { + let appParentActions = parentActionList?.map(action => "$" + action.name.toLowerCase().replaceAll(" ", "_")); + let notPresentAction = actions?.filter((action) => !appParentActions?.includes(action)) + // Extract all variable references from paramvalue + // Examples of what it matches: + // - Simple variables: $test, $myVar, $x + // - Variables with underscores: $my_variable, $ok_ok, $testing_nice, $nice_try_man + // - Workflow data access patterns: $not_customer_copy.body.subject.#, $not_customer.body.subject.text + // - Deep nested properties: $email_action.response.items[0].sender.address + + const variablePattern = /\$[a-zA-Z0-9_]+(?=\.|,|;|:|\s|"|'|`|\)|\]|\}|$)/g; + const foundVariables = paramvalue.match(variablePattern) || []; + + notPresentAction?.forEach((action) => { + action = action.replace(" ", "_"); + + // Check if the exact action is in the foundVariables list + if (foundVariables.includes(action)) { + errorVars.push(action); + } + }); + + // Check for variables that don't exist in the workflow at all + foundVariables.forEach(variable => { + // Skip checking for $ followed by system vars as exec and shuffle_cache. + if (variable.match(/\$(exec|shuffle_cache)/)) { + return; + } + // Create list of all valid action variables from newActionList + const newActionNames = newActionList?.map(action => "$" + action.name.toLowerCase()) || []; + + // Extract workflow and execution variables from newActionList + const workflowVars = newActionList + .filter(item => item.type === "workflow_variable" || item.type === "execution_variable") + .map(item => "$" + item.autocomplete.toLowerCase()); + + // If the variable doesn't exist in any workflow actions, parent actions, newActionList, or extracted variables, show error + if (!actions.includes(variable) && + !appParentActions.includes(variable) && + !newActionNames.includes(variable) && + !workflowVars.includes(variable.toLowerCase())) { + errorVars.push(variable); + } + }); + } + } + + let message = ""; + if (errorVars.length > 0) { + if (errorVars.length === 1) { + message = errorVars[0] + " is not accessible in this action."; + } else { + message = errorVars.join(", ") + " are not accessible in this action."; + } + } + + if (param?.configuration && param?.name !== "url") { + let regex = /(^|[^\\])\$/; + if (regex.test(paramvalue)) { + if (message.length > 0) { + message += "\nUse \"\\$\" instead of \"$\" if you want to escape $ (1)"; + } else { + message = "Use \"\\$\" instead of \"$\" if you want to escape $ (2)"; + } + } + } + return { ...param, value: paramvalue, error: message } + }) + + setSelectedActionParameters(newParameters) + setActionlist(newActionList) + }, [workflow.execution_variables, paramUpdate, workflow.workflow_variables, workflowExecutions, workflow, selectedAction, listCache, getParents, setNewSelectedAction]); + + useEffect(() => { + selectedNameChange(appActionName) + + if (actionDelayChange !== undefined) { + actionDelayChange(delay) + } + }, [appActionName, delay]) + + const handleParamChange = (event, count, data) => { + const newParams = [...selectedActionParameters]; + newParams.map((param) => { + if (param.name === data.name) { + param.value = event.target.value; + } + }) + setSelectedActionParameters(newParams); + setParamUpdate(event.target.value); + changeActionParameter(event, count, data) + } + + const calculateHelpertext = (input_data) => { + var helperText = "" + var looperText = "" + //const found = input_data.match(/[$]{1}([a-zA-Z0-9_-]+\.?){1}([a-zA-Z0-9#_-]+\.?){0,}/g) + var found = input_data.match(/[\\]{0,1}[$]{1}([a-zA-Z0-9_@-]+\.?){1}([a-zA-Z0-9#_@-]+\.?){0,}/g) + + if (found !== null && found !== undefined) { + var new_occurences = [] + for (let [key, keyval] in Object.entries(found)) { + if (found[key][0] !== "\\") { + new_occurences.push(found[key]) + } + } + + found = new_occurences.valueOf() + } + + if (found !== null) { + try { + // When the found array is empty. + for (let i = 0; i < found.length; i++) { + const variableSplit = found[i].split(".#") + if ((variableSplit.length - 1) > 1) { + //console.log("Larger than 1: ", variableSplit) + if (looperText.length === 0) { + looperText += "PS: Double looping (.#.#) may cause problems." + } + } + + var foundSlice = false + for (let j = 0; j < actionlist.length; j++) { + //console.log("ACTION: ", found[i], actionlist[j]) + //console.log("ACTION :", found[i].split(".")[0].slice(1,).toLowerCase(), actionlist[j].autocomplete.toLowerCase()) + if (found[i].split(".")[0].slice(1,).toLowerCase() == actionlist[j].autocomplete.toLowerCase()) { + //console.log("Found: ", found[i]) + // Validate path? + + foundSlice = true + } + } + + if (!foundSlice) { + if (!helperText.includes("Invalid variables")) { + helperText += "Invalid variables: " + } + helperText += found[i] + ", " + } + } + } catch (e) { + console.log("Parsing error: ", e) + } + } + + if (looperText.length > 0) { + if (helperText.length > 0) { + helperText += ". " + } + + helperText += looperText + } + + return helperText + } + + const changeActionParameter = (event, count, data, viewForceUpdate) => { + //console.log("Action change: ", selectedAction, data) + if (data.name.startsWith("${") && data.name.endsWith("}")) { + // PARAM FIX - Gonna use the ID field, even though it's a hack + const paramcheck = selectedAction.parameters.find((param) => param.name === "body"); + + if (paramcheck !== undefined) { + // Escapes all double quotes + //var toReplace = event.target.value.trim() + var toReplace = event.target.value + + + if (!toReplace?.startsWith("{") && !toReplace?.startsWith("[")) { + toReplace = toReplace?.replaceAll('\\"', '"').replaceAll('"', '\\"') + } + + if ( + paramcheck["value_replace"] === undefined || + paramcheck["value_replace"] === null + ) { + paramcheck["value_replace"] = [ + { + key: data.name, + value: toReplace, + }, + ]; + + } else { + const subparamindex = paramcheck["value_replace"].findIndex( + (param) => param.key === data.name + ); + if (subparamindex === -1) { + paramcheck["value_replace"].push({ + key: data.name, + value: toReplace, + }); + } else { + paramcheck["value_replace"][subparamindex]["value"] = toReplace; + } + } + + if (selectedActionParameters[count].value_replace === undefined) { + selectedActionParameters[count].value_replace = paramcheck + } + + if (selectedAction?.parameters[count] !== undefined && selectedAction?.parameters[count].value_replace === undefined) { + selectedAction.parameters[count].value_replace = paramcheck + } + + if (paramcheck["value_replace"] === undefined) { + selectedActionParameters[count]["value_replace"] = paramcheck + + if (selectedAction?.parameters[count] !== undefined) { + selectedAction.parameters[count]["value_replace"] = paramcheck + } + } else { + selectedActionParameters[count]["value_replace"] = paramcheck["value_replace"]; + if (selectedAction?.parameters[count] !== undefined) { + selectedAction.parameters[count]["value_replace"] = paramcheck["value_replace"]; + } + } + setSelectedAction(selectedAction); + //setUpdate(Math.random()) + return; + } + } + + + if (event.target.value[event.target.value.length - 1] === "$") { + if (!showDropdown) { + setShowAutocomplete(false); + setShowDropdown(true); + setShowDropdownNumber(count); + } + } else { + if (showDropdown) { + setShowDropdown(false); + } + } + + // bad detection mechanism probably + if (event.target.value[event.target.value.length - 1] === "." && actionlist.length > 0) { + console.log("GET THE LAST ARGUMENT FOR NODE!"); + // THIS IS AN EXAMPLE OF SHOWING IT + /* + + const inputdata = {"data": "1.2.3.4", "dataType": "4.5.6.6"} + setJsonList(GetParsedPaths(inputdata, "")) + if (!showDropdown) { + setShowAutocomplete(false) + setShowDropdown(true) + setShowDropdownNumber(count) + } + console.log(jsonList) + */ + + // Search for the item backwards + // 1. Reverse search backwards from . -> $ + // 2. Search the actionlist for the item + // 3. Find the data for the specific item + + var curstring = ""; + var record = false; + for (let [key, keyval] in Object.entries(selectedActionParameters[count].value)) { + const item = selectedActionParameters[count].value[key]; + if (record) { + curstring += item; + } + + if (item === "$") { + record = true; + curstring = ""; + } + } + + //console.log("CURSTRING: ", curstring) + if (curstring.length > 0 && actionlist !== null) { + // Search back in the action list + curstring = curstring.split(" ").join("_").toLowerCase(); + var actionItem = actionlist.find( + (data) => + data.autocomplete.split(" ").join("_").toLowerCase() === curstring + ); + if (actionItem !== undefined) { + console.log("Found item: ", actionItem); + + //actionItem.example = actionItem.example.trim() + //actionItem.example = actionItem.example.split(" None").join(" \"None\"") + //actionItem.example = actionItem.example.split("\'").join("\"") + + var jsonvalid = true; + try { + const tmp = String(JSON.parse(actionItem.example)); + if ( + !actionItem.example.includes("{") && + !actionItem.example.includes("[") + ) { + jsonvalid = false; + } + } catch (e) { + jsonvalid = false; + } + + if (jsonvalid) { + setJsonList(GetParsedPaths(JSON.parse(actionItem.example), "")); + + if (!showDropdown) { + setShowAutocomplete(false); + setShowDropdown(true); + setShowDropdownNumber(count); + } + } + } + } + } else { + if (jsonList.length > 0) { + setJsonList([]); + } + } + + selectedActionParameters[count].autocompleted = false + selectedAction.parameters[count].autocompleted = false + selectedActionParameters[count].value = event.target.value; + selectedAction.parameters[count].value = event.target.value; + + var forceUpdate = false + if (isCloud && (selectedAction.app_name === "Shuffle Tools" || selectedAction.app_name === "email") && (selectedAction.name === "send_email_shuffle" || selectedAction.name === "send_sms_shuffle") && data.name === "apikey") { + console.log("APIKEY - this shouldn't show up!") + } + + if (selectedAction.app_name === "Shuffle Tools" && (selectedAction.name === "filter_list" || selectedAction.name === "is_in_datastore") && data.name === "input_list") { + //console.log("FILTER LIST!: ", event, count, data) + const parsedvalue = event.target.value + if (parsedvalue.includes(".#")) { + const splitparsed = parsedvalue.split(".#.") + //console.log("Cant contain #: ", splitparsed) + if (splitparsed.length > 1) { + data.value = splitparsed[0] + + selectedActionParameters[count].value = splitparsed[0] + selectedAction.parameters[count].value = splitparsed[0] + + selectedActionParameters[1].value = splitparsed[1] + selectedAction.parameters[1].value = splitparsed[1] + + if (splitparsed.length >= 2) { + toast.warn("Datastore checker/Filter list only supports filtering on the first list. If you want multi-level filtering, please use the 'execute python' action with the 'filter a list' function in the code editor.", { + autoClose: 10000, + }) + } else if (selectedAction.parameters[1].value.includes(".#")) { + toast.warn("This filter may not work due to using .# indexing. Please use the 'execute python' action and try the 'filter a list' function in the code editor.") + } + } else { + // Remove .# and after + const splitparsed = parsedvalue.split(".#") + data.value = splitparsed[0] + selectedActionParameters[0].value = splitparsed[0] + selectedAction.parameters[0].value = splitparsed[0] + + selectedActionParameters[1].value = "" + selectedAction.parameters[1].value = "" + + toast.warn("No value found in the list. Please select an item in the list to filter based on.") + } + + forceUpdate = true + selectedActionParameters[0].autocompleted = true + selectedAction.parameters[0].autocompleted = true + selectedActionParameters[1].autocompleted = true + selectedAction.parameters[1].autocompleted = true + } + } + + setSelectedAction(selectedAction) + if (forceUpdate || viewForceUpdate === true) { + setUpdate(Math.random()) + } + + //console.log("END OF THIS THING") + //setUpdate(event.target.value) + }; + + + const changeActionParameterCodeMirror = (event, count, data) => { + if (data.startsWith("${") && data.endsWith("}")) { + // PARAM FIX - Gonna use the ID field, even though it's a hack + const paramcheck = selectedAction.parameters.find(param => param.name === "body") + if (paramcheck !== undefined) { + // Escapes all double quotes + const toReplace = event.target.value?.trim()?.replaceAll("\\\"", "\"")?.replaceAll("\"", "\\\""); + console.log("REPLACE WITH: ", toReplace) + if (paramcheck["value_replace"] === undefined || paramcheck["value_replace"] === null) { + paramcheck["value_replace"] = [{ + "key": data.name, + "value": toReplace, + }] + + console.log("IN IF: ", paramcheck) + + } else { + const subparamindex = paramcheck["value_replace"].findIndex(param => param.key === data.name) + if (subparamindex === -1) { + paramcheck["value_replace"].push({ + "key": data.name, + "value": toReplace, + }) + } else { + paramcheck["value_replace"][subparamindex]["value"] = toReplace + } + } + //console.log("PARAM: ", paramcheck) + //if (paramcheck.id === undefined) { + // console.log("Normal paramcheck") + //} else { + // selectedActionParameters[count]["value_replace"] = paramcheck + // selectedAction.parameters[count]["value_replace"] = paramcheck + //} + + if (paramcheck["value_replace"] === undefined) { + selectedActionParameters[count]["value_replace"] = paramcheck + selectedAction.parameters[count]["value_replace"] = paramcheck + } else { + selectedActionParameters[count]["value_replace"] = paramcheck["value_replace"] + selectedAction.parameters[count]["value_replace"] = paramcheck["value_replace"] + } + console.log("RESULT: ", selectedAction) + setSelectedAction(selectedAction) + //setUpdate(Math.random()) + return + } + } + + if (event.target.value[event.target.value.length - 1] === "$") { + if (!showDropdown) { + setShowAutocomplete(false) + setShowDropdown(true) + setShowDropdownNumber(count) + } + } else { + if (showDropdown) { + setShowDropdown(false) + } + } + + + // bad detection mechanism probably + if (event.target.value[event.target.value.length - 1] === "." && actionlist.length > 0) { + console.log("GET THE LAST ARGUMENT FOR NODE!") + // THIS IS AN EXAMPLE OF SHOWING IT + /* + const inputdata = {"data": "1.2.3.4", "dataType": "4.5.6.6"} + setJsonList(GetParsedPaths(inputdata, "")) + if (!showDropdown) { + setShowAutocomplete(false) + setShowDropdown(true) + setShowDropdownNumber(count) + } + console.log(jsonList) + */ + + // Search for the item backwards + // 1. Reverse search backwards from . -> $ + // 2. Search the actionlist for the item + // 3. Find the data for the specific item + + var curstring = "" + var record = false + for (let [key, keyval] in Object.entries(selectedActionParameters[count].value)) { + const item = selectedActionParameters[count].value[key] + if (record) { + curstring += item + } + + if (item === "$") { + record = true + curstring = "" + } + } + + //console.log("CURSTRING: ", curstring) + if (curstring.length > 0 && actionlist !== null) { + // Search back in the action list + curstring = curstring.split(" ").join("_").toLowerCase() + var actionItem = actionlist.find(data => data.autocomplete.split(" ").join("_").toLowerCase() === curstring) + if (actionItem !== undefined) { + console.log("Found item: ", actionItem) + + //actionItem.example = actionItem.example.trim() + //actionItem.example = actionItem.example.split(" None").join(" \"None\"") + //actionItem.example = actionItem.example.split("\'").join("\"") + + var jsonvalid = true + try { + const tmp = String(JSON.parse(actionItem.example)) + if (!actionItem.example.includes("{") && !actionItem.example.includes("[")) { + jsonvalid = false + } + } catch (e) { + jsonvalid = false + } + + if (jsonvalid) { + setJsonList(GetParsedPaths(JSON.parse(actionItem.example), "")) + + if (!showDropdown) { + setShowAutocomplete(false) + setShowDropdown(true) + setShowDropdownNumber(count) + } + } + } + } + } else { + if (jsonList.length > 0) { + setJsonList([]) + } + } + + setTimeout(() => { + selectedActionParameters[count].autocompleted = false + selectedAction.parameters[count].autocompleted = false + selectedActionParameters[count].value = data + selectedAction.parameters[count].value = data + }, 100); + setSelectedAction(selectedAction) + //setUpdate(Math.random()) + //setUpdate(event.target.value) + } + + + const changeActionParameterVariable = (fieldvalue, count) => { + //console.log("CALLED THIS ONE WITH VALUE!", fieldvalue) + //if (selectedVariableParameter === fieldvalue) { + // return + //} + + setSelectedVariableParameter(fieldvalue); + + selectedActionParameters[count].action_field = fieldvalue; + selectedAction.parameters = selectedActionParameters; + + setSelectedApp(selectedApp); + setSelectedAction(selectedAction); + setUpdate(fieldvalue); + }; + + // Sets ACTION_RESULT things + const changeActionParameterActionResult = (fieldvalue, count) => { + //cy.nodes().forEach(function( ele ) { + // if (ele.data()["label"] === fieldvalue) { + // selectedActionParameters[count].action_field = ele.id() + // return + // } + //}); + + selectedActionParameters[count].action_field = fieldvalue; + selectedAction.parameters = selectedActionParameters; + + // FIXME - check if startnode + + // Set value + setSelectedApp(selectedApp); + + setSelectedAction(selectedAction); + setUpdate(Math.random()); + }; + + const changeActionParameterVariant = (data, count) => { + selectedActionParameters[count].variant = data; + selectedActionParameters[count].value = ""; + + if (data === "ACTION_RESULT" && getParents !== undefined) { + var parents = getParents(selectedAction); + if (parents.length > 0) { + selectedActionParameters[count].action_field = parents[0].label; + } else { + selectedActionParameters[count].action_field = ""; + } + } else if (data === "WORKFLOW_VARIABLE") { + if ( + workflow.workflow_variables !== null && + workflow.workflow_variables !== undefined && + workflow.workflow_variables.length > 0 + ) { + selectedActionParameters[count].action_field = + workflow.workflow_variables[0].name; + } + } + + selectedAction.parameters = selectedActionParameters; + + // This is a stupid workaround to make it refresh rofl + setSelectedAction({}); + + if (setSelectedTrigger !== undefined) { + setSelectedTrigger({}); + setSelectedApp({}); + setSelectedEdge({}); + } + // FIXME - check if startnode + + // Set value + setSelectedApp(selectedApp); + setSelectedAction(selectedAction); + setUpdate(Math.random()); + }; + + + const returnHelperText = (name, value) => { + if (name === undefined || value === undefined || name === null || value === null || name === "" || value === "") { + //console.log("RETURNING EMPTY") + return "" + } + + var helperText = "" + if (name.includes("url")) { + if (value.includes("localhost") || value.includes("127.0.0.1")) { + helperText = "Can't use localhost in Shuffle. Please change to server's IP." + } + } + + // Helpertext for openapi fields + //if (helperText === "" && name === "body" && selectedApp.generated && selectedApp.activated) { + // helperText = + //} + + return helperText + } + + const errorHelperText = (name, value, error) => { + return ( +
+ {error} +
+ ); + } + + + const analyzeFields = () => { + + if (selectedAction === undefined || selectedAction === null) { + return null + } + + if (selectedActionParameters === undefined || selectedActionParameters === null || selectedActionParameters.length === 0) { + return null + } + + // Only shuffle tools for now + if (selectedAction.app_name !== "Shuffle Tools") { + return null + } + + // Custom rules + if (selectedAction.name === "set_cache_value") { + var actionKey = "" + var actionValue = "" + for (let [key, keyval] in Object.entries(selectedActionParameters)) { + const param = selectedActionParameters[key] + if (param.name === "key") { + actionKey = param.value + } + + if (param.name === "value") { + actionValue = param.value + } + } + + if (actionKey === "" || actionValue === "") { + return null + } + + if (!actionKey.includes(".#") && actionValue.includes(".#")) { + return When the key ({actionKey}) is static, but the value is a list ({actionValue}), it will overwrite the list. You may be looking for the { }} style={{ cursor: "pointer", color: "#FF8544", }}>Check Cache Contains action instead. + } + } + + return null + + } + + const suggestionInfo = () => { + const suggestionText = analyzeFields() + if (suggestionText === undefined || suggestionText === null) { + return null + } + + if (selectedAction.errors === undefined || selectedAction.errors === null || selectedAction.errors.length === 0) { + selectedAction.errors = ["Suggestion: " + suggestionText] + } + + return + + Tip: {suggestionText} + + + } + + // FIXME: Issue #40 - selectedActionParameters not reset + if (selectedAction && Object.getOwnPropertyNames(selectedAction).length > 0 && selectedActionParameters?.length > 0) { + var wrapperapp = { + "id": "", + "name": "noapp", + "large_image": "", + } + + var actionname = selectedAction.name.toLowerCase() + if (actionname === "email" || actionname === "communication") { + actionname = "comms" + } + + if (isAgent || isIntegration) { + // Check if actionname uppercase is in the parsedDatatypeImages() dictionary + if (isAgent) { + wrapperapp.large_image = theme.palette.singulBlackWhite + newimage = wrapperapp.large_image + } else if (isIntegration) { + wrapperapp.large_image = theme.palette.singulGreen + newimage = theme.palette.singulGreen + } else { + const uppercaseimage = parsedDatatypeImages()[actionname.toUpperCase()] + if (uppercaseimage !== undefined) { + var newimage = uppercaseimage + //newimage = + wrapperapp.large_image = newimage + } else { + console.log("Couldn't find actionname: ", actionname) + } + } + } + + var authWritten = false; + var noAppSelected = false + if (selectedAction.parameters !== undefined && selectedAction.parameters !== null && selectedAction.parameters.length > 0) { + var paramIndex = selectedAction.parameters.findIndex((param) => param.name === "app_name") + if (paramIndex === -1 || selectedAction.parameters[paramIndex].value === "" || selectedAction.parameters[paramIndex].value === "noapp") { + // Check the actual value and if it's the same + noAppSelected = true + } + } + } + + + const ActionSelectOption = (actionprops) => { + const { option, newActionname, newActiondescription, useIcon, extraDescription, } = actionprops; + const [hover, setHover] = React.useState(false); + + return ( + + {newActiondescription} + + } + > +
setHover(true)} onMouseLeave={() => setHover(false)} + onClick={(event) => { + // event.preventDefault() + //setSelectedAction(actionprops) + //setShowActionList(false) + //setUpdate(Math.random()) + // + if (option !== undefined && option !== null) { + setNewSelectedAction({ + target: { + value: option.name + } + }); + } + + document.activeElement.blur(); + + const disabledUiBox = localStorage.getItem("disabled_ui_box") + if (disabledUiBox === "true") { + } else { + setHiddenDescription(false) + } + }} + > +
0 ? 4 : 0 }}> + + {useIcon} + +
+
{newActionname}
+ {extraDescription.length > 0 ? + + {extraDescription} + + : null} +
+
+
+
+ ) + } + + const sortByCategoryLabel = (a, b) => { + + const aHasCategoryLabel = a.category_label !== undefined && a.category_label !== null && a.category_label.length > 0 + const bHasCategoryLabel = b.category_label !== undefined && b.category_label !== null && b.category_label.length > 0 + + // Sort by existence and length of "category_label" + if (aHasCategoryLabel && !bHasCategoryLabel) { + return -1 + } else if (!aHasCategoryLabel && bHasCategoryLabel) { + return 1 + } else { + return 0 + } + } + + // Function to deduplicate based on the "name" field + const deduplicateByName = (array) => { + const uniqueNames = {}; + return array.filter(item => { + if (!item.hasOwnProperty('name') || !item.name.length) { + return true + } + if (!uniqueNames[item.name]) { + uniqueNames[item.name] = true + return true + } + return false + }) + } + + + // Gets the most important actions first + const renderedActionOptions = deduplicateByName(( + selectedApp.actions === undefined || selectedApp.actions === null ? [] : + isIntegration ? selectedApp.actions : + selectedApp.actions.filter((a) => a.category_label !== undefined && a.category_label !== null && a.category_label.length > 0).concat(sortByKey(selectedApp.actions, "label")) + ).sort(sortByCategoryLabel)) + + + const selectedAppIcon = selectedAction.large_image + + var newAppname = selectedAction?.name?.charAt(0).toUpperCase() + selectedAction?.name?.substring(1) + if (newAppname === undefined || newAppname === null) { + newAppname = "" + } else { + try { + if (typeof newAppname === "string") { + newAppname = newAppname.replace(/_/g, " ") + } + } catch (e) { + console.log("Error in replace newappname: ", e) + } + + } + + var optionalFound = false + return ( +
+ {isSidebarLoading ? ( +
+ {/* Tabs bar skeleton */} +
+ + +
+ + {/* A couple of label + field rows to mimic config inputs */} + {[1, 2, 3].map((row) => ( +
+ + +
+ ))} +
+ ) : ( + + { + setActiveMainTab(newValue) + if (selectedAction?.id) { + setActionState(selectedAction.id, { tab: newValue, userSelectedTab: true }, workflow?.id) + } + }} + variant="fullWidth" + indicatorColor="primary" + textColor="inherit" + TabIndicatorProps={{ + style: { + top: 0, + bottom: "auto", + height: 2, + }, + }} + style={{ + marginBottom: 10, + minHeight: 50, + borderBottom: `1px solid ${theme.palette.slateGrayColor}`, + }} + > + + + + + {(activeMainTab === "setup" || activeMainTab === "configuration") ? ( + +
+ {hideExtraTypes === true ? null : ( + +
+
+
{ + //window.open("/apps/${selectedAction.app_id}", "_blank") + }} + > + + + + + + + {/*

+ {newAppname} +

*/} +
+ {activeMainTab === "setup" && ( +
+ { + if (workflowExecutions.length > 0) { + // Look for the ID + var found = false; + for (let [key, keyval] in Object.entries(workflowExecutions)) { + if (workflowExecutions[key].results === undefined || workflowExecutions[key].results === null) { + continue; + } + + var foundResult = workflowExecutions[key].results.find( + (result) => result.action.id === selectedAction.id + ) + + if (foundResult === undefined || foundResult === null) { + continue + } + + const oldstartnode = cy.getElementById(selectedAction.id); + if (oldstartnode !== undefined && oldstartnode !== null) { + const foundname = oldstartnode.data("label") + if (foundname !== undefined && foundname !== null) { + foundResult.action.label = foundname + } + } + + setSelectedResult(foundResult); + if (setCodeModalOpen !== undefined) { + setCodeModalOpen(true) + + found = true + } + + break + } + + if (!found) { + toast("No result for this action yet. Please run the workflow first.") + } + } + }} + > + + + + + { + setAuthenticationModalOpen(true) + }} + > + + + + + + { + if (setAiQueryModalOpen !== undefined) { + setAiQueryModalOpen(true) + } else { + aiSubmit("Fill based on previous values", undefined, undefined, selectedAction) + } + + setAutocompleting(true) + setTimeout(() => { + setAutocompleting(false) + }, 3000) + }} + > + + {autoCompleting ? + + : + + } + + + + + {(selectedAction?.generated === true && selectedAction?.app_version === "1.0.0") || (selectedAction?.app_name === "Shuffle Tools" && selectedAction?.app_version !== "1.2.0") ? + + : null} + + {selectedApp.versions !== null && + selectedApp.versions !== undefined && + selectedApp.versions.length > 1 ? ( + + ) : null} + +
+ )} +
+
+ + {activeMainTab === "setup" && ( + <> + {(isIntegration || isAgent) && selectedAction && Object.getOwnPropertyNames(selectedAction).length > 0 && selectedActionParameters?.length > 0 ? + apps !== undefined && apps !== null && apps.length > 0 && wrapperapp !== undefined && newimage !== undefined ? +
+
{ + selectedAction.example = "noapp" + selectedAction.large_image = newimage + if (cy !== undefined && cy !== null) { + const foundnode = cy.getElementById(selectedAction.id) + if (foundnode !== undefined && foundnode !== null) { + foundnode.data("large_image", newimage) + } + } + + /* + const iconInfo = GetIconInfo(selectedAction) + if (iconInfo !== undefined && iconInfo !== null) { + selectedAction.fillGradient = iconInfo.fillGradient + + selectedAction.iconBackground = iconInfo.iconBackgroundColor + selectedAction.fillstyle = "linear-gradient" + } + */ + + const paramIndex = selectedAction.parameters !== undefined && selectedAction.parameters !== null ? selectedAction.parameters.findIndex((param) => param.name === "app_name") : -1 + if (paramIndex === -1) { + console.log("Couldn't find app_name parameter") + selectedAction.parameters.push({ + name: "app_name", + value: wrapperapp.name, + autocompleted: false, + }) + } else { + selectedAction.parameters[paramIndex].value = wrapperapp.name + } + + setSelectedAction(selectedAction) + setUpdate(Math.random()) + + }}> + +
+ +
+
+
+ + {apps.map((app, appIndex) => { + // Forces it into every category (for now) + // This is to make it possible to "use" shuffle for Singul natively + if (app.name === "Shuffle Tools") { + if (actionname == "Intel" || actionname == "Intel") { + app.categories = [actionname] + } + } + + if (app.categories === undefined || app.categories === null || app.categories.length === 0) { + return null + } + + var newactionname = actionname.toLowerCase() + if (isAgent === true) { + newactionname = "ai" + } + + var found = false + for (var key in app.categories) { + + var localnewactionname = newactionname + if (newactionname == "comms") { + localnewactionname = "communication" + } + + if (app.categories[key].toLowerCase() !== localnewactionname) { + continue + } + + found = true + break + } + + if (!found) { + return null + } + + var isAppSelected = false + const paramIndex = selectedAction.parameters.findIndex((param) => param.name === "app_name") + if (paramIndex > -1) { + // Check the actual value and if it's the same + if (selectedAction.parameters[paramIndex].value === app.name) { + isAppSelected = true + } + } + + return ( +
{ + selectedAction.example = "" + selectedAction.large_image = app.large_image + if (cy !== undefined && cy !== null) { + const foundnode = cy.getElementById(selectedAction.id) + if (foundnode !== undefined && foundnode !== null) { + foundnode.data("large_image", app.large_image) + } + } + + if (paramIndex === -1) { + console.log("Couldn't find app_name parameter") + selectedAction.parameters.push({ + name: "app_name", + value: app.name, + autocompleted: false, + }) + } else { + selectedAction.parameters[paramIndex].value = app.name + } + + setSelectedAction(selectedAction) + setUpdate(Math.random()) + + + var requiresAuth = app?.authentication?.required + if (requiresAuth && appAuthentication?.length > 0) { + for (var key in appAuthentication) { + if (appAuthentication[key]?.app?.name === app?.name) { + requiresAuth = false + break + } + } + } + + setRequiresAuthentication(requiresAuth); + }}> + + + +
+ ) + })} +
+ : null + : + null + } + +
+
+ Name + 0} + placeholder={selectedAction.label} + value={appActionName} + onChange={(event) => { + let newValue = event.target.value + newValue = newValue?.replaceAll(" ", "_") + setAppActionName(newValue) + }} + onBlur={(e) => { + // Copy the name value + const name = e.target.value + const parsedBaseLabel = "$" + prevActionName?.toLowerCase()?.replaceAll(" ", "_") + const newname = "$" + name?.toLowerCase()?.replaceAll(" ", "_") + + // Check if it's the same as the current name in use + //if (name === selectedAction.label) { + // console.log("Returning from name thing") + // return + //} + + // Change in actions, triggers & conditions + // Highlight the changes somehow with a glow? + if (workflow.branches !== undefined && workflow.branches !== null) { + for (let [key, keyval] in Object.entries(workflow.branches)) { + if (workflow.branches[key].conditions !== undefined && workflow.branches[key].conditions !== null) { + for (let [subkey, subkeyval] in Object.entries(workflow.branches[key].conditions)) { + const condition = workflow.branches[key].conditions[subkey] + const sourceparam = condition.source + const destinationparam = condition.destination + + // Should have a smarter way of discovering node names + // Finding index(es) and replacing at the location + if (sourceparam.value.includes("$")) { + try { + var cnt = -1 + var previous = 0 + while (true) { + cnt += 1 + // Need to make sure e.g. changing the first here doesn't change the 2nd + // $change_me + // $change_me_2 + + const foundindex = sourceparam.value.toLowerCase().indexOf(parsedBaseLabel, previous) + if (foundindex === previous && foundindex !== 0) { + break + } + + if (foundindex >= 0) { + previous = foundindex + newname.length + // Need to add diff of length to word + + // Check location: + // If it's a-zA-Z_ then don't replace + if (sourceparam.value.length > foundindex + parsedBaseLabel.length) { + const regex = /[a-zA-Z0-9_]/g; + const match = sourceparam.value[foundindex + parsedBaseLabel.length].match(regex); + if (match !== null) { + continue + } + } + + console.log("Old found: ", workflow.branches[key].conditions[subkey].source.value) + const extralength = newname.length - parsedBaseLabel.length + sourceparam.value = sourceparam.value.substring(0, foundindex) + newname + sourceparam.value.substring(foundindex - extralength + newname.length, sourceparam.value.length) + + console.log("New: ", workflow.branches[key].conditions[subkey].source.value) + } else { + break + } + + // Break no matter what after 5 replaces. May need to increase + if (cnt >= 5) { + break + } + + } + } catch (e) { + console.log("Failed value replacement based on index: ", e) + } + } + + if (destinationparam.value.includes("$")) { + try { + var cnt = -1 + var previous = 0 + while (true) { + cnt += 1 + // Need to make sure e.g. changing the first here doesn't change the 2nd + // $change_me + // $change_me_2 + + const foundindex = destinationparam.value.toLowerCase().indexOf(parsedBaseLabel, previous) + if (foundindex === previous && foundindex !== 0) { + break + } + + if (foundindex >= 0) { + previous = foundindex + newname.length + // Need to add diff of length to word + + // Check location: + // If it's a-zA-Z_ then don't replace + if (destinationparam.value.length > foundindex + parsedBaseLabel.length) { + const regex = /[a-zA-Z0-9_]/g; + const match = destinationparam.value[foundindex + parsedBaseLabel.length].match(regex); + if (match !== null) { + continue + } + } + + console.log("Old found: ", workflow.branches[key].conditions[subkey].destination.value) + const extralength = newname.length - parsedBaseLabel.length + destinationparam.value = destinationparam.value.substring(0, foundindex) + newname + destinationparam.value.substring(foundindex - extralength + newname.length, destinationparam.value.length) + + console.log("New: ", workflow.branches[key].conditions[subkey].destination.value) + } else { + break + } + + // Break no matter what after 5 replaces. May need to increase + if (cnt >= 5) { + break + } + + } + } catch (e) { + console.log("Failed value replacement based on index: ", e) + } + } + } + } + } + } + + for (let [key, keyval] in Object.entries(workflow.actions)) { + if (workflow.actions[key].id === selectedAction.id) { + continue + } + + const params = workflow.actions[key].parameters + if (params === null || params === undefined) { + continue + } + + for (let [subkey, subkeyval] in Object.entries(params)) { + const param = workflow.actions[key].parameters[subkey]; + if (!param.value.includes("$")) { + continue + } + + // Should have a smarter way of discovering node names + // Do regex? + // Finding index(es) and replacing at the location + // + + try { + var cnt = -1 + var previous = 0 + while (true) { + cnt += 1 + // Need to make sure e.g. changing the first here doesn't change the 2nd + // $change_me + // $change_me_2 + const foundindex = param.value.toLowerCase().indexOf(parsedBaseLabel, previous) + if (foundindex === previous && foundindex !== 0) { + break + } + + if (foundindex >= 0) { + previous = foundindex + newname.length + // Need to add diff of length to word + + // Check location: + // If it's a-zA-Z_ then don't replace + if (param.value.length > foundindex + parsedBaseLabel.length) { + const regex = /[a-zA-Z0-9_]/g; + const match = param.value[foundindex + parsedBaseLabel.length].match(regex); + if (match !== null) { + continue + } + } + + const extralength = newname.length - parsedBaseLabel.length + param.value = param.value.substring(0, foundindex) + newname + param.value.substring(foundindex - extralength + newname.length, param.value.length) + + } else { + break + } + + // Break no matter what after 5 replaces. May need to increase + if (cnt >= 5) { + break + } + + } + } catch (e) { + console.log("Failed value replacement based on index: ", e) + } + } + } + + setWorkflow(workflow) + setUpdate(Math.random()) + setPrevActionName(name) + }} + /> +
+ {/*!isCloud ? null :*/} +
+ + + Delay + 0} + placeholder={selectedAction.execution_delay} + value={delay} + onChange={(event) => { + setDelay(event.target.value) + }} + /> + + +
+ {/**/} +
+ + )} +
+ )} + + {activeMainTab === "setup" && ( + <> + + + {false && selectedApp.name !== undefined && + ((selectedAction.authentication !== null && + selectedAction.authentication !== undefined && + selectedAction.authentication.length === 0) || isAgent || isIntegration) && + requiresAuthentication ? ( +
+ + + + + +
+ ) : null} + +{/* Change made in new release when we added Tabs system in it */} + {( + (selectedAction.authentication !== undefined && + selectedAction.authentication !== null && + selectedAction.authentication.length > 0) || + (selectedApp.name !== undefined && + (((selectedAction.authentication === undefined || + selectedAction.authentication === null || + selectedAction.authentication.length === 0)) || + isAgent || + isIntegration) && + requiresAuthentication) + ) ? ( + +
+
setAuthSectionOpen(!authSectionOpen)} + > + + + Authentication + + + {parentAction?.authentication_id !== undefined && parentAction?.authentication_id !== null && parentAction?.authentication_id !== "" && parentAction?.authentication_id !== selectedAction?.authentication_id ? + + { + selectedAction.authentication_id = parentAction.authentication_id + + // Check if we can select the parent auth or not + console.log("Changing selected auth to parent: ", parentAction, selectedAction.authentication) + if (selectedAction.authentication !== undefined && selectedAction.authentication !== null && selectedAction.authentication.length > 0) { + var found = false + for (var key in selectedAction.authentication) { + if (selectedAction.authentication[key].id === parentAction.authentication_id) { + selectedAction.selectedAuthentication = selectedAction.authentication[key] + found = true + break + } + } + + if (!found) { + toast.error("Couldn't find parent authentication. Is it distributed to this suborg?") + } + } else { + toast.warning("No matching authentication found for this app. Is it distributed to this suborg?") + selectedAction.selectedAuthentication = { + "id": parentAction.authentication_id, + "name": "Parent auth", + "label": "Parent auth", + } + } + + setSelectedAction(selectedAction) + setUpdate(Math.random()) + }} + /> + + + : null} +
+ + 0 && selectedAction?.selectedAuthentication && Object.getOwnPropertyNames(selectedAction.selectedAuthentication).length !== 0 ? ( + +
+ { + changeDistribution(selectedAction?.selectedAuthentication, "auth") + }} + name="distributeAuth" + color="primary" + /> + } + label="Distribute auth to suborgs" + /> +
+
+ ) : null + } placement="left"> +
+ + + + { + setAuthenticationModalOpen(true); + }} + > + + + +
+
+ {requiresAuthentication && (!selectedAction.authentication_id || selectedAction.authentication_id === "") ? ( +
+ + Authentication needed. + + + Some steps in this workflow won’t run until you connect your account. + + +
+ ) : null} +
+
+ ) : null} + + {selectedAction.authentication_id === "authgroups" && (authGroups === undefined || authGroups === null || authGroups.length === 0) ? + + + Create your first Authentication group + + + : null} + + + {/*showEnvironment !== undefined && showEnvironment && environments.length > 1 && !isIntegration ? ( +
+ Environment + + +
+ ) : null*/} + + {workflow.execution_variables !== undefined && workflow.execution_variables !== null && workflow.execution_variables.length > 0 ? ( +
+ Runtime variable (optional) + +
+ ) : null} + + )} + + {/* :) will be changed soon : Find action */} +
+ {isAgent ? ( + + Shuffle agent + + ) : + setNewSelectedAction !== undefined ? ( + 0)} + autoHighlight + value={selectedAction} + classes={{ inputRoot: classes.inputRoot }} + groupBy={(option) => { + // FIXME: Sorting + // Most popular + // Is categorized + // Uncategorized + return option.category_label !== undefined && option.category_label !== null && option.category_label.length > 0 ? "Most used" : "All Actions"; + }} + renderGroup={(params) => { + + return ( +
  • + {params.group} + {params.children} +
  • + ) + }} + options={renderedActionOptions} + ListboxProps={{ + style: { + backgroundColor: theme.palette.platformColor, + color: theme.palette.textColor, + border: "1px solid rgba(255,255,255,0.3)", + }, + }} + filterOptions={(options, { inputValue }) => { + const lowercaseValue = inputValue.toLowerCase() + options = options.filter(x => x.name?.replaceAll("_", " ").toLowerCase().includes(lowercaseValue) || x.description?.toLowerCase().includes(lowercaseValue)) + + return options + }} + getOptionLabel={(option) => { + if (option === undefined || option === null || option.name === undefined || option.name === null) { + return null; + } + + const newname = ( + option.name?.charAt(0).toUpperCase() + option.name?.substring(1) + )?.replaceAll("_", " "); + + return newname; + }} + fullWidth + sx={{ + '& .MuiOutlinedInput-root': { + height: 40, // Adjust the input height + }, + '& .MuiAutocomplete-input': { + padding: '8px', // Adjust the text padding + }, + }} + + style={{ + backgroundColor: theme.palette.cardBackgroundColor, + height: 35, + borderRadius: theme.palette?.borderRadius, + }} + onChange={(event, newValue) => { + // Workaround with event lol + if (newValue !== undefined && newValue !== null) { + setNewSelectedAction({ + target: { + value: newValue.name + } + }); + } + }} + renderOption={(props, option, state) => { + var newActionname = option.name; + if (option.label !== undefined && option.label !== null && option.label.length > 0) { + newActionname = option.label; + } + + var newActiondescription = option.description; + //console.log("DESC: ", newActiondescription) + if (option.description === undefined || option.description === null) { + newActiondescription = "Description: No description defined for this action" + } else { + newActiondescription = "Description: " + newActiondescription + } + + const iconInfo = GetIconInfo({ name: option.name }); + const iconColor = theme.palette?.mode === "dark" ? "white" : "#333"; + const useIcon = iconInfo.originalIcon && iconInfo.originalIcon !== "" + ? {iconInfo.originalIcon} + : null; + + if (newActionname === undefined || newActionname === null) { + newActionname = "No name" + option.name = "No name" + option.label = "No name" + } + + newActionname = (newActionname.charAt(0).toUpperCase() + newActionname.substring(1))?.replaceAll("_", " "); + + var method = getHttpMethodFromName(option.name) + var extraDescription = "" + + // FIXME: Should it require a base URL? + if (method.length > 0 && option.description !== undefined && option.description !== null && option.description.includes("http")) { + var extraUrl = "" + const descSplit = option.description.split("\n") + // Last line of descSplit + if (descSplit.length > 0) { + extraUrl = descSplit[descSplit.length - 1] + } + + if (extraUrl.length > 0) { + if (extraUrl.includes(" ")) { + extraUrl = extraUrl.split(" ")[0] + } + + if (extraUrl.includes("#")) { + extraUrl = extraUrl.split("#")[0] + } + + extraDescription = `${method} ${extraUrl}` + } else { + //console.log("No url found. Check again :)") + } + } + + return ( + + ); + }} + renderInput={(params) => { + if (params.inputProps?.value) { + const prefixes = ["Post", "Put", "Patch"]; + for (let prefix of prefixes) { + if (params.inputProps.value.startsWith(prefix)) { + let newValue = params.inputProps.value.replace(prefix + " ", ""); + if (newValue.length > 1) { + newValue = newValue.charAt(0).toUpperCase() + newValue.substring(1); + } + // Set the new value without mutating inputProps + params = { ...params, inputProps: { ...params.inputProps, value: newValue } }; + break; + } + } + // Check if it starts with "Get List" and method is "Get" + if (params.inputProps.value.startsWith("Get List")) { + console.log("Get List"); + } + } + + const currentMethod = getHttpMethodFromName(selectedAction !== undefined && selectedAction !== null ? selectedAction.name : "") + + + const actionDescription = null + + return ( + + 0 ? `1px dotted ${theme.palette.distributionColor}` : "inherit", + }} + InputProps={{ + ...params.InputProps, + startAdornment: currentMethod.length > 0 ? ( + + {currentMethod} + + ) : params.InputProps?.startAdornment, + }} + placeholder={isIntegration ? "Choose a category" : "Find Actions"} + variant="outlined" + name={`disable_autocomplete_${Math.random()}`} + disabled={isAgent || (selectedAction?.parent_controlled === true && workflow?.parentorg_workflow?.length > 0)} + sx={{ + '& .MuiOutlinedInput-root': { + borderRadius: "8px", + backgroundColor: theme.palette.actionSidebarField.backgroundColor, + '& fieldset': { border: 'none' }, + '&:hover fieldset': { border: 'none' }, + '&.Mui-focused fieldset': { border: 'none' }, + }, + '& .MuiInputBase-input': { + paddingY: 1.25, + }, + }} + /> + + ); + }} + /> + ) : null} +
    + + {activeMainTab === "setup" && ( + + )} +
    +
    + ) : null} + + {activeMainTab === "configuration" ? ( + + + +
    + {/*hideExtraTypes ? null : +
    + Actions +
    + */} + + {selectedAction?.app_name === "Shuffle AI" && selectedAction?.name === "run_llm" ? + selectedAction?.environment === "Cloud" && isCloud ? ( + + Info: Cloud Inference processing runs with Shuffle's GPUs in EU, Netherlands, and may be unstable. Your data is NOT stored there. + + ) + : + + This action is slow without GPU's. Use Shuffle's Cloud Runtime location for faster processing. + + : + null + } + +
    { + selectedActionParameters !== undefined && selectedActionParameters !== null && selectedAction && Object.getOwnPropertyNames(selectedAction).length > 0 && selectedActionParameters.length > 0 ? +
    + {/* */} +
    + {/* + + */} + {/* */} + + {selectedAction.template === true && selectedAction.matching_actions !== undefined && selectedAction.matching_actions !== null && selectedAction.matching_actions.length > 0 ? +
    + + Select an app you want to use + + { + console.log("LABEL: ", option) + if ( + option === undefined || + option === null || + option.app_name === undefined || + option.app_name === null + ) { + return null; + } + + const newname = ( + option.app_name?.charAt(0).toUpperCase() + option.app_name?.substring(1) + )?.replaceAll("_", " "); + return newname; + }} + options={selectedAction.matching_actions} + fullWidth + style={{ + backgroundColor: theme.palette.inputColor, + height: 35, + borderRadius: theme.palette?.borderRadius, + }} + onChange={(event, newValue) => { + console.log("SELECT: ", event, newValue) + // Workaround with event lol + //if (newValue !== undefined && newValue !== null) { + // setNewSelectedAction({ target: { value: newValue.name } }); + //} + }} + renderOption={(props, data, state) => { + var newActionname = data.app_name; + if ( + data.label !== undefined && + data.label !== null && + data.label.length > 0 + ) { + newActionname = data.label; + } + + const iconInfo = GetIconInfo({ name: data.app_name }); + const iconColor = theme.palette?.mode === "dark" ? "white" : "#333"; + const useIcon = iconInfo.originalIcon && iconInfo.originalIcon !== "" + ? {iconInfo.originalIcon} + : null; + + console.log("Actionname 1: ", newActionname) + + newActionname = ( + newActionname.charAt(0).toUpperCase() + + newActionname?.substring(1) + )?.replaceAll("_", " "); + + return ( +
    + + {useIcon} + + {newActionname} +
    + ); + }} + renderInput={(params) => { + if (params.inputProps !== undefined && params.inputProps !== null && params.inputProps.value !== undefined && params.inputProps.value !== null) { + const prefixes = ["Post", "Put", "Patch"] + for (let [key, keyval] in Object.entries(prefixes)) { + if (params.inputProps.value.startsWith(prefixes[key])) { + params.inputProps.value = params.inputProps.value.replace(prefixes[key] + " ", "", -1) + if (params.inputProps.value.length > 1) { + params.inputProps.value = params.inputProps.value.charAt(0).toUpperCase() + params.inputProps.value.substring(1) + } + break + } + } + } + + return ( + + ); + }} + /> +
    + : null} + {selectedAction.description !== undefined && selectedAction.description !== null && selectedAction.description.length > 0 && hiddenParameters === false ? ( +
    + + Description + + + {selectedAction.description} + +
    + ) : null} + + {suggestionInfo()} + {selectedActionParameters?.map((data, count) => { + if (data.variant === "") { + data.variant = "STATIC_VALUE"; + } + + if ((isIntegration || isAgent) && data.name === "app_name") { + return null + } + + /* + // Somehow autogenerate from the app itself + if (isAgent && data.name == "model") { + //console.log("Options: ", data.options) + } + */ + + if (data.value === "authgroup controlled") { + if (data?.name === "url" && authenticationType?.type === "oauth2-app") { + } else { + return null + } + } + + if (selectedAction.parameters === undefined || selectedAction.parameters === null || selectedAction.parameters.length !== selectedActionParameters.length) { + + //selectedAction.parameters = selectedActionParameters + //console.log("PARAM BUG - length change(?): ", selectedAction) + } + + //!selectedAction.auth_not_required && + if (selectedAction.selectedAuthentication !== undefined && selectedAction.selectedAuthentication.fields !== undefined && selectedAction.selectedAuthentication.fields[data.name] !== undefined) { + + // This sets the placeholder in the frontend. (Replaced in backend) + if (selectedActionParameters[count] !== undefined) { + selectedActionParameters[count].value = selectedAction.selectedAuthentication.fields[data.name] + } + + if (selectedAction.parameters[count] !== undefined) { + selectedAction.parameters[count].value = selectedAction.selectedAuthentication.fields[data.name] + } + + setSelectedAction(selectedAction); + //setUpdate(Math.random()) + + if (authWritten) { + return null; + } + + authWritten = true; + return null + + /* + // FIXME: Is this part necessary to show? + return ( + + Authentication fields are hidden + + ); + */ + } + + + // Added autofill to make this ALOT simpler + if (isCloud && (selectedAction.app_name === "Shuffle Tools" || selectedAction.app_name === "email") && (selectedAction.name === "send_email_shuffle" || selectedAction.name === "send_sms_shuffle") && data.name === "apikey") { + if (selectedActionParameters[count].length === 0) { + selectedAction.parameters[count].value = "TMP: Will be replaced during execution if cloud" + setSelectedAction(selectedAction) + } + + return null + } + + var staticcolor = "inherit"; + var actioncolor = "inherit"; + var varcolor = "inherit"; + var multiline + if ( + data.multiline !== undefined && + data.multiline !== null && + data.multiline === true + ) { + multiline = true; + } + + // make data.value from array to comma separated string if it is an array + if (data.value !== undefined && data.value !== null && Array.isArray(data.value)) { + data.value = data.value.join(",") + } + + if (data.value !== undefined && data.value !== null && + data.value.startsWith("{") && data.value.endsWith("}")) { + multiline = true + } + + var placeholder = "Value"; + if (data.example !== undefined && data.example !== null && data.example.length > 0) { + + placeholder = data.example; + + // if (data.name === "url") { + // data.value = data.example; + // } + // In case of data.example + if (data.value === undefined || data.value === null) { + data.value = "" + } + + if (data.value.length === 0) { + if (data.name.toLowerCase() === "headers") { + //console.log("Should show headers field instead with + and -!") + + // Check if file ID exists + // + const fileFound = selectedActionParameters.find(param => param.name === "file_id") + if (fileFound === undefined || fileFound === null) { + data.value = data.example + } else { + // Purposely unset it if set by default when using files + data.value = "" + } + } + } + + /* + if (data.name !== "queries" && data.name !== "key" && data.name !== "value" ) { + data.value = data.example + } + } + */ + } + + if (selectedAction.name === "custom_action" && data.name === "body") { + for (var key in selectedActionParameters) { + const param = selectedActionParameters[key] + if (param.name === "method") { + if (param.value === "GET") { + return null + } + } + } + } + + if (data.name.startsWith("${") && data.name.endsWith("}")) { + const paramcheck = selectedAction.parameters.find((param) => param.name === "body"); + + + if (paramcheck !== undefined && paramcheck !== null) { + if ( + paramcheck["value_replace"] !== undefined && + paramcheck["value_replace"] !== null + ) { + //console.log("IN THE VALUE REPLACE: ", paramcheck["value_replace"]) + const subparamindex = paramcheck["value_replace"].findIndex( + (param) => param.key === data.name + ); + if (subparamindex !== -1) { + data.value = + paramcheck["value_replace"][subparamindex]["value"]; + } + } + } + } + + + var showCacheConfig = false + if (data.name === "key" && selectedAction.name.includes("cache") && selectedAction.app_name === "Shuffle Tools") { + // Show a key popout button + showCacheConfig = true + } else if (data.name === "category" && selectedAction.app_name === "Shuffle Tools") { + showCacheConfig = true + } + + var disabled = false; + var rows = "3"; + var openApiHelperText = "This is an OpenAPI specific field"; + + + if (selectedApp.generated && data.name === "headers") { + //console.log("HEADER: ", data) + //if (data.value.length === 0) { + //} + //setSelectedActionParameters(selectedActionParameters) + } + + const toggleButtonSx = { + textTransform: "none", + backgroundColor: "transparent", + color: theme.palette.text.primary, + height: 36, + minWidth: 90, + padding: "8px 44px", + display: "flex", + alignItems: "center", + gap: 1, + fontSize: "16px", + fontWeight: 400, + boxShadow: "none", + fontFamily: theme?.typography?.fontFamily, + "&.Mui-selected": { + backgroundColor: themeMode === "dark" ? "#161616" : "#CCCCCC", + color: theme.palette.text.primary, + fontWeight: 600, + "&:hover": { + backgroundColor: themeMode === "dark" ? "rgba(0,0,0,0.3)" : "rgba(0,0,0,0.1)", + }, + }, + "&:hover": { + backgroundColor: themeMode === "dark" ? "rgba(255,255,255,0.05)" : "rgba(0,0,0,0.05)", + }, + } + + var hideBodyButtonValue = ( +
    + { + if (val === "simple") { + // Set localstorage + localStorage.setItem("hideBody", "true") + + setHideBody(false) + const updatedParameters = selectedActionParameters.map((param) => { + if (param.name === "body") { + return { + ...param, + id: "UNTOGGLED", + } + } + + if (param.description === openApiFieldDesc) { + // Check required fields here + if (selectedAction.required_body_fields !== undefined && selectedAction.required_body_fields !== null && selectedAction.required_body_fields.length > 0) { + // Look for the field name in the required_body_fields + if (selectedAction.required_body_fields.includes(param.name)) { + param.required = true + } else { + param.required = false + } + } + + return { ...param, field_active: true } + } + + return param + }) + + setSelectedActionParameters(updatedParameters) + } else if (val === "advanced") { + localStorage.setItem("hideBody", "false") + setHideBody(true) + // Make sure the body field is shown + var foundvalue = false + const updatedParameters = selectedActionParameters.map((param) => { + if (param.name === "body") { + return { + ...param, + id: "TOGGLED", + } + } + + if (param.description === openApiFieldDesc) { + if (param.value.length > 0) { + foundvalue = true + } + + return { ...param, field_active: false } + } + + return param + }) + + if (foundvalue === true) { + toast.info("Please fill either Simple fields OR Advanced body, not both") + } + + setSelectedActionParameters(updatedParameters) + } + }} + sx={{ + backgroundColor: themeMode === "dark" ? "rgba(255,255,255,0.08)" : "rgba(0,0,0,0.05)", + borderRadius: "8px", + padding: "3px", + border: "none", + "& .MuiToggleButtonGroup-grouped": { + border: "none", + "&:not(:first-of-type)": { + borderRadius: "6px", + borderTopLeftRadius: "0px", + borderBottomLeftRadius: "0px", + }, + "&:first-of-type": { + borderRadius: "6px", + borderTopRightRadius: "0px", + borderBottomRightRadius: "0px", + }, + }, + }} + > + + Simple + + + Advanced + + + {/* Legacy Tabs kept for reference, but logic is now handled by ToggleButtonGroup */} + {/* + { + // Set localstorage + localStorage.setItem("hideBody", "true") + + setHideBody(false) + const updatedParameters = selectedActionParameters.map((param) => { + if (param.name === "body") { + return { + ...param, + id: "UNTOGGLED", + } + } + + if (param.description === openApiFieldDesc) { + // Check required fields here + if (selectedAction.required_body_fields !== undefined && selectedAction.required_body_fields !== null && selectedAction.required_body_fields.length > 0) { + // Look for the field name in the required_body_fields + if (selectedAction.required_body_fields.includes(param.name)) { + param.required = true + } else { + param.required = false + } + } + + return { ...param, field_active: true } + } + + return param + }) + + setSelectedActionParameters(updatedParameters) + }} + /> + { + localStorage.setItem("hideBody", "false") + setHideBody(true) + // Make sure the body field is shown + var foundvalue = false + const updatedParameters = selectedActionParameters.map((param) => { + if (param.name === "body") { + return { + ...param, + id: "TOGGLED", + } + } + + if (param.description === openApiFieldDesc) { + if (param.value.length > 0) { + foundvalue = true + } + + return { ...param, field_active: false } + } + + return param + }) + + if (foundvalue === true) { + toast.info("Please fill either Simple fields OR Advanced body, not both") + } + + setSelectedActionParameters(updatedParameters) + }} + /> + */} +
    + ) + + var showButtonField = false + if (selectedApp.generated === true && data.name === "body") { + const regex = /\${(\w+)}/g; + const found = placeholder.match(regex); + + var newhidebody = hideBody + showButtonField = true + if (found === undefined || found === null || found.length === 0) { + newhidebody = false + hideBodyButtonValue = null + + if (hideBody === false) { + setHideBody(true) + } + } + + if (newhidebody === true) { + //toast("BODYBUTTON TRUE") + } else { + + rows = "1"; + disabled = true; + openApiHelperText = "OpenAPI spec: fill the following fields."; + + var changed = false; + var tempArray = [] + for (let specKey in found) { + const tmpitem = found[specKey]; + var skip = false; + + for (let innerkey in selectedActionParameters) { + if (selectedActionParameters[innerkey].name === tmpitem) { + skip = true; + break; + } + } + + if (skip) { + //console.log("SKIPPING ", tmpitem) + continue; + } + + changed = true; + var isRequired = false + // Check if original field name is in the selectedAction.required_body_fields + if (selectedAction.required_body_fields !== undefined && selectedAction.required_body_fields !== null) { + for (let innerkey in selectedAction.required_body_fields) { + if (selectedAction.required_body_fields[innerkey] === tmpitem) { + isRequired = true + break + } + } + } + + tempArray.push({ + action_field: "", + configuration: false, + description: openApiFieldDesc, + example: "", + id: "", + multiline: false, + name: tmpitem, + options: null, + required: isRequired, + schema: { type: "string" }, + skip_multicheck: false, + tags: null, + value: "", + variant: "STATIC_VALUE", + field_active: true, + + autocompleted: false, + }); + } + + var required = selectedActionParameters.filter(item => item.required === true) + var notRequired = selectedActionParameters.filter(item => item.required === false) + + if (tempArray.length > 0) { + // Sort tempArray based on tempArray.required + tempArray.sort((a, b) => (a.required < b.required) ? 1 : -1) + // Add all items to the selectedActionParameters array + for (let innerkey in tempArray) { + tempArray[innerkey].id = "ADDED" + + if (tempArray[innerkey].required === true) { + required.push(tempArray[innerkey]) + } else { + notRequired.push(tempArray[innerkey]) + } + } + } + + if (changed) { + // Sort selectedActionParameters based on selectedActionParameters.required + // Find the "headers" and "queries" field names and put them on the first indexes anyway + var newArray = required.concat(notRequired) + + + setSelectedActionParameters(newArray) + } + + } + } + + const clickedFieldId = "rightside_field_" + count; + const parameterFieldId = "param_" + data.name.replace(/[^a-zA-Z0-9_-]/g, '_'); + + var baseHelperText = "" + if (data !== undefined && data !== null && data.value !== undefined && data.value !== null && data.value.length > 0) { + baseHelperText = calculateHelpertext(data.value) + } + + var tmpitem = data.name.valueOf(); + if (data.name.startsWith("${") && data.name.endsWith("}")) { + tmpitem = tmpitem.slice(2, data.name.length - 1); + } + + if (tmpitem === "from_shuffle") { + tmpitem = "from" + } + + tmpitem = ( + tmpitem?.charAt(0).toUpperCase() + tmpitem?.substring(1) + )?.replaceAll("_", " "); + + if (tmpitem === "Username basic") { + tmpitem = "Username" + } else if (tmpitem === "Password basic") { + tmpitem = "Password" + } + + // No longer multiline for new fields + //multiline = data.name.startsWith("${") && data.name.endsWith("}") ? true : multiline + + if (data.name === "body") { + //console.log("BODY: ", data) + if (hideBody === false) { + return hideBodyButtonValue + } + + rows = "4" + multiline = true + disabled = false + } + + const description = data.description === undefined ? "" : data?.description; + + const tooltipDescription = ( + + + + {tmpitem.charAt(0).toUpperCase() + tmpitem.slice(1)} + + {/* { + setUiBox("closed") + }} + > + + */} + + + + + Required: {data.required === true || data.configuration === true ? "True" : "False"} + + { + data.description !== undefined && data.description !== null && data.description.length > 0 ? + + Description: {description} + + : null + } + + Ex. : {data?.example?.length > 0 ? data.example : "No example available"} + + { + data?.configuration === true ? + ( + + Auth: Use "\$" instead of "$" + + ) : null + } + + {/*
    { + e.preventDefault() + e.stopPropagation() + + localStorage.setItem("disabled_ui_box", "true") + setUiBox("closed") + }}> + + Don't show again + +
    */} +
    +
    + ); + + if (selectedApp.name === "email") { + //hideBody = false + showButtonField = false + } + + + if ((multiline === undefined || multiline === false) && ((data?.autocompleted === true || data?.field_active === true) || data.name.startsWith("${") && data.name.endsWith("}"))) { + multiline = true + } + + if (data?.autocompleted === true || data?.field_active === true) { + rows = "1" + } + + var datafield = ( + + + + + { + event.preventDefault() + + // Get cursor position + // This makes it so we can put it in the right location? + setMenuPosition({ + top: event.pageY + 10, + left: event.pageX + 10, + }); + setShowDropdownNumber(count); + setShowDropdown(true); + setShowAutocomplete(true); + }} + /> + + + + ), + }} + multiline={multiline} + onClick={() => { + /* + setExpansionModalOpen(false); + */ + + if (setScrollConfig !== undefined && scrollConfig !== null && scrollConfig !== undefined && scrollConfig.selected !== clickedFieldId) { + + scrollConfig.selected = clickedFieldId + setScrollConfig(scrollConfig) + } + }} + minRows={rows} + maxRows={6} + color="primary" + // defaultValue={data.value} + value={ + data?.value + } + error={ + data?.error?.length > 0 ? true : false + } + helperText={data?.error?.length > 0 ? errorHelperText(data?.name, data?.value, data?.error) : returnHelperText(data.name, data.value)} + //options={{ + // theme: 'gruvbox-dark', + // keyMap: 'sublime', + // mode: 'python', + //}} + //height={multiline ? 50 : 150} + type={ + placeholder.includes("***") || + (data.configuration && + (data.name.toLowerCase().includes("api") || + data.name.toLowerCase().includes("key") || + data.name.toLowerCase().includes("pass"))) + ? "password" + : "text" + } + placeholder={placeholder} + onChange={(event) => { + handleParamChange(event, count, data) + }} + onFocus={(event) => { + // Get local storage key "disabled_ui_box" and check if it's true + const disabledUiBox = localStorage.getItem("disabled_ui_box") + if (disabledUiBox === "true") { + } else { + //setUiBox(event.target.id) + } + }} + onBlur={(event) => { + handleParamChange(event, count, data) + + baseHelperText = calculateHelpertext(event.target.value) + if (setLastSaved !== undefined) { + setLastSaved(false) + } + + // Check if we clicked the tooltip or not + const tooltipid = "rightside_field_tooltip" + count + const foundElement = document.getElementById(tooltipid) + if (foundElement !== null && foundElement !== undefined) { + console.log("FOUND: ", foundElement) + } else { + //console.log("TOOLTIP -> NOT FOUND") + //setUiBox("closed") + } + }} + /> + + ) + + // Finds headers from a string to be used for autocompletion + const findHeaders = (inputdata) => { + var splitdata = inputdata.split("\n") + + var foundnewline = false + var allValues = [] + for (let [key, keyval] in Object.entries(splitdata)) { + const line = splitdata[key] + if (line === "") { + foundnewline = true + continue + } + + var splitvalue = "" + if (line.includes(":")) { + splitvalue = ":" + } + + if (line.includes("=")) { + splitvalue = "=" + } + + if (splitvalue.length === 0) { + allValues.push({ + key: line, + value: "", + }) + continue + } + + var splitKeys = line.split(splitvalue) + if (splitKeys.length > 1) { + allValues.push({ + key: splitKeys[0].trim(), + value: splitKeys[1].trim(), + }) + } else { + console.log("No keys for ", line) + } + } + + // Just add one + if (foundnewline) { + allValues.push({ + key: "", + value: "", + }) + } + + return allValues + } + + if (data.name.toLowerCase() === "headers") { + //var tmpheaders = findHeaders(data.value) + var tmpheaders = findHeaders(selectedActionParameters[count].value) + const tmpdatafield = +
    + {tmpheaders.map((inputdata, index) => { + const oldkey = inputdata.key + const oldval = inputdata.value + + return ( + +
    + { + console.log("Change from oldkey to new: ", oldkey, e.target.value) + + // Find the right line to replace! + //const newval = selectedActionParameters[count].value.replace(oldval, e.target.value, 1) + const tmpsplit = selectedActionParameters[count].value.split("\n") + var valsplit = [] + var add_empty = false + for (let [key, keyval] in Object.entries(tmpsplit)) { + if (tmpsplit[key] === "") { + add_empty = true + continue + } + + valsplit.push(tmpsplit[key]) + } + + if (add_empty) { + valsplit.push("") + } + console.log("Split: ", valsplit) + + var newarr = [] + for (let [key, keyval] in Object.entries(valsplit)) { + var line = valsplit[key] + + if (key == index) { + if (oldkey === "") { + if (line.includes("=") || line.includes(":")) { + newarr.push(e.target.value + line) + } else { + newarr.push(e.target.value + ": " + line) + } + } else { + newarr.push(line.replace(oldkey, e.target.value, 1)) + } + + } else { + newarr.push(line) + } + } + + var newval = newarr.join("\n") + console.log("Fixed: ", newval) + + selectedActionParameters[count].value = newval + selectedAction.parameters[count].value = newval + setSelectedAction(selectedAction) + setSelectedActionParameters(selectedActionParameters) + }} + /> + { + console.log("Change from oldval to new: ", oldval, e.target.value) + + // Find the right line to replace! + //const newval = selectedActionParameters[count].value.replace(oldval, e.target.value, 1) + var tmpsplit = selectedActionParameters[count].value.split("\n") + var valsplit = [] + var add_empty = false + for (let [key, keyval] in Object.entries(tmpsplit)) { + if (tmpsplit[key] === "") { + add_empty = true + continue + } + + valsplit.push(tmpsplit[key]) + } + + if (add_empty) { + valsplit.push("") + } + console.log("Split: ", valsplit) + + var newarr = [] + for (let [key, keyval] in Object.entries(valsplit)) { + var line = valsplit[key] + + if (key == index) { + if (oldval === "") { + if (line.includes("=") || line.includes(":")) { + newarr.push(line + e.target.value) + } else { + newarr.push(line + ": " + e.target.value) + } + } else { + newarr.push(line.replace(oldval, e.target.value, 1)) + } + + } else { + newarr.push(line) + } + } + + var newval = newarr.join("\n") + console.log("Fixed: ", newval) + + selectedActionParameters[count].value = newval + selectedAction.parameters[count].value = newval + setSelectedAction(selectedAction) + setSelectedActionParameters(selectedActionParameters) + }} + /> +
    +
    + ) + })} + +
    + } + + //const regexp = new RegExp("\W+\.", "g") + //let match + //while ((match = regexp.exec(data.value)) !== null) { + // console.log(`Found ${match[0]} start=${match.index} end=${regexp.lastIndex}.`); + //} + + //const str = = data.value.search(submatch) + //console.log("FOUND? ", n) + //for (var key in keywords) { + // const keyword = keywords[key] + // if (data.value.includes(keyword)) { + // console.log("INCLUDED: ", keyword) + // } + //} + + if (files !== undefined && files !== null && data.name.toLowerCase() === "file_category") { + //selectedActionParameters[count].options.length > 0 + console.log("FileS: ", files) + if (files.namespaces !== undefined && files.namespaces !== null && files.namespaces.length > 0) { + data.options = files.namespaces + } + } + + //const keywords = ["len", "lower", "upper", "trim", "split", "length", "number", "parse", "join"] + if ( + selectedActionParameters[count].schema !== undefined && + selectedActionParameters[count].schema !== null && + selectedActionParameters[count].schema.type === "file" + ) { + datafield = ( + + + { + setMenuPosition({ + top: event.pageY + 10, + left: event.pageX + 10, + }); + setShowDropdownNumber(count); + setShowDropdown(true); + setShowAutocomplete(true); + }} + /> + + + ), + }} + helperText={returnHelperText(data.name, data.value)} + fullWidth + multiline={multiline} + minRows={3} + maxRows={6} + color="primary" + defaultValue={data.value} + type={"text"} + placeholder={"The file ID to get"} + id={"rightside_field_" + count} + onChange={(event) => { + changeActionParameter(event, count, data); + }} + onBlur={(event) => { }} + /> + ) + } else if ( + (data.options !== undefined && data.options !== null && data.options.length > 0) || + (selectedActionParameters[count].options !== undefined && selectedActionParameters[count].options !== null && selectedActionParameters[count].options.length > 0)) { + const parsedoptions = data.options !== undefined && data.options !== null && data.options.length > 0 ? data.options : selectedActionParameters[count].options + + if (selectedActionParameters[count].value === "") { + // && selectedActionParameters[count].required) { + // Rofl, dirty workaround :) + const e = { + target: { + value: parsedoptions[0], + }, + }; + + changeActionParameter(e, count, data); + } + + var multi = false + if (selectedActionParameters[count].multiselect !== undefined && selectedActionParameters[count].multiselect !== null && selectedActionParameters[count].multiselect === true) { + multi = true + + selectedActionParameters[count].value = selectedActionParameters[count].value.split(",") + } + + datafield = ( + + ); + } else if (data.variant === "STATIC_VALUE") { + staticcolor = "#FF8544"; + } + + if (data.field_active === false) { + //console.log("Field not active: ", data?.name) + return null + } + + // Shows nested list of nodes > their JSON lists + const ActionlistWrapper = (props) => { + const handleMenuClose = () => { + setShowAutocomplete(false); + + if ( + !selectedActionParameters[count].value[ + selectedActionParameters[count].value.length - 1 + ] === "$" + ) { + setShowDropdown(false); + } + + setUpdate(Math.random()); + setMenuPosition(null); + }; + + const handleItemClick = (values) => { + if (values === undefined || values === null || values.length === 0) { + return; + } + + var toComplete = selectedActionParameters[count].value.trim() + .endsWith("$") + ? values[0].autocomplete + : "$" + values[0].autocomplete; + + toComplete = toComplete?.toLowerCase()?.replaceAll(" ", "_"); + for (let [key, keyval] in Object.entries(values)) { + if (key == 0 || values[key]?.autocomplete?.length === 0) { + continue; + } + + toComplete += values[key].autocomplete; + } + + + + // Handles the fields under OpenAPI body to be parsed. + if (data.name.startsWith("${") && data.name.endsWith("}")) { + const paramcheck = selectedAction.parameters.find( + (param) => param.name === "body" + ) + + if (paramcheck !== undefined) { + if (paramcheck["value_replace"] === undefined || paramcheck["value_replace"] === null) { + paramcheck["value_replace"] = [ + { + key: data.name, + value: toComplete, + }, + ] + } else { + const subparamindex = paramcheck["value_replace"] + .findIndex((param) => param.key === data.name); + + if (subparamindex === -1) { + paramcheck["value_replace"].push({ + key: data.name, + value: toComplete, + }) + + } else { + paramcheck["value_replace"][subparamindex]["value"] += + toComplete; + } + } + + selectedActionParameters[count]["value_replace"] = paramcheck; + + selectedAction.parameters = selectedActionParameters + //selectedAction.parameters[count]["value_replace"] = paramcheck; + setSelectedAction(selectedAction); + setUpdate(Math.random()); + + setShowDropdown(false); + setMenuPosition(null); + return; + } + } + + console.log("In nestedclick!!") + var newValue = selectedActionParameters[count].value + toComplete + changeActionParameter({ target: { value: newValue } }, count, data, true) + //selectedActionParameters[count].value += toComplete; + //selectedAction.parameters[count].value = selectedActionParameters[count].value; + //setSelectedAction(selectedAction); + //setUpdate(Math.random()); + + setShowDropdown(false); + setMenuPosition(null); + }; + + const iconStyle = { + marginRight: 15, + }; + + return ( + { + handleMenuClose(); + }} + open={!!menuPosition} + style={{ + color: theme.palette.text.primary, + marginTop: 2, + maxHeight: 650, + }} + > + {actionlist.map((innerdata) => { + const icon = + innerdata.type === "action" ? ( + + ) : innerdata.type === "workflow_variable" || + innerdata.type === "execution_variable" ? ( + + ) : ( + + ); + + const handleExecArgumentHover = (inside) => { + var exec_text_field = document.getElementById( + "execution_argument_input_field" + ); + if (exec_text_field !== null) { + if (inside) { + exec_text_field.style.border = "2px solid #FF8544"; + } else { + exec_text_field.style.border = ""; + } + } + + // Also doing arguments + if ( + workflow.triggers !== undefined && + workflow.triggers !== null && + workflow.triggers.length > 0 + ) { + for (let [key, keyval] in Object.entries(workflow.triggers)) { + const item = workflow.triggers[key]; + + if (cy !== undefined) { + var node = cy.getElementById(item.id); + if (node.length > 0) { + if (inside) { + node.addClass("shuffle-hover-highlight"); + } else { + node.removeClass("shuffle-hover-highlight"); + } + } + } + } + } + }; + + const handleActionHover = (inside, actionId) => { + if (cy !== undefined) { + var node = cy.getElementById(actionId); + if (node.length > 0) { + if (inside) { + node.addClass("shuffle-hover-highlight"); + } else { + node.removeClass("shuffle-hover-highlight"); + } + } + } + }; + + const handleMouseover = () => { + if (innerdata.type === "Runtime Argument") { + handleExecArgumentHover(true); + } else if (innerdata.type === "action") { + handleActionHover(true, innerdata.id); + } + }; + + const handleMouseOut = () => { + if (innerdata.type === "Runtime Argument") { + handleExecArgumentHover(false); + } else if (innerdata.type === "action") { + handleActionHover(false, innerdata.id); + } + }; + + var parsedPaths = []; + if (innerdata.type === "workflow_variable") { + // Try to parse the value if it's a string that could be JSON + if (typeof innerdata.value === "string") { + try { + const parsedValue = JSON.parse(innerdata.value) + if (typeof parsedValue === "object") { + parsedPaths = GetParsedPaths(parsedValue, ""); + } + } catch (e) { + // Not valid JSON, use the value directly + parsedPaths = GetParsedPaths(innerdata.value, ""); + } + } else if (typeof innerdata.value === "object") { + parsedPaths = GetParsedPaths(innerdata.value, ""); + } + } else if (typeof innerdata.example === "object") { + parsedPaths = GetParsedPaths(innerdata.example, ""); + } + + const coverColor = "#82ccc3" + //menuPosition.left -= 50 + //menuPosition.top -= 250 + //console.log("POS: ", menuPosition1) + var menuPosition1 = menuPosition + if (menuPosition1 === null) { + menuPosition1 = { + "left": 0, + "top": 0, + } + } else if (menuPosition1.top === null || menuPosition1.top === undefined) { + menuPosition1.top = 0 + } else if (menuPosition1.left === null || menuPosition1.left === undefined) { + menuPosition1.left = 0 + } + + //console.log("POS1: ", menuPosition1) + + return parsedPaths.length > 0 ? ( + + {icon} {innerdata.name} +
    + } + parentMenuOpen={!!menuPosition} + style={{ + color: theme.palette.text.primary, + minWidth: 250, + maxWidth: 250, + maxHeight: 50, + overflow: "hidden", + }} + onClick={() => { + console.log(innerdata.example) + handleItemClick([innerdata]); + }} + > + + { + //console.log("HOVER: ", pathdata); + }} + onClick={() => { + handleItemClick([innerdata]); + }} + > + + {innerdata.name} + + + {parsedPaths.map((pathdata, index) => { + // FIXME: Should be recursive in here + // + const icon = + pathdata.type === "value" ? ( + + ) : pathdata.type === "list" ? ( + + ) : ( + + ); + // + + const indentation_count = (pathdata.name.match(/\./g) || []).length + 1 + const baseIndent =
    + //const boxPadding = pathdata.type === "object" ? "10px 0px 0px 0px" : 0 + const boxPadding = 0 + const namesplit = pathdata.name.split(".") + const newname = namesplit[namesplit.length - 1] + return ( + { + //console.log("HOVER: ", pathdata); + }} + onClick={() => { + handleItemClick([innerdata, pathdata]); + }} + > + +
    + {Array(indentation_count).fill().map((subdata, subindex) => { + return ( + baseIndent + ) + })} + {icon} {newname} + {pathdata.type === "list" ? { + e.preventDefault() + e.stopPropagation() + + console.log("INNER: ", innerdata, pathdata) + + // Removing .list from autocomplete + var newname = pathdata.name + if (newname.length > 5) { + newname = newname.slice(0, newname.length - 5) + } + + //selectedActionParameters[count].value += `{{ $${innerdata.name}.${newname} | size }}` + selectedActionParameters[count].value += `$${innerdata.name}.${newname}` + selectedAction.parameters[count].value = selectedActionParameters[count].value; + setSelectedAction(selectedAction); + setUpdate(Math.random()); + setShowDropdown(false); + setMenuPosition(null); + }} /> : null} +
    +
    +
    + ); + })} + + + ) : ( + handleMouseover()} + onMouseOut={() => { + handleMouseOut(); + }} + onClick={() => { + handleItemClick([innerdata]); + }} + > + +
    + {icon} {innerdata.name} +
    +
    +
    + ); + })} + + ); + } + + const buttonTitle = `Authenticate the ${selectedApp?.name?.replaceAll("_", " ")} API` + const hasAutocomplete = data?.autocompleted === true + if (data.variant === undefined || data.variant === null) { + data.variant = "STATIC_VALUE" + } + + var isFirstOptional = optionalFound === false && data.configuration === false && data.required === false ? true : false + if (optionalFound === false && data.configuration === false && data.required === false) { + optionalFound = true + } + + if (isFirstOptional) { + // Check if any required fields are found + var foundRequired = false + for (var key in selectedActionParameters) { + if (selectedActionParameters[key]?.required === true) { + foundRequired = true + break + } + } + + if (!foundRequired) { + isFirstOptional = false + } + } + + var parentParamValue = "" + if (parentAction !== undefined && parentAction !== null && parentAction !== "" && parentAction.parameters !== undefined && parentAction.parameters !== null) { + const foundParamIndex = parentAction.parameters.findIndex((param) => param.name === data.name) + if (foundParamIndex !== -1) { + parentParamValue = parentAction.parameters[foundParamIndex].value + } + } + + const isOptional = data.configuration === false && data.required === false + + return ( +
    + {isFirstOptional ? ( + <> + + + + ) : null} + +
    + {showButtonField === true ? hideBodyButtonValue : null} +
    +
    + {data.configuration === true ? ( + + { + setAuthenticationModalOpen(true); + }} + /> + + ) : null} + + {hasAutocomplete === true ? + data.field_active === true ? + + + + + : + + + + : + null} + + {showCacheConfig === true ? + + + + + + : null} + + + + {tmpitem} {selectedActionParameters[count].required || selectedActionParameters[count].configuration ? "*" : ""} + + + + {parentParamValue !== undefined && parentParamValue !== null && parentParamValue !== "" && parentParamValue !== data.value ? + + { + selectedActionParameters[count].value = parentParamValue + selectedAction.parameters[count].value = parentParamValue + setSelectedAction(selectedAction) + setUpdate(Math.random()) + }} + /> + + : null} +
    + + {((data.options !== undefined && data.options !== null && data.options.length > 0) || (selectedActionParameters[count].options !== undefined && selectedActionParameters[count].options !== null && selectedActionParameters[count].options.length > 0)) ? null : + + { + if(selectedAction?.name !== "filter_list"){ + const clickedField = document.getElementById(clickedFieldId) + if (clickedField !== null) { + clickedField.focus() + } + } + }} + onClick={(event) => { + // Set focus to the Textfield we just clicked + // This is to ensure focus is set correctly at all times with blur + if(selectedAction?.name !== "filter_list"){ + const clickedField = document.getElementById(clickedFieldId) + if (clickedField !== null) { + clickedField.focus() + } + } + + event.preventDefault() + setFieldCount(count) + setExpansionModalOpen(true) + setActiveDialog("codeeditor") + //setcodedata(data.value) + var parsedvalue = data.value + if (parsedvalue === undefined || parsedvalue === null) { + parsedvalue = "" + } + + //console.log("Required fields: ", selectedActionParameters[count]) + navigate(`?action_id=${selectedAction.id}&field=${data.name}&action_name=${selectedAction.name}&app_name=${selectedAction?.label}`) + setEditorData({ + "name": data.name, + "value": fixExample(parsedvalue), + "field_number": count, + "actionlist": actionlist, + "field_id": clickedFieldId, + + "example": fixExample(selectedActionParameters[count].example), + }) + + }} + /> + + } + + +
    + {datafield} + {/*shufflecode*/} + {showDropdown && + showDropdownNumber === count && + data.variant === "STATIC_VALUE" && + jsonList.length > 0 ? ( + + + + Autocomplete + + + + ) : null} + {showDropdown && + showDropdownNumber === count && + data.variant === "STATIC_VALUE" && + jsonList.length === 0 ? ( + + ) : null} +
    +
    +
    + ); + })} +
    + : null + } + +
    +
    + + ) : null} + + {activeMainTab === "configuration" ? ( +
    + + Rerun this action with results from your previously selected workflow run. Allows testing individual action changes without rerunning the full workflow. + + } + placement="top" + > + + + + +
    + ) : null} + + )} +
    + ); +}; + +export default ParsedAction; diff --git a/frontend/src/components/PartnerApps.jsx b/frontend/src/components/PartnerApps.jsx index 60e6e1b5..124067c3 100644 --- a/frontend/src/components/PartnerApps.jsx +++ b/frontend/src/components/PartnerApps.jsx @@ -13,9 +13,9 @@ const PartnerApps = () => { minHeight: "500px", }} > - Partner Apps + Partner's Apps ) } -export default PartnerApps +export default PartnerApps \ No newline at end of file diff --git a/frontend/src/components/PartnerHeader.jsx b/frontend/src/components/PartnerHeader.jsx index 3a6a910b..51f14791 100644 --- a/frontend/src/components/PartnerHeader.jsx +++ b/frontend/src/components/PartnerHeader.jsx @@ -46,10 +46,10 @@ const useStyles = makeStyles({ }); const defaultImage = - "data:image/png;base64,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"; + "data:image/png;base64,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" const defaultLandscapeImage = - "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIMAAAAcCAYAAABCrQzwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAARPSURBVHgB7VpdUtswEF4zvJOeAHECwgnqzNDnpifAPQHhmc4QZtrnhBNgTlD63HZiTpD0BLgnIL1A3f3Qqiiu/0KcOC76ZnYkSxtLWq1WuxsTOTg4OKThJR+PE1ojvA/fPHJoBXbIwUGwSy8ASZL4XCjUPc8LqQHIHEB7TD+YIp5LnOLpcBEw7TP9Ep5I+vpcoD82bXWj1crAAlJczFk48xLWgOlE6iFtEDLHCYkyWnjHFFt8F1wMUzxvmY6kPpJ3hEwRrQFZyjCncuE2Ajk5p6RPmG+1Y763TDfrOjUrAJuspB4x3TEdkrWhojBDecRabqQe0waRpQwz7/xrj7YMBScMMOY1YL4xK8QZbQ/6UoY8r/clPECP+WbUANrkQNqKcMkEhT0gbUYh5Fj6BqwQ17Q96Ej5swIPNaUIQCt8BtsBZODkD1MsEGDIfCFp36A0nJUrRzF1tvBqWQliRZU8zryK135bLIOy6nd5TLzogIszKTMBJWCClXlgmjJN+PlBHLgs/vtEY5LTP0yeoKTNNw0W60WyCFxpE+G5sN5n456WACIOmSd+NxHC2q7N3IpQyTIkn94ERL9fL7bu3LFvES7yHfOiFgf1zr/n3ZPPBeZxm9fJijCmYkBAXdKOGkiRNtPY1M6W+RuVkYpGsK6Y9LoUaX8KCtpLh7M2qlmGhD33hE+bTf8oRx5fLcDmG1M3kNM6kOtjWSjS1uMVE3wOUGy9u0MrAteOJ7CaQ28ReO4Jz6XFd2DxHFQZj+cM5R7K442840h+jxDWKH2hL9WKa0LuPFgYoxCKdNw9MSacaVRROS5t6yEn5crq71P7YK4ZJKQC20fgOg6SWZ9fpOytSTphUbwQOIpD0nF61+r2hQbiRJ4VOE1hRltjHnxN8KWc5RwIWxaQW5TB064MpJziAHXRcF/okJ4Egn5FOvTMesdWJtSeC5GDOe19KrdsBZbBezxJFjIcDA+alKT+3dzJ9eo3AdnUWyETTiE6eFQScQb/q42vgJjKs5a5/btVvH2JGkJqEAjhSOcYMjcYVoN54DydShOUYhPKsE8NAvKQdDzW+4WfB/RMtMKBlIwinKRRibe/Z9XrVgSV0+5T8zA+z0mWfGA1kdcoeUdrkk5mgQHTVBI2ynTKYkfSD4Q1XhHmOlSJlZiS5BWUVNFmoTLaTGgKOV2nZIO2z9I+pQK0JbRErGzCI0U6Xr63snzIuBnzGNNi3L4qxlZ9KNlKjIcMZkAFCbCaYU6/L3mWx1wLGiSdbtYMBxJ9U2ueJvK6Khpgl7OLS6U8C1FDwiYPuAt5cRA8fIIsjxmWAIsd1+k44o8jHhfKaL4nMN47xsAGRLSZ3AQyo12Zg5K2mTVPKGpMWj5dWgy9I9L5lahogNZ+AylZN6N8cVGadQ1jbmS8gjmQzGGew4M5mpR7XPVwtPZLpyb+6vUa/Ht5mTnI5ke0JNwHsQ5/4ZTBwcHBwaEAfwCepixlSVZHRgAAAABJRU5ErkJggg=="; + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAV4AAAB4CAIAAAA1yw3SAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAEvmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSfvu78nIGlkPSdXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQnPz4KPHg6eG1wbWV0YSB4bWxuczp4PSdhZG9iZTpuczptZXRhLyc+CjxyZGY6UkRGIHhtbG5zOnJkZj0naHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyc+CgogPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9JycKICB4bWxuczpBdHRyaWI9J2h0dHA6Ly9ucy5hdHRyaWJ1dGlvbi5jb20vYWRzLzEuMC8nPgogIDxBdHRyaWI6QWRzPgogICA8cmRmOlNlcT4KICAgIDxyZGY6bGkgcmRmOnBhcnNlVHlwZT0nUmVzb3VyY2UnPgogICAgIDxBdHRyaWI6Q3JlYXRlZD4yMDI1LTExLTE4PC9BdHRyaWI6Q3JlYXRlZD4KICAgICA8QXR0cmliOkV4dElkPmRhNGQ3YjEyLWI5M2ItNGY0NS1iNTA3LTAzODliZmQ4N2EyZDwvQXR0cmliOkV4dElkPgogICAgIDxBdHRyaWI6RmJJZD41MjUyNjU5MTQxNzk1ODA8L0F0dHJpYjpGYklkPgogICAgIDxBdHRyaWI6VG91Y2hUeXBlPjI8L0F0dHJpYjpUb3VjaFR5cGU+CiAgICA8L3JkZjpsaT4KICAgPC9yZGY6U2VxPgogIDwvQXR0cmliOkFkcz4KIDwvcmRmOkRlc2NyaXB0aW9uPgoKIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PScnCiAgeG1sbnM6ZGM9J2h0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvJz4KICA8ZGM6dGl0bGU+CiAgIDxyZGY6QWx0PgogICAgPHJkZjpsaSB4bWw6bGFuZz0neC1kZWZhdWx0Jz5VbnRpdGxlZCBkZXNpZ24gLSAxPC9yZGY6bGk+CiAgIDwvcmRmOkFsdD4KICA8L2RjOnRpdGxlPgogPC9yZGY6RGVzY3JpcHRpb24+CgogPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9JycKICB4bWxuczpwZGY9J2h0dHA6Ly9ucy5hZG9iZS5jb20vcGRmLzEuMy8nPgogIDxwZGY6QXV0aG9yPk1vbmlsIFByYWphcGF0aTwvcGRmOkF1dGhvcj4KIDwvcmRmOkRlc2NyaXB0aW9uPgoKIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PScnCiAgeG1sbnM6eG1wPSdodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvJz4KICA8eG1wOkNyZWF0b3JUb29sPkNhbnZhIChSZW5kZXJlcikgZG9jPURBRzVDTzFYREZNIHVzZXI9VUFHNUNORmVmeUEgYnJhbmQ9QkFHNUNEcXhDOUEgdGVtcGxhdGU9PC94bXA6Q3JlYXRvclRvb2w+CiA8L3JkZjpEZXNjcmlwdGlvbj4KPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KPD94cGFja2V0IGVuZD0ncic/PhEaojUAAC8vSURBVHic7b1ptF1lmS085/Outfc5aU56mkCIhEYERUsUSz9LRZECLHssy9Kqokrr1rXu/cpR495x/3zjG6PG+Lo/9169NrRKUEDRAkVRUFR6CH0TIIRAOpJAupP+5Jyz13qf+f1419rnJCAYSE4T1uSQnCZ777X32e98n/d55jMfSkKDBg0a7Asb7wto0KDBRERDDQ0aNHgZNNTQoEGDl0FDDQ0aNHgZNNTQoEGDl0FDDQ0aNHgZNNTQoEGDl0FDDQ0aNHgZZON9AQ1G8FLxGUf9iC/56WGKN9JzncBoqGFCQAm2z5KgoFHcoMN9vbAiRwcIkAIAcjQ9pn8FVT8a+2t8A4GNUHpiQIC0324p7RNJ8LBfCxQECBLAV3y+bCKLQ42GGiYC9HKHiTcsEks6gDoyIGCAd1NjkgA7/KlyXNEcKMYdqheDuM9bHyS7vP1GWgXUSPwkgSl6Im3ULvYGejnGCw01TAgILtXpN0GQtE9ALdWH78MZ3WNC4kSBhACxyk2Sckkiadaww6FFQw3jifQuByiHewE5BJrFKBKEzAwU4IKgMN7Xe9Dho7+QACoFCWBwSi4KJOWRhMAUOwR2X7qRCGtcnsBhjIYaxhfuzoQoAnT3ztDQ1q3bNr64eXioE2NMb3kBPPw0KML+RwMCgqfMi7Gnpz171syjjpzb09vTboUqcKIkJ6u3bkMKhwgNNYwzSAGMEVHZnj17Hnrw0dtvv+uZ5c/192/bu3fI3Y3BHaDxMEzL++j8qyCCIJEiAsIM8+bNPvbYo8989zvP+vAHjlkwHxDllpFEN4PesMOhQFOhGFN0w+AElxMSbHC4fHTp8uuvv+GuO+/ZtWuADEQWo0hLITRB7Rt+HxbYf0lX0gaJohmlMsaOmaZMbZ9yykmf+vQnPvDB9/f1TenJaSTJhh0OHRpqGBdIgiQR8ujO395698WXff/pp1bkWavT8Sxry8EQ3EWambn74fiL2ucpUV4FUWSAxRjdyzyjUGYBorfbrS9+8fNf+MIF8+b0BRvJNZjZvsqohiYOApoDxVhDciAKKB1ySrz/waXf/tYVq1etN+WIgTJjyykvZSGXUHQKCzgcqWEfCCAE0R0wwoIFOgShKEqCw7H80TXXG/Mv/s1nZ/X1WiihEgpAS1WBMwpOZMDhl7IdazTUMPYQSXcnzUJYvXrd5ZddsXbtOjmD5STN5F5KsqAYCzPLc7qiDndZFEdqlAa6JLnMCFSnC8B27xr8yY+vX7To2HPO+UDLSNph/qKMHxpqGGMIlKBUui+K8tbf3/HoI0tjaWQmIcay1cqKYggSKSPMQlF0QNIO851Q6YWpekcCDSZJNAaIZYxBNGZbt2y/7rob3vGO0xYcOxdNluGQoaGGMUY3bWaQNm3ccuutd0qZWXAHoBDYKfZOndo+9dRTTjjh+N4pvSEEh9wFt8P9FB0r1adkFkov169fv3zZsy9s2GzM8iyP0c2yPLOnnlz+8EOPHHvMOXIPWag7shocTDTUMMZQnYMXac88s2LtmnUek9CJMRZgnN435cv/8Lcf//g5M2dPz7OsVkny8BdLj+4vE12+d2Dvww89eemli5cvezZGGTN3mHFoaOihhx4+77wP97SzJu14iNBQw5hCQhL/ulCW/uyzz+0dHJSChUw0ZsgCzz77zz53wcfmzplRh8qVXtgPcAGM9DBPEozQQhI4IrSyqR/4szNj2fl//u+vb98+EKPTDEb3sGb1hj17Bts9UytNtURayl7WNdBJ9NQnIhpqGFOQTPxgZmXZ2bKlvyg8y6eUETGWYDl1avuss86aMaOPVZTAJGoAD9iQqzq4T8IVkvRM7tHAPON7znzXCYsWPvDg4xZyMglIw/ZtO3fv3jN37jRwRCi9X+NJg9eDhhrGGoJIg0NSWToYIHOJwYw2fdqUo446Mnm7AGSljq56jA7gUURWvZsv22Iw0dVTLpfcLLir3crmzp0dAmiI0d2VBRaFF0V0d8CzEA77yu7Yo6GG8UFSPEV3d0WUAo0oy45Lcg+BpKNyf6pucYCdl0kFpJoT9rvtfg4R+0mwx5k4kteTmUkIgRboXpaxoEBkIQRJcsCZ9E773HZfvWmD14yGGsYUqvwHkgIaLk+nhZSDDCEQsoyCVw0FI8fml1+uqv97yXpQjBGAmb2Mmpj73NvEOXTUzzqVMUVapY805lkWq0KEAEQHaBJslKtFg4OIhhrGCJ5SDDQCIEwkmNGYlA6iBZMPkx7EwECIlWrYAPAPyPsIuDzGgrCQmUskyxglGcPwcGdwcG9nuOOABWu3271TeoOZBYIOuAmBQXSCqo4sVp9ivI4mxo44WP9pJlQdmE4CymPMiAyiw2VS5q4YglWucVUFx+r040Qhu8mLhhrGDvvtbd2CZLeU8IrV+Vd4r1cpidRm4S6PtnXrjieWPvHkU8tWrVq9rX+byHa7feRRR55yyslvf/vbTj7phJ7eFjjajDJ1K41+lNGB+livtOrxSFXXRIlV1KPq/9EXP+qTpjZxcNBQw2RFOlRL6rYnS4iOwcHOkvsf/vG1P122bPnu3XvcHTIzSh697O393Zw5s84956Of/MwnFi48ziwyA6qCX7eX0UadXyaMS4T2O/c0p4hDi4YaDgNUB4EY1b9tx1VXXX/jjb/u799JM6iVZ3lZRlfM88y87HTixo39V11z3WOPr/jyVy5873tODyFjlYYgRvIRzcb7RseE2RMaHDhSsOAeAQC2Y+fuSy658sc//mn/1j1gRuTuIUaSGZR7DB5zqOXeijF/6OGn/vv/+M5d9z5cRLnkLnd39zpnOdZZhgYTDQ01TChwVCbuVZelupt9VDY0XPzshpt/+cvfDQ1BYHQrIpwWpQiBjA6BtEzKSjeE7LmVz3/vu1cvX7aq6MAdklIfx8SI1SfCNbyh0VDDxMQfuV1Xy5i0J59cfv31P9+zZ0iiVImF61MCRIhyKEIwA2BmFsKyZct/8uPrdu/ancQBZo21T4MKDTVMYnTFjp1O8etf/27D+k153utOIggSJcT04ZDLYRKcAYCKokOaO+64457ly1e4J3HUfuMeGrxx0VDDZIcArl+/8eFHHo+lPHoWcmfti0IHI+hkpCmpK8uyDMGyPIsxkmHX7r133Xlf0YmSV7OiGjVhg4YaJi9YDWwwd6xY8ez6devzVu5SjFHVsEwBDnpihNSyRIJkGQvAaRBYdPy559bs2rUHoDTReysajBkaapis6Ib97v7CCxvLUh5hDCDIQBkUgEAPJgYZI4PMnEEMMEaZR0UX2hs392/buTtCnnTcIw/iQGwygm9MNNQwWVFHDXD3HTt2xOggJE8BwqjqY9Xe3RVf1p9Y8hMXNLB3YO/gYNJle+V13dDBGx0NNUxisNIoEZQU09IGUluiaskwoZcphSYjRjMC0YyJE4KFJtHQIKGhhskKCVVNwXjEvHlZFlIn0j5zYvWHlEtdXZNcnWnTe6dPnyao253QoEFDDZMYaSmbcf78o9vtPB0lkjs7gFfu1TILSBOx6EccMbevr48gJ1B/doNxRkMNkxdMPVbGcOJJC4897mhXR6i0zkozp6sC5v5mJx5hZmbMQp6F7LS3njp9Wtvoco30NcIbp+Y3MhpqmKxgnWkAMH/+/Pe+90+zLKsDBxt9jujWHLqeSKSiD4OxKAfnzO1773vfneUGiVXEoeqfp1s37PCGREMNkxUjznBQyMLZZ3944cLjSCUkMyQCQBTKyn6WLGNJQwghNWKHLJx3/rmnnHISWXtHsHv/qC0qG7wR0VDDZAUrZxNPWqaT33zC5z9/Qd+MqUIpRCkSnkwODJY6qySlwbFlGYkgxxlnnPHZCz7V09uup2PURtTAH+NWX2urGhyGaKhhsmJf8YHyPJz/sXO+8Nefn943FYgW3MwJyAmFRCQkJScFK8Hht5x6wpe/8sXjFh4VQqVxeKlt9SuUMuuHV52VcMBTzLJvf3eDSYnGymWyop68YEnVFEzTpra/+IVPT5vS89Prb1i7boOc0ZGFtjGL7oCTIjzGOGVa693veudXvvx3b3vrm9vJeprZqP6JlF945RGbSYUtQe5O0l0Ezcy9sp+qp2e8ZusHjoQk0stMCm+Y51CioYbJjbQIgxnBLGDWzGl/9flPn/aWk2/4+S/vu+/B/v6dQ8N7ADMGM0jllKk9C990/Lnnn/Pnf/6Ro46YEwxWnUxG2yvu98k+qI3nRvo+UckrJXh0N4YUO1jyw33tGqpu2mPkq3StDSeMARpqmMRQN1UoGS1ZT7fa4Yx3ve3kNy965pnnVq5cs/zpZ3bs2Nkpi57engULjn3zm098y1tOOXr+Ua08I9SddvEaFrCEFCAYsxijmZGIpcsScZi7m40OGV7XqL1GcTHGaKhhcqN2WabkRgM8CwCtr2/qu999+jvefmoZzxka7pAMwdrtVquVCwKM9O5ErAPkBXVdJEm6p5uH4aFyzdrn+7f2T502bdHxx/dO6cnz9O7q8sLr9XoefZ3c5/svc9po8DrRUMNkRddRGkyu8U6moXkGQcEhtXtDS5w6vQ1UpwAjvQr1newOzjsA1DbWlfe0BcaooaHytlvvWnzFlRs3buqb0XfBBZ/99Gc+PnPmNMJHpT8bJ4jJhIYaJiu6kicCtECYlEY9RRGhGoOXVEvJFrLyd7E0eJvka8oOSp5UElJq04RH3XvPA5dcfMXaNevdfdfu4SsWX5NnrU9/+uPtdgiBFtw9pi6P2tger3a+GDWXI83LGJF4EfAk+KwKK3U8oiYLcfDQFC8PB4xk6Fj/TRqN6aMmkNSgbbT6p3xNO3l3Nbqg4SIufXL5xRdfsXr1BiEHcinv79+1ePHVd9xxb1F4UlhagODdBOIfUddMQ6lGZNv7kkQYfdHdO2vyEQcRDTUcBhjl6VStE5NMMsgIIwKRPjFgtIb6NdYUU5XApaKIK1eu/cY3Lnr2ubVAq4yB1lOWRrY2bdl+8UXfffSRpUUnSnQ/IF6oLi49qWQwkW5j1fEEdbN5uqADfxINXg0NNUx27McLKcbu1gVMSpro0aTwerQGwMja5qbN2y695HuPP/YEFAAzZkXhZplEKFuzZsNF37ns2RWri+ESsO6WzlrIuZ9c6qWj/7qD9gQHRcrlkldEwfRvXtuTaPAqaKhh0iOpD2v7lqqmuO8/oGRSYoqKPv54XtAo3WX3E3dt27Zz8RXX3H3XkhhRFiVAMzDQkSIEwvInnlh+0XcuXbN2QywksRJL1neoSslUsVt9bd0HHcmHZBmh0lWQbpY4Ikra5zxUxRJNCHFw0KQhJyUkpTHa1ddMbOBJwljETlDuXoaMctGytMQgG72S6l36D20PPhK3M2UfVZalWZCwc+fAD6+5/qYbb+0MGmAwRpSQFCx1d0UFd6Nw95LHpvZd/dV//vvjjpufBVYjuanoHZoTltKJEGGSDHVnFyEoSp5l/NCH3zewd++yZc9u3767KMo8QyvQAI8ELQRUz6WKZhp2OAhoqGFSYpQmMMml5RIrabM8au/AQKud0zJJuREQq/oFagFy2rpfNQspWnKaTrbUdNfwUPmrX91y7Y+uG9hTjGYWpcG8rCznqpyE9Nvf3TJzVvtrX/tPU6f2VGyTPCWUcpMGERQFwCkDJEUArph6zM8++4NnnnnG6lXr7r3nwdtvu3Pl6ufkZSqIutC0jR8KNNQwecG6YgcJZqEsRWnlqjU33XTzunUb5s8/5pOf/IsFx80PwavC34HpjpKPLKSYChwCy+hFEX9/612LF1+zY+cA0K4uBLW6AgYHCaN7HM4tEvGkE49ftOhNRkFViECSCukpxOgAQrCUPxCSb62XsQypUZSxlducuVNnzzn11NNOOvf8s26//Y6lS5e6SpenuVuN6umgo6GGyYuKF0ADg0RIa5/f+I2vX7zk3vuGi5jn7aefXvOv/+WfTzp5QZ7RWB3vOZKDfNWMQ0Ul9dxdSvbAA49885uXbtq0w0LbI+vMhSEta8nM3N1CzFtxVt+UT37y/PPOP2fRCcf19LRQSREQHbAQSx8YHN65Y9fOnTsHB/cODQ2VnTJv5e1Wa+rU3hkzp8+ePaPdbpOKimZmpinTspNOXrDwTV/Y+OJHjpg3F4ikSf4atFsNXhkNNUwyjE7U1f4rIK2M2rh522WXX3nPvQ/HGIK1PGb337d08RXXfO1fvzz/qDkMVXhvIyLIV15OlAA6SI8CM3c99eQzF1102cYXt7tyEo6SDPWhBpAFA4BAGeNpp5zw9xd+6U/f+55p09pChASaXMPDnS1b+59dueapZc+sWLFy06ZNW7du27t3MMboEUaGLJs6rXfevDlHH330SSefcNqpb154/IJ5c2dlefK1Y9YKC46bT3jqAQErvcOhe9nfgGioYaKj3uf3/WYaNiFJLrBwbN+x55pr/v2WW24vSwMyr2xccPttd8+Y0fNP/+HCufNmEuloYNUd/4HDRWKf1POd8o4uufuKFWu/c9Hly55+jmxToYxlNSGzNrMPGWIZQ6a8beec/eG//Zu/WrRoYZ4HCyYxOgb27Fm5cvWSJfffs+TBlavWDg4VneGCtJRHlYsWIALFzp3DG9Zve/yx5279/b1TprZOOHHhn77n3e9537tPPPFNvb2ZUWaIHjOaqjRKI4Q8yGioYZKBtccbIDKkE/bA3uHrr/v5tddePzwkKAfQtXIbGOj87PqbZ0yfceHff3FGX49QJjUxq2Tky1DDqIeA0dxNsI0b+6/43g/vvvuhvNUeGuy0271kEOQAYRCNcDmtbLezz/3lJ//+wr+e0TfVTDSBHO7EZ55+9ubf3PL739++8cUtnVJZqw0RCO5IRxILqRpiEtwBZAA7wyyKzv33PfHIw0//7IZff/Scsz72sY8uWnSctSxYKItOCMYQmqjhoKOhhsmEbvd0yv4XRSfLsoG9Qzff9Ntrrv73oiMoANWICslBM+bDw/7vP75pZt+cz/3lx9q9FkKqZfDVuiEJmDtd3L5915WLr77ttjuzvKcs1Gr1uKfFaABEkRZVGjWzr/dLf/O5z/3lJ2bPmkYCTpi9sHHzTTf99uc//9XqNes7nUhrIVgZYSBGCitddSO7ZtaSkSYPWZjq7hvWbb3qquvuuXvJZz/z8Y9+9INHHTnHrOVemjURw8FHQw2TA4kURnc9ugtgp1Pee88Dl16yeGv/LnlIA6kcbkrCgAiZKd+xc3jx4h/NnN137rkfMFrSN9Qzcv+gWYM7PNrA3r3XXffTn//ixsGhgsxheYpZVE3AgoFSJGJvK/vLv/r0F790wdQpmZkDIQJPP/XMd7931Z133jc4HIE8a02N7gCRCqIVj7GKZWRVQGMRKCGLAhGYnGNoQ0PDK1et+ea3Ll7+9LIL/+5Lxx13TDAgq1tHRnxumwji9aKhhkmAup4nM6t935RSiY899ugll35386ZtQOZglmXuDmPadKHoAGSx9G07dl92+ZUzZkz7sw+8J2mY6jri/k0NyWuNoGDDxfCNN/72B1deOzwIKE9NDB5LmgiCVR8noTyz88475/N/dcHUqe1gpeRDQ75kySOXXfa9p59ZWUS68mBZUbpZIJX0TBYMYnQZ64ILKLjUoYnMFGOqrOQhi6W38ikey6HB8uZf3fr8mg3//NV/fOcZp3s0C0jDewVAzXi+g4Cm5DORUXUfussdcsldUvToUlHqmWdWfevbVyxfvhbKJZpRybUBLqZeA4KuUCgUw7Fcs27zpZdf/eRTK4c73omx9DIidvdZyYHosYhlIY+dstgzOPz7Wx/4wQ9+tmNn4Z4BwZ2KMQQD4JAZXUWwSA2fecbbvvwPX5gzoy8gSNnevX7rrfd/4+vfffKJ1bEI9JCBJuSQeQEvBcgsQqJoElX3VSZplhSFKCMzs2AWYzTQo1MWY1YU2WOPPfc/v37Z/fc9PjhYlKVHj9EL9yK5147vb+4wQEMNExkjnYWpm0AjDQ22du2Giy6+/OGHl6Z0XdWxpMq7SRBJwgiKLiq6F4U/8cTT3/r2ZavXbIgO0DRiL5/M4BzwwBCjottDDz1+2WU/WLf+xSxve00g1RkESdFMwsqyWHDs/Asv/Otjjz2q3TZ3FR0sWfLQt755yYoVq8hMTqaKikfIWc3srbztQ2CMnbIcjHEYioqFvKSYWQhmBsaylHsKc1IhwhhcBMKyZSv+59e/+fAjS93hKUJSlHxcfluHGZoDxUSGUjMyAZrKMpqZJME2b976gyt/dPfdD5K5FJTY4CVBtKqWpZSnMNLKMt57z4PfnXnl//4v/3TU0bMzkxQrrWPFJybJncuefubyy69YuXIlELqZjhgjRgymSCFYRuiTnzr/ne98G+lR7tJjjz116SVXrl+/Kc9bXQPLrgoDkNEgSohehDx///veNWvmDKNBZrQydrb2b136xFNl4S4PIUtOk3Vdhkm+XZYxz7NVq9Zecsnls2b9l9NOO7Fuy2oyDQcBDTVMZHTf5HL3EKyMkRa2b99z1dU/+cUvf1MUCKEdHVU0Pgqk1asIREYjnLGMwcws3Pq7O6dPm/af/vN/mNnXRiag6mJMRxEJ6za8eMml33v08SdgLUQkV9gYY10fsbp50zx23nXGW88//yPt3hCMRVn2bxtYvPhHzz77vFme1nO6baKoQCoVNAVI7ZCd8KYF//ovX33Tm44BYYmgTMueXvl//J//14sbNkOI7mZJZ0kJMcYQMveYZVmMJc2WPrH8+9+/5r/9t3+dM2eaVd2aDT+8XjQHiskByR10ce/A8A03/PL6627sDDuQdYo0/2H/o/XI3AZRokfG6FmWkwZZZ1g3/uKWH1/7s4GBoarPCajLh7Zh/ZbLLr7yvnsfgXIyS1t9N16oOEEwBrlmzJj26c98Yv78eVJRxs6ePYNXX/XvDz7whHseo6egJQ2qSAN4vXapJJBZUPRWZrNmTp8+radvWnv69FbfjNb0ae0ZM2e0W1XEEULoJl8T0UgKofKvkuiuO+64+6c//cXQUElmZNakIV8/GmqYQKj6kzjSuZCWtwNOk6vTib/+zW0//OF1O3cOCEGpo1l6CTOgOtyP/o6ZuwPwqBg1NNi55qqf/OqmWweHyzKm/gh2hr1/664fXXv9b357W1Gax6DorNsz6z8BloATlIrT33bK+993Zp5ZnmWEPfXk0zf+8qbBYe+UAi1lSbreLd0+cAgGk5xwQsEYAkKQBdCimZvRLAOQ/OMk77rcSXIoumKMSKVN5IOD5fU/veGpZcvdOdpHarTZRIMDQkMNEwnd1Fy9grrjbQUUhZYseeiSS763aXM/LQPMVXVnv4J/STrg0wS4GVI+z90l9vfvWLz46jvvXFIWilHFcLl79+D11/3iZ9f/ojMcXcEsq13Y0vTtdI3VWjP6lN5w9tkfmDd3Jgl3DAwM//wXN23atDX6SL/Tfhu41X2aSDWRigBT15UIgRIruScg1u5PGnlNKMJZB0akC2XEiy9uvvEXNw0NDrmPfgHZRBCvDQ01TCDUYXP3G4KScIAxYukTz1x++fc3vrhVHuRZmhuXsgyv4qRMATGtIFp0xizLY1Sr1bth/eZvf/vyBx98LEaWJX//uzuuueYnuwc6lrUlA0yVbnIE7skSxlyd4xYeccYZp9GctBjxyKNPLFnycKvVaxnFOPqqqohjVKpUFeuw7sAAas+G9PNupNC9j5GXBQ7CmaodcjBkreh2z5KHnnhyudQECwcBDTVMCIxsiaM8ipgmxMHlWL9u07e/ddlTT65wD1IQgypbxK6H2is/gFIkTiOpsixISkZkzz6z9jsXfW/VyvX33vvQd7/7g+3b97hbjAwhi46ql3sUSJrRCCKe8c5T5x89T7GQODgYb/nNnf1bd7unTb97omHVPbVvCaWOjuSpuIpk8WKQjbBAqlTS6jzI/uyQQgYLWVk6kW/euP3WW+/eOzTcsMLrR1OhmCioCv1VpF3l20AKtm7Dxm/+r4sffXQpEMyCYPKkhqwS/a+YjXeg8neiwRVpZlkgGD2Slrd6n3zyuf/3//vG7p07Xty4RcwAEIwuwF3OUfdvZoBoUtT0vmnvfe97enraFGOJtWteuP+BR8GslIuW5mLIZdZVZ9SeTEC6w2rOBAHCCcICu50UFSOkETxEUkt2rWWT3ZsI0UzRg2VJRH7vvQ9dcMEnTjxpQaW8bk4TrxUNNYwvKidlVm4olo7bEKSYdL/bd+z6/lU/uf3O+zslzZLva6xsXPjHzGRJKqGA2lQWXlNFElhGAnzokScBkVlqm3JFVkcVr51czCo7qSh3QPOOOPItbz7ZCIGF+6OPL928eQtoJqskR2SE5DAzELR6pwddMMtgDtJMZoK8NrUT3KpEAw1uLhqzZCmZajFBQXJItEgwilGpYBHWrN+8bPnK4xctED3LkstLOqaNMNTB/zUejmgOFGOKlx8YxVof3HV8lCwEd+3etffaa396442/HhouQesG3kmkVPs0vfJ7vTulyuqt2EYOIaz0yYIJwcVU20hbdJ0UrKwefaQSiZBx4cIFs+fMEUTj0PDg408sLaMbjQBcaQxO1gqWQYhClEcxCqVQkhEq0/PwGN1Lq8ZLCEKdcilcwwwuJC/pEiyBkiyDRcpT+ZMQrHoiUeh04tInlnU6HQth34xDc8g4MDRRw7iDKdNApijBQYEsSx8eir/61S1XX/3jvXuLunN53KB6200OtRKPP/74Vp4LoNjfv2P1qrVIZxum5IJHRcCzEEBBXhnFmeQiM6FI8oQQjBqZtedpPp9KyrNAqjTLXZGQEIHYfRQzK8voAizrXiSEp59etnPn7im9PYkzk6qiUUAdKBpqGGfUsl4XYjcn746i8Ftvu3vx4h/u3jUsZOO+6ZEkZDR3AsiycNzC47I8c0WX+vv7+/v7zUKSP5uZexkCUiogDwhmwYIAS3VOWBRDQBmH263khU8oCiElXwPZbuU9eU5mREgabpCAKVVJoLKMlJlClJsFT60a0JYtW/r7+486ag6qkb8NKbwWNNQwrqCNGprgqBqS4a6HH3r80ksXv/jCVrDtVVPTOF5oQrX3hhBarfa8eUdUnRHC5s1b9+zeYxbK0gkzEmauEijP+ejZH/zge9p5sMrfNUVFQTRQMRZ906fNnj3LvV77gKQjj5j7tX/5j3v2DISQA4YRAk0HLhvqFLf85vbbb19CZlX6NqkcjAMDA9u2bRtlY4d6Fl6DA0BDDWOKStUz8mVKuZuQeqgRQSk8/vjyb33rsjWr1kmZaK4IuU0I0+TKO6LVyqf29lbuKeD2bduiR/eqT8uj0xAMed4644y3/sXHzjYLIbVPVCMwyHoqhuohVOlUkYZWzZrV96EPvb96yJSbrFMy7nKxLMstW7bde8/9ZRQoRa/YgSo6Q7t370qnDgDNUeK1oaGG8UXqI3Yay+i5BTJ7fu2LP/vZzx9/YlkeehRr10OGcT1TsOq/kptZURQ9PT3tdisdBCKwd2BvjBH1jJyUTzSAQrDkVFuZu6YZVWnNWr2nVw+i7lf7nAJSxFSvdFpViWUIiUnEUT5RgMqyGBjYA9bF3wavCQ01jC8MEsnoyqxVluXgYFz8/R+teGZFFtrRK2MDd4TA8aIGCTZKO0EzRbVaeSvPScjhrqHBQXdPRvajlAtGGKtOChCpptkdrtcVKbx6tJ8yHbVOipWWGo6UpKgqO2nYH2KMg3uHRinHDu7r8UZBQw3jiWSIkja8MipYvm3b7p3b9wCILlrIsnyo08nzzD2+apXyUF1kVZlA6nWWEEKI0WNypyRS93elWazooLptMpFM/dfVeJoqBcBRnpQ17Wjk8V7qWFmbNdSvGLtK67rgC+9eTLrDOjxJzNMwxIFhIhxf37hIQydBhZARATTAzDLCLIQoL2IRAt1Lo8bxrV0NrQbSzMtgrU6nKDqlR0luxilTpwIGBNRdEHXUX1Ut07TubudYbTzb7bMY0Yl3Y6P9HSspka5U4+26U3WVTDBCcsFDCFOm9KZQI2V10cQOB44mahgH1AsDAAi40xEtvbkNhZdEvalSJJUaECpR8VheaN0oWSUCk+IAghUFhjolAUc0y6b39QW2XJk7RTkKWpSXMQsDezuDnZi83GrNdMqwwAyCyz0P9SzvZC8ndy+jI1gWY9J3QRAMknuUgE5HQ0MxRpRlQZqFDADcM7M8tGbMmElIioQLTpoQktxrTF/AyYyGGsYW6kbA1UKvCaI7qL6rgarX0R82gz/UqCWX9XXUZwF3DA0VO3bsptGQCZg7b15Pb09RujFEpPHWCMHKMv7yV7c8+9zKVitzRdQ9V8mu2hBBX3DsUZ/9zKfmzpkDSrC0yb+4acvPfnrjxhe3BuuR4IhKakwaZZAXRVy2bEWMCiF3lxzupRng7O2d2je9L11qbXKT9OENLxwAGmoYa1Tdyd2YYaR3aULn0keF9wQwPNzZ+OLmGJns7+fNnd3XN3XP7p1ioCBQyZZO4dkVq1Y88ywJGpKuoTr60wiR5alvOfFj532sqjNAiXp27Rz8zc13PvfsuhB6Qbo66S4dZghAkabfZFm76DhJdycDAI/qmz59zpzZ9WWPR7R1WKDh0TFF0t5U2v66l/olNg0TC11S6KYbJJVlXLv2+VgCCBJmzZlx5FFzyjgMRFryaKMU6hsGwOQpGRGSL5MxJ1oes1javmY0AmGWuTKGnqI0oA21pBxoQz1AO1gPkUnBI0LIyeQImaqlOvbY+fOOmAuolkhb3ULSEMQBoKGGscY+2fu642AS+aNLyvO8LItVq1bt3TuUmrZnzZpxyiknAaWrA6a0nxlzIiQ6kAegZWwDOZBJWSwzeQ5lyTB69COw6uZyEq08lzwZXKJqvWL0KIEIqlrBkokEU6n3lLecPGVKb9fhNpVOGlY4UDTUMHYgRtzTgJHMQrdSUX9/YsUPozMdkiB3L0Jm6zes37DhxVQgbLfzt51+Wu+UthDrLCNjUjogJCtXOT1SbvIMCqRFl5zu9BFRs+oXSSHAVUaVUVGUIzpKoBCiADJ0DR3qixSgWbP7Tn/7W7MsADALqW+UCM1b/UDRvF5jh1EbF0mGkE2dOoVI5+QR2c/EbAfqnibMKMDM+vu3P/bYUncXFKi3v/20409YkCbqkQyBQAS8OzNmtOnTqPuteLGu2VRd5pLECJawEiHKCtDBlIysChyJQysvXI80An7SyceffPKiEKqrRfc+mwPFAaKhhrFGWgXuarXyY445NsuyLMvqd/AkgATCIBRF+cADj+zevSdpnObNm/3hs/6s3ZNZ8nxGWpJeaxm6eVeO6iNx0EHvfl0PzhgRP3otkgark0Z1X9U9J+qJNJfKLOfZHzlr7tzZo1/LCRaETRo01DCmSGX7JO0NgSeeePy0aVNcsVoN9Wliop0pRoOsZtVI9tSTT69etRYUiZ7e7ENnvf/YBUe7YtrSa7Wi72vclvQRAiJYEp4CplF5yJSpJZVLuTyHWlAOBcj23fwdkFkayRfB8oQTFr73fWe22oFMoml1zx1j/jpNejTUMKaoeAFIGbUTTjr+9HecFuNwllnXkihZDExsdqBEs9C/dcdddy0ZHixccPnC4xecffZZlkGKHL2GWUcAiSPSlCyUpAtdsUM9Uqcu2ySZlaVqqAgFKqdnlBGAIi35R3m6w3Y7O+/8Pz/q6CPSK9mVS0/MA9rER0MNY4M0T6FMYXB30c+e03fun39o+rQADVoAaAJBo1UT3ro77LgwhV7uwymXCzHGONSJt9157+rnXyiiYvRWnn30nA+dcvLxgYKLzFJ5AggiYspCEjHJneVGurtL0WOsWq8dSKMyIEWpMCuAgu4Go2B0ojSIZl6b5WXBgHj66ad9+MPv7+lJb2nbr32zwYGioYYxQ2KHyiSWJOAhs/f/b3/6sfM+Yozunkp+gqJH1n6H6XDOkWE1YwgkldG+HxKMUQ6DLDy7au1Nv/7t4OCw5EYsOn7BP1z4pXmzZlDyCCgDApi50pwcRjisGkIjIVUui1Kle4zu8pgcr6IktVqZx8IIo1EyCCqIQoruiLGyhC3L4qgj5174d19acOzRhFdW+MmLvmGH14rwb//2b+N9DW8I7FOeGCm5WWb5SSeeuP6FF9auXRe9rA7JkiBL7kb1bSYM2NUSkAbFFzase+tbTz1m/lEhEOTRRx+9d2Bw2bLlAj16nuexLC3LKif9lMWECJmBZN5qLVq0qDPc2bK1f9PmbZs3b9u4sf+Z5Svvvuu+PXsGjVka1Q0kQ23RmObiGi0EC0TftN4vfenz55//kVZuRpkZG1J43ZjQZ9rDAl0h9OhsnLnDXaS5K0atWr328iuu/t2tt3c6RWe4CFlOBMiSI2JtRjCeT2M00kneXVkW3Auxc+65H/qv//VrR8ydZQwSt27e+d//x0U333xbFQmFrHSv+kQASWYI9NjptNpZWZazZ8/s6WkptVGSLg4NDu/aNRAjidDVUKcPV5lqkx7dDCH4BZ/9+Fe/+pU5c/ugAlQWWqNcIRq8RjRRw6FGt2jXhdVNyhIFIc+sb0bf6e84/Ygj5uzZtXNocKAsOpAbWNuTOOmp6DfuH2nDZ11MDLmMeOGF9bPnzHrbaW+1QCOnTOk58cQTNm/ZvH7DeveYFqnLSYOSbroq4qY65ODg8O49g3t27929e3j3rqGBgaGBvUPBco8AKCfNRttD0ShFI/Lczjv3I1/+yt8effQ899IVQzCjNdTw+tFEDYcao/uoRn1XXaORSgMYHZ2y3Lhx40MPPfb440+++MKm3TuHorvR0ugHYoL8pkY1hAlikZoqFi485j/+0z+eeOIiygG5wsrVL3znW5f/7ve3m4XSAZgUXDAL7k5GY91IQnbN6KWkqirlSNYV7mmQlwA5IpGqlZ4FnXve2f/5n//x2AVHkjKDvCQJhCbL8PrRUMNEgaTCXUKM3hnuDA4Vw0MlBAuWBH82UajBa+PnVGtxSTQRmjFj+rSpU9NI7ugsSq5bu/Han1z3y1/+es+evS66jJbFUiEL8srHpcpBVL0kqfdShphm6ki17yPdDK6SiIGaMWP6Jz/1F1/4688eecSsLLNQWc8lfhnnmR2HBxpqGGeo1jIAcPe6LxNmVaCRMn7oFvwnCITaCsH2HcdLymkSWJSKEbv37P3db+/4wQ+uXbt2vYVQRoAZYDGWFkK6odca6aSEhERFMkPdqEoqkDF2LHOpc/JJx1/4d39z1oc+ML2vJ83FS4rMlK1MB5YGrxMNNYwvRl58Se5VH7FZWg9hH+3TBImQq7GZQBJ3Iqm3guqZFARcJQ3DRRnMwDA85MuWPXvd9Tfcddf9O3cOuAcXAbpc0KjQvzpVGAQ5YMYgCZR7pBQC5s6b8eGPvP9Tnzj/lFNOyltBXgplnmVkGCW2bkryBwENNYwvYvpLSiEDzQyoLE9GC/lqS5KJgPSeUTXcFmnsfReq6cKjHBBpUogld+7a8+ijT93861see/TJLVt3lBHuAtKoq0ggjcsUnBIBq88XQtlq50ccMfddZ/7JOR896x1/ctr0qb1SDMGAKHmwUEcKjZbhoKGhhvFFt5yZzu37bnfJ+CD9dAK1AXSvCIKIUDeWp2v05FgnKBkupGlVgLlL4uDg8FPLli+598FHHn1y1arnd+3aE8uyjCUEWjJ6lcpIMrOQZ9nMWTMXLTruXe/+kzPf8ycnn3JCb08r6ciDhfrVo6AqvmiU0QcPDTU0GAuoGrHNGB3A8HCxc8fe1WvWPb9+3YYNL2zetGXnrl1Dg0NFWbSydm+7PWPGjPnzjz5m/lELjltw3ML506dP7enNALeQzi9Zs/4PNRpqaHDIoX3bSaU0DdtcbsGKIg4ODhadwivvFwuW9fT0tFp5yExKso6q64zd3GcTHRxiNNTQYCxQtUxUvOBm1Qi82r4RAFJzN2Gq2qYszdFT7andbVtPicaGGg4pmlxug0OIUZGCkjojmTWqFjCgavFOxcu676qq2soMUnRPvWeoVaRAwwuHHk3U0GCMUMcNMEt9l5VJvFk1UrMODtIbMo37VG2rSzPrVjRGi0EaHCI01NBgfPAH3ni1ygsSvLZ+4qiPBmOEZkRNgwmFihVIjraEa0hh7NFQQ4NxxMsGDl03uEa8NJ5oqKHBOEIv+XJ0pNBgPPH/AzQJEEJzi69iAAAAAElFTkSuQmCC"; const PartnerHeader = (props) => { const { diff --git a/frontend/src/components/PartnerTab.jsx b/frontend/src/components/PartnerTab.jsx index c32bed0e..5a992c0e 100644 --- a/frontend/src/components/PartnerTab.jsx +++ b/frontend/src/components/PartnerTab.jsx @@ -126,9 +126,9 @@ const PartnerTab = (props) => { case `apps`: return ; case 'articles' : - return ; - case `ai_agents`: - return ; + return ; + case `aiagents`: + return ; case 'branding': return { !isCloud && (tabName === "Usecases" || tabName === "Apps" || - tabName === "AI Agents" || - tabName === "Articles") + tabName === "AI Agents") ) { return true; } @@ -166,7 +165,6 @@ const PartnerTab = (props) => { // Disable Apps, Articles, and AI Agents for all non-support users if ( tabName === "Apps" || - tabName === "Articles" || tabName === "AI Agents" ) { return true; @@ -196,7 +194,7 @@ const PartnerTab = (props) => { } const isSupportOnlyTab = (tabName) => { - return tabName === "Apps" || tabName === "Articles" || tabName === "AI Agents"; + return tabName === "Apps" || tabName === "AI Agents"; } return ( diff --git a/frontend/src/components/PartnersArticles.jsx b/frontend/src/components/PartnersArticles.jsx index 80653231..104446ee 100644 --- a/frontend/src/components/PartnersArticles.jsx +++ b/frontend/src/components/PartnersArticles.jsx @@ -1,7 +1,8 @@ import { Box, Typography } from '@mui/material' import React from 'react' -const PartnersArticles = () => { +const PartnersArticles = (props) => { + const {tabName = "Partner's Articles"} = props; return ( { minHeight: "500px", }} > - Partner Articles + {tabName} ) } -export default PartnersArticles +export default PartnersArticles \ No newline at end of file diff --git a/frontend/src/components/PartnersUsecasesTab.jsx b/frontend/src/components/PartnersUsecasesTab.jsx index 39944ff5..9cbc31a1 100644 --- a/frontend/src/components/PartnersUsecasesTab.jsx +++ b/frontend/src/components/PartnersUsecasesTab.jsx @@ -24,6 +24,7 @@ import { import React, { useContext, useEffect, useState } from "react"; import { getTheme } from "../theme.jsx"; import { Context } from "../context/ContextApi.jsx"; +import { triggers } from "../views/AngularWorkflow.jsx"; import AddIcon from "@mui/icons-material/Add"; import CloseIcon from "@mui/icons-material/Close"; import { toast } from "react-toastify"; @@ -37,6 +38,35 @@ import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; import InfoIcon from "@mui/icons-material/Info"; import { Link, useNavigate } from "react-router-dom"; +import { grey } from "../views/AngularWorkflow.jsx" + +const IMAGE_UPLOAD_PLACEHOLDER = "Uploading image..."; + +// Helper to extract the first markdown image URL from navigation content +export const getFirstImageFromNavigation = (navigation) => { + if (!navigation || !Array.isArray(navigation.items)) { + return ""; + } + + for (const item of navigation.items) { + if (!item || !Array.isArray(item.content)) { + continue; + } + + for (const paragraph of item.content) { + if (typeof paragraph !== "string") { + continue; + } + + const match = paragraph.match(/!\[[^\]]*]\(([^)]+)\)/); + if (match && match[1]) { + return match[1]; + } + } + } + + return ""; +}; // Helper function to get the correct image path based on app category export const getCategoryImagePath = (category) => { @@ -62,16 +92,132 @@ export const getCategoryImagePath = (category) => { return "/images/appCategories/intel.svg"; } else if (lowerCategory.includes("email")) { return "/images/appCategories/email.svg"; + } else if (lowerCategory.includes("webhook")) { + return triggers.find((trigger) => trigger?.name.toLowerCase() === "webhook")?.large_image; + } else if (lowerCategory.includes("schedule")) { + return triggers.find((trigger) => trigger?.name.toLowerCase() === "schedule")?.large_image; + } else if (lowerCategory.includes("pipelines")) { + return triggers.find((trigger) => trigger?.name.toLowerCase() === "pipelines")?.large_image; + } else if (lowerCategory.includes("shuffle workflow")) { + return triggers.find((trigger) => trigger?.name.toLowerCase() === "shuffle workflow")?.large_image; + } else if (lowerCategory.includes("user input")) { + return triggers.find((trigger) => trigger?.name.toLowerCase() === "user input")?.large_image; } else { return "/images/appCategories/other.svg"; } }; // Skeleton component for loading state -const UsecaseCardSkeleton = () => { +const UsecaseCardSkeleton = ({ isArticlesTab }) => { const { themeMode } = useContext(Context); const theme = getTheme(themeMode); - + + // Skeleton for Articles Tab + if (isArticlesTab) { + return ( + + {/* Header image skeleton */} + + + {/* Body skeleton */} + + + + + + + + + + + ); + } + + // Skeleton for Usecases Tab return ( { ); }; -const UsecaseCard = ({ usecase, handleToggle, handleOpenDialog, handleDeleteUsecase }) => { +// Card renderer for both usecases and articles +const UsecaseCard = ({ usecase, handleToggle, handleOpenDialog, handleDeleteUsecase, isArticlesTab, partnerData }) => { const [anchorEl, setAnchorEl] = useState(null); const open = Boolean(anchorEl); const { themeMode } = useContext(Context); @@ -209,9 +356,215 @@ const UsecaseCard = ({ usecase, handleToggle, handleOpenDialog, handleDeleteUsec navigate(`/usecases/${usecase.id}`); }; + // Article Card Layout + if (isArticlesTab) { + return ( + + + + + + {usecase.name} + + + {usecase.created && ( + + {new Date(usecase.created * 1000).toLocaleDateString("en-US", { + year: "numeric", + month: "short", + day: "numeric", + })} + + )} + + e.stopPropagation()} + > + { + e.stopPropagation(); + window.open(`${window.location.origin}/partners/${partnerData?.name.toLowerCase().replaceAll(" ", "_")}/articles/${usecase.name.toLowerCase().replaceAll(" ", "_")}`, "_blank"); + }} + sx={{ + color: theme.palette.primary.main, + cursor: "pointer", + }} + /> + { + e.stopPropagation(); + }} + onChange={(e) => { + e.preventDefault(); + e.stopPropagation(); + handleToggle(usecase.id); + }} + size="medium" + sx={{ + "& .MuiSwitch-switchBase.Mui-checked": { + color: "#4CAF50", + }, + "& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track": { + backgroundColor: "#4CAF50", + }, + }} + /> + { + e.stopPropagation(); + handleClick(e); + }} + size="small" + sx={{ + color: theme.palette.text.primary, + "&:hover": { + backgroundColor: + themeMode === "dark" + ? "rgba(255, 255, 255, 0.1)" + : "rgba(0, 0, 0, 0.05)", + }, + }} + > + + + + + e.stopPropagation()} + PaperProps={{ + sx: { + backgroundColor: theme.palette.DialogStyle.backgroundColor, + border: theme.palette.defaultBorder, + borderRadius: "8px", + boxShadow: theme.palette.DialogStyle.boxShadow, + "& .MuiMenuItem-root": { + fontSize: "14px", + color: theme.palette.text.primary, + "&:hover": { + backgroundColor: + themeMode === "dark" + ? "rgba(255, 255, 255, 0.1)" + : "rgba(0, 0, 0, 0.05)", + }, + }, + }, + }} + transformOrigin={{ horizontal: "right", vertical: "top" }} + anchorOrigin={{ horizontal: "right", vertical: "bottom" }} + > + { + e.stopPropagation(); + handleOpenDialog(usecase); + setAnchorEl(null); + }} + > + + + + Edit Article + + { + e.stopPropagation(); + handleDeleteUsecase(usecase.id); + }} + > + + + + Delete Article + + + + + ); + } + + // Usecase Card Layout return (
    + { + e.stopPropagation(); + window.open(`${window.location.origin}/usecases/${usecase.name.toLowerCase().replaceAll(" ", "_")}`, "_blank"); + }} + sx={{ + color: theme.palette.primary.main, + cursor: "pointer", + }} + /> { @@ -414,9 +777,11 @@ const UsecaseCard = ({ usecase, handleToggle, handleOpenDialog, handleDeleteUsec ); }; -const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPartnerData }) => { +const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPartnerData, selectedTab }) => { const { themeMode } = useContext(Context); const theme = getTheme(themeMode); + const isArticlesTab = selectedTab?.toLowerCase() === "articles"; + const entityLabel = isArticlesTab ? "Article" : "Usecase"; // Dialog state const [openDialog, setOpenDialog] = useState(false); @@ -438,12 +803,13 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar navigation: { items: [ { - name: "About Usecase", + name: `About ${entityLabel}`, content: [""], }, ], }, public: false, + created: null, }); // App categories for dropdowns @@ -458,6 +824,11 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar { value: "assets", label: "Assets" }, { value: "intel", label: "Intel" }, { value: "email", label: "Email" }, + {value: "webhook", label: "Webhook"}, + {value: "schedule", label: "Schedule"}, + {value: "pipelines", label: "Pipelines"}, + {value: "shuffle workflow", label: "Shuffle Workflow"}, + {value: "user input", label: "User Input"}, { value: "other", label: "Other" } ]; @@ -505,7 +876,9 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar description: usecase.mainContent?.description, navigation: usecase.navigation || { items: [] }, categories: usecase.mainContent?.categories || [], - public: usecase?.public || false, + public: usecase?.public || false, + created: usecase?.created || null, + headerImage: getFirstImageFromNavigation(usecase.navigation), })); if (usecases.length > 0) { setUsecaseData(usecases); @@ -526,7 +899,195 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar // Sample categories const categoryOptions = ["Collect","Enrich", "Detect", "Respond", "Verify"]; + // Uploading image to the public bucket + const handleImageUpload = async (imageData) => { + const folderName = "usecase_images"; + const usecaseId = formData?.id || partnerData?.id || "usecase"; + + try { + const response = await fetch(`${globalUrl}/api/v1/image_upload`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + body: JSON.stringify({ + imageData, + folder: folderName, + id: usecaseId, + }), + }); + + // if (response.status !== 200) { + // toast.error("Failed to upload image" + response?.reason); + // return null; + // } + + const responseJson = await response.json(); + if (!responseJson?.success || !responseJson?.url) { + toast.error("Failed to upload image: " + responseJson?.reason); + return null; + } + + return responseJson.url; + } catch (error) { + toast.error("Failed to upload image" + error?.message); + return null; + } + }; + + // This function is used to update the content with the public image url + const updateContentWithImageUrl = (itemIndex, contentIndex, imageUrl) => { + setFormData((prevData) => { + const newItems = [...(prevData.navigation.items || [])]; + const item = newItems[itemIndex]; + if (!item) { + return prevData; + } + + const contents = [...(item.content || [])]; + const existingContent = contents[contentIndex] || ""; + const markdownImage = imageUrl ? `![image](${imageUrl})` : ""; + + const placeholderIndex = existingContent.indexOf(IMAGE_UPLOAD_PLACEHOLDER); + if (placeholderIndex !== -1) { + const before = existingContent.slice(0, placeholderIndex); + const after = existingContent.slice( + placeholderIndex + IMAGE_UPLOAD_PLACEHOLDER.length, + ); + + const trimmedBefore = before.replace(/\s*$/, ""); + const needsNewline = + trimmedBefore.length > 0 && !trimmedBefore.endsWith("\n"); + const separator = needsNewline ? "\n" : ""; + + contents[contentIndex] = `${trimmedBefore}${separator}${markdownImage}${after}`; + } else { + const needsNewline = + existingContent.length > 0 && !existingContent.endsWith("\n"); + const separator = needsNewline ? "\n" : ""; + + contents[contentIndex] = `${existingContent}${separator}${markdownImage}`; + } + + newItems[itemIndex] = { + ...item, + content: contents, + }; + + return { + ...prevData, + navigation: { + items: newItems, + }, + }; + }); + }; + + // This function is used to insert the image placeholder (Uploading image...) in the content to show that the image is being uploaded + const insertImagePlaceholder = (itemIndex, contentIndex) => { + setFormData((prevData) => { + const newItems = [...(prevData.navigation.items || [])]; + const item = newItems[itemIndex]; + if (!item) { + return prevData; + } + + const contents = [...(item.content || [])]; + const existingContent = contents[contentIndex] || ""; + const needsNewline = + existingContent.length > 0 && !existingContent.endsWith("\n"); + const separator = needsNewline ? "\n" : ""; + + contents[contentIndex] = `${existingContent}${separator}${IMAGE_UPLOAD_PLACEHOLDER}`; + + newItems[itemIndex] = { + ...item, + content: contents, + }; + + return { + ...prevData, + navigation: { + items: newItems, + }, + }; + }); + }; + + // This function is used to process the image file and upload it to the public bucket + const processImageFile = (imageFile, itemIndex, contentIndex) => { + const reader = new FileReader(); + reader.onload = async (loadEvent) => { + const imageData = loadEvent.target?.result; + if (!imageData) { + toast.error("Failed to read image"); + updateContentWithImageUrl(itemIndex, contentIndex, ""); + return; + } + + const imageUrl = await handleImageUpload(imageData); + if (!imageUrl) { + // Clean up the placeholder if upload failed + updateContentWithImageUrl(itemIndex, contentIndex, ""); + return; + } + + updateContentWithImageUrl(itemIndex, contentIndex, imageUrl); + }; + reader.readAsDataURL(imageFile); + }; + + // This function is used to handle the image paste event + const handleContentPaste = async (event, itemIndex, contentIndex) => { + const items = event.clipboardData?.items; + if (!items || items.length === 0) { + return; + } + + let imageFile = null; + for (let i = 0; i < items.length; i += 1) { + const item = items[i]; + if (item.kind === "file" && item.type.startsWith("image/")) { + imageFile = item.getAsFile(); + break; + } + } + + if (!imageFile) { + return; + } + + event.preventDefault(); + + insertImagePlaceholder(itemIndex, contentIndex); + processImageFile(imageFile, itemIndex, contentIndex); + }; + + // This function is used to handle the image drop event + const handleContentDrop = async (event, itemIndex, contentIndex) => { + event.preventDefault(); + + const files = event.dataTransfer?.files; + if (!files || files.length === 0) { + return; + } + + const imageFile = Array.from(files).find((file) => file.type.startsWith("image/")); + if (!imageFile) { + toast.error("Failed to upload image, only images are supported"); + return; + } + + insertImagePlaceholder(itemIndex, contentIndex); + processImageFile(imageFile, itemIndex, contentIndex); + }; + const getUserProfileWorkflows = (orgId) => { + if (selectedTab === "articles") { + return; + } setIsWorkflowLoading(true); fetch(`${globalUrl}/api/v1/partners/${orgId}/workflows`, { method: "GET", @@ -713,12 +1274,13 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar navigation: usecase?.navigation || { items: [ { - name: "About Usecase", + name: `About ${entityLabel}`, content: [""], }, ], }, public: usecase?.public || false, + created: usecase?.created || Math.floor(Date.now() / 1000), }); setOpenDialog(true); }; @@ -753,15 +1315,15 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar const validationErrors = []; // Check for public workflow selection - if (!formData.mainContent.publicWorkflowId || formData.mainContent.publicWorkflowId.trim() === "") { + if (!isArticlesTab && (!formData.mainContent.publicWorkflowId || formData.mainContent.publicWorkflowId.trim() === "")) { validationErrors.push("Please select a public workflow"); } - if (!formData.mainContent.sourceAppType || formData.mainContent.sourceAppType.trim() === "") { + if (!isArticlesTab && (!formData.mainContent.sourceAppType || formData.mainContent.sourceAppType.trim() === "")) { validationErrors.push("Source app type is required"); } - if (!formData.mainContent.destinationAppType || formData.mainContent.destinationAppType.trim() === "") { + if (!isArticlesTab && (!formData.mainContent.destinationAppType || formData.mainContent.destinationAppType.trim() === "")) { validationErrors.push("Destination app type is required"); } @@ -783,7 +1345,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar } // Check categories - if (!formData.mainContent.categories || !Array.isArray(formData.mainContent.categories) || formData.mainContent.categories.length === 0) { + if (!isArticlesTab && (!formData.mainContent.categories || !Array.isArray(formData.mainContent.categories) || formData.mainContent.categories.length === 0)) { validationErrors.push("At least one category must be selected"); } @@ -846,7 +1408,6 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar return res.json(); }) .then((responseData) => { - console.log("Response data:", responseData); // Reset loading state setIsSubmitting(false); @@ -863,7 +1424,9 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar description: formData.mainContent?.description.trim(), categories: formData.mainContent?.categories || [], navigation: formData.navigation || {}, - public: formData?.public || false, + public: formData?.public || false, + created: formData?.created || null, + headerImage: getFirstImageFromNavigation(formData.navigation), } setUsecaseData((prevData) => { @@ -921,6 +1484,28 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar }); }; + // This is used to filter the usecases based on the selected tab + const filteredUsecases = Array.isArray(usecaseData) + ? usecaseData.filter((usecase) => { + const hasWorkflowId = + !!usecase.publicWorkflowId && + usecase.publicWorkflowId.toString().trim() !== ""; + + if (selectedTab === "articles") { + // Articles: only items without a public workflow ID + return !hasWorkflowId; + } + + if (selectedTab === "usecases") { + // Usecases: only items with a public workflow ID + return hasWorkflowId; + } + + // Fallback: show all + return true; + }) + : []; + return ( - {/* Add Usecase Button */} + {/* Add Usecase / Article Button */} - Usecases + {selectedTab === "usecases" && "Usecases"} + {selectedTab === "articles" && "Articles"} - {/* Usecases Grid */} + {/* Usecases / Articles Grid */} {isLoading ? ( // Skeleton loading state {/* Display 4 skeleton cards while loading */} {[...Array(4)].map((_, index) => ( - + ))} - ) : usecaseData.length > 0 ? ( - // Actual data display + ) : filteredUsecases.length > 0 ? ( + // Actual data display (filtered based on public workflow ID) - {usecaseData.map((usecase) => ( + {filteredUsecases.map((usecase) => ( ))} @@ -1027,7 +1614,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar color: "#FFFFFF", }} > - No usecases found + {isArticlesTab ? "No articles found" : "No usecases found"} )} @@ -1064,7 +1651,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar }} > - {formData?.id ? "Update" : "Add New"} Usecase : {formData?.public ? "Published" : "Draft"} + {formData?.id ? "Update" : "Add New"} {entityLabel} : {formData?.public ? "Published" : "Draft"} + {!isArticlesTab && ( + )} + + {/* Other data selection (use all data types) */} + {!isArticlesTab && ( + + + + Coming soon: Files (detection) + + + + + + Coming soon: Datastore category (threatlists) + + + + )} {/* App Type Selection */} - + {!isArticlesTab && ( + + + )} + {!isArticlesTab && ( + )} @@ -1648,7 +2271,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar { const newItems = [...formData.navigation.items]; @@ -1659,12 +2282,15 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar navigation: { items: newItems }, }); }} + onPaste={(event) => handleContentPaste(event, itemIndex, contentIndex)} + onDrop={(event) => handleContentDrop(event, itemIndex, contentIndex)} + onDragOver={(event) => event.preventDefault()} placeholder="Content (Markdown supported)" helperText={ - Markdown syntax is supported + Markdown syntax is supported and Use ### for subItem for Table of Contents. Drag and drop or paste images to the content to upload them. } @@ -1708,7 +2334,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar )} ))} - +{/* + */} ))} @@ -1791,7 +2417,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar > {isSubmitting ? (formData?.id ? "Updating..." : "Adding...") - : (formData?.id ? "Update Usecase" : "Add Usecase") + : (formData?.id ? `Update ${entityLabel}` : `Add ${entityLabel}`) } diff --git a/frontend/src/components/RunDetectionTest.jsx b/frontend/src/components/RunDetectionTest.jsx index 465374d9..52bd8e87 100644 --- a/frontend/src/components/RunDetectionTest.jsx +++ b/frontend/src/components/RunDetectionTest.jsx @@ -168,11 +168,13 @@ const RunDetectionTest = (props) => { return foundCorrect } + console.log("DETECTION CHECK: ", haveDetectionPipelines().length, ticketWebhook, detectionWorkflowId, detectionTestRunning) + return (
    - : null} + : null}
    } -
    { onDrop={(e) => { console.log("DROP: ", e) }} - > + > {(availableVariables !== undefined && availableVariables !== null && availableVariables.length > 0) || isFileEditor || isWorkflowEditor ? ( { backgroundColor: "rgba(40,40,40,1)", zIndex: activeDialog === "codeeditor" ? 1200 : 1100, - + }} onLoad={(editor) => { highlight_variables(localcodedata) @@ -2469,12 +2646,12 @@ const CodeEditor = (props) => {
    {isFileEditor || isWorkflowEditor ? null : -
    @@ -2490,59 +2667,59 @@ const CodeEditor = (props) => { }} >
    - {actionId === null && triggerId === null ? -
    - {`Condition ${selectedEdge?.conditions?.findIndex(cond => cond.condition.id === conditionId) + 1 || "0"}`} - {/* Source node image */} - {selectedEdge?.source ? - Source - : null - } + {actionId === null && triggerId === null ? +
    + {`Condition ${selectedEdge?.conditions?.findIndex(cond => cond.condition.id === conditionId) + 1 || "0"}`} + {/* Source node image */} + {selectedEdge?.source ? + Source + : null + } - {/* Add arrow icon */} - { - selectedEdge && Object.keys(selectedEdge).length > 0 ? - 0 ? + : null - } + } - {/* Destination node image */} - {selectedEdge?.target ? - Destination - : null - } -
    - : - - {selectedAction.name === "execute_python" || selectedAction.name === "execute_bash" ? - "Code to run" : - triggerId ? + {/* Destination node image */} + {selectedEdge?.target ? + Destination + : null + } +
    + : + + {selectedAction.name === "execute_python" || selectedAction.name === "execute_bash" ? + "Code to run" : + triggerId ? `Output: ${triggerName?.replaceAll("_", " ").slice(0, 1).toUpperCase() + triggerName?.replaceAll("_", " ").slice(1)} (${triggerField})` : `Output: ${appName?.replaceAll("_", " ").slice(0, 1).toUpperCase() + appName?.replaceAll("_", " ").slice(1)} (${fieldName})` } @@ -2574,7 +2751,7 @@ const CodeEditor = (props) => { backdropFilter: "blur(8px)", boxShadow: "0 4px 6px rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.08)", transition: "all 0.2s ease", - paddingRight: 20, + paddingRight: 20, color: "#FF8544", borderRadius: theme.palette?.borderRadius, "&:hover": { @@ -2596,18 +2773,18 @@ const CodeEditor = (props) => { - {selectedAction === undefined ? Try it : selectedAction.name === "execute_python" ? "Run Python Code" : selectedAction.name === "execute_bash" ? "Run Bash" : "Try it"} - - Ctrl + - + {selectedAction === undefined ? Try it : selectedAction.name === "execute_python" ? "Run Python Code" : selectedAction.name === "execute_bash" ? "Run Bash" : "Try it"} + + Ctrl + + } @@ -2717,7 +2894,7 @@ const CodeEditor = (props) => { Output is based on the last VALID run of the node(s) you are referencing. Refresh the page to get updated Variable values.  {selectedAction?.name === "execute_python" ? "For Python: exit() to stop a python script ANYWHERE." - : null} + : null} No test output yet. @@ -2739,7 +2916,7 @@ const CodeEditor = (props) => {
    -
    +
    + + + {userSSOConnected && ( + + + + + + )} +
    + +
    +
    {SSORequired ? "Required" : "Optional"} @@ -369,7 +554,6 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle sx={{marginBottom: 0.6, marginTop: 0.6}} name="onOffSwitch" color="primary" - title="Disable auto-provisioning of users in SSO" />
    @@ -399,6 +583,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle sx={{marginBottom: 0.6, marginTop: 0.6}} name="onOffSwitch" color="primary" + disabled={!isAdmin} title="Disable auto-provisioning of users in SSO" />
    @@ -428,58 +613,12 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle sx={{marginBottom: 0.6, marginTop: 0.6}} name="onOffSwitch" color="primary" + disabled={!isAdmin} title="Disable auto-provisioning of users in SSO" />
    } - -
    - - You can test your SSO configuration by clicking the button below. - Before testing, ensure you have set Open ID Connect or SAML SSO - credentials. - - 0 || - ssoCertificate?.length > 0 || - openidAuthorization?.length > 0 || - openidClientId?.length > 0 - ) - ? "Please ensure all SSO credentials are set before testing." - : "" - } - > - - - - -
    OpenID connect @@ -514,6 +653,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle onChange={(e) => setShowOpenIdCred(e.target.checked)} name="showOpenIdCred" color="primary" + disabled={!isAdmin} /> @@ -536,6 +676,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle variant="outlined" placeholder="The OpenID client ID from the identity provider" value={showOpenIdCred ? openidClientId : openidClientId?.length > 0 ? "•".repeat(50) : ""} + disabled={!isAdmin} onChange={(e) => setOpenidClientId(e.target.value)} onFocus={(e) => setShowOpenIdCred(true)} onBlur={(e) => setShowOpenIdCred(false)} @@ -574,6 +715,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle variant="outlined" placeholder="The OpenID client secret - DONT use this if dealing with implicit auth / PKCE" value={showOpenIdCred ? openidClientSecret : openidClientSecret?.length > 0 ? "•".repeat(50) : ""} + disabled={!isAdmin} onChange={(e) => { setOpenidClientSecret(e.target.value); }} @@ -616,6 +758,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle rows={2} placeholder="The OpenID authorization URL (usually ends with /authorize)" value={showOpenIdCred ? openidAuthorization : openidAuthorization?.length > 0 ? "•".repeat(50) : ""} + disabled={!isAdmin} onChange={(e) => { setOpenidAuthorization(e.target.value) }} @@ -656,6 +799,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle rows={2} placeholder="The OpenID token URL (usually ends with /token)" value={showOpenIdCred ? openidToken : openidToken?.length > 0 ? "•".repeat(50) : ""} + disabled={!isAdmin} onChange={(e) => { setOpenidToken(e.target.value) }} @@ -682,8 +826,8 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle {/*isCloud ? null : */} SAML SSO (v1.1) - - IdP URL for Shuffle SAML/SSO: {`${globalUrl}/api/v1/login_sso`} + + Note: Support for SAML SSO was deprecated due to potential security issues. Please consider migrating to OpenID Connect for better compatibility and features.
    setShowSamlCred(e.target.checked)} name="showSamlCred" color="primary" + disabled={!isAdmin} />
    @@ -725,6 +870,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle variant="outlined" placeholder="The entrypoint URL from your provider" value={showSamlCred ? ssoEntrypoint : ssoEntrypoint?.length > 0 ? "•".repeat(50) : ""} + disabled={!isAdmin} onChange={(e) => { setSsoEntrypoint(e.target.value); }} @@ -765,6 +911,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle rows={2} placeholder="The X509 certificate to use" value={showSamlCred ? ssoCertificate : ssoCertificate?.length > 0 ? "•".repeat(50) : ""} + disabled={!isAdmin} onFocus={(e) => setShowSamlCred(true)} onBlur={(e) => setShowSamlCred(false)} onChange={(e) => { diff --git a/frontend/src/defaultCytoscapeStyle.jsx b/frontend/src/defaultCytoscapeStyle.jsx index e1eece63..fea9d354 100644 --- a/frontend/src/defaultCytoscapeStyle.jsx +++ b/frontend/src/defaultCytoscapeStyle.jsx @@ -1,4 +1,6 @@ -export default function defaultCytoscapeStyle(theme) { +import { red, } from "./views/AngularWorkflow.jsx" + +export default function defaultCytoscapeStyle(theme, apps) { return [ { selector: "node", @@ -85,11 +87,12 @@ export default function defaultCytoscapeStyle(theme) { selector: `node[type="ACTION"]`, css: { shape: "roundrectangle", - "background-color": "#213243", - "border-color": "#81c784", + "background-color": theme.palette.cytoscapeBackgroundColor, + "border-width": "2px", + "border-color": "#02CB70", "background-width": "100%", "background-height": "100%", - "border-radius": "5px", + "corner-radius": 12, "z-index": 5001, }, }, @@ -168,7 +171,9 @@ export default function defaultCytoscapeStyle(theme) { "background-width": "100%", "background-height": "100%", - "background-color": "data(iconBackground)", + "background-color": function(element) { + return element.data("iconBackground") || undefined; + }, "background-fill": "data(fillstyle)", "background-gradient-direction": "to-bottom-right", "background-gradient-stop-colors": "data(fillGradient)", @@ -185,7 +190,9 @@ export default function defaultCytoscapeStyle(theme) { "font-size": "0px", "background-width": "75%", "background-height": "75%", - "background-color": "data(iconBackground)", + "background-color": function(element) { + return element.data("iconBackground") || undefined; + }, "background-fill": "data(fillstyle)", "background-gradient-direction": "to-right", "background-gradient-stop-colors": "data(fillGradient)", @@ -212,6 +219,8 @@ export default function defaultCytoscapeStyle(theme) { css: { "background-image": "data(large_image)", "text-halign": "right", + "background-width": "85%", + "background-height": "85%", }, }, { @@ -267,7 +276,9 @@ export default function defaultCytoscapeStyle(theme) { "font-size": "0px", border: "1px solid rgba(255,255,255,0.9)", "background-image": "data(icon)", - "background-color": "data(iconBackground)", + "background-color": function(element) { + return element.data("iconBackground") || null; + } || "null", }, }, { @@ -327,43 +338,6 @@ export default function defaultCytoscapeStyle(theme) { "background-height": "100%", }, }, - { - selector: `node[app_id="shuffle_agent"]`, - css: { - "shape": function(element) { - return "roundrectangle" - }, - "height": "74px", - "width": "222px", - "background-image": "data(large_image)", - "label": function(element) { - var elementname = element.data("label") - if (elementname === null || elementname === undefined) { - return "" - } - - elementname = elementname.replaceAll("_", " ", -1) - - if (elementname.length > 15) { - elementname = elementname.substring(0, 15) + ".." - } - - return elementname - }, - "background-width": "65px", - "background-height": "65px", - "background-position-x": "20px", - "background-repeat": "no-repeat", - "background-opacity": "0.5", - - "font-size": "14px", - "text-halign": "center", - "text-valign": "center", - "text-margin-x": "20px", - "text-margin-y": "0px", - - }, - }, { selector: "node[!is_valid]", css: { @@ -382,9 +356,9 @@ export default function defaultCytoscapeStyle(theme) { { selector: ".skipped-highlight", css: { - "background-color": "grey", + "background-color": "transparent", "border-color": "grey", - "border-width": "8px", + "border-width": "3px", "transition-property": "background-color", "transition-duration": "0.5s", }, @@ -433,7 +407,7 @@ export default function defaultCytoscapeStyle(theme) { selector: ".executing-highlight", css: { //"background-color": "#ffef47", - "border-color": "#FECC00", + "border-color": "#FFC633", "border-width": "8px", "transition-property": "border-width", "transition-duration": "0.25s", @@ -475,8 +449,8 @@ export default function defaultCytoscapeStyle(theme) { selector: "edge.executing-highlight", css: { width: "5px", - "target-arrow-color": "#FECC00", - "line-color": "#FECC00", + "target-arrow-color": "#FFC633", + "line-color": "#FFC633", "transition-property": "line-color, width", "transition-duration": "0.25s", }, @@ -533,6 +507,8 @@ export default function defaultCytoscapeStyle(theme) { { selector: ".eh-preview, .eh-ghost-edge", style: { + "curve-style": "unbundled-bezier", + "control-point-step-size": 60, "background-color": "#337ab7", "line-color": "#337ab7", "target-arrow-color": "#337ab7", @@ -575,7 +551,14 @@ export default function defaultCytoscapeStyle(theme) { height: "data(height)", padding: "0px", margin: "0px", - "background-color": "data(iconBackground)", + "background-color": function(element) { + const value = element.data() + if (value?.backgroundcolor === null || value?.backgroundcolor === undefined) { + return "#888888" + } + + return element.data("backgroundcolor") + }, "background-fill": "data(fillstyle)", "background-gradient-direction": "to-right", "background-gradient-stop-colors": "data(fillGradient)", @@ -593,8 +576,26 @@ export default function defaultCytoscapeStyle(theme) { { selector: "node:selected", css: { - "border-color": "#f86a3e", - "border-width": "7px", + "border-color": "#FF8444", + "border-width": "3px", + "background-color": "transparent", + }, + }, + { + // This is to show the underlay orange effect on action on selection + selector: 'node[type="ACTION"]:selected', + css: { + "underlay-color": "rgba(255, 132, 68, 0.15)", + "underlay-padding": 10, + "underlay-opacity": 0.3, + "underlay-corner-radius": 16, + }, + }, + { + // For the startNode + selector: 'node[?isStartNode][app_id != "shuffle_agent"]:selected', + css: { + "underlay-shape": "ellipse", }, }, { @@ -605,6 +606,120 @@ export default function defaultCytoscapeStyle(theme) { "background-color": "#f85a3e", }, }, + { + selector: `node[large_image="/images/singul_green.png"]`, + css: { + "background-image": function(element) { + const nodeData = element.data() + if (apps === null || apps === undefined) { + return nodeData?.large_image + } + + const params = nodeData["parameters"] + if (params === null || params === undefined) { + return nodeData?.large_image + } + + var foundappname = "" + for (var key in params) { + const param = params[key] + if (param.name === "app_name") { + foundappname = param?.value.toLowerCase().replaceAll(" ", "_") + break + } + } + + if (foundappname == "") { + return nodeData?.large_image + } + + for (var key in apps) { + const app = apps[key] + const newappname = app.name.toLowerCase().replaceAll(" ", "_") + if (newappname == foundappname) { + if (app.large_image !== null && app.large_image !== undefined && app.large_image != "") { + element.data("large_image", app.large_image) + return app.large_image + } + } + } + + return nodeData?.large_image + }, + }, + }, + { + selector: 'node[flowOrientation = "horizontal"][?small_image], node[flowOrientation = "horizontal"][?large_image]', + css: { + "text-halign": "center", + "text-valign": "bottom", + "text-margin-x": "0px", + "text-margin-y": "12px", + }, + }, + { + selector: 'node[flowOrientation = "vertical"][?small_image], node[flowOrientation = "vertical"][?large_image]', + css: { + "text-halign": "right", + "text-valign": "center", + "text-margin-x": "15px", + "text-margin-y": "0px", + }, + }, + { + selector: 'node[?isStartNode], node[?isStartNode][flowOrientation = "vertical"]', + css: { + "text-halign": "right", + "text-valign": "center", + "text-margin-x": "15px", + "text-margin-y": "0px", + }, + }, + { + selector: 'node[?isStartNode][flowOrientation = "horizontal"]', + css: { + "text-halign": "center", + "text-valign": "bottom", + "text-margin-x": "0px", + "text-margin-y": "12px", + }, + }, + { + selector: `node[app_id="shuffle_agent"]`, + css: { + "shape": function(element) { + return "roundrectangle" + }, + "height": "74px", + "width": "222px", + "background-image": "data(large_image)", + "label": function(element) { + var elementname = element.data("label") + if (elementname === null || elementname === undefined) { + return "" + } + + elementname = elementname.replaceAll("_", " ", -1) + + if (elementname.length > 15) { + elementname = elementname.substring(0, 15) + ".." + } + + return elementname + }, + "background-width": "65px", + "background-height": "65px", + "background-position-x": "20px", + "background-repeat": "no-repeat", + "background-color": "#212121", + "font-size": "14px", + "text-halign": "center", + "text-valign": "center", + "text-margin-x": "20px", + "text-margin-y": "0px", + + }, + }, { selector: `node[name="switch"]`, css: { diff --git a/frontend/src/theme.jsx b/frontend/src/theme.jsx index 8772ff23..ea04b13b 100644 --- a/frontend/src/theme.jsx +++ b/frontend/src/theme.jsx @@ -177,6 +177,7 @@ export const getTheme = (themeMode, brandColor) => surfaceColor: themeMode === "dark" ? "#27292d" : "#EFEFEF", platformColor: themeMode === "dark" ? "#212121" : "#ffffff", backgroundColor: themeMode === "dark" ? "#1a1a1a" : "#f1f1f1", + cytoscapeBackgroundColor: themeMode === "dark" ? "#161616" : "#f5f5f5", distributionColor: themeMode === "dark" ? "#40E0D0" : "#008080", cardBackgroundColor: themeMode === "dark" ? "#1e1e1e" : "#eaeaea", cardHoverColor: themeMode === "dark" ? "#323232" : "#F0F0F0", @@ -188,6 +189,8 @@ export const getTheme = (themeMode, brandColor) => green: themeMode === "dark" ? "#5cc879" : "#008000", defaultBorder: themeMode === "dark" ? '1px solid #494949' : '1px solid #CCCCCC', linkColor: brandColor === "#ff8544" ? "#f86a3e" : brandColor, + slateGrayColor: themeMode === "dark" ? "#494949" : "#CCCCCC", + parsedAppPaperColor: themeMode === "dark" ? "#2f2f2f" : "#CCCCCC", borderRadius: 10, loaderColor: themeMode === "dark" ? "#1a1a1a" : "#E0E0E0", @@ -214,6 +217,13 @@ export const getTheme = (themeMode, brandColor) => drawer: { backgroundColor: themeMode === "dark" ? "#262626" : "#f9f9f9" }, + actionSidebarField: { + backgroundColor: themeMode === "dark" ? "#2F2F2F" : "#F1F1F1", + color: themeMode === "dark" ? "#ffffff" : "#000000", + borderRadius: 8, + height: 40, + border: "none", + }, reactJsonStyle: { padding: 5, width: "98%", @@ -258,7 +268,7 @@ export const getTheme = (themeMode, brandColor) => defaultImage: "/images/no_image.png", singulOrange: "/images/singul_orange.png", singulGreen: "/images/singul_green.png", - singulBlackWhite: "/images/singul_black_white.png", + singulBlackWhite: "/icons/workflow-page/shuffle_agent.png", scrollbarColor: themeMode === "dark" ? "#494949 #2f2f2f": "#c1c1c1 #f1f1f1", scrollbarColorTransparent: themeMode === "dark" ? '#494949 transparent': "#c1c1c1 transparent", }, @@ -337,6 +347,7 @@ export const getTheme = (themeMode, brandColor) => borderRadius: '4px', whiteSpace: "nowrap", textWrap: "normal", + transition: 'background-color 0.2s ease-in-out', '&:hover': { fontWeight: 600, backgroundColor: themeMode === 'dark' ? brandColor || "#ff955c" : brandColor || '#FF8D4F', @@ -352,6 +363,7 @@ export const getTheme = (themeMode, brandColor) => borderRadius: '4px', boxShadow: 'none', whiteSpace: "nowrap", + transition: 'all 0.2s ease-in-out', textWrap: "normal", '&:hover': { fontWeight: 600, @@ -523,3 +535,4 @@ export const getTheme = (themeMode, brandColor) => }, }, }); + diff --git a/frontend/src/views/AgentUI.jsx b/frontend/src/views/AgentUI.jsx index 9aef5284..6b2fbbd9 100644 --- a/frontend/src/views/AgentUI.jsx +++ b/frontend/src/views/AgentUI.jsx @@ -24,6 +24,9 @@ import { TextField, Popover, Divider, + + AvatarGroup, + Avatar, } from '@mui/material' import { @@ -40,6 +43,7 @@ import { Refresh as RefreshIcon, Add as AddIcon, Warning as WarningIcon, + Pause as PauseIcon, } from '@mui/icons-material' import { @@ -68,6 +72,27 @@ const AgentUI = (props) => { const [appPickerAnchor, setAppPickerAnchor] = React.useState(null) const [chosenApps, setChosenApps] = useState([]) + const activateApp = (appId) => { + if (appId === undefined || appId === null || appId === "") { + return + } + + const url = `${globalUrl}/api/v1/apps/${appId}/activate` + fetch(url, { + method: "GET", + credentials: "include", + }) + .then((response) => { + return response.json() + }) + .then((responseJson) => { + if (responseJson.success !== false) { + getApps() + } else { + toast.warn("Failed to activate app: " + responseJson.reason) + } + }) + } useEffect(() => { if (newSelectedApp.objectID === undefined || newSelectedApp.objectID === null || newSelectedApp.objectID === "") { @@ -83,9 +108,76 @@ const AgentUI = (props) => { id: newSelectedApp.objectID, image: newSelectedApp.image_url, }])) + + var found = false + if (apps?.length > 0) { + for (var key in apps) { + const app = apps[key] + if (app?.id !== newSelectedApp?.objectID) { + continue + } + + found = true + break + } + } + + if (!found) { + activateApp(newSelectedApp.objectID) + } } }, [newSelectedApp]) + useEffect(() => { + if (chosenApps?.length > 0) { + return + } + + if (apps?.length === 0) { + console.log("No apps loaded yet") + return + } + + if (data === undefined || data === null) { + return + } + + if (data?.allowed_actions === undefined || data?.allowed_actions === null) { + return + } + + console.log("HERE!") + console.log("Data, apps: ", data, apps) + + var newChosenApps = [] + for (var key in data?.allowed_actions) { + const allowedAction = data?.allowed_actions[key] + if (!allowedAction?.startsWith("app:")) { + newChosenApps.push({ + "name": allowedAction, + "large_image": "", + }) + + continue + } + + const appId = allowedAction.split(":")[1] + for (var appKey in apps) { + const app = apps[appKey] + if (app.id !== appId) { + continue + } + + newChosenApps.push(app) + break + } + } + + if (newChosenApps.length > 0) { + setChosenApps(newChosenApps) + } + }, [data]) + const {themeMode} = useContext(Context) const theme = getTheme(themeMode) const navigate = useNavigate(); @@ -95,12 +187,13 @@ const AgentUI = (props) => { } const agentWrapperStyle = { - width: 1000, - height: 1000, + width: "100%", + maxHeight: "100vh", margin: "auto", - paddingTop: 100, - paddingBottom: 1000, backgroundColor: theme.palette.backgroundColor, + + paddingBottom: showAgentStarter ? 0 : 1500, + } if (data.input === undefined || data.input === null) { @@ -382,7 +475,7 @@ const AgentUI = (props) => { getAppAuth() }, []) - const maxTimelineWidth = 380 + const maxTimelineWidth = 375 const submitQuestions = (decisionId, questionAnswers, isContinuation) => { console.log("Submitting questions: ", decisionId, questionAnswers) @@ -411,10 +504,15 @@ const AgentUI = (props) => { } else { for (var key in questionAnswers) { - const answer = questionAnswers[key] - if (isContinuation === true) { - newArgument["question_"+(answer.index)] = answer.value + // Special handler for continuation of normal actions + // Approves/denies continuation + if (key === "approve") { + newArgument[key] = questionAnswers[key] + break } + + const answer = questionAnswers[key] + newArgument["question_"+(answer.index)] = answer.value } } @@ -552,6 +650,7 @@ const AgentUI = (props) => { item.label = item?.details?.reason || item.label } else if (item?.category === "ask" || item?.details?.action === "ask") { + console.log("IN HERE?: ", item) item.type = "question" item.category = "ask" @@ -572,6 +671,8 @@ const AgentUI = (props) => { item.label = item?.details?.reason || item.label } + console.log("ITEM: ", item, questions) + var parsedCategory = item.category === "singul" ? @@ -592,7 +693,14 @@ const AgentUI = (props) => { if (item?.details?.tool !== undefined && item?.details?.tool !== null && item?.details?.tool?.length > 0 && item?.details?.tool !== "singul" && item?.details?.tool !== item?.details?.action) { // Find the app and inject the image - const toolName = item.details.tool.toLowerCase().replaceAll(" ", "_").replaceAll("-", "_") + var toolName = item?.details?.tool?.toLowerCase().replaceAll(" ", "_").replaceAll("-", "_") + if (toolName.includes("app:")) { + const toolNameSplit = toolName.split(":") + if (toolNameSplit.length >= 2) { + toolName = toolNameSplit[2] + } + } + for (var appKey in apps) { const app = apps[appKey] @@ -657,10 +765,17 @@ const AgentUI = (props) => { if (item?.details === undefined || item?.details === null || item?.details?.input === undefined || item?.details?.input === null) { toast.error("No decision details found to rerun. Cannot proceed. Please go back to your workflow or /agents to start over.") } else { + if (item?.details?.original_input !== undefined && item?.details?.original_input !== null && item?.details?.original_input !== "") { + setActionInput(item?.details?.original_input) + setDisableButtons(true) + submitInput(item?.details?.original_input) + return + } + //console.log("DETAILS: ", item?.details) for (var messagekey in item?.details?.input?.messages) { const message = item?.details?.input?.messages[messagekey] - if (message.role === "user") { + if (message.role === "user" && !message?.role?.includes("USER CONTEXT")) { setActionInput(message.content) setDisableButtons(true) @@ -678,8 +793,8 @@ const AgentUI = (props) => { - const rerunButton = - + const rerunButton = item?.type === "processing" ? null : + { + var itemLabel = item?.label?.replaceAll("_", " ") || "" + if (item?.details?.reason !== undefined && item?.details?.reason !== null && item?.details?.reason !== "") { + itemLabel = item?.details?.reason + } + return (
    {
    - {item.label} + {itemLabel}
    - +
    {currentDuration != 0 && !isNaN(timelineMarginLeft) && !isNaN(timelineWidth) && timelineWidth > 0 ?
    { {item.category === "ask" ? {rerunButton} - {/* - - - { - e.preventDefault() - e.stopPropagation() - - toast.info("Approving this step.") - }} - > - - - - - - - { - e.preventDefault() - e.stopPropagation() - - toast.info("Stopping on this step.") - }} - > - - - - - */} @@ -872,9 +974,63 @@ const AgentUI = (props) => { rerunAgentButton : item?.type === "decision" ? + item?.details?.run_details?.status === "WAITING" ? + agentRequestLoading ? null : +
    + + +
    + :
    {rerunButton} - + { : rerunButton } - - - - {open ? - - : - - } - - - + {item?.type === "processing" ? null : + + + + {open ? + + : + + } + + + + + }
    {showAuthentication && selectedApp.id !== undefined ? -
    +
    {
    : null} - {questions?.length > 0 && item?.status === "RUNNING" ? + {questions?.length > 0 && item?.status === "RUNNING" || item?.status === "WAITING" ?
    {questions.map((q, questionIndex) => { return ( @@ -976,13 +1135,17 @@ const AgentUI = (props) => {
    : null} @@ -1078,6 +1241,7 @@ const AgentUI = (props) => { } var finishDecisionId = "" + var finishAnswer = "" var sortedTimelineItems = [] for (var key in agent_data?.decisions) { const item = agent_data.decisions[key] @@ -1105,17 +1269,73 @@ const AgentUI = (props) => { if (item?.details?.action === "finish" || item.action === "finish" || item.category == "finish" || item?.details?.action == "finalise") { finishDecisionId = item?.run_details?.id - } + finishAnswer = item?.reason || "" + if (item?.fields !== undefined && item?.fields !== null) { + // Just choosing the first one + for (var fieldKey in item?.fields) { + const field = item?.fields[fieldKey] + finishAnswer = field?.value || finishAnswer + break + } + } + } } - timelineItems.sort((a, b) => { - if (a.start_time < b.start_time) { - return 1; + // Fill blank space between items in timeline by adding "empty" decisions: + // 1. Sort + // 2. Fill blank space + // 3. Sort again + timelineItems.sort((a, b) => a.start_time - b.start_time) + for (var i = 0; i < timelineItems.length - 1; i++) { + if (timelineItems?.length <= 2) { + break } - return 0; - }) + if (i === timelineItems.length - 1) { + break + } + + const currentItem = timelineItems[i] + if (currentItem?.type === "processing") { + continue + } + + const nextItem = timelineItems[i + 1] + if (nextItem?.type === "processing") { + continue + } + + if (nextItem.start_time - currentItem.end_time > 1) { + /* + timelineItems?.push({ + label: "", + type: "processing", + category: "processing", + status: "FINISHED", + start_time: currentItem.end_time, + end_time: nextItem.start_time, + }) + */ + } + } + timelineItems.sort((a, b) => a.start_time - b.start_time) + + const handleKeyDownCont = (e) => { + if (finishDecisionId === "") { + return + } + + const isCmdEnter = e.metaKey && e.key === "Enter"; // macOS + const isCtrlEnter = e.ctrlKey && e.key === "Enter"; // Windows/Linux + if (isCmdEnter || isCtrlEnter) { + e.preventDefault() + + submitQuestions(finishDecisionId, { + "continue": continuationText, + }, true) + } + } return (
    @@ -1151,6 +1371,7 @@ const AgentUI = (props) => { { e.preventDefault(); @@ -1161,15 +1382,35 @@ const AgentUI = (props) => { }, true) }} > -
    + {finishAnswer !== "" ? +
    + + {finishAnswer} + +
    + : null} + +
    { console.log("Value: ", e.target.value) //setActionInput(e.target.value) @@ -1195,7 +1436,7 @@ const AgentUI = (props) => { }} /> - Any failed tasks will be set to ignored (TBD). Learn more + Any failed tasks will be set to ignored. Learn more
    @@ -1242,7 +1483,7 @@ const AgentUI = (props) => { // 1. Run the execution. Can this be a single-action run? // 2. Get the execution ID and node ID from the response. const uuid = uuidv4() - var parsedAction = "list_tickets,API" // Default action for now + var parsedAction = "API" // Default action for now if (chosenApps.length > 0) { parsedAction = "" for (var appKey in chosenApps) { @@ -1319,155 +1560,270 @@ const AgentUI = (props) => { cursor: "pointer", } + + const typeButtonGroup = + + + + + + return ( -
    - - - {showAgentStarter ? - { - e.preventDefault(); - submitInput(actionInput); - }} - > - - -
    - - - Shuffle AI Agents - +
    +
    +
    +
    +
    { - setActionInput(e.target.value) - }} - InputProps={{ - endAdornment: ( - agentRequestLoading ? - - : - - - - - - ), - }} + id="copy_element_shuffle" + style={{ display: "none" }} /> -
    -
    - } label="Select Apps" - style={chipStyle} - onClick={() => { - setAppPickerAnchor(document.getElementById("add_app_chip")) + {showAgentStarter ? + { + e.preventDefault(); + submitInput(actionInput); + }} + > + {/* + + */} + +
    + + - { - setAppPickerAnchor(null) + + What do you want to do? + + { + setActionInput(e.target.value) }} - anchorOrigin={{ - vertical: 'bottom', - horizontal: 'left', + InputProps={{ + style: { + borderRadius: 30, + }, + endAdornment: ( + agentRequestLoading ? + + : + + + + + + ), }} - > - + +
    +
    + } label="Select Apps / MCPs" + style={chipStyle} + onClick={() => { + setAppPickerAnchor(document.getElementById("add_app_chip")) + }} /> - + { + setAppPickerAnchor(null) + }} + anchorOrigin={{ + vertical: 'bottom', + horizontal: 'left', + }} + > + + +
    + + {chosenApps.map((app, index) => { + var chosenName = (app?.name?.charAt(0).toUpperCase() + app?.name?.slice(1))?.replaceAll("_", " ").replaceAll("-", " ") + if (chosenName === "API") { + chosenName = "default" + } + + const chosenImagePath = app?.image === undefined || app?.image === null || app?.image === "" ? app?.large_image === undefined || app?.large_image === null || app?.large_image === "" ? theme.palette.singulBlackWhite : app.large_image : app.image + const chosenImage = + + return ( + { + const newChosenApps = chosenApps.filter((a, i) => i !== index) + setChosenApps(newChosenApps) + }} + /> + ) + })} +
    + +
    + {execution === undefined || execution === null || execution?.execution_id === undefined || execution?.execution_id === null ? null : typeButtonGroup} +
    + + {/* +
    + */} + + : +
    +
    +
    + {typeButtonGroup} + + + + + + + + + {execution?.status === "EXECUTING" ? + + + + + + : null} + +
    + +
    + + {chosenApps?.map((app, index) => { + return( + + { + window.open(`/apps/${app.id}`, '_blank', 'noopener,noreferrer'); + }} + style={{ + cursor: "pointer", + width: 30, + height: 30, + }} + /> + + ) + })} + +
    +
    + + {buttonState === "timeline" ? + + : + null + }
    - - {chosenApps.map((app, index) => { - const chosenName = (app?.name?.charAt(0).toUpperCase() + app?.name?.slice(1))?.replaceAll("_", " ").replaceAll("-", " ") - const chosenImagePath = app?.image - const chosenImage = - - return ( - { - const newChosenApps = chosenApps.filter((a, i) => i !== index) - setChosenApps(newChosenApps) - }} - /> - ) - })} -
    - - : -
    - - - - - - - - - - - - {buttonState === "timeline" ? - - : - null }
    - } +
    ) } diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index d6e33cef..b39f22dd 100755 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -72,6 +72,8 @@ import { Radio, ButtonGroup, Box, + ToggleButton, + ToggleButtonGroup, } from "@mui/material"; @@ -150,11 +152,11 @@ import ShuffleCodeEditor from "../components/ShuffleCodeEditor1.jsx"; import LineChartWrapper from "../components/LineChartWrapper.jsx"; import WorkflowValidationTimeline from "../components/WorkflowValidationTimeline.jsx" -import { validateJson, collapseField, GetIconInfo, handleReactJsonClipboard, HandleJsonCopy, } from "../views/Workflows2.jsx"; +import { validateJson, collapseField, GetIconInfo, buildIconSvgPath, handleReactJsonClipboard, HandleJsonCopy, } from "../views/Workflows2.jsx"; import { GetParsedPaths, internalIds, } from "../views/Apps.jsx"; import ConfigureWorkflow from "../components/ConfigureWorkflow.jsx"; import AuthenticationOauth2 from "../components/Oauth2Auth.jsx"; -import ParsedAction from "../components/ParsedAction.jsx"; +import ParsedAction from "../components/ParsedActionNew.jsx"; import PaperComponent from "../components/PaperComponent.jsx" import ExtraApps from "../components/ExtraApps.jsx" import EditWorkflow from "../components/EditWorkflow.jsx" @@ -203,7 +205,7 @@ export const triggers = [ trigger_type: "WEBHOOK", errors: null, large_image: - "data:image/png;base64,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", + "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDgiIGhlaWdodD0iNDgiIHZpZXdCb3g9IjAgMCA0OCA0OCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4IiByeD0iOCIgZmlsbD0iIzIxQTBCRCIvPgo8cGF0aCBkPSJNMjMuOTYxNyAxOC4wOTk0QzI1LjAwMDQgMTguMDY5NiAyNS42ODE3IDE5LjE3NTIgMjUuMTg4MiAyMC4wODk2TDI1LjE3NDYgMjAuMTE1TDI1LjE5MDIgMjAuMTM5NEwyNy45ODMyIDI0LjY4MDRMMjguMDA0NiAyNC43MTU2TDI4LjA0MzcgMjQuN0MyOC41MzgzIDI0LjUwMDcgMjkuMDY3MSAyNC4zOTgyIDI5LjYwMDMgMjQuMzk4MkMzMS4wODE0IDI0LjM5ODkgMzIuNDQ5OSAyNS4xODkyIDMzLjE5MTIgMjYuNDcxNEMzNC43ODk2IDI5LjIzNzMgMzIuNzk0OCAzMi42OTY1IDI5LjYwMDMgMzIuNjk4QzI5LjM2ODEgMzIuNjk4IDI5LjE1MzkgMzIuNTczOSAyOS4wMzc4IDMyLjM3MjhDMjguNzg3NyAzMS45Mzk1IDI5LjEgMzEuMzk4MiAyOS42MDAzIDMxLjM5ODJDMzEuMjA2NyAzMS4zOTg4IDMyLjQ5NjEgMzAuMDcyMyAzMi40NSAyOC40NjY2QzMyLjM4NjcgMjYuMjczNiAyOS45NzM3IDI0Ljk3MTYgMjguMTA2MiAyNi4xMjI4QzI3LjgwMDggMjYuMzEwOCAyNy40MDAxIDI2LjIxNSAyNy4yMTE3IDI1LjkwOTlIMjcuMjEyNkwyNC4wODE4IDIwLjgyMkwyNC4wNjcxIDIwLjc5ODZIMjQuMDAwN0MyMy41MzIzIDIwLjc5ODYgMjMuMDk3MSAyMC41NTU4IDIyLjg1MTMgMjAuMTU3QzIyLjMwNjIgMTkuMjcyMyAyMi45MjMgMTguMTI5MyAyMy45NjE3IDE4LjA5OTRaTTE1LjA4MDggMjYuMDU3NEMxNS4zODE0IDI1LjY1NzIgMTYuMDAyNiAyNS43MzI4IDE2LjE5OSAyNi4xOTMxQzE2LjI3ODcgMjYuMzgwMSAxNi4yNjU3IDI2LjU5MTkgMTYuMTY3NyAyNi43NjY0TDE2LjEyMDggMjYuODM4NkMxNS43NDkxIDI3LjMzMSAxNS41NDg1IDI3LjkzMTYgMTUuNTUwNSAyOC41NDg2QzE1LjU1MDcgMzAuNzQyMyAxNy45MjYxIDMyLjExMzIgMTkuODI1OSAzMS4wMTY0QzIwLjcwNzUgMzAuNTA3MyAyMS4yNTA2IDI5LjU2NjYgMjEuMjUwNyAyOC41NDg2QzIxLjI1MDcgMjguMTg5NyAyMS41NDE0IDI3Ljg5ODQgMjEuOTAwMSAyNy44OTgySDI4LjQxNzdMMjguNDMyNCAyNy44NzM4QzI4Ljk1MiAyNi45NzM4IDMwLjI1MTYgMjYuOTczOCAzMC43NzEyIDI3Ljg3MzhDMzEuMjkwMyAyOC43NzM3IDMwLjY0MDMgMjkuODk4MiAyOS42MDEzIDI5Ljg5ODJDMjkuMTE5MiAyOS44OTgxIDI4LjY3MzUgMjkuNjQwOSAyOC40MzI0IDI5LjIyMzRMMjguNDE3NyAyOS4xOThIMjIuNDk4OEwyMi40OTE5IDI5LjI0QzIxLjk1OTcgMzIuMzg5NyAxOC4yMTc0IDMzLjc4MjIgMTUuNzU1NiAzMS43NDY4QzE0LjA0ODEgMzAuMzM0NyAxMy43NTA4IDI3LjgyOTYgMTUuMDgwOCAyNi4wNTc0Wk0yMC42NjA5IDE2Ljk5QzIyLjU1NTQgMTQuNDE3OSAyNi41MjQxIDE0Ljg2MTIgMjcuODA0NCAxNy43ODc4QzI3Ljg5NzMgMTguMDAwOCAyNy44NjkxIDE4LjI0NzQgMjcuNzMxMiAxOC40MzQzQzI3LjQzNCAxOC44MzY0IDI2LjgxMzQgMTguNzY2NCAyNi42MTMgMTguMzA4M1YxOC4zMDc0QzI1Ljk0NTQgMTYuNzc5MyAyNC4xMTM0IDE2LjE0ODUgMjIuNjQ2MiAxNi45NDEyQzIwLjcxNjIgMTcuOTg0IDIwLjYzODYgMjAuNzI1NiAyMi41MDY2IDIxLjg3NTdDMjIuODEyIDIyLjA2MzkgMjIuOTA3MyAyMi40NjM3IDIyLjcxOTUgMjIuNzY5M0wxOS41ODk2IDI3Ljg1NjJMMTkuNTc1IDI3Ljg4MDZMMTkuNTg4NiAyNy45MDZDMjAuMDc1IDI4LjgwNTUgMTkuNDIzNiAyOS44OTgzIDE4LjQwMTEgMjkuODk4MkMxNy45MTk2IDI5Ljg5NzggMTcuNDc0MyAyOS42NDEyIDE3LjIzMzIgMjkuMjI0NEMxNi43MTI5IDI4LjMyNDggMTcuMzYxOSAyNy4xOTg4IDE4LjQwMTEgMjcuMTk4SDE4LjQ2NzVMMTguNDgyMiAyNy4xNzQ2TDIxLjI3NjEgMjIuNjM1NUwyMS4yOTg2IDIyLjU5OTRMMjEuMjY2NCAyMi41NzFDMTkuNjQ2MSAyMS4xNTAxIDE5LjM4MjkgMTguNzI1MiAyMC42NjA5IDE2Ljk5WiIgZmlsbD0id2hpdGUiIHN0cm9rZT0iIzIxQTBCRCIgc3Ryb2tlLXdpZHRoPSIwLjEiLz4KPC9zdmc+Cg==", is_valid: true, label: "Webhook", environment: "onprem", @@ -229,8 +231,7 @@ export const triggers = [ status: "uninitialized", trigger_type: "SCHEDULE", errors: null, - large_image: - "data:image/png;base64,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", + large_image: "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDgiIGhlaWdodD0iNDgiIHZpZXdCb3g9IjAgMCA0OCA0OCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4IiByeD0iOCIgZmlsbD0iIzIxQTBCRCIvPgo8Y2lyY2xlIGN4PSIyNCIgY3k9IjI0IiByPSI4Ljc1IiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjEuNSIvPgo8cGF0aCBkPSJNMjguNSAyNEgyNC4yNUMyNC4xMTE5IDI0IDI0IDIzLjg4ODEgMjQgMjMuNzVWMjAuNSIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K", label: "Schedule", is_valid: true, environment: "onprem", @@ -258,8 +259,7 @@ export const triggers = [ status: "uninitialized", trigger_type: "SUBFLOW", errors: null, - large_image: - "data:image/png;base64,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", + large_image: "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDgiIGhlaWdodD0iNDgiIHZpZXdCb3g9IjAgMCA0OCA0OCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4IiByeD0iOCIgZmlsbD0idXJsKCNwYWludDBfbGluZWFyXzMxN181OTcpIi8+CjxwYXRoIGQ9Ik0xNS45OTk4IDE2LjAwMTVMMTUuOTk5OCAyNS43NDA2TDE5LjIwMDEgMjUuNzQwNkwxOS4yMDAxIDE5LjI0ODVMMzEuOTk5OCAxOS4yNDg1TDMxLjk5OTggMTYuMDAxNUwxNS45OTk4IDE2LjAwMTVaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNMjguNzk5NCAyMi4yNjE3TDI4Ljc5OTQgMjguNzUzOEwxNS45OTk4IDI4Ljc1MzhMMTUuOTk5OCAzMi4wMDA4TDMxLjk5OTggMzIuMDAwOEwzMS45OTk4IDIyLjI2MTdMMjguNzk5NCAyMi4yNjE3WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTI1LjczOSAyMi4yNjE3TDIyLjI2MDcgMjIuMjYxN0wyMi4yNjA3IDI1Ljc0TDI1LjczOSAyNS43NEwyNS43MzkgMjIuMjYxN1oiIGZpbGw9IndoaXRlIi8+CjxkZWZzPgo8bGluZWFyR3JhZGllbnQgaWQ9InBhaW50MF9saW5lYXJfMzE3XzU5NyIgeDE9Ii03LjcwNzc2ZS0wOSIgeTE9IjI3LjA3NjkiIHgyPSI0OC4wMDE0IiB5Mj0iMjYuOTM3NyIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPgo8c3RvcCBzdG9wLWNvbG9yPSIjRkY4NDQ0Ii8+CjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI0YyNjQzQiIvPgo8L2xpbmVhckdyYWRpZW50Pgo8L2RlZnM+Cjwvc3ZnPgo=", is_valid: true, label: "Subflow", environment: "onprem", @@ -303,7 +303,7 @@ export const triggers = [ } ], status: "running", - large_image: "/images/workflows/UserInput2.svg", + large_image: "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDgiIGhlaWdodD0iNDgiIHZpZXdCb3g9IjAgMCA0OCA0OCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4IiByeD0iOCIgZmlsbD0iIzg2MzZCNCIvPgo8cGF0aCBkPSJNMjEuMjgyMiAxNi43VjE0LjdNMTYuNzgyMiAyMS4ySDE0Ljc4MjJNMTcuOTc5MSAyNC41MDMyTDE2LjU2NDkgMjUuOTE3NE0yNS45OTk3IDE2LjQ4MjRMMjQuNTg1NCAxNy44OTY2TTE2LjU2NDggMTYuNDgyNUwxNy45NzkgMTcuODk2N00yNS4yODk4IDMyLjM1MDRMMjYuNzc3NyAyOC40NTc4QzI2Ljk3OTYgMjcuOTI5NSAyNy40NDggMjcuNTQ5IDI4LjAwNjUgMjcuNDU5N0wzMS42MjY0IDI2Ljg4MDVDMzMuMTkwOSAyNi42MzAyIDMzLjQ1ODUgMjQuNDkxNiAzMi4wMDQgMjMuODYzNEwyMi4zNDUyIDE5LjY5MjdDMjEuMTc1MSAxOS4xODc0IDE5LjkxNzkgMjAuMjAxMyAyMC4xNjQgMjEuNDUxOUwyMi4yNTczIDMyLjA5MDhDMjIuNTY0MyAzMy42NTEgMjQuNzIyMSAzMy44MzU4IDI1LjI4OTggMzIuMzUwNFoiIHN0cm9rZT0id2hpdGUiIHN0cm9rZS13aWR0aD0iMS4yNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+Cjwvc3ZnPgo=", description: "Wait for user input trigger", trigger_type: "USERINPUT", is_valid: true, @@ -312,7 +312,7 @@ export const triggers = [ environment: "cloud", long_description: "Take user input to continue execution", id: "", - }, + } ]; // Adds specific text to items @@ -406,7 +406,7 @@ export function SetJsonDotnotation(jsonInput, inputKey) { //export const green = "#86c142"; export const green = "#2BC07E" -export const yellow = "#FECC00"; +export const yellow = "#FFC633"; export const red = "#F53434"; export const grey = "#b0b0b0"; @@ -438,6 +438,66 @@ export function removeParam(key, sourceURL) { return rtn; } +const ACTION_STATES_STORAGE_KEY = "shuffle_action_tab_states"; + +export function getActionState(actionId, workflowId = null) { + const defaultState = { + tab: "setup", + showOptionalParams: false, + userSelectedTab: false, + }; + + try { + const stored = localStorage.getItem(ACTION_STATES_STORAGE_KEY); + if (!stored) return defaultState; + + const allStates = JSON.parse(stored); + + // Handle both old flat structure and new nested structure for backward compatibility + let actionState = {}; + if (workflowId && allStates[workflowId] && allStates[workflowId][actionId]) { + // New nested structure: workflowId -> actionId -> state + actionState = allStates[workflowId][actionId]; + } + + // Validate tab value + const validTabs = ["setup", "configuration"]; + const tab = validTabs.includes(actionState.tab) ? actionState.tab : defaultState.tab; + + return { + tab, + showOptionalParams: actionState.showOptionalParams === true, + userSelectedTab: actionState.userSelectedTab === true, + }; + } catch (e) { + return defaultState; + } +} + +export function setActionState(actionId, updates, workflowId = null) { + if (!actionId || !updates) return; + + try { + const stored = localStorage.getItem(ACTION_STATES_STORAGE_KEY); + const allStates = stored ? JSON.parse(stored) : {}; + + if (workflowId) { + // New nested structure: workflowId -> actionId -> state + if (!allStates[workflowId]) { + allStates[workflowId] = {}; + } + if (!allStates[workflowId][actionId]) { + allStates[workflowId][actionId] = {}; + } + Object.assign(allStates[workflowId][actionId], updates); + } + + localStorage.setItem(ACTION_STATES_STORAGE_KEY, JSON.stringify(allStates)); + } catch (e) { + console.error("Failed to save action state:", e); + } +} + const splitter = "|~|"; const svgSize = 24; const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); @@ -463,9 +523,9 @@ const AngularWorkflow = (defaultprops) => { var to_be_copied = ""; const [firstrequest, setFirstrequest] = React.useState(true); - const cystyle = useMemo(() => defaultCytoscapeStyle(theme), [themeMode]); - // const cystyle = useMemo(() => defaultCytoscapeStyle, [themeMode]); + const [apps, setApps] = React.useState([]); + const cystyle = useMemo(() => defaultCytoscapeStyle(theme, apps), [themeMode, apps]); const [cy, setCy] = React.useState(); const useStyles = makeStyles({ @@ -593,7 +653,6 @@ const AngularWorkflow = (defaultprops) => { const [showDropdown, setShowDropdown] = React.useState(false); const [triggerActionList, setTriggerActionList] = React.useState([]); - const [apps, setApps] = React.useState([]); const [filteredApps, setFilteredApps] = React.useState([]); const [prioritizedApps, setPrioritizedApps] = React.useState([]) @@ -670,7 +729,7 @@ const AngularWorkflow = (defaultprops) => { }) const [executionArgumentModalOpen, setExecutionArgumentModalOpen] = React.useState(false); const [searchModalOpen, setSearchModalOpen] = React.useState(false); - + const [buildDebugMode, setBuildDebugMode] = useState("build"); // "build" or "debug" useEffect(() => { if (!firstrequest && isLoaded && isLoggedIn && editWorkflowModalOpen === false) { @@ -974,6 +1033,7 @@ const AngularWorkflow = (defaultprops) => { "id": "shuffle_agent", "name": "AI Agent", "type": "ACTION", + "is_valid": true, "app_version": "1.0.0", "loop_versions": ["1.0.0"], "authentication": { @@ -1001,7 +1061,7 @@ const AngularWorkflow = (defaultprops) => { */ { "name": "input", - "value": "Take the data below and run the LLM query\n\n$exec", + "value": "What are you able to do?\n\n$exec", "required": true, "multiline": true, "description": "The input data for the LLM query", @@ -1311,7 +1371,7 @@ const AngularWorkflow = (defaultprops) => { if (appId === "integration" || appId === "shuffle_agent") { if (appId === "shuffle_agent") { - // Get the apps for OpenAI, Gemini, Mistral, DeepSeek + // Get sample apps for OpenAI, Gemini, Mistral, DeepSeek... loadAppConfig("1275a420a6a8b8d782483ac0c22f492c") // Shuffle AI loadAppConfig("5d19dd82517870c68d40cacad9b5ca91") // OpenAI //loadAppConfig("5d19dd82517870c68d40cacad9b5ca91") // Gemini @@ -1656,8 +1716,40 @@ const AngularWorkflow = (defaultprops) => { cy.on("mouseout", "node", (e) => onNodeHoverOut(e)); // Handles dragging - cy.on("drag", "node", (e) => onNodeDrag(e, selectedAction)); - cy.on("free", "node", (e) => onNodeDragStop(e, selectedAction)); + cy.on("drag", "node", (e) => { + const node = e.target; + // This is used to calculate the movement deltas for group/multi-drag. + if (!node.data("dragStartPos")) { + node.data("dragStartPos", { ...node.position() }); + } + + // Move all selected nodes together when Ctrl is held + handleMultiDrag(e); + + // Continuously update orientation while dragging (including neighbors) + updateNodeAndNeighborsOrientation(node); + + onNodeDrag(e, selectedAction); + }); + + cy.on("free", "node", (e) => { + const node = e.target; + const startPos = node.data("dragStartPos"); + + if (startPos) { + node.removeData("dragStartPos"); + } + + // Clear multidrag state + if (cy) { + cy.nodes().forEach((n) => n.removeData("multiDragStartPos")); + } + + // Update orientation after drag ends (including neighbors) + updateNodeAndNeighborsOrientation(node); + + onNodeDragStop(e, selectedAction); + }); cy.on("cxttap", "node", (e) => onCtxTap(e)); @@ -1675,6 +1767,10 @@ const AngularWorkflow = (defaultprops) => { return false }, + handlePosition: (node) => { + const orientation = node.data("flowOrientation"); + return orientation === "vertical" ? "middle top" : "right middle"; + }, preview: true, toggleOffOnLeave: true, loopAllowed: function (node) { @@ -2560,7 +2656,7 @@ const AngularWorkflow = (defaultprops) => { // Center and zoom to the node with smooth animation if requested if (config.center) { - cy.animate({ + cy.stop().animate({ center: { eles: cyNode }, @@ -3318,13 +3414,13 @@ const AngularWorkflow = (defaultprops) => { foundnode.data("errors", [error]) const parsedStyle = { - "border-width": "10px", + "border-width": "2px", "border-opacity": ".9", "border-color": red, } const animationDuration = 150 - foundnode.animate( + foundnode.stop().animate( { style: parsedStyle, }, @@ -3445,6 +3541,18 @@ const AngularWorkflow = (defaultprops) => { return } + for (var i = 0; i < workflowExecutions.length; i++) { + const exec = workflowExecutions[i] + if (exec.execution_id === curAction.source_execution) { + if (exec?.execution_argument === undefined || exec?.execution_argument === null || exec?.execution_argument.length === 0) { + break + } + + setExecutionText(exec.execution_argument) + break + } + } + setExecutionRunning(true) setExecutionRequestStarted(true) var headers = { @@ -3676,6 +3784,60 @@ const AngularWorkflow = (defaultprops) => { }) }; + useEffect(() => { + + const handleDebugKeyDown = (event) => { + if ((event.metaKey || event.ctrlKey) && event.key === '/') { + event.preventDefault(); // Prevent default browser behavior (like opening search bar) + if (!workflow.public && !executionRequestStarted) { + executeWorkflow(executionText, workflow.start, lastSaved); + } + } + if ((event.ctrlKey || event.metaKey) && event.key === "'") { + // Check if Ctrl (Windows/Linux) or Command (Mac) key is pressed along with '/' + if (!workflow.public && !executionModalOpen) { + setExecutionModalOpen(true); + getWorkflowExecution(props.match.params.key, ""); + } else if (!workflow.public && executionModalOpen) { + setExecutionModalOpen(false); + } + } + + if ((event.ctrlKey || event.metaKey) && event.key === "]") { + console.log("Show workflow revisions key pressed") + if (!workflow.public) { + setShowWorkflowRevisions(true) + //setOriginalWorkflow(workflow) + } + } + + if ((event.ctrlKey || event.metaKey) && event.key === ";") { + if (!workflow.public && executionModalOpen) { + getWorkflowExecution(props.match.params.key, ""); + } + } + + /* + if (( event.ctrlKey || event.metaKey ) && event.shiftKey) { + console.log("Shift key pressed") + if (!workflow.public && executionModalOpen) { + setExecutionRunning(false); + stop() + const newitem = removeParam("execution_id", cursearch); + navigate(curpath + newitem) + setExecutionModalView(0); + } + } + */ + }; + + document.addEventListener('keydown', handleDebugKeyDown); + + return () => { + document.removeEventListener('keydown', handleDebugKeyDown); + } + }, [executeWorkflow, executionText, workflow, lastSaved, executionRequestStarted]) + // This can be used to only show prioritzed ones later // Right now, it can prioritize authenticated ones //"Testing", @@ -3830,7 +3992,7 @@ const AngularWorkflow = (defaultprops) => { } else { console.log("Closing auth modal? FAIL") - toast("Failed to find new authentication. See details in Oauth2 popup window where auth was attempted."); + toast.error("Failed to find new authentication. See details in Oauth2 popup window where auth was attempted."); shouldClose = false } } else { @@ -4184,7 +4346,7 @@ const AngularWorkflow = (defaultprops) => { } const animationDuration = 150 - node.animate( + node.stop().animate( { style: parsedStyle, }, @@ -4201,7 +4363,7 @@ const AngularWorkflow = (defaultprops) => { "border-color": "#81c784", } - node.animate( + node.stop().animate( { style: parsedStyle, }, @@ -4250,7 +4412,7 @@ const AngularWorkflow = (defaultprops) => { } const animationDuration = 150 - node.animate( + node.stop().animate( { style: parsedStyle, }, @@ -4267,7 +4429,7 @@ const AngularWorkflow = (defaultprops) => { "border-color": "#81c784", } - node.animate( + node.stop().animate( { style: parsedStyle, }, @@ -4298,7 +4460,7 @@ const AngularWorkflow = (defaultprops) => { } const animationDuration = 150 - node.animate( + node.stop().animate( { style: parsedStyle, }, @@ -4314,7 +4476,7 @@ const AngularWorkflow = (defaultprops) => { "line-gradient-stop-colors": ["grey", "grey"], } - node.animate( + node.stop().animate( { style: parsedStyle, }, @@ -4346,7 +4508,7 @@ const AngularWorkflow = (defaultprops) => { } const animationDuration = 150 - node.animate( + node.stop().animate( { style: parsedStyle, }, @@ -4363,7 +4525,7 @@ const AngularWorkflow = (defaultprops) => { "border-color": color, } - node.animate( + node.stop().animate( { style: parsedStyle, }, @@ -4394,7 +4556,7 @@ const AngularWorkflow = (defaultprops) => { } const animationDuration = 150 - node.animate( + node.stop().animate( { style: parsedStyle, }, @@ -4450,7 +4612,7 @@ const AngularWorkflow = (defaultprops) => { } const animationDuration = 150 - node.animate( + node.stop().animate( { style: parsedStyle, }, @@ -4484,7 +4646,7 @@ const AngularWorkflow = (defaultprops) => { } const animationDuration = 150 - node.animate( + node.stop().animate( { style: parsedStyle, }, @@ -5406,6 +5568,13 @@ const AngularWorkflow = (defaultprops) => { }; const onEdgeSelect = (event) => { + + const selectedNodes = cy.$(':selected'); + if (selectedNodes && selectedNodes.length > 1) { + event.target.unselect(); + return; + } + ReactDOM.unstable_batchedUpdates(() => { setRightSideBarOpen(true); setLastSaved(false); @@ -7096,8 +7265,20 @@ const AngularWorkflow = (defaultprops) => { cy.removeListener("drag"); cy.removeListener("free"); - cy.on("drag", "node", (e) => onNodeDrag(e, curaction)); - cy.on("free", "node", (e) => onNodeDragStop(e, curaction)); + cy.on("drag", "node", (e) => { + const node = e.target; + + updateNodeAndNeighborsOrientation(node); + + onNodeDrag(e, curaction); + }); + + cy.on("free", "node", (e) => { + const node = e.target; + updateNodeAndNeighborsOrientation(node); + + onNodeDragStop(e, curaction); + }); } if (environments !== undefined && environments !== null && (typeof environments === "array" || typeof environments === "object")) { @@ -7697,6 +7878,153 @@ const AngularWorkflow = (defaultprops) => { return dstdata; }; + // Dynamic Label Positioning - Core Logic Function + // This looks at all edges connected to a node and figures out if the workflow + // is flowing horizontally (left-to-right) or vertically (top-to-bottom) + // Based on that, it sets the node's "flowOrientation" which controls where the label appears + const updateNodeLabelOrientation = (node) => { + + if (!node || typeof node.position !== "function") { + return + } + + // Get all edges (connections) attached to this node + const edges = node.connectedEdges() + + // If node has no edges, default to horizontal layout (label at bottom) + if (!edges || edges.length === 0) { + node.data("flowOrientation", "horizontal") // Set the orientation data + return + } + + // Get the current position of this node (x, y coordinates) + const nodePos = node.position() + + let hasHorizontalOutgoing = false + let hasStrongVertical = false + + // Loop through each edge connected to this node + edges.forEach((edge) => { + // Check if this node is the source (start) of the edge + const isSource = edge.source().id() === node.id() + // Get the other node connected by this edge (target if we're source, source if we're target) + const other = isSource ? edge.target() : edge.source() + + if (!other || typeof other.position !== "function") { + return + } + + const otherPos = other.position() + // Calculating the distance between the nodes + const dx = Math.abs(otherPos.x - nodePos.x) + const dy = Math.abs(otherPos.y - nodePos.y) + + // Ignore edges that are too small + if (dx < 2 && dy < 2) { + return + } + + // Determine if edge is mostly vertical (going up/down) + const isVerticalish = dx < 30 && dy > 30 + + // Determine if edge is mostly horizontal (going left/right) + const isHorizontalish = dy < 30 && dx > 30 + + if (isSource && isHorizontalish) { + hasHorizontalOutgoing = true + } + + if (isVerticalish) { + hasStrongVertical = true + } + }) + + // Decision: Choose the orientation based on what we found + // Priority order: + // 1) If we have a horizontal OUTGOING edge → stay horizontal (label at bottom) + // (This is the default workflow style - left to right) + // 2) Else if we have any strong vertical edge → go vertical (label on right) + // (This prevents labels from being cut by vertical edges) + // 3) Else → default to horizontal (label at bottom) + let newOrientation = "horizontal" + if (!hasHorizontalOutgoing && hasStrongVertical) { + newOrientation = "vertical" + } + + // Save the orientation to the node's data + // This will trigger the CSS rules we defined in defaultCytoscapeStyle.jsx + node.data("flowOrientation", newOrientation) + } + + // This updates node orientation and its connected neighbors + const updateNodeAndNeighborsOrientation = (node) => { + updateNodeLabelOrientation(node); + + const edges = node.connectedEdges(); + if (edges && edges.length > 0) { + edges.forEach((edge) => { + const source = edge.source(); + const target = edge.target(); + + if (source && source.id() !== node.id()) { + updateNodeLabelOrientation(source); + } + + if (target && target.id() !== node.id()) { + updateNodeLabelOrientation(target); + } + }); + } + }; + + // Multi-Node Dragging + const handleMultiDrag = (e) => { + + const node = e.target + const ev = e.originalEvent + + if (!ev || !ev.ctrlKey) { + return; + } + + const start = node.data("multiDragStartPos") + + if (!start) { + const selectedNodes = cy.$(":selected") + + selectedNodes.forEach((n) => { + n.data("multiDragStartPos", { ...n.position() }) + }) + return + } + + // SUBSEQUENT DRAG EVENTS: Move all selected nodes together + // Calculate how far the dragged node has moved + const cur = node.position() + const dx = cur.x - start.x + const dy = cur.y - start.y + + // Get all selected nodes again + const selectedNodes = cy.$(":selected") + + // Move each selected node by the same distance (dx, dy) + selectedNodes.forEach((n) => { + const s = n.data("multiDragStartPos") + + // Skip if no starting position saved, or if this is the node being dragged + // (the dragged node is already being moved by Cytoscape automatically) + if (!s || n.id() === node.id()) { + return + } + + // Move this node to: original position + how far the dragged node moved + n.position({ + x: s.x + dx, // Original x + horizontal movement + y: s.y + dy, // Original y + vertical movement + }) + }) + } + // Checks for errors in edges when they're added const onEdgeAdded = (event) => { setLastSaved(false); @@ -7721,6 +8049,8 @@ const AngularWorkflow = (defaultprops) => { if (sourcenode === undefined || sourcenode === null || destinationnode === undefined || destinationnode === null) { console.log("Source or destination node is undefined or null: ", sourcenode, destinationnode) } else { + updateNodeLabelOrientation(sourcenode) + updateNodeLabelOrientation(destinationnode) if (sourcenode.data("name") === "switch") { event.target.remove() return @@ -7938,6 +8268,11 @@ const AngularWorkflow = (defaultprops) => { const node = event.target; const nodedata = JSON.parse(JSON.stringify(event.target.data())) + // Default to horizontal layout for new nodes if not explicitly set + if (node.data("flowOrientation") === undefined) { + node.data("flowOrientation", "horizontal"); + } + if (nodedata.finished === false || (nodedata.id !== undefined && nodedata.is_valid === undefined)) { return } @@ -8002,7 +8337,8 @@ const AngularWorkflow = (defaultprops) => { if (nodedata.app_name === "Shuffle Tools") { const iconInfo = GetIconInfo(nodedata); - const svg_pin = ``; + const iconViewBox = iconInfo.svgViewBox || `0 0 ${svgSize} ${svgSize}`; + const svg_pin = `${buildIconSvgPath(iconInfo, "white")}`; const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin); nodedata.large_image = svgpin_Url; nodedata.fillGradient = iconInfo.fillGradient; @@ -8242,8 +8578,40 @@ const AngularWorkflow = (defaultprops) => { setWorkflow(workflow); } + // Check if user is typing in an input field, textarea, or contenteditable element + var isInputField = function(target) { + if (!target) return false + const tagName = target.localName || target.tagName?.toLowerCase() + const isInput = tagName === "input" || tagName === "textarea" + const isContentEditable = target.isContentEditable === true || target.contentEditable === "true" + const isEditable = target.getAttribute && (target.getAttribute("contenteditable") === "true" || target.getAttribute("contenteditable") === "") + return isInput || isContentEditable || isEditable + } + //var previouskey = 0 const handleKeyDown = (event) => { + + // Check if any modal or sidebar is open - if so, disable custom shortcuts + var isAnyModalOrSidebarOpen = + variablesModalOpen || + aiQueryModalOpen || + workflowGenerationModalOpen || + executionVariablesModalOpen || + authenticationModalOpen || + conditionsModalOpen || + authgroupModalOpen || + codeModalOpen || + executionModalOpen || + editWorkflowModalOpen || + tenzirConfigModalOpen || + codeEditorModalOpen || + executionArgumentModalOpen || + searchModalOpen || + rightSideBarOpen || + leftSideBarOpenByClick || + selectionOpen || + (suggestionBox && suggestionBox.open) + switch (event.keyCode) { case 27: if (configureWorkflowModalOpen === true) { @@ -8271,58 +8639,97 @@ const AngularWorkflow = (defaultprops) => { } break; + // FEATURE: Multi-Node Copy (Cmd+C / Ctrl+C) case 67: - if (event.ctrlKey && !event.shiftKey) { - if ( - event.path !== undefined && - event.path !== null && - event.path.length > 0 - ) { - if (event.path[0].localName !== "body") { - console.log("Skipping because body is not targeted"); - return; - } + if ((event.ctrlKey || event.metaKey) && !event.shiftKey) { + + // If any modal/sidebar is open, let browser handle normal copy + if (isAnyModalOrSidebarOpen) { + return } - if ( - event.target !== undefined && - event.target !== null - ) { - if (event.target.localName !== "body") { - console.log("Skipping because body is not targeted") - return; - } + // Do not enter if it's an input field, textarea, or contenteditable element + // (we don't want to interfere with normal text copying) + if (isInputField(event.target) || isInputField(document.activeElement)) { + return; } - console.log("CTRL+C"); + // Make sure Cytoscape is initialized if (cy !== undefined && cy !== null) { - var cydata = cy.$(":selected").jsons(); - if (cydata !== undefined && cydata !== null && cydata.length > 0) { - console.log(cydata); - const elementName = "copy_element_shuffle"; - var copyText = document.getElementById(elementName); - if (copyText !== null && copyText !== undefined) { - const clipboard = navigator.clipboard; - if (clipboard === undefined) { - toast("Can only copy over HTTPS (port 3443)"); - return; - } + // Get all selected nodes (this includes decorator nodes, buttons, nodes etc.) + const allSelected = cy.$(":selected").filter("node"); + + // Filter to get only actual workflow nodes (not UI elements) + const selectedNodes = allSelected.filter(node => { + const data = node.data(); + // Only copy nodes that are: + // - Not decorator nodes (small icon nodes attached to main nodes) + // - Not descriptor nodes (UI elements) + // - Not button nodes (delete/copy buttons) + // - Either ACTION or TRIGGER type (actual workflow nodes) + return data.decorator !== true && + data.isDescriptor !== true && + data.isButton !== true && + (data.type === "ACTION" || data.type === "TRIGGER"); + }); - navigator.clipboard.writeText(JSON.stringify(cydata)); - copyText.select(); - copyText.setSelectionRange(0, 99999); /* For mobile devices */ - document.execCommand("copy"); - toast(`Copied ${cydata.length} element(s)`); - } + // Only proceed if we have valid nodes to copy + if (selectedNodes.length > 0) { + + const nodeIds = selectedNodes.map(n => n.id()); + + // Prepare node data for clipboard + // We save: node data, position, and original ID (for remapping later) + const nodesData = selectedNodes.map(n => ({ + data: n.data(), + position: n.position(), + originalId: n.id() + })); + + const internalBranches = workflow.branches.filter(branch => + nodeIds.includes(branch.source_id) && // Source node is selected + nodeIds.includes(branch.destination_id) // Destination node is selected + ); + + const clipboardData = { + type: "copy_nodes_shuffle", + nodes: nodesData, + branches: internalBranches + }; + + // Write to clipboard as JSON string + navigator.clipboard.writeText(JSON.stringify(clipboardData)) + .then(() => toast(`Copied ${selectedNodes.length} node(s) with ${internalBranches.length} branch(es)`)) + .catch(() => toast("Failed to copy to clipboard")); + + // Prevent browser's default copy behavior + event.preventDefault(); } } } break; + // FEATURE: Multi-Node Paste (Cmd+V / Ctrl+V) case 86: - console.log("CTRL+V? ctrl: ", event.ctrlKey) - if (event.ctrlKey) { - // Paste is handled in the handlePaste() function. + // Check if Ctrl (Windows/Linux) or Cmd (Mac) is pressed + if (event.ctrlKey || event.metaKey) { + + // If any modal/sidebar is open, let browser handle normal paste + if (isAnyModalOrSidebarOpen) { + return + } + + // Don't paste if it's an input field, textarea, or contenteditable element + if (isInputField(event.target) || isInputField(document.activeElement)) { + return; + } + + // Make sure Cytoscape is initialized + if (cy !== undefined && cy !== null) { + handlePaste(event); + } + // Prevent browser's default paste behavior + event.preventDefault(); } break; case 88: @@ -8338,6 +8745,56 @@ const AngularWorkflow = (defaultprops) => { // As a poweruser myself, I found myself hitting this a few // too many times to just edit text. Need a better bind, which does NOT work while inside a field break; + // FEATURE: Multi-Selection Delete (Delete / Backspace keys) + // Key code 46 = Delete key (Mac/Windows) + // Key code 8 = Backspace key (Windows) + case 46: // Delete key + case 8: // Backspace key + // If any modal/sidebar is open, let browser handle normal delete + if (isAnyModalOrSidebarOpen) { + return + } + + // Don't delete if user is typing in an input field, textarea, or contenteditable element + // (we don't want to interfere with normal text deletion) + if (isInputField(event.target) || isInputField(document.activeElement)) { + return; // Exit early - let the browser handle normal deletion + } + + // Make sure Cytoscape is initialized + if (cy !== undefined && cy !== null) { + // Get all selected nodes + const selectedNodes = cy.$(":selected").filter("node"); + + // Only proceed if we have nodes selected + if (selectedNodes.length > 0) { + // IMPORTANT: Collect node IDs first before deleting + // This prevents issues if the selection changes while we're deleting + const nodeIdsToDelete = []; + + // Loop through selected nodes and collect their IDs + selectedNodes.forEach(node => { + const nodeData = node.data(); + // Skip decorator nodes (small icon nodes) - they shouldn't be deleted directly + // Exception: COMMENT type decorators can be deleted + if (nodeData.decorator === true && nodeData.type !== "COMMENT") { + return; // Skip this node + } + // Add this node's ID to our delete list + nodeIdsToDelete.push(nodeData.id); + }); + + // Delete each node using the existing removeNode function + // This function handles proper cleanup (triggers, workflow updates, history, etc.) + nodeIdsToDelete.forEach(nodeId => { + removeNode(nodeId); // This will trigger onNodeRemoved and clean up properly + }); + + // Prevent browser's default delete behavior + event.preventDefault(); + } + } + break; default: break; } @@ -8345,35 +8802,90 @@ const AngularWorkflow = (defaultprops) => { //previouskey = event.keyCode }; - const handlePaste = (event) => { + const handlePaste = async (event) => { if (event.path !== undefined && event.path !== null && event.path.length > 0) { if (event.path[0].localName !== "body") { - console.log("Skipping paste because body is not targeted") - return; + return; } } if (event.target !== undefined && event.target !== null) { - // If it's an input area, skip paste - if (event.target.localName === "input" || event.target.localName === "textarea") { - console.log("Skipping paste because body is not targeted (1). Target: ", event?.target?.localName) - return; - } + if (event.target.localName === "input" || event.target.localName === "textarea") { + return; + } } - // Does this stop things? - //event.preventDefault() - const clipboard = (event.originalEvent || event).clipboardData.getData("text/plain") - console.log("CLIPBOARD TO PASTE: ", clipboard) + if (cy === undefined || cy === null) return; try { - const allnodes = cy.nodes().jsons() - var parsedjson = JSON.parse(clipboard); - if (!Array.isArray(parsedjson)) { - console.log("Not array! Adding to array.") - parsedjson = [parsedjson] + let clipboardText; + if (event.clipboardData) { + clipboardText = event.clipboardData.getData("text/plain"); + } else { + clipboardText = await navigator.clipboard.readText(); } + const clipboardData = JSON.parse(clipboardText); + + // Handle new multi-node copy format - use same logic as individual copy button + if (clipboardData.type === "copy_nodes_shuffle" && clipboardData.nodes && clipboardData.branches) { + const idMap = {}; + clipboardData.nodes.forEach(node => { + idMap[node.originalId] = uuidv4(); + }); + + clipboardData.nodes.forEach(node => { + const newNodeData = JSON.parse(JSON.stringify(node.data)); + const newId = idMap[node.originalId]; + newNodeData.id = newId; + if (newNodeData.position !== undefined) { + newNodeData.position = { + x: node.position.x + 450, + y: node.position.y + 450, + } + } + + newNodeData.isStartNode = false; + newNodeData.errors = []; + newNodeData.is_valid = true; + newNodeData.isValid = true; + newNodeData.label = newNodeData.label + "_copy"; + + cy.add({ + group: "nodes", + data: newNodeData, + position: newNodeData.position, + }); + + workflow.actions.push(newNodeData); + }); + + // Copy internal branches (between pasted nodes only - no connections to old nodes) + clipboardData.branches.forEach(branch => { + const newbranch = JSON.parse(JSON.stringify(branch)); + newbranch.id = uuidv4(); + newbranch.source_id = idMap[branch.source_id]; + newbranch.destination_id = idMap[branch.destination_id]; + newbranch._id = newbranch.id; + newbranch.source = newbranch.source_id; + newbranch.target = newbranch.destination_id; + cy.add({ + group: "edges", + data: newbranch, + }); + }); + + // Clear clipboard after successful paste + navigator.clipboard.writeText("").catch(() => {}); + + event.preventDefault(); + return; + } + + // Fallback to old format for backward compatibility + const allnodes = cy.nodes().jsons(); + var parsedjson = Array.isArray(clipboardData) ? clipboardData : [clipboardData]; + for (let jsonkey in parsedjson) { var item = parsedjson[jsonkey]; @@ -8416,10 +8928,10 @@ const AngularWorkflow = (defaultprops) => { group: item.group, data: item.data, position: { - x: item.position.x+20, - y: item.position.y+20, - }, - }) + x: (item.position?.x || 0) + 20, + y: (item.position?.y || 0) + 20, + }, + }); } } catch (e) { console.log("Error pasting: ", e); @@ -8615,19 +9127,19 @@ const AngularWorkflow = (defaultprops) => { } var parsedStyle = { - "border-width": "1px", + "border-width": "2px", "font-size": "18px", //"cursor": "default", } if ((nodedata.app_name === "Testing" || nodedata.app_name === "Shuffle Tools") && !nodedata.isStartNode) { parsedStyle = { - "border-width": "1px", + "border-width": "2px", "font-size": "0px", }; } - event.target.animate( + event.target.stop().animate( { style: parsedStyle, }, @@ -8657,8 +9169,14 @@ const AngularWorkflow = (defaultprops) => { return; } - const px = parentNode.position("x") - 65; - const py = parentNode.position("y") - 45; + var xDiff = 0 + var yDiff = 0 + if (parentNode.data("app_id") === "shuffle_agent") { + xDiff = 70 + } + + const px = parentNode.position("x") - 65 - xDiff; + const py = parentNode.position("y") - 45 - yDiff; const circleId = (newNodeId = uuidv4()); parentNode.data("circleId", circleId); @@ -9367,7 +9885,7 @@ const AngularWorkflow = (defaultprops) => { if (nodedata.type === "TRIGGER") { if (nodedata.trigger_type === "SUBFLOW" || nodedata.trigger_type === "USERINPUT") { addCopyButton(event); - } else { + } else { // Check how many executions from the source addRunCountButton(event); } @@ -9375,9 +9893,9 @@ const AngularWorkflow = (defaultprops) => { addCopyButton(event); - if (nodedata.app_id !== "shuffle_agent") { - addStartnodeButton(event); - } + // if (nodedata.app_id !== "shuffle_agent") { + addStartnodeButton(event); + // } } // autocomplete @@ -9393,14 +9911,14 @@ const AngularWorkflow = (defaultprops) => { } } - if (nodedata.name === "switch" || nodedata.app_id === "shuffle_agent") { + if (nodedata.name === "switch") { return } var parsedStyle = { - "border-width": "7px", + "border-width": "6px", "border-opacity": ".7", - "font-size": "25px", + "font-size": nodedata.app_id !== "shuffle_agent" && "25px", //"cursor": "pointer", } @@ -9413,7 +9931,7 @@ const AngularWorkflow = (defaultprops) => { const item = typeIds[idkey] if (item.data.id === nodedata.id) { //console.log("items: ", item.data.id, nodedata.id) - parsedStyle["border-width"] = "7px" + parsedStyle["border-width"] = "6px" break } } @@ -9423,7 +9941,7 @@ const AngularWorkflow = (defaultprops) => { } if (event.target !== undefined && event.target !== null) { - event.target.animate( + event.target.stop().animate( { style: parsedStyle, }, @@ -9632,7 +10150,65 @@ const AngularWorkflow = (defaultprops) => { } } - const setupGraph = (inputworkflow) => { + function roundBase64Image(base64, radius = 12) { + if (!base64 || typeof base64 !== "string") { + return Promise.resolve(base64); + } + + return new Promise((resolve) => { + const img = new Image(); + if (!base64.startsWith("data:")) { + img.crossOrigin = "anonymous"; + } + + img.onload = () => { + const size = Math.min(img.width, img.height); + if (size === 0) { + resolve(base64); + return; + } + + const canvas = document.createElement("canvas"); + canvas.width = size; + canvas.height = size; + + const ctx = canvas.getContext("2d"); + + // Draw rounded rect mask + ctx.beginPath(); + ctx.moveTo(radius, 0); + ctx.lineTo(size - radius, 0); + ctx.quadraticCurveTo(size, 0, size, radius); + ctx.lineTo(size, size - radius); + ctx.quadraticCurveTo(size, size, size - radius, size); + ctx.lineTo(radius, size); + ctx.quadraticCurveTo(0, size, 0, size - radius); + ctx.lineTo(0, radius); + ctx.quadraticCurveTo(0, 0, radius, 0); + ctx.closePath(); + ctx.clip(); + + // Draw image centered & cropped square + const sx = (img.width - size) / 2; + const sy = (img.height - size) / 2; + ctx.drawImage(img, sx, sy, size, size, 0, 0, size, size); + + try { + resolve(canvas.toDataURL("image/png")); + } catch (e) { + resolve(base64); + } + }; + + img.onerror = () => { + resolve(base64); + }; + + img.src = base64; + }); + } + + const setupGraph = async (inputworkflow) => { // Reset cytoscape nodes and branches if (cy !== undefined && cy !== null) { if (inputworkflow.actions !== undefined && inputworkflow.actions !== null && inputworkflow.actions.length > 0) { @@ -9660,12 +10236,13 @@ const AngularWorkflow = (defaultprops) => { inputworkflow.visual_branches = [] } - const actions = inputworkflow.actions.map((action) => { + const actions = await Promise.all(inputworkflow.actions.map(async (action) => { const node = {}; if (!action.isStartNode && action.app_name === "Shuffle Tools") { const iconInfo = GetIconInfo(action) - const svg_pin = ``; + const iconViewBox = iconInfo.svgViewBox || `0 0 ${svgSize} ${svgSize}`; + const svg_pin = `${buildIconSvgPath(iconInfo, "white")}`; const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin); action.large_image = svgpin_Url; action.fillGradient = iconInfo.fillGradient; @@ -9686,6 +10263,12 @@ const AngularWorkflow = (defaultprops) => { action.iconBackground = iconInfo.iconBackgroundColor action.fillstyle = "linear-gradient" } + }else if(!action.isStartNode) { + // This is to round the corners of the image + const originalBase64 = action.large_image !== undefined && action.large_image !== null && action.large_image !== "" ? action.large_image : theme.palette.defaultImage + const roundedImage = await roundBase64Image(originalBase64, 16); + action = {...action, large_image: roundedImage} + } node.position = action.position; @@ -9734,14 +10317,14 @@ const AngularWorkflow = (defaultprops) => { const foundnode = cy.getElementById(action.id) if (foundnode !== undefined && foundnode !== null) { const parsedStyle = { - "border-width": "3px", + "border-width": "2px", "border-opacity": "1", "border-color": "#40E0D0", "opacity": "0.4", } const animationDuration = 150 - foundnode.animate( + foundnode.stop().animate( { style: parsedStyle, }, @@ -9755,7 +10338,7 @@ const AngularWorkflow = (defaultprops) => { } return node - }) + })) // What are these again? Where are they used? const decoratorNodes = [] @@ -9903,14 +10486,14 @@ const AngularWorkflow = (defaultprops) => { const foundnode = cy.getElementById(trigger.id) if (foundnode !== undefined && foundnode !== null) { const parsedStyle = { - "border-width": "3px", + "border-width": "2px", "border-opacity": "1", "border-color": "#40E0D0", "opacity": "0.4", } const animationDuration = 150 - foundnode.animate( + foundnode.stop().animate( { style: parsedStyle, }, @@ -10651,6 +11234,10 @@ const AngularWorkflow = (defaultprops) => { return false }, + handlePosition: (node) => { + const orientation = node.data("flowOrientation"); + return orientation === "vertical" ? "middle top" : "right middle"; + }, preview: false, toggleOffOnLeave: true, loopAllowed: function (node) { @@ -10674,7 +11261,20 @@ const AngularWorkflow = (defaultprops) => { console.log("Error fitting cytoscape (4): ", error) } - + // Ensure all nodes have correct initial orientation based on edges + // after graph is rendered / fitted. + try { + cy.nodes().forEach((node) => { + if (node.data("flowOrientation") === undefined) { + node.data("flowOrientation", "horizontal") + } + + updateNodeLabelOrientation(node) + }) + } catch (error) { + console.log("Error setting initial flowOrientation: ", error) + } + cy.on("boxselect", "node", (e) => { e.preventDefault() e.stopPropagation() @@ -10749,8 +11349,19 @@ const AngularWorkflow = (defaultprops) => { }); // Handles dragging - cy.on("drag", "node", (e) => onNodeDrag(e, selectedAction)); - cy.on("free", "node", (e) => onNodeDragStop(e, selectedAction)); + cy.on("drag", "node", (e) => { + const node = e.target; + updateNodeAndNeighborsOrientation(node); + + onNodeDrag(e, selectedAction); + }); + + cy.on("free", "node", (e) => { + const node = e.target; + updateNodeAndNeighborsOrientation(node); + + onNodeDragStop(e, selectedAction); + }); cy.on("cxttap", "node", (e) => onCtxTap(e)); @@ -11005,6 +11616,7 @@ const AngularWorkflow = (defaultprops) => { marginLeft: 5, marginRight: 5, display: "flex", + backgroundColor: theme.palette.backgroundColor, flexDirection: "column", minHeight: isMobile ? bodyHeight - appBarSize * 4 : parsedHeight, maxHeight: isMobile ? bodyHeight - appBarSize * 4 : parsedHeight, @@ -11353,17 +11965,13 @@ const AngularWorkflow = (defaultprops) => { orientation={isMobile ? "vertical" : "horizontal"} style={{ display: "flex", + width: "100%", }} > - - - - {isMobile ? null : Apps} - + "Apps" } style={tabStyle} /> @@ -11390,12 +11998,7 @@ const AngularWorkflow = (defaultprops) => { - - - - {isMobile ? null : Vars} - + "Vars" } style={tabStyle} /> @@ -11645,7 +12248,8 @@ const AngularWorkflow = (defaultprops) => { if (newAppData.app_name === "Shuffle Tools") { const iconInfo = GetIconInfo(newAppData) - const svg_pin = ``; + const iconViewBox = iconInfo.svgViewBox || `0 0 ${svgSize} ${svgSize}`; + const svg_pin = `${buildIconSvgPath(iconInfo, "white")}`; const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin) newAppData.large_image = svgpin_Url @@ -11778,7 +12382,7 @@ const AngularWorkflow = (defaultprops) => { zoom: 0.9, } - const handleAppDrag = (e, app) => { + const handleAppDrag = async (e, app) => { if (cy === undefined || cy === null) { return } @@ -11935,6 +12539,10 @@ const AngularWorkflow = (defaultprops) => { } } + const originalBase64 = app.large_image !== undefined && app.large_image !== null && app.large_image !== "" ? app.large_image : app.palette.defaultImage + const roundedImage = await roundBase64Image(originalBase64, 16); + + const newAppData = { name: app.actions[actionIndex].name, label: actionLabel, @@ -11953,7 +12561,7 @@ const AngularWorkflow = (defaultprops) => { type: actionType, parameters: parameters, isStartNode: false, - large_image: app.large_image, + large_image: roundedImage, run_magic_output: false, authentication: [], execution_variable: undefined, @@ -12151,7 +12759,16 @@ const AngularWorkflow = (defaultprops) => { // Change the Large Image const iconInfo = GetIconInfo(action) - const svg_pin = ``; + const bgColor = iconInfo.iconBackgroundColor || "#f76b1c"; + var pathElement; + if (iconInfo.svgViewBox === "0 0 48 48") { + pathElement = buildIconSvgPath(iconInfo, iconInfo.iconColor); + } else if (iconInfo.useStroke) { + pathElement = ``; + } else { + pathElement = ``; + } + const svg_pin = `${pathElement}`; const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin); app.large_image = svgpin_Url app.fillGradient = iconInfo.fillGradient @@ -12199,7 +12816,8 @@ const AngularWorkflow = (defaultprops) => { // If action is set, look for the icon of it with the normal setup } - newAppStyle.backgroundColor = theme.palette.backgroundColor + newAppStyle.backgroundColor = hover ? theme.palette.parsedAppPaperColor : "transparent" + return ( { @@ -12223,6 +12841,7 @@ const AngularWorkflow = (defaultprops) => { > { if (!dragRef.current) { @@ -12330,9 +12949,9 @@ const AngularWorkflow = (defaultprops) => { - + { userDrag: "none", userSelect: "none", borderRadius: theme.palette?.borderRadius, - height: isMobile ? 40 : 45, - width: isMobile ? 40 : 45, - + height: isMobile ? 40 : (small === true || app.type === "TRIGGER") ? 52 : 45, + width: isMobile ? 40 : (small === true || app.type === "TRIGGER") ? 52 : 45, background: `linear-gradient(to right, ${app.fillGradient})`, }} /> @@ -12487,7 +13105,7 @@ const AngularWorkflow = (defaultprops) => { //} safeRefine(event.currentTarget.value) }} - limit={5} + limit={15} /> {/*isSearchStalled ? 'My search is stalled' : ''*/} @@ -12692,7 +13310,7 @@ const AngularWorkflow = (defaultprops) => {
    {title} @@ -12704,15 +13322,15 @@ const AngularWorkflow = (defaultprops) => { // Popular Shuffle Tools actions const popularActions = [ - ["repeat_back_to_me", "filter_list", "execute_python", "parse_ioc"], - ["set_cache_value", "get_file_meta", "merge_lists", "send_sms_shuffle"] + ["repeat_back_to_me", "execute_python", "get_file_value", "set_cache_value"], + ["get_file_meta", "send_sms_shuffle", "filter_list", "merge_lists"], ]; return (
    { InputProps={{ style: { height: 40, + borderRadius: theme.palette?.borderRadius, + backgroundColor: theme.palette.usecaseCardColor, }, startAdornment: ( @@ -12782,7 +13402,7 @@ const AngularWorkflow = (defaultprops) => { )} {visibleApps.length > extraApps.length ? - + Your Apps : null} @@ -12798,9 +13418,9 @@ const AngularWorkflow = (defaultprops) => { return null } - if ((app.id === "shuffle_agent") && userdata.support !== true) { - return null - } + //if ((app.id === "shuffle_agent") && userdata.support !== true) { + // return null + //} if (viewedApps.includes(app.id)) { return null @@ -13034,7 +13654,8 @@ const AngularWorkflow = (defaultprops) => { if (newSelectedAction.app_name === "Shuffle Tools") { const iconInfo = GetIconInfo(newSelectedAction); - const svg_pin = ``; + const iconViewBox = iconInfo.svgViewBox || `0 0 ${svgSize} ${svgSize}`; + const svg_pin = `${buildIconSvgPath(iconInfo, "white")}`; const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin); newSelectedAction.large_image = svgpin_Url; newSelectedAction.fillGradient = iconInfo.fillGradient; @@ -14360,8 +14981,7 @@ const AngularWorkflow = (defaultprops) => { padding: 30, pointerEvents: "auto", color: theme.palette.text.primary, - minWidth: isMobile ? "90%" : 650, - border: theme.palette.defaultBorder, + minWidth: isMobile ? "90%" : 750, borderRadius: theme.palette.borderRadius, backgroundColor: "black", @@ -14474,18 +15094,24 @@ const AngularWorkflow = (defaultprops) => { { return collapseField(jsonField) }} @@ -14499,10 +15125,11 @@ const AngularWorkflow = (defaultprops) => { } return ( - { setExecutionText(data) - executeWorkflow(data, workflow.start, lastSaved); + //executeWorkflow(data, workflow.start, lastSaved); setExecutionArgumentModalOpen(false) }} @@ -14759,6 +15386,7 @@ const AngularWorkflow = (defaultprops) => { color: theme.palette.textColor, backgroundColor: 'inherit', zIndex: 10000, + fontSize: 12, }} > PS: Conditions can't be used for loops [ .# ]. Use the filters list action.{" "} @@ -15110,7 +15738,9 @@ const AngularWorkflow = (defaultprops) => { key={condition.condition.id} square style={paperVariableStyle} - onClick={() => { }} + onClick={() => { + setLastSaved(false) + }} >
    { return } + setLastSaved(false) setSourceValue({ name: "source", value: "", @@ -15384,17 +16015,15 @@ const AngularWorkflow = (defaultprops) => { // Change Direction of the branch target/source const foundBranch = cy.getElementById(selectedEdge.id) if (foundBranch !== undefined && foundBranch !== null) { - console.log("BRANCH: ", foundBranch) const source = foundBranch.data("source") const target = foundBranch.data("target") var branchdata = JSON.parse(JSON.stringify(foundBranch.data())) - console.log("BEFORE: ", branchdata) - console.log("Start node", workflow.start) const startNode = workflow.start if (source === startNode) { toast("Can't point to Start Node") } else { + setLastSaved(false) const newid = uuidv4() branchdata.source = target branchdata.target = source @@ -19229,27 +19858,25 @@ const AngularWorkflow = (defaultprops) => { const cytoscapeViewWidths = isMobile ? 50 : 950; const bottomBarStyle = { position: "absolute", - transition: "all 0.3s ease", - minWidth: windowWidth < 1600 && leftSideBarOpenByClick ? 800 : cytoscapeViewWidths, - maxWidth: windowWidth < 1600 && leftSideBarOpenByClick ? 800 : cytoscapeViewWidths, - marginLeft: 20, - marginBottom: 30, + left: `${leftBarSize + (cytoscapeWidth / 2)}px`, + transform: "translateX(-50%)", + bottom: 20, zIndex: 10, - transform: isMobile - ? `translateX(20px)` - : `translateX(${leftBarSize}px)`, - top: isMobile ? appBarSize + 55 : undefined, - bottom: isMobile ? undefined : 0, + display: "flex", + justifyContent: "center", + alignItems: "center", }; const topBarStyle = { position: "absolute", - - top: isMobile ? 30 : isSafari ? 15 : 25, - transform: isMobile ? "translateX(20px)" : `translateX(${leftBarSize}px)`, + top: 10, + transform: `translateX(${leftBarSize}px)`, + width: bodyWidth - leftBarSize + 25, transition: "all 0.3s ease", + paddingLeft : 30, zoom: isSafari ? undefined : 0.9, - } + overflow: "hidden", + } const changeOrg = () => { @@ -19298,7 +19925,7 @@ const AngularWorkflow = (defaultprops) => { }, 2000); } - toast.info("Successfully changed active organisation - refreshing!"); + toast.success("Successfully changed active organisation - refreshing!"); } else { if (responseJson.reason !== undefined && responseJson.reason !== null && responseJson.reason.length > 0) { toast(responseJson.reason); @@ -19316,6 +19943,38 @@ const AngularWorkflow = (defaultprops) => { const TopCytoscapeBar = (props) => { const [hovered, setHovered] = useState(false) + const toggleButtonSx = { + textTransform: "none", + backgroundColor: "transparent", + color: theme.palette.text.primary, + height: 36, + minWidth: 90, + padding: "8px 44px", + display: "flex", + alignItems: "center", + gap: 1, + fontSize: "16px", + fontWeight: 400, + boxShadow: "none", + fontFamily: theme?.typography?.fontFamily, + "&.Mui-selected": { + backgroundColor: themeMode === "dark" ? "#1e1e1e" : "#CCCCCC", + color: theme.palette.text.primary, + fontWeight: 600, + "&:hover": { + backgroundColor: themeMode === "dark" ? "rgba(0,0,0,0.3)" : "rgba(0,0,0,0.1)", + }, + }, + "&:hover": { + backgroundColor: themeMode === "dark" ? "rgba(255,255,255,0.05)" : "rgba(0,0,0,0.05)", + }, + } + + const toggleOptions = [ + { value: "build", label: "Build", icon: "/icons/workflow-page/buildIcon.svg", darkIcon: "/icons/workflow-page/buildIcon-dark.svg"}, + { value: "debug", label: "Debug", icon: "/icons/workflow-page/runningMan.svg", darkIcon: "/icons/workflow-page/run-dark.svg"}, + ] + if (workflow?.public === true) { return null } @@ -19328,678 +19987,761 @@ const AngularWorkflow = (defaultprops) => { return (
    +
    + {/* Top Row: Workflow Name | Build/Debug Toggle | Right Side Buttons */}
    - { - setHovered(true) + {/* Left: Workflow Name Container */} +
    + { - setHovered(false) - }} - onClick={() => { - setEditWorkflowModalOpen(true) - setLastSaved(false) - }} - > - {workflow?.name !== undefined && workflow?.name !== null && workflow?.name?.length > 0 ? - - : - null - } - {workflow.name} - - {workflowAsCode && ( + onMouseEnter={() => { + setHovered(true) + }} + onMouseLeave={() => { + setHovered(false) + }} + onClick={() => { + setEditWorkflowModalOpen(true) + setLastSaved(false) + }} + > + {workflow?.name !== undefined && workflow?.name !== null && workflow?.name?.length > 0 ? + + : + null + } + {workflow.name} + +
    + + {/* Center: Build/Debug Toggle */} + {workflow.public === true ? null : ( +
    + { + if (newMode !== null) { + setBuildDebugMode(newMode) + if (newMode === "debug") { + setExecutionModalOpen(true); + getWorkflowExecution(workflow.id, "", executionFilter, workflow.org_id) + } + } + }} + sx={{ + backgroundColor: themeMode === "dark" ? "rgba(255,255,255,0.08)" : "rgba(0,0,0,0.05)", + borderRadius: "8px", + padding: "3px", + border: "none", + "& .MuiToggleButtonGroup-grouped": { + border: "none", + "&:not(:first-of-type)": { + borderRadius: "6px", + borderTopLeftRadius: "0px", + borderBottomLeftRadius: "0px", + }, + "&:first-of-type": { + borderRadius: "6px", + borderTopRightRadius: "0px", + borderBottomRightRadius: "0px", + }, + }, + }} + > + {toggleOptions.map(({ value, label, icon, darkIcon }) => ( + + {label} + {label} + + ))} + +
    )} + + {/* Right: Buttons Container */} +
    + {originalWorkflow?.suborg_distribution === undefined || originalWorkflow?.suborg_distribution === null || originalWorkflow?.suborg_distribution?.length === 0 || originalWorkflow?.suborg_distribution.includes("none") ? + <> + {originalWorkflow?.parentorg_workflow !== undefined && originalWorkflow?.parentorg_workflow !== null && originalWorkflow?.parentorg_workflow?.length > 0 || workflow?.parentorg_workflow !== undefined && workflow?.parentorg_workflow !== null && workflow?.parentorg_workflow?.length > 0 ? + null + + /* + * Disabled because redirects occurred anyway + * + + */ + + : null} + + {userdata !== undefined && userdata !== null && userdata?.orgs !== undefined && userdata?.orgs !== null && userdata?.orgs?.length > 1 && workflow?.id !== undefined && workflow?.id && workflow?.id?.length > 0 && userdata?.active_org?.creator_org?.length === 0 && userdata?.active_org?.id == workflow?.org_id ? + + : null} + + : + + Save the workflow first + + : null} arrow placement="right"> + + + Select an Org ({originalWorkflow?.suborg_distribution?.length}) + + + + + } + + {showEnvironment === true && environments?.length > 0 ? + 0 && Object.getOwnPropertyNames(selectedActionEnvironment || {})?.length !== 0 && selectedActionEnvironment?.Name !== "Cloud" && savingState === 0 ? ( + +
    + 0} + onChange={(event) => { + toast("Opening in new tab. Refresh this page after adding it.") + + setTimeout(() => { + window.open(`/admin?tab=locations`, "_blank", "noopener,noreferrer") + }, 1500) + //selectedActionEnvironment.suborg_distribution = event.target.checked ? userdata.orgs.map((org) => org.id) : [] + //changeDistribution(selectedAction?.selectedAuthentication) + }} + name="distributeAuth" + color="primary" + /> + } + label={ + + Distribute location + + } + /> +
    +
    + ) : null + } placement="right"> + + + Runtime Location + + + +
    + : null} + + {workflowAsCode && ( - - )} - - {!distributedFromParent || userdata?.support === true ? - isCorrectOrg ? null : - - Warning: { - toast("Changing to correct organisation. Please wait a few seconds.") - - changeOrg() - - - - }} - >Change Active Organization to edit this Workflow. - - : - - suborgWorkflows?.length === 0 ? - - Warning: This workflow is controlled by your parent org and may not be editable. - - : - null - } - - {originalWorkflow?.suborg_distribution === undefined || originalWorkflow?.suborg_distribution === null || originalWorkflow?.suborg_distribution?.length === 0 || originalWorkflow?.suborg_distribution.includes("none") ? - -
    - {originalWorkflow?.parentorg_workflow !== undefined && originalWorkflow?.parentorg_workflow !== null && originalWorkflow?.parentorg_workflow?.length > 0 || workflow?.parentorg_workflow !== undefined && workflow?.parentorg_workflow !== null && workflow?.parentorg_workflow?.length > 0 ? - - null - - /* - * Disabled because redirects occurred anyway - - - */ - - : null} - - {userdata !== undefined && userdata !== null && userdata?.orgs !== undefined && userdata?.orgs !== null && userdata?.orgs?.length > 1 && workflow?.id !== undefined && workflow?.id && workflow?.id?.length > 0 && userdata?.active_org?.creator_org?.length === 0 && userdata?.active_org?.id == workflow?.org_id ? - - : null} -
    - : - - - Save the workflow first - - : null} arrow placement="right"> - - - - - Select an Org ({originalWorkflow?.suborg_distribution?.length}) - - - - - } -
    - - {showEnvironment === true && environments?.length > 0 && selectedActionEnvironment !== undefined && selectedActionEnvironment !== null && selectedActionEnvironment.Name !== undefined && selectedActionEnvironment.Name !== null ? - 0 && Object.getOwnPropertyNames(selectedActionEnvironment).length !== 0 && selectedActionEnvironment?.Name !== "Cloud" && savingState === 0 ? ( - -
    - 0} - onChange={(event) => { - toast("Opening in new tab. Refresh this page after adding it.") - - setTimeout(() => { - window.open(`/admin?tab=locations`, "_blank", "noopener,noreferrer") - }, 1500) - //selectedActionEnvironment.suborg_distribution = event.target.checked ? userdata.orgs.map((org) => org.id) : [] - //changeDistribution(selectedAction?.selectedAuthentication) - }} - name="distributeAuth" - color="primary" - /> - } - label={ - - Distribute location - - } - /> -
    -
    - ) : null - } placement="right"> - - - - - Runtime Location - - - -
    - : null} - - - {parentWorkflows === undefined || parentWorkflows === null || parentWorkflows.length === 0 ? null : - +
    + + {/* Warning Messages */} + {!distributedFromParent || userdata?.support === true ? + isCorrectOrg ? null : + + Warning: { + toast("Changing to correct organisation. Please wait a few seconds.") + changeOrg() + }} + >Change Active Organization to edit this Workflow. + + : + + suborgWorkflows?.length === 0 ? + + Warning: This workflow is controlled by your parent org and may not be editable. + + : + null } - +
    + + {parentWorkflows === undefined || parentWorkflows === null || parentWorkflows.length === 0 ? null : + + } +
    ); }; @@ -20648,303 +21390,472 @@ const AngularWorkflow = (defaultprops) => { return null; } - const buttonHeights = 45 - const boxSize = buttonHeights + const saveIcon = ( + + + + ); + + const toolbarHeight = 40; + const buttonSize = 40; + const borderRadius = "8px"; + + const iconSvgSize = 21; + + // Play/Stop button const executionButton = executionRunning ? ( - - - - - ) : ( - - - ) + + ) : ( + + + + ); return (
    -
    - - {executionButton} - { + setExecutionModalOpen(true); + getWorkflowExecution(workflow.id, "", executionFilter, workflow.org_id); + }} > - - - - { - setExecutionText(e.target.value); - }} - - // Start adornment - /> - - - - {/*userdata.avatar === creatorProfile.github_avatar ? null :*/} - + + + - - - - - - {workflow.public || userdata.support == true ? + ]) + } + } else { + console.log("No query to handle") + } + }} + sx={{ + height: toolbarHeight, + borderRadius: borderRadius, + fontFamily: theme?.typography?.fontFamily, + backgroundColor: !(lastSaved && !workflow.public) ? "rgba(255, 133, 68, 0.2)" : theme.palette.slateGrayColor, + color: "#ffffff", + fontSize: "12px", + textTransform: "none", + padding: "0px 14px", + marginLeft: "8px", + maxHeight: 35, + display: "flex", + alignItems: "center", + gap: "6px", + "& .MuiTypography-root": { + fontSize: "14px", + color: !(lastSaved && !workflow.public) ? "#ff8544" : theme.palette.text.primary + }, + "& .MuiSvgIcon-root": { + color: !(lastSaved && !workflow.public) ? "#ff8544" : theme.palette.text.primary + }, + "&:hover": { + backgroundColor: "rgba(255, 133, 68, 0.2)", + border: "0.5px solid #ff8544", + }, + "&:disabled": { + backgroundColor: "#494949", + color: "#9E9E9E", + "& .MuiSvgIcon-root": { + color: "#9E9E9E", + }, + "& .MuiTypography-root": { + color: "#9E9E9E", + }, + }, + }} + > + {savingState === 2 ? ( + + ) : savingState === 1 ? ( + + ) : ( + + {saveIcon} + + )} + + Save + + + + + + {/* Separator */} + + + + {/* Download */} - - - - - : null} - - {/* - - - - - - */} - - - - - + Download + + + + {/* Undo */} + + handleHistoryUndo(history)} + sx={{ + width: toolbarHeight, + height: toolbarHeight, + color: theme.palette.text.primary, + "&:hover": { + backgroundColor: "rgba(255, 255, 255, 0.05)", + }, + "&:disabled": { + color: "#9E9E9E", + }, + }} + > + Undo + + + {/* Fit to Screen */} + + cy.fit(null, 50)} + sx={{ + width: toolbarHeight, + height: toolbarHeight, + color: theme.palette.text.primary, + "&:hover": { + backgroundColor: "rgba(255, 255, 255, 0.05)", + }, + }} + > + Fit to Screen + + + + {/* Delete/Trash */} - - - + { + const selectedNode = cy.$(":selected"); + if (selectedNode.data() === undefined) { + return; + } + removeNode(selectedNode.data("id")); + }} + sx={{ + width: toolbarHeight, + height: toolbarHeight, + color: theme.palette.text.primary, + "&:hover": { + backgroundColor: "rgba(255, 255, 255, 0.05)", + }, + "&:disabled": { + color: "#9E9E9E", + }, + }} + > + Delete + - - - - + {/* Separator */} + + + {/* History */} + + setShowWorkflowRevisions(true)} + sx={{ + width: toolbarHeight, + height: toolbarHeight, + color: theme.palette.text.primary, + "&:hover": { + backgroundColor: "rgba(255, 255, 255, 0.05)", + }, + "&:disabled": { + color: "#9E9E9E", + }, + }} + > + History + + {/* Add */} + + addCommentNode()} + sx={{ + width: toolbarHeight, + height: toolbarHeight, + color: theme.palette.text.primary, + "&:hover": { + backgroundColor: "rgba(255, 255, 255, 0.05)", + }, + "&:disabled": { + color: "#9E9E9E", + }, + }} + > + Add Comment + + + + {/* WorkflowMenu - was conditionally rendered in old code */} {workflow.configuration !== null && workflow.configuration !== undefined && workflow.configuration.exit_on_error !== undefined ? ( @@ -20974,78 +21885,78 @@ const AngularWorkflow = (defaultprops) => { - */} + */} - - - - - + {userdata.support === true && ( + + )} - - {userdata?.support === true ? - - - - - - : null} - - {userdata.support == true || !isCloud ? + {userdata?.support === true && ( - - - + setupWorkflowYaml(workflow)} + sx={{ + width: toolbarHeight, + height: toolbarHeight, + color: theme.palette.text.primary, + "&:hover": { + backgroundColor: "rgba(255, 255, 255, 0.05)", + }, + "&:disabled": { + color: "#9E9E9E", + }, + }} + > + YAML + - : null} + )} + - - -
    + {/* AI Button with Gradient */} + {(userdata.support == true || !isCloud) && ( + + + + )} +
    ); }; @@ -21383,7 +22294,7 @@ const AngularWorkflow = (defaultprops) => { return ( {
    @@ -21644,6 +22557,8 @@ const AngularWorkflow = (defaultprops) => { minWidth: leftBarSize, maxWidth: leftBarSize, borderRight: "1px solid rgb(91, 96, 100)", + transition: "min-width 0.3s ease, max-width 0.3s ease", + overflow: "hidden", }} > @@ -21654,6 +22569,8 @@ const AngularWorkflow = (defaultprops) => { minWidth: leftBarSize, maxWidth: leftBarSize, borderRight: "1px solid rgb(91, 96, 100)", + transition: "min-width 0.3s ease, max-width 0.3s ease", + overflow: "hidden", }} >
    { return ( -
    +
    { onSelect={(select) => { HandleJsonCopy(validate.result, select, "exec"); }} - name={false} + name={"$exec"} />
    ) @@ -21787,7 +22704,26 @@ const AngularWorkflow = (defaultprops) => { ) } - if (execution.execution_source === "authgroups") { + if (execution.execution_source?.startsWith("datastore")) { + const iconMargin = 7 + return ( +
    + +
    + ) + } else if (execution.execution_source === "authgroups") { const iconMargin = 7 return (
    { open={executionModalOpen} onClose={() => { setExecutionModalOpen(false) - + setBuildDebugMode("build") //const newitem = removeParam("execution_id", cursearch); //navigate(curpath + newitem) }} @@ -22290,6 +23226,8 @@ const AngularWorkflow = (defaultprops) => { borderLeft: theme.palette.defaultBorder, borderRadius: theme.palette.borderRadius, + borderTopRightRadius: 0, + borderBottomRightRadius: 0, backgroundColor: themeMode === "dark" ? "black" : theme.palette.drawer.backgroundColor, }, }} @@ -22943,6 +23881,7 @@ const AngularWorkflow = (defaultprops) => {
    ) : null} + {executionData.execution_source !== undefined && executionData.execution_source !== null && executionData.execution_source.length > 0 && @@ -22954,8 +23893,17 @@ const AngularWorkflow = (defaultprops) => { - - {executionData.execution_source === "authgroups" || (executionData.authgroup !== undefined && executionData.authgroup !== null && executionData.authgroup.length > 0) ? + {executionData?.execution_source?.startsWith("datastore") ? + 2 ? "&category="+executionData.execution_source.split("|")[1]+"&key="+executionData.execution_source.split("|")[2] : ""}`} + target="_blank" + style={{ textDecoration: "none", color: theme.palette.linkColor }} + > + Datastore Automation + + : + executionData.execution_source === "authgroups" || (executionData.authgroup !== undefined && executionData.authgroup !== null && executionData.authgroup.length > 0) ? { {data?.action?.name === "run_schemaless" || data?.action?.name === "run_singul" || data?.action?.name === "singul" && data?.action?.parameters?.length > 4 ? : null} @@ -23614,7 +24576,7 @@ const AngularWorkflow = (defaultprops) => { HandleJsonCopy(showResult, select, data.action.label); console.log("SELECTED!: ", select); }} - name={"Results for " + data.action.label} + name={`$${data?.action?.label?.toLowerCase()}`} /> @@ -23627,19 +24589,23 @@ const AngularWorkflow = (defaultprops) => { whiteSpace: "pre-wrap", }} > - - Result  - - - {data.result} - + {validate?.result?.length > 0 ? +
    + + Result  + + + {data.result} + +
    + : null}
    )}
    @@ -24108,7 +25074,11 @@ const AngularWorkflow = (defaultprops) => { /> ) : (
    - Result + {validate?.result?.length > 0 ? + + Result + + : null}
    { > {/*isMobile ? null : leftView*/} {leftView} + + {userdata.support === true && ( +
    +
    { + console.log("Toggle sidebar!") + setLeftBarSize(leftViewOpen ? 0 : 235) + setLeftViewOpen(!leftViewOpen) + }} + style={{ + position: "absolute", + left: 0, + top: "50%", + transform: "translateY(-50%)", + zIndex: 1000, + backgroundColor: theme.palette.surfaceColor, + border: `1px solid ${theme.palette.defaultBorder}`, + borderLeft: "none", + borderTopRightRadius: "8px", + borderBottomRightRadius:"8px", + borderTopLeftRadius: "0", + borderBottomLeftRadius: "0", + width: leftViewOpen ? 24 : 32, + height: 48, + display: "flex", + alignItems: "center", + justifyContent: "center", + transition: "left 0.3s ease, width 0.3s ease, border-radius 0.3s ease", + cursor: "pointer", + }} + > + {leftViewOpen ? + : + + } +
    +
    + )} {workflow.id === undefined || workflow.id === null || appsLoaded === false ? (
    { style={{ width: workflow?.public && !isLoggedIn ? cytoscapeWidth + 50 : cytoscapeWidth, height: workflow?.public && !isLoggedIn ? (bodyHeight - 50) : bodyHeight - 2, - backgroundColor: theme.palette.surfaceColor, + backgroundColor: theme.palette.cytoscapeBackgroundColor, }} stylesheet={cystyle} boxSelectionEnabled={true} @@ -24266,7 +25274,101 @@ const AngularWorkflow = (defaultprops) => { {executionModal} - { + + {/* New parsedaction in new release */} + 0 && + rightSideBarOpen && + Object.getOwnPropertyNames(selectedTrigger || {}).length === 0 && + Object.getOwnPropertyNames(selectedComment || {}).length === 0 + } + hideBackdrop={true} + transitionDuration={{ enter: 180, exit: 0 }} + onClose={() => { + setRightSideBarOpen(false); + }} + ModalProps={{ + keepMounted: true, + }} + sx={{ + pointerEvents: "none", + "& .MuiDrawer-paper": { + pointerEvents: "auto", + }, + }} + PaperProps={{ + style: { + width: rightsidebarStyle.width, + minWidth: rightsidebarStyle.minWidth, + maxWidth: rightsidebarStyle.maxWidth, + overflow: "hidden", + }, + }} + > + + + + + {/* old code just for backup */} + {/* { rightSideBarOpen && Object.getOwnPropertyNames(selectedAction).length > 0 ?
    @@ -24331,7 +25433,7 @@ const AngularWorkflow = (defaultprops) => {
    : null - } + } */} {/* Looks for triggers" */} {/* Only fixed the ones that require scrolling on a small screen */} {/* Most important: Actions. But these are a lot more complex */} @@ -24401,7 +25503,7 @@ const AngularWorkflow = (defaultprops) => { {shownErrors} - + {/* */} }
    diff --git a/frontend/src/views/AppCreator.jsx b/frontend/src/views/AppCreator.jsx index bb08e468..6ff6793b 100755 --- a/frontend/src/views/AppCreator.jsx +++ b/frontend/src/views/AppCreator.jsx @@ -563,7 +563,7 @@ const AppCreator = (defaultprops) => { }; - const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (import.meta.env.VITE_IS_SSR === "true"); + const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; useEffect(() => { if (window.location.pathname.includes("apps/edit")) { @@ -699,20 +699,20 @@ const AppCreator = (defaultprops) => { // This is the data FROM the database, not what's being saved const parseIncomingOpenapiData = (data) => { - var parsedDecoded = "" - try { - const decoded = base64_decode(data.openapi) - parsedDecoded = decoded + var parsedDecoded = "" + try { + const decoded = base64_decode(data.openapi) + parsedDecoded = decoded } catch (e) { - console.log("Failed JSON parsing: ", e) - parsedDecoded = data - } + console.log("Failed JSON parsing: ", e) + parsedDecoded = data + } - if (data.openapi === null) { - toast("Failed to load OpenAPI for app. Please contact support if this persists.") - setIsAppLoaded(true); - return - } + if (data.openapi === null) { + toast("Failed to load OpenAPI for app. Please contact support if this persists.") + setIsAppLoaded(true); + return + } //console.log("Decoded: ", parsedDecoded) const parsedapp = @@ -791,6 +791,9 @@ const AppCreator = (defaultprops) => { } if (data.info["x-categories"] !== undefined && data.info["x-categories"].length > 0) { + if (typeof data.info["x-categories"] === "array") { + } else { + } setNewWorkflowCategories(data.info["x-categories"]); } } @@ -913,36 +916,41 @@ const AppCreator = (defaultprops) => { body: "", errors: [], example_response: "", - action_label: "No Label", + action_label: ["No Label"], required_bodyfields: [], }; if (methodvalue["x-label"] !== undefined && methodvalue["x-label"] !== null) { - console.log("LABEL: ", methodvalue["x-label"]) + const labels = typeof methodvalue["x-label"] === 'object' || Array.isArray(methodvalue["x-label"]) ? methodvalue["x-label"] : (typeof methodvalue["x-label"] === 'string' ? methodvalue["x-label"].split(",") : []) - var correctlabel = "" - const labels = methodvalue["x-label"].split(",") + var correctlabels = [] + console.log("LABEL1: ", methodvalue["x-label"]) for (let labelkey in labels) { - var label = labels[labelkey].trim() - if (label.toLowerCase() === "no label") { + var label = labels[labelkey]?.trim() + if (label?.toLowerCase() === "no label") { continue } // Remove quotes and escapes - label = label.replace(/['"]+/g, '') - label = label.replace(/\\/g, '') + label = label?.replace(/['"]+/g, '') + label = label?.replace(/\\/g, '') - //label = label.replace("_", " ", -1) - //label = label.charAt(0).toUpperCase() + label.slice(1) + // Uppercase first + if (label === undefined || label === null || label?.length === 0 || label?.toLowerCase().replaceAll(" ", "_") === "no_label") { + label = "No Label" + } - correctlabel = label - break + label = label?.replace("_", " ") + label = label?.charAt(0).toUpperCase() + label?.slice(1) + + correctlabels?.push(label) } - console.log("LABEL: ", correctlabel) + console.log("LABEL: ", correctlabels) + // FIX: Map labels only if they're actually in the category list //newaction.action_label = methodvalue["x-label"] - newaction.action_label = correctlabel + newaction.action_label = correctlabels } if (methodvalue["x-required-fields"] !== undefined && methodvalue["x-required-fields"] !== null) { @@ -1678,7 +1686,7 @@ const AppCreator = (defaultprops) => { } } - //newaction.action_label = "No Label" + //newaction.action_label = ["No Label"] newActions.push(newaction); } } @@ -1961,9 +1969,9 @@ const AppCreator = (defaultprops) => { setProjectCategories(all_categories); // Rearrange them by which has action_label - const firstActions = newActions.filter(data => data.action_label !== undefined && data.action_label !== null && data.action_label !== "No Label") + const firstActions = newActions.filter(data => data.action_label !== undefined && data.action_label !== null && data.action_label[0] !== "No Label") console.log("First actions: ", firstActions) - const secondActions = newActions.filter(data => data.action_label === undefined || data.action_label === null || data.action_label === "No Label") + const secondActions = newActions.filter(data => data.action_label === undefined || data.action_label === null || data.action_label[0] === "No Label") newActions = firstActions.concat(secondActions) setActions(newActions); //data.paths[item.url][item.method.toLowerCase()]["x-label"] = item.action_label @@ -2114,17 +2122,16 @@ const AppCreator = (defaultprops) => { }, }; - if (item.action_label !== undefined && item.action_label !== "" && item.action_label !== "No Label") { - //console.log("Action label: ", item.action_label) - data.paths[item.url][item.method.toLowerCase()]["x-label"] = item.action_label - } + if (item.action_label !== undefined && item.action_label !== "" && item.action_label[0] !== "No Label") { + data.paths[item.url][item.method.toLowerCase()]["x-label"] = item.action_label + } - if (item.required_bodyfields !== undefined && item.required_bodyfields !== null && item.required_bodyfields.length > 0) { - console.log("Required bodyfields: ", item.required_bodyfields) - data.paths[item.url][item.method.toLowerCase()]["x-required-fields"] = item.required_bodyfields - } + if (item.required_bodyfields !== undefined && item.required_bodyfields !== null && item.required_bodyfields.length > 0) { + console.log("Required bodyfields: ", item.required_bodyfields) + data.paths[item.url][item.method.toLowerCase()]["x-required-fields"] = item.required_bodyfields + } - //console.log("ACTION: ", item) + //console.log("ACTION: ", item) if (item.example_response !== undefined && item.example_response !== null && item.example_response.length > 0) { @@ -3448,7 +3455,7 @@ const AppCreator = (defaultprops) => { errors: [], example_response: "", method: actionNonBodyRequest[0], - action_label: "No Label", + action_label: ["No Label"], required_bodyfields: [], }); @@ -4450,6 +4457,31 @@ const AppCreator = (defaultprops) => { const url = data.url; const hasFile = (data["file_field"] !== undefined && data["file_field"] !== null && data["file_field"].length > 0) || data["example_response"] === "shuffle_file_download" + if (data?.action_label[0] !== 'No Label') { + console.log("Action label: ", data?.name, data?.action_label) + } + + var currentActionLabels = JSON.parse(JSON.stringify(actionLabels)) + for (var labelKey in data?.action_label) { + const label = data?.action_label[labelKey] + if (label.toLowerCase().replaceAll(" ", "_") === "no_label") { + continue + } + + var found = false + for (var acatKey in currentActionLabels) { + const acat = currentActionLabels[acatKey] + if (acat === label) { + found = true + break + } + } + + if (!found) { + console.log(`Not found: '${label}' in ${currentActionLabels}`) + currentActionLabels.push(data?.action_label[labelKey]) + } + } return ( @@ -4532,23 +4564,38 @@ const AppCreator = (defaultprops) => { */} {/* From 2023: Example of handling action labels */} - {actionLabels.length > 0 && newWorkflowCategories !== undefined && newWorkflowCategories !== null && newWorkflowCategories.length > 0 && categories.length > 0 ? + {currentActionLabels.length > 0 && newWorkflowCategories !== undefined && newWorkflowCategories !== null && newWorkflowCategories.length > 0 && categories.length > 0 ?