import React, { useEffect, useContext } from "react"; import theme from '../theme.jsx'; import { isMobile } from "react-device-detect" import { MuiChipsInput } from "mui-chips-input"; import { toast } from "react-toastify" import UsecaseSearch from "../components/UsecaseSearch.jsx" import WorkflowGrid from "../components/WorkflowGrid.jsx" import dayjs from 'dayjs'; import WorkflowTemplatePopup from "./WorkflowTemplatePopup.jsx"; import WorkflowValidationTimeline from "../components/WorkflowValidationTimeline.jsx" import { Badge, Avatar, Grid, InputLabel, Select, ListSubheader, Paper, Tooltip, Divider, Button, TextField, IconButton, Menu, MenuItem, Link, FormControlLabel, Chip, Switch, Typography, Zoom, CircularProgress, Drawer, Dialog, DialogTitle, DialogActions, DialogContent, OutlinedInput, Checkbox, ListItemText, Radio, RadioGroup, FormControl, FormLabel, Slider, } from "@mui/material"; import { DatePicker, LocalizationProvider, } from '@mui/x-date-pickers' import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs' import { useStyles } from '../views/AppCreator.jsx' import { ExpandLess as ExpandLessIcon, ExpandMore as ExpandMoreIcon, Publish as PublishIcon, OpenInNew as OpenInNewIcon, Add as AddIcon, Remove as RemoveIcon, EditNote as EditNoteIcon, } from "@mui/icons-material"; const EditWorkflow = (props) => { const { globalUrl, workflow, setWorkflow, modalOpen, setModalOpen, showUpload, usecases, setNewWorkflow, appFramework, isEditing, userdata, apps, saveWorkflow, expanded, scrollTo, setRealtimeMarkdown, boxWidth, setBoxWidth, } = props const [_, setUpdate] = React.useState(""); // Used for rendering, don't remove const [submitLoading, setSubmitLoading] = React.useState(false); const [showMoreClicked, setShowMoreClicked] = React.useState(isEditing !== false ? true : false); const [innerWorkflow, setInnerWorkflow] = React.useState(workflow) const [newWorkflowTags, setNewWorkflowTags] = React.useState(workflow.tags !== undefined && workflow.tags !== null ? JSON.parse(JSON.stringify(workflow.tags)) : []) const [description, setDescription] = React.useState(workflow.description !== undefined ? workflow.description : "") const [selectedUsecases, setSelectedUsecases] = React.useState(workflow.usecase_ids !== undefined && workflow.usecase_ids !== null ? JSON.parse(JSON.stringify(workflow.usecase_ids)) : []); const [foundWorkflowId, setFoundWorkflowId] = React.useState("") const [name, setName] = React.useState(workflow.name !== undefined ? workflow.name : "") const [workflowAsCode, setWorkflowAsCode] = React.useState(false) const [dueDate, setDueDate] = React.useState(workflow.due_date !== undefined && workflow.due_date !== null && workflow.due_date !== 0 ? dayjs(workflow.due_date * 1000) : dayjs().subtract(1, 'day')) const [inputQuestions, setInputQuestions] = React.useState(workflow.input_questions !== undefined && workflow.input_questions !== null ? JSON.parse(JSON.stringify(workflow.input_questions)) : []) const [inputMarkdown, setInputMarkdown] = React.useState(workflow?.form_control?.input_markdown !== undefined && workflow?.form_control?.input_markdown !== null ? workflow?.form_control?.input_markdown : "") const [scrollDone, setScrollDone] = React.useState(false) const [selectedYieldActions, setSelectedYieldActions] = React.useState(workflow?.form_control?.output_yields !== undefined && workflow?.form_control?.output_yields !== null ? JSON.parse(JSON.stringify(workflow?.form_control?.output_yields)) : []) const [selectedCleanupActions, setSelectedCleanupActions] = React.useState(workflow?.form_control?.cleanup_actions !== undefined && workflow?.form_control?.cleanup_actions !== null ? JSON.parse(JSON.stringify(workflow?.form_control?.cleanup_actions)) : []) const [formWidth, setFormWidth] = React.useState(boxWidth === undefined || boxWidth === null ? 500 : boxWidth) const classes = useStyles(); useEffect(() => { if (setBoxWidth !== undefined && boxWidth !== formWidth) { setBoxWidth(formWidth) } }, [formWidth]) if (scrollTo !== undefined && scrollTo !== null && scrollTo.length > 0 && scrollDone === false) { setTimeout(() => { const foundScroll = document.getElementById(scrollTo) if (foundScroll !== null) { // Smooth scroll foundScroll.scrollIntoView({ behavior: "smooth", }) } }, 200) setScrollDone(true) } // Gets the generated workflow const getGeneratedWorkflow = (workflow_id) => { const url = `${globalUrl}/api/v1/workflows/${workflow_id}` fetch(url, { method: "GET", headers: { "Content-Type": "application/json", Accept: "application/json", }, credentials: "include", }) .then((response) => { if (response.status !== 200) { console.log("Status not 200 when getting workflow"); } return response.json(); }) .then((responseJson) => { if (responseJson.id === workflow_id) { console.log("GOT WORKFLOW: ", responseJson) if (name === "") { innerWorkflow.name = responseJson.name setName(responseJson.name) } if (description === "") { innerWorkflow.description = responseJson.description setDescription(description) } if (newWorkflowTags === []) { innerWorkflow.tags = responseJson.tags setNewWorkflowTags(responseJson.tags) } if (selectedUsecases === []) { selectedUsecases = responseJson.usecase_ids } innerWorkflow.id = responseJson.id innerWorkflow.blogpost = responseJson.blogpost innerWorkflow.actions = responseJson.actions innerWorkflow.triggers = responseJson.triggers innerWorkflow.branches = responseJson.branches innerWorkflow.comments = responseJson.comments innerWorkflow.workflow_variables = responseJson.workflow_variables innerWorkflow.execution_variables = responseJson.execution_variables setInnerWorkflow(innerWorkflow) setUpdate(Math.random()) } }) .catch((error) => { //toast(error.toString()); console.log("Get workflow error: ", error.toString()); }) } if (foundWorkflowId.length > 0) { getGeneratedWorkflow(foundWorkflowId) setFoundWorkflowId("") } else { } if (modalOpen !== true) { return null } const newWorkflow = isEditing === true ? false : true const priority = userdata === undefined || userdata === null ? null : userdata.priorities.find(prio => prio.type === "usecase" && prio.active === true) var upload = ""; var total_count = 0 const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; return ( { setModalOpen(false); }} PaperProps={{ style: { color: "white", minWidth: isMobile ? "90%" : 650, maxWidth: isMobile ? "90%" : 650, minHeight: 400, paddingTop: 25, paddingLeft: 50, //minWidth: isMobile ? "90%" : newWorkflow === true ? 1000 : 550, //maxWidth: isMobile ? "90%" : newWorkflow === true ? 1000 : 550, borderRadius: theme.palette.borderRadius, backgroundColor: "black", }, }} >
{newWorkflow ? "New" : "Editing"} workflow {newWorkflow === true ? null : }
Workflows can be built from scratch, or from templates. Usecases can help you discover next steps, and you can search for them directly. Learn more
{showUpload === true ?
: null}
{/*newWorkflow === true ?
Use a Template Start your workflow from our templating system. This uses publied workflows from our Creators to generate full Usecases or parts of your Workflow.
: null*/}
{ setName(event.target.value) }} InputProps={{ style: { color: "white", }, }} color="primary" placeholder="Name" required margin="dense" defaultValue={innerWorkflow.name} label="Name" autoFocus fullWidth id="Enter-Workflow-Name" />
{usecases !== null && usecases !== undefined && usecases.length > 0 ? Usecases : null} { setNewWorkflowTags(chip); }} onBlur={(event) => { if (event.target.value.length === 0) { return } if (newWorkflowTags.includes(event.target.value)) { return } newWorkflowTags.push(event.target.value) setNewWorkflowTags(newWorkflowTags) setUpdate(Math.random()) }} onAdd={(chip) => { newWorkflowTags.push(chip) setNewWorkflowTags(newWorkflowTags) }} onDelete={(chip, index) => { console.log("Deleting: ", chip, index) newWorkflowTags.splice(index, 1) setNewWorkflowTags(newWorkflowTags) setUpdate(Math.random()) }} />
{showMoreClicked === true ?
{ setDescription(event.target.value) }} InputProps={{ style: { color: "white", }, }} multiline rows={3} color="primary" defaultValue={innerWorkflow.description} placeholder="Description" multiline label="Description" margin="dense" fullWidth />
Status { console.log("Data: ", e.target.value) //innerWorkflow.workflow_type = e.target.value innerWorkflow.status = e.target.value setInnerWorkflow(innerWorkflow) }} > } label="Test" /> } label="Staging" /> } label="Pre-production" /> } label="Production" />
{ innerWorkflow.default_return_value = event.target.value setInnerWorkflow(innerWorkflow) }} InputProps={{ style: { color: "white", }, }} color="primary" defaultValue={innerWorkflow.default_return_value} placeholder="Default return value (used for Subflows if the subflow fails)" rows="3" multiline label="Default return value" margin="dense" fullWidth /> Multi-Tenancy, Backups & Security Control mechanisms for multi-tenancy, backups, and security. Multi-Tenant Workflows Make one workflow, and keep a separate, synced copy in your other tenants. Control distributed auth, runtime locations, files, datastore keys etc. Can only distribute from parent org to child org. Need help trying it? Contact us for a demo {userdata !== undefined && userdata !== null && userdata.orgs !== undefined && userdata.orgs !== null && userdata.orgs.length > 0 ? userdata.orgs.filter(org => org.creator_org === userdata.active_org.id).length === 0 ? userdata.active_org.creator_org === undefined || userdata.active_org.creator_org === null || userdata.active_org.creator_org === "" ? Your organization does not have any suborgs yet OR your user may not have access to available suborgs. Please make one or get access to suborgs by another admin, then try again. : {innerWorkflow.parentorg_workflow !== undefined && innerWorkflow.parentorg_workflow !== null && innerWorkflow.parentorg_workflow.length > 0 ? This workflow is distributed from your parent workflow (you may not have access). : null}

You can only distribute to suborgs from a parent org.
: : Create a sub-org to distribute workflows to suborgs. } Git Backup Repository Decide where this workflow is backed up in a Git repository. Will create logs and notifications if upload fails. The repository and branch must already have been initialized. Files will show up in the root folder in the format 'orgid/workflow status/workflow id.json' and be formatted, with removed images, to make diffing easy. Overrides your default backup repository. Credentials are encrypted. Creates notifications if it fails. Workflow Backup Repository { //setUploadRepo(e.target.value); innerWorkflow.backup_config.upload_repo = e.target.value setInnerWorkflow(innerWorkflow) }} InputProps={{ classes: { notchedOutline: classes.notchedOutline, }, style: { color: "white", }, }} /> Branch { innerWorkflow.backup_config.upload_branch = e.target.value setInnerWorkflow(innerWorkflow) }} InputProps={{ classes: { notchedOutline: classes.notchedOutline, }, style: { color: "white", }, }} /> Username { innerWorkflow.backup_config.upload_username = e.target.value setInnerWorkflow(innerWorkflow) }} InputProps={{ classes: { notchedOutline: classes.notchedOutline, }, style: { color: "white", }, }} /> Git token/password { innerWorkflow.backup_config.upload_token = e.target.value setInnerWorkflow(innerWorkflow) }} InputProps={{ classes: { notchedOutline: classes.notchedOutline, }, style: { color: "white", }, }} type="password" />
Result cleanup ({selectedCleanupActions.length === 0 ? "No cleanup yet" : selectedCleanupActions.length === 1 ? "Cleaning up 1 node" : `Cleaning up ${selectedCleanupActions.length} nodes`}) Beta Feature: When a workflow run is done, the data from the selected actions will be removed by replacing it with a default value. This is useful for cleaning up sensitive data, or data that is no longer needed. This is done after a workflow run is finished or aborted, and is not reversible. Data will remain in the workflow run result (last node value) even if the action result itself is cleaned up.
Form Control Form Control is used to control how the Form for the workflow is shown to users. You can add input fields, markdown, and more. This is the first step in the workflow, and is required for all workflows. Input fields
Input fields are fields that will be used during the startup of the workflow. These will be formatted in JSON and is most commonly used from the Form page for this workflow. If chosen in the User Input node, these will be required fields. Use Semi-Colon ";" to create dropdown options. The first key will be the name shown, and subsequent keys will be the available values. {inputQuestions.map((data, index) => { var showListinfo = false if (data.value !== undefined && data.value !== null && data.value.length > 0) { if (data.value.includes(";")) { showListinfo = true } } return (
{ inputQuestions[index].name = e.target.value setInputQuestions(inputQuestions) setUpdate(Math.random()); }} InputProps={{ classes: { notchedOutline: classes.notchedOutline, }, }} /> { // Replace multiple semicolon with one e.target.value = e.target.value.replace(";;", ";") inputQuestions[index].value = e.target.value setInputQuestions(inputQuestions) setUpdate(Math.random()); }} InputProps={{ classes: { notchedOutline: classes.notchedOutline, }, }} />
) })}
Input Markdown Markdown will be shown on the Form page. The first image added will be used in your Form Toolbox list. Output for a Workflow is shown in Markdown, and is controlled by the LAST action that runs. Supports HTML. { //console.log("KEY: ", e.key) if (e.key === "Tab") { e.preventDefault() } }} onChange={(e) => { if (setRealtimeMarkdown !== undefined) { setRealtimeMarkdown(e.target.value) } setInputMarkdown(e.target.value) workflow.form_control.input_markdown = e.target.value setWorkflow(workflow) setUpdate(Math.random()) }} />
Form Size Control the width of the form. It will grow vertically as needed. { setFormWidth(value) }} />
Form Output Control ({selectedYieldActions.length === 0 ? "No Returns" : selectedYieldActions.length === 1 ? "Returning 1 node" : `Returning ${selectedYieldActions.length} nodes`}) When running this workflow as a form, the output will be shown as a Markdown object by default, with JSON objects being rendered. By adding nodes below, they will be shown while the workflow is running as soon as they get a result. Failing/Skipped nodes are not shown. This makes it possible to track progress for more complex usecases.
Publishing Publishing is related to making this workflow itself public. When publishing a workflow, all the details (except sensitive info) become available to anyone. The fields below will help a user and Shuffle's system understand your workflow better. When a workflow is published, you keep the original, and a copy enters the Shuffle workflow search, and is associated with your creator or partner account, if you have one. You can always unpublish the workflow after. When ready to publish, click the three dots next to a workflow on the main workflow page. You can always unpublish a workflow after. { setDueDate(newValue) }} /> Type { console.log("Data: ", e.target.value) innerWorkflow.workflow_type = e.target.value setInnerWorkflow(innerWorkflow) }} > } label="Agentic" /> } label="Trigger" /> } label="Subflow" /> } label="Standalone" /> { innerWorkflow.blogpost = event.target.value setInnerWorkflow(innerWorkflow) }} InputProps={{ style: { color: "white", }, }} color="primary" defaultValue={innerWorkflow.blogpost} placeholder="A blogpost or other reference for how this work workflow was built, and what it's for." rows="1" label="blogpost" margin="dense" fullWidth /> { innerWorkflow.video = event.target.value setInnerWorkflow(innerWorkflow) }} InputProps={{ style: { color: "white", }, }} color="primary" defaultValue={innerWorkflow.video} placeholder="A youtube or loom link to the video" rows="1" label="Video" margin="dense" fullWidth />
: null}
{newWorkflow === true ? Relevant Workflows {priority === null || priority === undefined ? null :
2 ? priority.description.split("&")[0] : ""} img1={priority.description.split("&").length > 2 ? priority.description.split("&")[1] : ""} dstapp={priority.description.split("&").length > 3 ? priority.description.split("&")[2] : ""} img2={priority.description.split("&").length > 3 ? priority.description.split("&")[3] : ""} title={priority.name} description={priority.description.split("&").length > 4 ? priority.description.split("&")[4] : ""} apps={apps} />
}
: null} {/*newWorkflow === true && name.length > 2 ?
: null*/} ) } export default EditWorkflow;