{
}}
>
Click one of the public apps below to Activate it for your organization.
{
console.log("CLICKED")
}}>
:
Apps need to be activated before they can be used. Search in the search bar from our 2500+ apps to activate them for.
}
) : apps.length > 0 ? (
{
console.log("Should load in extra apps?")
}}
>
Couldn't find the apps you were looking for? Searching unactivated apps. Click one of these apps to Activate it for your organisation.
{
console.log("CLICKED")
}}>
) : (
Loading Apps
)}
);
}
const getNextActionName = (appName) => {
var highest = "";
const allitems = workflow.actions.concat(workflow.triggers);
if (allitems !== undefined && allitems !== null) {
for (let itemkey in allitems) {
const item = allitems[itemkey];
if (item.app_name === appName && item.label !== undefined && item.label !== null) {
var number = item.label.split("_");
if (isNaN(number[-1]) && parseInt(number[number.length - 1]) > highest) {
highest = number[number.length - 1];
}
}
}
}
appName = appName.replaceAll(" ", "_")
if (highest) {
return appName + "_" + (parseInt(highest) + 1);
} else {
return appName + "_" + 1;
}
};
const setNewSelectedAction = (e) => {
if (selectedApp.actions === undefined || selectedApp.actions === null) {
return
}
if (selectedApp.actions.length === 1) {
// Find if there's a new app
const newApp = apps.find((app) => (app.name === selectedApp.name && app.app_version !== selectedApp.app_version) || app.id == selectedApp.id)
if (newApp !== undefined && newApp !== null) {
if (selectedApp.actions !== undefined && selectedApp.actions !== null && selectedApp.actions.length > 1) {
setSelectedApp(newApp)
}
selectedApp.actions = newApp.actions
}
}
const newaction = selectedApp.actions.find((a) => a.name === e.target.value)
if (newaction === undefined || newaction === null) {
toast(`Failed to find the action you selected. Please try again or contact ${supportEmail} if it persists.`);
return;
}
if (workflow.actions !== undefined && workflow.actions !== null) {
const foundInfo = workflow.actions.find(ac => ac.id === selectedAction.id)
}
// Setting an old reference just to use the same memory space elsewhere
// for selectedAction
const oldaction = JSON.parse(JSON.stringify(selectedAction))
// Does this one find the wrong one?
//var newSelectedAction = JSON.parse(JSON.stringify(selectedAction))
var newSelectedAction = selectedAction
newSelectedAction.name = newaction.name;
newSelectedAction.parameters = JSON.parse(JSON.stringify(newaction.parameters))
newSelectedAction.errors = [];
newSelectedAction.isValid = true;
newSelectedAction.is_valid = true;
newSelectedAction.required_body_fields = newaction.required_body_fields
// Simple action swap autocompleter
if (oldaction.parameters !== undefined && oldaction.parameters !== null && newSelectedAction.parameters !== undefined && oldaction.id === newSelectedAction.id) {
var fileid_found = false
for (let [paramkey, paramkeyval] in Object.entries(oldaction.parameters)) {
const param = oldaction.parameters[paramkey];
if (param.name === "file_id") {
fileid_found = true
}
if (param.value === null || param.value === undefined || param.value.length === 0) {
continue
}
if (param.name === "body") {
//console.log("Param: ", param)
continue
}
if (param.name === "headers") {
if (fileid_found) {
newSelectedAction.parameters[paramkey].value = ""
newSelectedAction.parameters[paramkey].autocompleted = true
continue
}
}
if (newSelectedAction.parameters === undefined || newSelectedAction.parameters === null) {
continue
}
// Not doing options fields
const newParamIndex = newSelectedAction.parameters.findIndex(paramdata => paramdata.name === param.name)
if (newParamIndex < 0) {
continue
}
if (newSelectedAction.parameters[newParamIndex].name === "headers") {
if (param.value !== undefined && param.value !== null && param.value.includes("=undefined")) {
if (newSelectedAction.parameters[newParamIndex].example !== undefined && newSelectedAction.parameters[newParamIndex].example !== null) {
newSelectedAction.parameters[newParamIndex].value = newSelectedAction.parameters[newParamIndex].example
} else {
newSelectedAction.parameters[newParamIndex].value = ""
}
continue
}
}
newSelectedAction.parameters[newParamIndex].value = param.value
newSelectedAction.parameters[newParamIndex].autocompleted = true
if (param.options !== undefined && param.options !== null && param.options.length > 0) {
newSelectedAction.parameters[newParamIndex].autocompleted = false
}
}
}
if (newSelectedAction.app_name === "Shuffle Tools") {
const iconInfo = GetIconInfo(newSelectedAction);
const iconViewBox = iconInfo.svgViewBox || `0 0 ${svgSize} ${svgSize}`;
const svg_pin = ``;
const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin);
newSelectedAction.large_image = svgpin_Url;
newSelectedAction.fillGradient = iconInfo.fillGradient;
newSelectedAction.fillstyle = "solid";
if (newSelectedAction.fillGradient !== undefined && newSelectedAction.fillGradient !== null && newSelectedAction.fillGradient.length > 0) {
newSelectedAction.fillstyle = "linear-gradient";
} else {
newSelectedAction.iconBackground = iconInfo.iconBackgroundColor;
}
const foundnode = cy.getElementById(newSelectedAction.id);
if (foundnode !== null && foundnode !== undefined) {
foundnode.data(newSelectedAction);
}
}
for (var preparamindex in newSelectedAction.parameters) {
const param = newSelectedAction.parameters[preparamindex]
if (param.configuration === true) {
continue
}
if (param?.value?.toLowerCase().includes("secret. replace")) {
newSelectedAction.parameters[preparamindex].value = ""
}
}
// Takes an action as input, then runs through and updates the relevant parameters based on previous actions' results (parent nodes)
// Further checks if those fields are already set in a previously used action
newSelectedAction = RunAutocompleter(newSelectedAction);
if (
newaction.return !== undefined &&
newaction.return !== null &&
newaction.returns.example !== undefined &&
newaction.returns.example !== null &&
newaction.returns.example.length > 0
) {
newSelectedAction.example = newaction.returns.example;
}
if (
newaction.description !== undefined &&
newaction.description !== null &&
newaction.description.length > 0
) {
newSelectedAction.description = newaction.description
}
// FIXME - this is broken sometimes lol
//var env = environments.find(a => a.Name === newaction.environment)
//if ((!env || env === undefined) && selectedAction.environment === undefined ) {
// env = environments[defaultEnvironmentIndex]
//}
//setSelectedActionEnvironment(env)
if (workflow.actions !== undefined && workflow.actions !== null && workflow.actions.length > 0) {
const foundActionIndex = workflow.actions.findIndex(actiondata => actiondata.id === newSelectedAction.id)
if (foundActionIndex >= 0) {
workflow.actions[foundActionIndex] = newSelectedAction
setWorkflow(workflow)
}
}
// Last fix for params
if (newSelectedAction.parameters !== undefined && newSelectedAction.parameters !== null && newSelectedAction.parameters.length > 0) {
for (let paramkey in newSelectedAction.parameters) {
const param = newSelectedAction.parameters[paramkey]
if (param.name !== "body") {
continue
}
if (param.example !== undefined && param.example !== null && param.example.length > 0) {
if (param.value === undefined || param.value === null || param.value.length === 0) {
param.value = param.example
}
}
newSelectedAction.parameters[paramkey] = param
}
}
setSelectedAction(newSelectedAction)
setUpdate(Math.random())
const allNodes = cy.nodes().jsons()
if (allNodes !== undefined && allNodes !== null) {
for (let nodekey in allNodes) {
const currentNode = allNodes[nodekey];
if (
currentNode.data.attachedTo === oldaction.id &&
currentNode.data.isDescriptor
) {
const foundnode = cy.getElementById(currentNode.data.id);
if (foundnode !== null && foundnode !== undefined) {
const iconInfo = GetIconInfo(newaction);
const svg_pin = ``;
const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin);
foundnode.data("image", svgpin_Url);
foundnode.data("imageColor", iconInfo.iconBackgroundColor);
}
break;
}
}
}
// Send it in here, after all fields are filled
// Disabled for now :(
// const aiMsg = "Fill based on previous values"
// aiSubmit(aiMsg, undefined, undefined, newSelectedAction)
};
// APPSELECT at top
// appname & version
// description
// ACTION select
const selectedNameChange = (appActionName) => {
if (appActionName === undefined || appActionName === null) {
return
}
appActionName = appActionName.replaceAll("(", "");
appActionName = appActionName.replaceAll(")", "");
appActionName = appActionName.replaceAll("]", "");
appActionName = appActionName.replaceAll("[", "");
appActionName = appActionName.replaceAll("{", "");
appActionName = appActionName.replaceAll("}", "");
appActionName = appActionName.replaceAll("*", "");
appActionName = appActionName.replaceAll("!", "");
appActionName = appActionName.replaceAll("@", "");
appActionName = appActionName.replaceAll("#", "");
appActionName = appActionName.replaceAll("$", "");
appActionName = appActionName.replaceAll("%", "");
appActionName = appActionName.replaceAll("&", "");
appActionName = appActionName.replaceAll("#", "");
appActionName = appActionName.replaceAll(".", "");
appActionName = appActionName.replaceAll(",", "");
appActionName = appActionName.replaceAll(" ", "_");
appActionName = appActionName.replaceAll("^", "_");
appActionName = appActionName.replaceAll("'", "_");
appActionName = appActionName.replaceAll("\"", "_");
appActionName = appActionName.replaceAll("\"", "_");
appActionName = appActionName.replaceAll(":", "_");
appActionName = appActionName.replaceAll(";", "_");
appActionName = appActionName.replaceAll("=", "_");
appActionName = appActionName.replaceAll("+", "_");
selectedAction.label = appActionName;
setSelectedAction(selectedAction);
};
const actionDelayChange = (delay) => {
if (isNaN(delay)) {
console.log("NAN: ", delay)
return
}
const parsedNumber = parseInt(delay)
if (parsedNumber > 86400) {
console.log("Max number is 1 day (86400)")
return
}
selectedAction.execution_delay = parsedNumber
setSelectedAction(selectedAction)
}
const selectedTriggerChange = (event) => {
selectedTrigger.label = event.target.value;
setSelectedTrigger(selectedTrigger);
workflow.triggers[selectedTriggerIndex].label = event.target.value;
setWorkflow(workflow);
};
// Starts on current node and climbs UP the tree to the root object.
// Sends back everything in it's path
// FIXME: Use the GetParentNodes in WorkflowValidationTimeline.jsx instead
const getParents = (action) => {
if (action === undefined || action === null) {
return []
}
var allkeys = [action.id];
var handled = [];
var results = [];
if (cy === undefined || cy === null) {
return []
}
// maxiter = max amount of parent nodes to loop
// also handles breaks if there are issues
var iterations = 0;
var maxiter = 100;
while (true) {
for (let parentkey in allkeys) {
var currentnode = cy.getElementById(allkeys[parentkey]);
if (currentnode === undefined || currentnode === null) {
continue;
}
if (currentnode.data() === undefined) {
handled.push(allkeys[parentkey]);
results.push({ id: allkeys[parentkey], type: "TRIGGER" });
} else {
if (handled.includes(currentnode.data().id)) {
continue
} else {
handled.push(currentnode.data().id);
results.push(currentnode.data());
}
}
// Get the name / label here too?
if (currentnode.length === 0) {
continue;
}
const incomingEdges = currentnode.incomers("edge");
if (incomingEdges.length === 0) {
continue;
}
for (let i = 0; i < incomingEdges.length; i++) {
var tmp = incomingEdges[i];
if (tmp.data("decorator")) {
continue
}
if (!allkeys.includes(tmp.data("source"))) {
allkeys.push(tmp.data("source"));
}
}
}
if (results.length === allkeys.length || iterations === maxiter) {
break;
}
iterations += 1;
}
// Remove on the end as we don't want to remove everything
results = results.filter((data) => data.id !== action.id)
results = results.filter((data) => data.type === "ACTION" || data.app_name === "Shuffle Workflow" || data.app_name === "User Input")
results.push({ label: "Runtime Argument", type: "INTERNAL" })
return results
}
// BOLD name: type: required?
// FORM
// Dropdown -> static, action, local env, global env
// VALUE (JSON)
// {data.name}, {data.description}, {data.required}, {data.schema.type}
//height: "100%",
const appApiViewStyle = {
display: "flex",
flexDirection: "column",
backgroundColor: theme.palette.DialogStyle.backgroundColor,
color: theme.palette.text.primary,
paddingRight: 15,
paddingLeft: 15,
minHeight: "100%",
zIndex: 1000,
resize: "vertical",
overflow: "auto",
paddingBottom: 100,
overflowAnchor: "none",
};
const minSize = 370
var rightsidebarStyle = {
position: "fixed",
top: workflow?.public && !isLoggedIn ? 100 : showWorkflowRevisions ? 100 : appBarSize - 35,
right: 25,
height: "90vh",
maxWidth: 600,
maxHeight: "100vh",
border: "1px solid rgb(91, 96, 100)",
zIndex: 1000,
borderRadius: theme.palette?.borderRadius,
resize: "both",
overflow: "auto",
overflowAnchor: "none",
minWidth: minSize,
width: isMobile ? "100%" : minSize,
zoom: isSafari ? undefined : 0.9,
transform: isSafari ? "scale(0.9)" : undefined,
transformOrigin: isSafari ? "top right" : undefined,
};
const setTriggerFolderWrapperMulti = (event) => {
const { options } = event.target;
var value = [];
for (let i = 0, l = options.length; i < l; i += 1) {
if (options[i].selected) {
value.push(options[i].value);
}
}
if (selectedTrigger.parameters === null) {
selectedTrigger.parameters = [[]];
workflow.triggers[selectedTriggerIndex].parameters = [[]];
}
// Max 1 folder for office for some reason. MailFolders('MAILBOX_ID') in resource
// Can't parse URL with multiple folders.
if (selectedTrigger.name === "Office365" & value !== undefined && value !== null && value.length > 1) {
toast("Max 1 folder at a time allowed for Office365")
console.log("VALUE: ", value)
value = [value[0]]
}
// This is a dirty workaround for the static values in the go backend and datastore db
const fixedValue = value.join(splitter);
selectedTrigger.parameters[0] = {
value: fixedValue,
name: "outlookfolder",
};
workflow.triggers[selectedTriggerIndex].parameters[0] = {
value: fixedValue,
name: "outlookfolder",
};
// This resets state for some reason (:
setSelectedAction({});
setSelectedTrigger({});
setSelectedApp({});
setSelectedEdge({});
// Set value
setSelectedTrigger(selectedTrigger);
setWorkflow(workflow);
};
const setTriggerCronWrapper = (value) => {
if (selectedTrigger.parameters === null) {
selectedTrigger.parameters = [];
}
selectedTrigger.parameters[0] = {
value: value,
name: "cron",
};
workflow.triggers[selectedTriggerIndex].parameters[0] = {
value: value,
name: "cron",
};
setWorkflow(workflow);
setSelectedTrigger(selectedTrigger);
};
const setTriggerOptionsWrapper = (value) => {
if (selectedTrigger.parameters === null || selectedTrigger.parameters === undefined) {
selectedTrigger.parameters = [
{ name: "", value: "" },
{ name: "", value: "" },
{ name: "", value: "" },
]
}
if (selectedTrigger.parameters.length < 3) {
selectedTrigger.parameters.push({ name: "", value: "" })
}
const splitItems = workflow.triggers[selectedTriggerIndex].parameters[2].value.split(",");
console.log(splitItems);
if (splitItems.includes(value)) {
for (let i = 0; i < splitItems.length; i++) {
if (splitItems[i] === value) {
splitItems.splice(i, 1);
}
}
} else {
splitItems.push(value);
}
for (let i = 0; i < splitItems.length; i++) {
if (splitItems[i] === "") {
splitItems.splice(i, 1);
}
}
workflow.triggers[selectedTriggerIndex].parameters[2].value = splitItems.join(",");
setWorkflow(workflow);
setLocalFirstrequest(!localFirstrequest);
setUpdate(Math.random());
};
const setTriggerTextInformationWrapper = (value) => {
if (selectedTrigger.parameters === null) {
selectedTrigger.parameters = [];
}
selectedTrigger.parameters[0] = {
value : value
}
workflow.triggers[selectedTriggerIndex].parameters[0] = {
value: value,
name: "alertinfo",
};
setSelectedTrigger(selectedTrigger)
setWorkflow(workflow);
};
const setTriggerBodyWrapper = (value) => {
if (selectedTrigger.parameters === null) {
selectedTrigger.parameters = [];
workflow.triggers[selectedTriggerIndex].parameters[0] = {
value: value,
name: "cron",
}
}
workflow.triggers[selectedTriggerIndex].parameters[1] = {
value: value,
name: "execution_argument",
};
setWorkflow(workflow);
};
const AppConditionHandler = (props) => {
const { tmpdata, type, setExpansionModalOpen, setActiveDialog, setEditorData, } = props;
const [data] = useState(tmpdata);
const [multiline, setMultiline] = useState(false);
const [showDropdown, setShowDropdown] = React.useState(false);
const [actionlist, setActionlist] = React.useState([]);
const [menuPosition, setMenuPosition] = React.useState(null);
if (tmpdata === undefined) {
return tmpdata;
}
if (data.variant === "") {
data.variant = "STATIC_VALUE";
}
// Set actions based on NEXT node, since it should be able to involve those two
if (actionlist.length === 0) {
// Base execution variables that should always be available
if (workflowExecutions.length > 0) {
for (let execution of workflowExecutions) {
const execArg = execution.execution_argument;
if (execArg && execArg.length > 0) {
const valid = validateJson(execArg);
if (valid.valid) {
actionlist.push({
type: "Runtime Argument",
name: "Runtime Argument",
value: "$exec",
highlight: "exec",
autocomplete: "exec",
example: valid.result,
})
break
}
}
}
}
// Add default Runtime Argument if none were added
if (actionlist.length === 0) {
actionlist.push({
type: "Runtime Argument",
name: "Runtime Argument",
value: "$exec",
highlight: "exec",
autocomplete: "exec",
example: "",
})
}
// Add Shuffle DB with cache keys if available
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 };
}
}
}
actionlist.push(cacheKey);
// Add workflow variables
if (workflow.workflow_variables?.length > 0) {
workflow.workflow_variables.forEach(item => {
actionlist.push({
type: "workflow_variable",
name: item.name,
value: item.value,
id: item.id,
autocomplete: `${item.name.split(" ").join("_")}`,
example: item.value,
})
})
}
// Add execution variables with examples from workflow executions
if (workflow.execution_variables?.length > 0) {
workflow.execution_variables.forEach(item => {
let exampleOutput = "";
if (workflowExecutions?.length > 0) {
for (let exec of workflowExecutions) {
const foundExec = exec.execution_variables?.find(exvar => exvar.name === item.name);
if (foundExec?.value) {
exampleOutput = foundExec.value;
break;
}
}
}
actionlist.push({
type: "execution_variable",
name: item.name,
value: item.value,
id: item.id,
autocomplete: `${item.name.split(" ").join("_")}`,
example: exampleOutput,
})
})
}
// Get parent nodes and their data
if (selectedEdge) {
const nodeId = selectedEdge.target;
const node = cy.getElementById(nodeId);
if (node?.length > 0) {
const parents = getParents(node.data())
// Add parent actions to the list
parents.forEach(item => {
if (item.label === "Runtime Argument") {
return
}
// Get example data from workflow executions if available
let exampleData = item.example ?? "";
if (!exampleData && workflowExecutions?.length > 0) {
for (let exec of workflowExecutions) {
const foundResult = exec.results?.find(result => result.action.id === item.id);
if (foundResult) {
const valid = validateJson(foundResult.result);
if (valid.valid && valid.result.success !== false) {
exampleData = valid.result;
break;
}
}
}
}
// Add action with its outputs
actionlist.push({
type: "action",
id: item.id,
name: item.label || "",
autocomplete: `${item?.label?.split(" ")?.join("_")}`,
example: exampleData,
})
// If the parent is a subflow, add its outputs
if (item.app_name === "Shuffle Workflow") {
const subflowOutputs = item.parameters?.find(param => param.name === "workflow_variables")?.value
if (subflowOutputs) {
try {
const outputs = JSON.parse(subflowOutputs)
outputs.forEach(output => {
actionlist.push({
type: "subflow_variable",
id: `${item.id}_${output.name}`,
name: `${item.label} - ${output.name}`,
value: output.value,
autocomplete: `${item.label.split(" ").join("_")}.${output.name}`,
example: output.value,
})
})
} catch (e) {
console.log("Failed to parse subflow outputs:", e)
}
}
}
})
}
}
setActionlist(actionlist);
}
if (
data.multiline !== undefined &&
data.multiline !== null &&
data.multiline === true
) {
setMultiline(true);
}
var placeholder = "Static value";
if (
data.example !== undefined &&
data.example !== null &&
data.example.length > 0
) {
placeholder = data.example;
}
var datafield = (
{
// Prevent the event from bubbling up
event.preventDefault();
event.stopPropagation();
const rect = event.currentTarget.getBoundingClientRect();
// Ensure we have valid numbers for positioning
const newPosition = {
top: rect.bottom + window.scrollY,
left: rect.left + window.scrollX,
};
// Only set state if we have valid coordinates
if (typeof newPosition.top === 'number' && typeof newPosition.left === 'number') {
setMenuPosition(newPosition);
setShowDropdown(true);
}
}}
>
{workflow.triggers[selectedTriggerIndex].parameters.length > 4 ?
{
if (selectedTrigger.parameters === null) {
selectedTrigger.parameters = [];
}
// Sets the webhook to run as version 2.. kinda
var value = "v2"
if (workflow.triggers[selectedTriggerIndex].parameters[4].value.includes("v2")) {
value = "v1"
}
workflow.triggers[selectedTriggerIndex].parameters[4] = {
name: "await_response",
value: value
}
setWorkflow(workflow)
setUpdate(Math.random())
}}
color="primary"
value="await_response"
/>
}
label={
{/* 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 :
);
//return null;
};
const unPublishWorkflow = (data) => {
data.id = props.match.params.key
if (!isCloud) {
toast("Function only supported on cloud")
return
}
if (data.public !== true) {
toast("Workflow is not public. Can't unpublish");
return
}
// This ALWAYS talks to Shuffle cloud
data = JSON.parse(JSON.stringify(data));
const url = `${globalUrl}/api/v1/workflows/${props.match.params.key}/unpublish`;
fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify(data),
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for workflow publish :O!");
}
return response.json();
})
.then((responseJson) => {
if (responseJson.reason !== undefined) {
toast("Unpublishing: " + responseJson.reason)
}
if (responseJson.success === true) {
workflow.public = false
setWorkflow(workflow)
}
})
.catch((error) => {
toast("Failed publishing: is the workflow valid? Remember to save the workflow first.")
console.log(error.toString())
})
}
// This can execute a workflow with firestore. Used for test, as datastore is old and stuff
// Too much work to move everything over alone, so won't touch it for now
//
//
// A list used for FRONTEND handling of whether a public workflow
// should be change-able
const allowList = ["frikky", "m1nk-code", "DavidtheGoliath"]
// console.log(allowList, userdata.public_username)
const leftView = workflow.public === true ?
{workflow.name}
{workflow.validated === true ?
: null}
This workflow is public and {
saveWorkflow(workflow)
}}>must be saved or exported before use.
{Object.getOwnPropertyNames(creatorProfile).length !== 0 && creatorProfile.github_avatar !== undefined && creatorProfile.github_avatar !== null ?
You can see these buttons because you may have the correct access rights as a creator to help modify this workflow.
{userdata.support === true ?
Manual Verification: {workflow.validated === undefined || workflow.validated === null || workflow.validated === false ? "Not valided" : "Validated"}
)
} else if (execution.execution_source === "webhook") {
return (
trigger.trigger_type === "WEBHOOK")
.large_image
}
style={{
width: size,
height: size,
borderRadius: borderRadius,
}}
/>
);
} else if (execution.execution_source === "outlook") {
return (
trigger.trigger_type === "EMAIL")
.large_image
}
style={{
width: size,
height: size,
borderRadius: borderRadius,
}}
/>
);
} else if (execution.execution_source === "schedule") {
return (
trigger.trigger_type === "SCHEDULE")
.large_image
}
style={{
width: size,
height: size,
borderRadius: borderRadius,
}}
/>
);
} else if (execution.execution_source === "EMAIL") {
return (
trigger.trigger_type === "EMAIL")
.large_image
}
style={{
width: size,
height: size,
borderRadius: borderRadius,
}}
/>
);
} else if (execution.execution_source === "ShuffleGPT") {
return (
);
} else if (execution.execution_source === "pipeline") {
return (
trigger.trigger_type === "PIPELINE")
.large_image
}
style={{ width: size, height: size }}
/>
);
}
if (
execution.execution_parent !== null &&
execution.execution_parent !== undefined &&
execution.execution_parent.length > 0
) {
return (
trigger.trigger_type === "SUBFLOW")
.large_image
}
style={{
width: size,
height: size,
borderRadius: borderRadius,
}}
/>
);
}
return (
);
};
// Not used because of issue with state updates.
const ShowReactJsonField = (props) => {
const { validate, jsonValue, collapsed, label, autocomplete } = props
const [parsedCollapse, setParsedCollapse] = React.useState(collapsed)
const [open, setOpen] = React.useState(false);
const [anchorPosition, setAnchorPosition] = React.useState({
top: 750,
left: 16,
});
const isFirstRender = React.useRef(true)
useEffect(() => {
console.log("IN useeffectt " + autocomplete)
if (isFirstRender.current) {
isFirstRender.current = false;
console.log("IN useeffectt (2)" + collapsed)
return;
}
}, [])
/*
componentWillUpdate = (nextProps, nextState) => {
console.log(nextProps, nextState)
//nextState.value = nextProps.a + nextProps.b;
}
*/
const jsonRef = React.useRef()
return (
{
return collapseField(jsonField)
}}
iconStyle={theme.palette.jsonIconStyle}
collapseStringsAfterLength={theme.palette.jsonCollapseStringsAfterLength}
displayArrayKey={false}
enableClipboard={(copy) => {
handleReactJsonClipboard(copy);
}}
displayDataTypes={false}
onClick={(event) => {
const pos = {
top: event.screenX,
left: event.screenY,
}
setAnchorPosition(pos)
}}
onSelect={(select) => {
setOpen(true)
setTimeout(() => {
setOpen(false)
}, 2500)
//setAnchorPosition({
// top: 300,
// right: 300,
//})
//setAnchorEl(jsonRef.current)
HandleJsonCopy(jsonValue, select, autocomplete);
console.log("SELECTED!: ", select);
}}
name={label}
/>
{anchorPosition !== null ?
{
setAnchorPosition({
top: 750,
left: 16,
})
}}
disableRestoreFocus
>
Copying
: null}
)
}
const changeExecution = (data) => {
if ((data.result === undefined || data.result === null || data.result.length === 0) && data.status !== "FINISHED" && data.status !== "ABORTED") {
start()
setExecutionRunning(true)
setExecutionRequestStarted(false)
}
var checkStarted = false
if (data.results !== undefined && data.results !== null && data.results.length > 0) {
if (data.execution_argument !== undefined && data.execution_argument !== null && data.execution_argument.includes("too large")) {
setExecutionData({});
checkStarted = true
start();
setExecutionRunning(true);
setExecutionRequestStarted(false);
} else {
if (data.results !== undefined && data.results !== null) {
for (let resultkey in data.results) {
if (data.results[resultkey].status !== "SUCCESS") {
continue
}
if (data.results[resultkey].result.includes("too large")) {
setExecutionData({});
checkStarted = true
start();
setExecutionRunning(true);
setExecutionRequestStarted(false);
break
}
}
}
}
}
const cur_execution = {
execution_id: data.execution_id,
authorization: data.authorization,
}
setExecutionRequest(cur_execution)
setExecutionModalView(1)
if (!checkStarted) {
handleUpdateResults(data, cur_execution)
if (cy !== undefined && cy !== null) {
cy.elements().removeClass("success-highlight failure-highlight executing-highlight");
for (let actionKey in data.workflow.actions) {
var actionitem = data.workflow.actions[actionKey]
handleColoring(actionitem.id, "", actionitem.label)
}
for (let resultKey in data.results) {
var item = data.results[resultKey]
handleColoring(item.action.id, item.status, item.action.label)
}
}
setExecutionData(data)
}
}
// Should probably put this on the backend instead when notifications are made :))
const getErrorSuggestion = (result) => {
if (result === undefined || result === null) {
return ""
}
// Check if array with json inside to handle one item at a time~
if (typeof result === "object" && result.length !== undefined) {
if (result.length > 0) {
// Check type inside
if (typeof result[0] === "object") {
result = result[0]
}
}
}
if (result.success === true && result.status === 200) {
if (result.body !== undefined && result.body !== null) {
const stringbody = result.body.toString()
if ((stringbody.startsWith("{") && stringbody.endsWith("}")) || (stringbody.startsWith("[") && stringbody.endsWith("]"))) {
return ""
}
if (stringbody.length > 1000) {
return "Body looks to be big in a standard format. Consider using the 'To File' parameter to automatically make it into a file."
}
}
}
if (result.status === 429) {
return "Rate limit exceeded. Consider using a different API key or wait a bit before trying again."
}
if (result.status === 405) {
return `Method not allowed. Check the URL to ensure it has all the required parameters. If you keep getting a 405, please forward a screenshot of this to ${supportEmail}`
}
if (result.status === 415) {
return "Content-Type header missing or wrong. Please add the correct Content-Type header and save the workflow."
}
if (result.status === 401) {
return "Authentication failed (401). The URL or auth key is wrong. Check the body of the result for more information."
}
if (result.status === 403) {
return "Authorization failed (403). The API user most likely doesn't have the correct permissions. Check the body of the result for more information."
}
if (result.status === 404) {
return "The URL, or content of the URL is incorrect. Check it and try again."
}
if (result.status === 400) {
return "The queries or data sent to the API is most likely wrong (400). Check the body of the result for more information."
}
if (result.status === 200 || result.status === 201 || result.status === 204) {
return "It looks like the result was successful! If it didn't work, make sure to check if the body you are sending was correct."
}
// Validate and check for newlines
if (result.success !== false) {
var stringjson = result
const valid = validateJson(stringjson, true)
if (valid.valid === false) {
if (stringjson.startsWith("{") && stringjson.endsWith("}")) {
// Look for newline
if (stringjson.includes("\n") && !stringjson.includes("\n")) {
return "Looks like you have a newline problem. Consider using the | replace: '\n', '\\n' }} filter in Liquid."
} else {
return "The result looks like it should be JSON, but is invalid. Look for potential single quotes instead of double quotes, missing commas or newlines"
}
}
}
//return ""
}
try {
stringjson = JSON.stringify(result)
} catch (e) {
}
stringjson = stringjson.toLowerCase()
if (stringjson.includes("localhost")) {
return "You can't use localhost in apps. Use the external ip or url of the server instead"
}
if (stringjson.includes("manifest unknown")) {
return "The app's Docker Image is not available in the environment yet. Re-run the app to force a re-download of the app. If the problem persists, contact support"
}
if (stringjson.toLowerCase().includes("too many values to unpack")) {
return "This is a known error with old apps. Please rebuild the app. Contact support@shuffler.io if it persists after rebuild."
}
if (result.status !== 200 && result.url !== undefined && result.url !== null && typeof result.url === "string" && (result.url.includes("192.168") || result.url.includes("172.16") || result.url.includes("10.0"))) {
return "Consider whether your Orborus environment can connect to a local IP or not."
}
if (stringjson.includes("kms/")) {
return `KMS authentication most likely failed. Check your notifications for more details on this page: /admin?admin_tab=notifications. If you need help with KMS, please contact ${supportEmail}`
}
if (stringjson.includes("string indices must be integers")) {
return `String indices must be integers typically means you are getting a list, while you expected a dictionary. Check the Variable & Debug for more information.`
}
if (stringjson.includes("invalidurl")) {
// IF count of "http" is more than one, 1, it's prolly invalid
var additionalinfo = ""
if (stringjson.includes("http") && stringjson.match(/http/g).length > 1) {
additionalinfo = "You may be using multiple 'http' in the URL. "
}
return "The URL is invalid. Change the URL to a valid one, and try again. " + additionalinfo
}
if (stringjson.includes("result too large to handle")) {
return "Execution loading failed. Reload the execution by closing it and clicking it again"
}
if (isCloud && stringjson.toLowerCase().includes("timeout error")) {
return "Run this workflow in a local environment to increase the timeout. Go to https://shuffler.io/admin?tab=locations to create an environment to connect to"
}
if (stringjson.toLowerCase().includes("invalid header")) {
return "A header or authentication token in the app is invalid. Check the app's configuration"
}
if (stringjson.includes("connectionerror")) {
if (stringjson.includes("kms")) {
return `KMS authentication most likely failed (2). Check your notifications for more details on this page: /admin?admin_tab=notifications&kms=true. If you need help with KMS, please contact ${supportEmail}`
}
return "The URL is incorrect, or Shuffle can't reach it. Set up a Shuffle Environment in the same VLAN, or whitelist Shuffle's IPs."
}
return ""
}
const ShowCopyingTooltip = () => {
const [showCopying, setShowCopying] = React.useState(true)
if (!showCopying) {
return false
}
return (
)
}
var nonskippedResults = []
if (executionData.results !== undefined) {
const newSkipped = executionData.results.find((result) => result.status !== "SKIPPED")
if (newSkipped !== undefined) {
nonskippedResults = newSkipped
}
}
const envStatus = !(executionData.workflow !== undefined && executionData.workflow !== null && executionData.workflow.actions !== undefined && executionData.workflow.actions !== null && executionData.workflow.actions.length > 0) ? "loading" : "success"
var executionDelay = -75
const executionModal = (
{
setExecutionModalOpen(false)
setBuildDebugMode("build")
//const newitem = removeParam("execution_id", cursearch);
//navigate(curpath + newitem)
}}
style={{
resize: "both",
overflow: "auto",
}}
hideBackdrop={false}
variant="temporary"
BackdropProps={{
style: {
//backgroundColor: "transparent",
}
}}
PaperProps={{
style: {
resize: "both",
overflow: "auto",
minWidth: isMobile ? "100%" : 490,
maxWidth: isMobile ? "100%" : 490,
color: theme.palette.text.primary,
fontSize: 18,
borderLeft: theme.palette.defaultBorder,
borderRadius: theme.palette.borderRadius,
borderTopRightRadius: 0,
borderBottomRightRadius: 0,
backgroundColor: themeMode === "dark" ? "black" : theme.palette.drawer.backgroundColor,
},
}}
>
{isMobile ?
{
e.preventDefault();
setExecutionModalOpen(false)
}}
>
: null}
{executionModalView === 0 ? (
{
console.log(environments, defaultEnvironmentIndex, nonskippedResults)
}} />
{environments.length > 0 && defaultEnvironmentIndex < environments.length && nonskippedResults.length === 0 && environments[defaultEnvironmentIndex].Name !== "Cloud" ?
No results yet. Is Orborus running for the "{environments[defaultEnvironmentIndex].Name}" environment? Find out here. If the Workflow doesn't start within 30 seconds with Orborus running, contact support: {supportEmail}
No results yet. Is Orborus running for the "{environments[defaultEnvironmentIndex].Name}" environment? Find out here. If the Workflow doesn't start within 30 seconds with Orborus running, contact support: support@shuffler.io
: null}
{environments.length > 0 && defaultEnvironmentIndex < environments.length && nonskippedResults.length === 0 && environments[defaultEnvironmentIndex].Name !== "Cloud" ?
No results yet. Is Orborus running for the "{environments[defaultEnvironmentIndex].Name}" environment? Learn more. If the Workflow doesn't start within 30 seconds with Orborus running, contact support: {supportEmail}
:
null}
) : (
executionData.results.map((data, index) => {
if (executionData.results.length !== 1 && !showSkippedActions && (data.status === "SKIPPED")) {
return null;
}
const showRerun = new URLSearchParams(cursearch).get("rerun")
if (showRerun === "true") {
const showNode = new URLSearchParams(cursearch).get("node")
if (data.action.id !== showNode) {
return null
}
}
// FIXME: The latter replace doens't really work if ' is used in a string
var showResult = data.result.trim();
const validate = validateJson(showResult);
const curapp = apps.find(
(a) =>
a.name === data.action.app_name &&
a.app_version === data.action.app_version
);
const imgsize = 50;
const statusColor =
data.status === "FINISHED" || data.status === "SUCCESS"
? green
: data.status === "ABORTED" || data.status === "FAILURE"
? "red"
: yellow;
var imgSrc = curapp === undefined ? "" : curapp.large_image;
if (imgSrc.length === 0 && workflow.actions !== undefined && workflow.actions !== null) {
// Look for the node in the workflow
const action = workflow.actions.find(
(action) => action.id === data.action.id
)
if (action !== undefined && action !== null) {
imgSrc = action.large_image;
}
}
if ((imgSrc === undefined || imgSrc === null || imgSrc.length === 0) && cy !== undefined && cy !== null) {
const foundnode = cy.getElementById(data.action.id)
if (foundnode !== undefined && foundnode !== null && foundnode.length > 0) {
// FIXME: Find image from cytoscape action
} else {
for (let actionkey in workflow.actions) {
if (workflow.actions[actionkey].app_name === data.action.app_name || workflow.actions[actionkey].id === data.action.id || workflow.actions[actionkey].label === data.action.label || workflow.actions[actionkey].name === data.action.name) {
if (workflow.actions[actionkey].large_image !== undefined && workflow.actions[actionkey].large_image !== null && workflow.actions[actionkey].large_image.length > 0) {
imgSrc = workflow.actions[actionkey].large_image
break
}
}
}
}
}
var actionimg =
curapp === null ? null : (
{
if (isCloud) {
window.open(`/apps/${data?.action?.app_name}`, "_blank")
}
}}
/>
);
if (triggers.length > 2) {
if (data.action.app_name === "shuffle-subflow") {
const parsedImage = triggers[3].large_image;
actionimg = (
);
}
if (data?.action?.name === "User Input" || data?.action?.app_name === "User Input" || data?.action?.name === "run_userinput") {
actionimg = (
);
}
}
if (data.action.app_name === "Shuffle Tools" && data.action.id !== undefined && cy !== undefined) {
const nodedata = cy.getElementById(data.action.id).data();
//if (nodedata !== undefined && nodedata !== null && nodedata.fillstyle === "linear-gradient") {
if (nodedata !== undefined && nodedata !== null) {
var imgStyle = {
marginRight: 20,
width: imgsize,
height: imgsize,
border: `2px solid ${statusColor}`,
borderRadius: executionData.start === data.action.id ? 25 : 5,
background: `linear-gradient(to right, ${nodedata.fillGradient})`,
};
actionimg = (
);
} else {
//console.log("Node not found: ", nodedata)
actionimg = (
)
}
}
if (validate.valid && typeof validate.result === "string") {
validate.result = JSON.parse(validate.result);
}
if (validate.valid && typeof validate.result === "object") {
if (
validate.result.result !== undefined &&
validate.result.result !== null
) {
try {
validate.result.result = JSON.parse(validate.result.result);
} catch (e) {
//console.log("ERROR PARSING: ", e)
}
}
}
var similarActionsView = null
if (data.similar_actions !== undefined && data.similar_actions !== null) {
var minimumMatch = 85
var matching_executions = []
if (data.similar_actions !== undefined && data.similar_actions !== null) {
for (let [k, kval] in Object.entries(data.similar_actions)) {
if (data.similar_actions.hasOwnProperty(k)) {
if (data.similar_actions[k].similarity > minimumMatch) {
matching_executions.push(data.similar_actions[k].execution_id)
}
}
}
}
if (matching_executions.length !== 0) {
var parsed_url = matching_executions.join(",")
similarActionsView =
{
navigate(`?execution_highlight=${parsed_url}`)
}}
>
}
}
const chosenNodeId = new URLSearchParams(cursearch).get("node");
const highlightNode = chosenNodeId !== null && chosenNodeId !== undefined && chosenNodeId !== "" && chosenNodeId === data.action.id
var relevant_errors = []
if (data?.action?.parameters !== undefined && data?.action?.parameters !== null && data?.action?.parameters.length > 0) {
for (var i = 0; i < data.action.parameters.length; i++) {
// Specific error patterns in params
const param = data.action.parameters[i]
if (param?.name?.endsWith("_error") && (param?.name?.startsWith("shuffle_") || param?.name?.startsWith("liquid_"))) {
relevant_errors.push(param)
}
}
}
if (relevant_errors.length === 0) {
const foundError = getErrorSuggestion(validate.result)
if (foundError !== undefined && foundError !== null && foundError !== "") {
relevant_errors = [foundError]
}
}
return (
Action Logs
More log details for this action are not available without an onprem environment with the SHUFFLE_LOGS_DISABLED environment variable set to false: SHUFFLE_LOGS_DISABLED=false. Logs are enabled by default, except in scale mode.
)
}
var showlink = false
if (data.name.endsWith("-Url")) {
//data.name = data.name.toLowerCase().replaceAll("-", "_")
if (data.value.startsWith(", ")) {
data.value = data.value.substring(2)
}
if (data.value.startsWith("http") || (data.value.startsWith("/") && data.value.includes("?"))) {
showlink = true
}
}
return (
{data.value.length > 60 || checked.valid ?
{
if (!showVariable) {
setOpen(!open)
}
}}
>
{data.name}
{checked.valid ?
: null}
{showVariable ? data.value : null}
{
var copyText = document.getElementById("copy_element_shuffle");
if (copyText !== undefined && copyText !== null) {
const clipboard = navigator.clipboard;
if (clipboard === undefined) {
toast("Can only copy over HTTPS (port 3443)");
return;
}
navigator.clipboard.writeText(data.value);
copyText.select();
copyText.setSelectionRange(
0,
99999
); /* For mobile devices */
/* Copy the text inside the text field */
document.execCommand("copy");
toast("Copied to clipboard");
} else {
console.log("Couldn't find the copy field: ", copyText);
}
}}
edge="end"
>
:
{data.name}:
{showVariable ? data.value : null}
}
{open ?
checked.valid ?
{
return collapseField(jsonField)
}}
iconStyle={theme.palette.jsonIconStyle}
collapseStringsAfterLength={theme.palette.jsonCollapseStringsAfterLength}
displayArrayKey={false}
displayDataTypes={false}
name={"Parsed data for variable " + data.name}
/>
:
{
if (showlink) {
e.preventDefault()
e.stopPropagation()
window.open(data.value, "_blank")
}
}}
color={showlink ? "#ff8544" : "textSecondary"}
>
{data.value}
: null}
: 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 */}
{rightSideBarOpen && (selectedTrigger.trigger_type === "SCHEDULE" || selectedTrigger.trigger_type === "WEBHOOK" || selectedTrigger.trigger_type === "PIPELINE" || selectedTrigger.trigger_type === "USERINPUT" || selectedTrigger.trigger_type === "SUBFLOW") ?
);
const ExecutionVariableModal = (props) => {
const { variableInfo } = props
const [newVariableName, setNewVariableName] = React.useState(variableInfo.name !== undefined ? variableInfo.name : "");
const [newVariableDescription, setNewVariableDescription] = React.useState(variableInfo.description !== undefined ? variableInfo.description : "");
const [newVariableValue, setNewVariableValue] = React.useState(variableInfo.value !== undefined ? variableInfo.value : "");
if (!executionVariablesModalOpen) {
return null
}
return (
)
}
const VariablesModal = (props) => {
const { setVariableInfo, variableInfo } = props
const [newVariableName, setNewVariableName] = React.useState(variableInfo.name !== undefined ? variableInfo.name : "");
const [newVariableDescription, setNewVariableDescription] = React.useState(variableInfo.description !== undefined ? variableInfo.description : "");
const [newVariableValue, setNewVariableValue] = React.useState(variableInfo.value !== undefined ? variableInfo.value : "");
if (!variablesModalOpen) {
return null
}
return (
)
}
const AuthenticationData = (props) => {
const selectedApp = props.app;
const [authenticationOption, setAuthenticationOptions] = React.useState({
app: JSON.parse(JSON.stringify(selectedApp)),
fields: {},
label: "",
usage: [
{
workflow_id: workflow.id,
},
],
id: uuidv4(),
active: true,
});
if (
selectedApp.authentication === undefined ||
selectedApp.authentication.parameters === null ||
selectedApp.authentication.parameters === undefined ||
selectedApp.authentication.parameters.length === 0
) {
return null
/*
(
{selectedApp.name} does not require authentication
);
*/
}
authenticationOption.app.actions = [];
for (let paramkey in selectedApp.authentication.parameters) {
if (
authenticationOption.fields[
selectedApp.authentication.parameters[paramkey].name
] === undefined
) {
authenticationOption.fields[
selectedApp.authentication.parameters[paramkey].name
] = "";
}
}
const handleSubmitCheck = () => {
if (authenticationOption.label.length === 0) {
authenticationOption.label = `Auth for ${selectedApp.name}`;
}
// Automatically mapping fields that already exist (predefined).
// Warning if fields are NOT filled
for (let paramkey in selectedApp.authentication.parameters) {
if (authenticationOption.fields[selectedApp.authentication.parameters[paramkey].name].length === 0) {
if (
selectedApp.authentication.parameters[paramkey].value !== undefined &&
selectedApp.authentication.parameters[paramkey].value !== null &&
selectedApp.authentication.parameters[paramkey].value.length > 0
) {
authenticationOption.fields[
selectedApp.authentication.parameters[paramkey].name
] = selectedApp.authentication.parameters[paramkey].value;
} else {
if (
selectedApp.authentication.parameters[paramkey].schema.type === "bool"
) {
authenticationOption.fields[
selectedApp.authentication.parameters[paramkey].name
] = "false";
} else {
toast(
"Field " +
selectedApp.authentication.parameters[paramkey].name +
" can't be empty. If you want it empty, put space."
);
return;
}
}
}
}
selectedAction.authentication_id = authenticationOption.id;
selectedAction.selectedAuthentication = authenticationOption;
console.log("auth option 4: ", authenticationOption)
if (selectedAction.authentication === undefined || selectedAction.authentication === null) {
selectedAction.authentication = [authenticationOption]
} else {
try {
selectedAction.authentication.push(authenticationOption)
} catch (e) {
//console.log("Error: ", e)
}
}
setSelectedAction(selectedAction)
var newAuthOption = JSON.parse(JSON.stringify(authenticationOption));
var newFields = [];
var warningsent = false
for (let authkey in newAuthOption.fields) {
var value = newAuthOption.fields[authkey];
if (value?.toLowerCase().includes("secret. replace")) {
value = ""
if (authkey === "url") {
// Use default value of the url
const urlparam = selectedApp.authentication.parameters.find((data) => data.name === "url")
if (urlparam !== undefined && urlparam !== null) {
if (urlparam.example !== undefined && urlparam.example !== null && urlparam.example.length > 0) {
value = urlparam.example
}
}
} else {
if (!warningsent) {
warningsent = true
toast("Warning: As you didn't fill in all fields, be aware that the authentication may fail.")
}
}
}
newFields.push({
"key": authkey,
"value": value,
});
}
newAuthOption.fields = newFields
setNewAppAuth(newAuthOption)
if (configureWorkflowModalOpen) {
//setSelectedAction({})
}
setUpdate(authenticationOption.id)
}
if (authenticationOption.label === null || authenticationOption.label === undefined) {
authenticationOption.label = selectedApp.name + " authentication";
}
return (
Authentication for {selectedApp.name.replaceAll("_", " ", -1)}
What is app authentication?
These are required fields for authenticating with {selectedApp.name}
Label for you to remember {
authenticationOption.label = event.target.value;
}}
/>
{selectedApp.authentication.parameters.map((data, index) => {
// FIXME: Look for relevant fields in the action that may already be filled in with the same name
if (data.value === "" || data.value === null || data.value === undefined || data.name === "url") {
if (selectedAction !== undefined && selectedAction !== null && selectedAction.parameters !== undefined && selectedAction.parameters !== null) {
for (var fieldkey in selectedAction.parameters) {
const field = selectedAction.parameters[fieldkey]
if (field.name !== data.name) {
continue
}
if (field.value !== undefined && field.value !== null && field.value.length > 0) {
data.value = field.value
data.autocomplete = true
break
}
}
}
}
return (
Version History
Versions are stored for every change made, up to once per minute. When restoring a version, the changes will not take effect until you save the workflow.