@saltcorn/server 1.1.0-beta.9 → 1.1.1-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/auth/admin.js +1 -1
- package/auth/roleadmin.js +10 -2
- package/help/{Cordova Builder.tmd → Capacitor Builder.tmd } +1 -1
- package/help/Configuration keys.tmd +5 -0
- package/help/index.js +2 -0
- package/load_plugins.js +12 -5
- package/locales/en.json +30 -1
- package/locales/pl.json +19 -2
- package/markup/admin.js +7 -3
- package/package.json +9 -9
- package/public/codemirror.css +33 -0
- package/public/flatpickr-dark.css +795 -0
- package/public/gridedit.js +1 -1
- package/public/mermaid.min.js +1077 -792
- package/public/saltcorn-common.js +93 -44
- package/public/saltcorn.css +27 -6
- package/public/saltcorn.js +60 -22
- package/routes/actions.js +1041 -4
- package/routes/admin.js +91 -81
- package/routes/api.js +51 -0
- package/routes/config.js +39 -25
- package/routes/eventlog.js +41 -1
- package/routes/fields.js +8 -2
- package/routes/homepage.js +13 -3
- package/routes/list.js +17 -1
- package/routes/plugins.js +7 -2
- package/routes/registry.js +45 -3
- package/routes/tables.js +58 -20
- package/routes/tenant.js +10 -2
- package/routes/viewedit.js +8 -8
- package/wrapper.js +3 -1
package/routes/actions.js
CHANGED
|
@@ -17,8 +17,13 @@ const Trigger = require("@saltcorn/data/models/trigger");
|
|
|
17
17
|
const FieldRepeat = require("@saltcorn/data/models/fieldrepeat");
|
|
18
18
|
const { getTriggerList } = require("./common_lists");
|
|
19
19
|
const TagEntry = require("@saltcorn/data/models/tag_entry");
|
|
20
|
+
const WorkflowStep = require("@saltcorn/data/models/workflow_step");
|
|
21
|
+
const WorkflowRun = require("@saltcorn/data/models/workflow_run");
|
|
22
|
+
const WorkflowTrace = require("@saltcorn/data/models/workflow_trace");
|
|
20
23
|
const Tag = require("@saltcorn/data/models/tag");
|
|
21
24
|
const db = require("@saltcorn/data/db");
|
|
25
|
+
const MarkdownIt = require("markdown-it"),
|
|
26
|
+
md = new MarkdownIt();
|
|
22
27
|
|
|
23
28
|
/**
|
|
24
29
|
* @type {object}
|
|
@@ -29,7 +34,13 @@ const db = require("@saltcorn/data/db");
|
|
|
29
34
|
*/
|
|
30
35
|
const router = new Router();
|
|
31
36
|
module.exports = router;
|
|
32
|
-
const {
|
|
37
|
+
const {
|
|
38
|
+
renderForm,
|
|
39
|
+
link,
|
|
40
|
+
mkTable,
|
|
41
|
+
localeDateTime,
|
|
42
|
+
post_delete_btn,
|
|
43
|
+
} = require("@saltcorn/markup");
|
|
33
44
|
const Form = require("@saltcorn/data/models/form");
|
|
34
45
|
const {
|
|
35
46
|
div,
|
|
@@ -45,8 +56,13 @@ const {
|
|
|
45
56
|
td,
|
|
46
57
|
h6,
|
|
47
58
|
pre,
|
|
59
|
+
th,
|
|
48
60
|
text,
|
|
49
61
|
i,
|
|
62
|
+
ul,
|
|
63
|
+
li,
|
|
64
|
+
h2,
|
|
65
|
+
h4,
|
|
50
66
|
} = require("@saltcorn/markup/tags");
|
|
51
67
|
const Table = require("@saltcorn/data/models/table");
|
|
52
68
|
const { getActionConfigFields } = require("@saltcorn/data/plugin-helper");
|
|
@@ -154,11 +170,15 @@ const triggerForm = async (req, trigger) => {
|
|
|
154
170
|
.filter(([k, v]) => v.hasChannel)
|
|
155
171
|
.map(([k, v]) => k);
|
|
156
172
|
|
|
157
|
-
const allActions = Trigger.action_options({
|
|
173
|
+
const allActions = Trigger.action_options({
|
|
174
|
+
notRequireRow: false,
|
|
175
|
+
workflow: true,
|
|
176
|
+
});
|
|
158
177
|
const table_triggers = ["Insert", "Update", "Delete", "Validate"];
|
|
159
178
|
const action_options = {};
|
|
160
179
|
const actionsNotRequiringRow = Trigger.action_options({
|
|
161
180
|
notRequireRow: true,
|
|
181
|
+
workflow: true,
|
|
162
182
|
});
|
|
163
183
|
|
|
164
184
|
Trigger.when_options.forEach((t) => {
|
|
@@ -326,6 +346,11 @@ router.get(
|
|
|
326
346
|
isAdmin,
|
|
327
347
|
error_catcher(async (req, res) => {
|
|
328
348
|
const form = await triggerForm(req);
|
|
349
|
+
if (req.query.table) {
|
|
350
|
+
const table = Table.findOne({ name: req.query.table });
|
|
351
|
+
if (table) form.values.table_id = table.id;
|
|
352
|
+
}
|
|
353
|
+
|
|
329
354
|
send_events_page({
|
|
330
355
|
res,
|
|
331
356
|
req,
|
|
@@ -467,6 +492,471 @@ router.post(
|
|
|
467
492
|
})
|
|
468
493
|
);
|
|
469
494
|
|
|
495
|
+
function genWorkflowDiagram(steps) {
|
|
496
|
+
const stepNames = steps.map((s) => s.name);
|
|
497
|
+
const nodeLines = steps.map(
|
|
498
|
+
(s) => ` ${s.name}["\`**${s.name}**
|
|
499
|
+
${s.action_name}\`"]:::wfstep${s.id}`
|
|
500
|
+
);
|
|
501
|
+
|
|
502
|
+
nodeLines.unshift(` _Start@{ shape: circle, label: "Start" }`);
|
|
503
|
+
const linkLines = [];
|
|
504
|
+
let step_ix = 0;
|
|
505
|
+
for (const step of steps) {
|
|
506
|
+
if (step.initial_step) linkLines.push(` _Start --> ${step.name}`);
|
|
507
|
+
if (step.action_name === "ForLoop") {
|
|
508
|
+
linkLines.push(
|
|
509
|
+
` ${step.name} --> ${step.configuration.for_loop_step_name}`
|
|
510
|
+
);
|
|
511
|
+
} else if (stepNames.includes(step.next_step)) {
|
|
512
|
+
linkLines.push(
|
|
513
|
+
` ${step.name}-- <i class="fas fa-plus add-btw-nodes btw-nodes-${step.id}-${step.next_step}"></i> ---${step.next_step}`
|
|
514
|
+
);
|
|
515
|
+
} else if (step.next_step) {
|
|
516
|
+
let found = false;
|
|
517
|
+
for (const otherStep of stepNames)
|
|
518
|
+
if (step.next_step.includes(otherStep)) {
|
|
519
|
+
linkLines.push(` ${step.name} --> ${otherStep}`);
|
|
520
|
+
found = true;
|
|
521
|
+
}
|
|
522
|
+
if (!found) {
|
|
523
|
+
linkLines.push(
|
|
524
|
+
` ${step.name}-- <a href="/actions/stepedit/${step.trigger_id}/${step.id}">Error: missing next step in ${step.name}</a> ---_End_${step.name}`
|
|
525
|
+
);
|
|
526
|
+
nodeLines.push(
|
|
527
|
+
` _End_${step.name}:::wfadd${step.id}@{ shape: circle, label: "<i class='fas fa-plus with-link'></i>" }`
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
} else if (!step.next_step) {
|
|
531
|
+
linkLines.push(` ${step.name} --> _End_${step.name}`);
|
|
532
|
+
nodeLines.push(
|
|
533
|
+
` _End_${step.name}:::wfadd${step.id}@{ shape: circle, label: "<i class='fas fa-plus with-link'></i>" }`
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
if (step.action_name === "EndForLoop") {
|
|
537
|
+
// TODO this is not correct. improve.
|
|
538
|
+
let forStep;
|
|
539
|
+
for (let i = step_ix; i >= 0; i -= 1) {
|
|
540
|
+
if (steps[i].action_name === "ForLoop") {
|
|
541
|
+
forStep = steps[i];
|
|
542
|
+
break;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
if (forStep) linkLines.push(` ${step.name} --> ${forStep.name}`);
|
|
546
|
+
}
|
|
547
|
+
step_ix += 1;
|
|
548
|
+
}
|
|
549
|
+
if (!steps.length || !steps.find((s) => s.initial_step)) {
|
|
550
|
+
linkLines.push(` _Start --> _End`);
|
|
551
|
+
nodeLines.push(
|
|
552
|
+
` _End:::wfaddstart@{ shape: circle, label: "<i class='fas fa-plus with-link'></i>" }`
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
const fc =
|
|
556
|
+
"flowchart TD\n" + nodeLines.join("\n") + "\n" + linkLines.join("\n");
|
|
557
|
+
//console.log(fc);
|
|
558
|
+
|
|
559
|
+
return fc;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const getWorkflowConfig = async (req, id, table, trigger) => {
|
|
563
|
+
let steps = await WorkflowStep.find(
|
|
564
|
+
{ trigger_id: trigger.id },
|
|
565
|
+
{ orderBy: "id" }
|
|
566
|
+
);
|
|
567
|
+
const initial_step = steps.find((step) => step.initial_step);
|
|
568
|
+
if (initial_step)
|
|
569
|
+
steps = [initial_step, ...steps.filter((s) => !s.initial_step)];
|
|
570
|
+
const trigCfgForm = new Form({
|
|
571
|
+
action: addOnDoneRedirect(`/actions/configure/${id}`, req),
|
|
572
|
+
onChange: "saveAndContinue(this)",
|
|
573
|
+
noSubmitButton: true,
|
|
574
|
+
formStyle: "vert",
|
|
575
|
+
fields: [
|
|
576
|
+
{
|
|
577
|
+
name: "save_traces",
|
|
578
|
+
label: "Save step traces for each run",
|
|
579
|
+
type: "Bool",
|
|
580
|
+
},
|
|
581
|
+
],
|
|
582
|
+
});
|
|
583
|
+
trigCfgForm.values = trigger.configuration;
|
|
584
|
+
return (
|
|
585
|
+
pre({ class: "mermaid" }, genWorkflowDiagram(steps)) +
|
|
586
|
+
script(
|
|
587
|
+
{ defer: "defer" },
|
|
588
|
+
`function tryAddWFNodes() {
|
|
589
|
+
const ns = $("g.node");
|
|
590
|
+
if(!ns.length) setTimeout(tryAddWFNodes, 200)
|
|
591
|
+
else {
|
|
592
|
+
$("i.add-btw-nodes").on("click", (e)=>{
|
|
593
|
+
const $e = $(e.target || e);
|
|
594
|
+
const cls = $e.attr('class');
|
|
595
|
+
const idnext = cls.split(" ").find(c=>c.startsWith("btw-nodes-")).
|
|
596
|
+
substr(10);
|
|
597
|
+
const [idprev, nmnext] = idnext.split("-");
|
|
598
|
+
location.href = '/actions/stepedit/${trigger.id}?after_step='+idprev+'&before_step='+nmnext;
|
|
599
|
+
})
|
|
600
|
+
$("g.node").on("click", (e)=>{
|
|
601
|
+
const $e = $(e.target || e).closest("g.node")
|
|
602
|
+
const cls = $e.attr('class')
|
|
603
|
+
if(!cls) return;
|
|
604
|
+
//console.log(cls)
|
|
605
|
+
if(cls.includes("wfstep")) {
|
|
606
|
+
const id = cls.split(" ").find(c=>c.startsWith("wfstep")).
|
|
607
|
+
substr(6);
|
|
608
|
+
location.href = '/actions/stepedit/${trigger.id}/'+id;
|
|
609
|
+
}
|
|
610
|
+
if(cls.includes("wfaddstart")) {
|
|
611
|
+
location.href = '/actions/stepedit/${trigger.id}?initial_step=true';
|
|
612
|
+
} else if(cls.includes("wfadd")) {
|
|
613
|
+
const id = cls.split(" ").find(c=>c.startsWith("wfadd")).
|
|
614
|
+
substr(5);
|
|
615
|
+
location.href = '/actions/stepedit/${trigger.id}?after_step='+id;
|
|
616
|
+
}
|
|
617
|
+
//console.log($e.attr('class'), id)
|
|
618
|
+
})
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
window.addEventListener('DOMContentLoaded',tryAddWFNodes)`
|
|
622
|
+
) +
|
|
623
|
+
a(
|
|
624
|
+
{
|
|
625
|
+
href: `/actions/stepedit/${trigger.id}${
|
|
626
|
+
initial_step ? "" : "?initial_step=true"
|
|
627
|
+
}`,
|
|
628
|
+
class: "btn btn-secondary",
|
|
629
|
+
},
|
|
630
|
+
i({ class: "fas fa-plus me-2" }),
|
|
631
|
+
"Add step"
|
|
632
|
+
) +
|
|
633
|
+
a(
|
|
634
|
+
{
|
|
635
|
+
href: `/actions/runs/?trigger=${trigger.id}`,
|
|
636
|
+
class: "d-block",
|
|
637
|
+
},
|
|
638
|
+
"Show runs »"
|
|
639
|
+
) +
|
|
640
|
+
renderForm(trigCfgForm, req.csrfToken())
|
|
641
|
+
);
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
const jsIdentifierValidator = (s) => {
|
|
645
|
+
if (!s) return "An identifier is required";
|
|
646
|
+
if (s.includes(" ")) return "Spaces not allowd";
|
|
647
|
+
let badc = "'#:/\\@()[]{}\"!%^&*-+*~<>,.?|"
|
|
648
|
+
.split("")
|
|
649
|
+
.find((c) => s.includes(c));
|
|
650
|
+
|
|
651
|
+
if (badc) return `Character ${badc} not allowed`;
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
const getWorkflowStepForm = async (
|
|
655
|
+
trigger,
|
|
656
|
+
req,
|
|
657
|
+
step_id,
|
|
658
|
+
after_step,
|
|
659
|
+
before_step
|
|
660
|
+
) => {
|
|
661
|
+
const table = trigger.table_id ? Table.findOne(trigger.table_id) : null;
|
|
662
|
+
const actionExplainers = {};
|
|
663
|
+
|
|
664
|
+
let stateActions = getState().actions;
|
|
665
|
+
const stateActionKeys = Object.entries(stateActions)
|
|
666
|
+
.filter(([k, v]) => !v.disableInWorkflow)
|
|
667
|
+
.map(([k, v]) => k);
|
|
668
|
+
|
|
669
|
+
const actionConfigFields = [];
|
|
670
|
+
for (const [name, action] of Object.entries(stateActions)) {
|
|
671
|
+
if (!stateActionKeys.includes(name)) continue;
|
|
672
|
+
|
|
673
|
+
if (action.description) actionExplainers[name] = action.description;
|
|
674
|
+
|
|
675
|
+
try {
|
|
676
|
+
const cfgFields = await getActionConfigFields(action, table, {
|
|
677
|
+
mode: "workflow",
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
for (const field of cfgFields) {
|
|
681
|
+
let cfgFld;
|
|
682
|
+
if (field.isRepeat)
|
|
683
|
+
cfgFld = new FieldRepeat({
|
|
684
|
+
...field,
|
|
685
|
+
showIf: {
|
|
686
|
+
wf_action_name: name,
|
|
687
|
+
...(field.showIf || {}),
|
|
688
|
+
},
|
|
689
|
+
});
|
|
690
|
+
else
|
|
691
|
+
cfgFld = {
|
|
692
|
+
...field,
|
|
693
|
+
showIf: {
|
|
694
|
+
wf_action_name: name,
|
|
695
|
+
...(field.showIf || {}),
|
|
696
|
+
},
|
|
697
|
+
};
|
|
698
|
+
if (cfgFld.input_type === "code") cfgFld.input_type = "textarea";
|
|
699
|
+
actionConfigFields.push(cfgFld);
|
|
700
|
+
}
|
|
701
|
+
} catch {}
|
|
702
|
+
}
|
|
703
|
+
const actionsNotRequiringRow = Trigger.action_options({
|
|
704
|
+
notRequireRow: true,
|
|
705
|
+
noMultiStep: true,
|
|
706
|
+
builtInLabel: "Workflow Actions",
|
|
707
|
+
builtIns: [
|
|
708
|
+
"SetContext",
|
|
709
|
+
"TableQuery",
|
|
710
|
+
"Output",
|
|
711
|
+
"DataOutput",
|
|
712
|
+
"WaitUntil",
|
|
713
|
+
"WaitNextTick",
|
|
714
|
+
"UserForm",
|
|
715
|
+
],
|
|
716
|
+
forWorkflow: true,
|
|
717
|
+
});
|
|
718
|
+
const triggers = Trigger.find({
|
|
719
|
+
when_trigger: { or: ["API call", "Never"] },
|
|
720
|
+
});
|
|
721
|
+
triggers.forEach((tr) => {
|
|
722
|
+
if (tr.description) actionExplainers[tr.name] = tr.description;
|
|
723
|
+
});
|
|
724
|
+
actionExplainers.SetContext = "Set variables in the context";
|
|
725
|
+
actionExplainers.TableQuery = "Query a table into a variable in the context";
|
|
726
|
+
actionExplainers.Output =
|
|
727
|
+
"Display a message to the user. Pause workflow until the message is read.";
|
|
728
|
+
actionExplainers.DataOutput =
|
|
729
|
+
"Display a value to the user. Arrays of objects will be displayed as tables. Pause workflow until the message is read.";
|
|
730
|
+
actionExplainers.WaitUntil = "Pause until a time in the future";
|
|
731
|
+
actionExplainers.WaitNextTick =
|
|
732
|
+
"Pause until the next scheduler invocation (at most 5 minutes)";
|
|
733
|
+
actionExplainers.UserForm =
|
|
734
|
+
"Ask a user one or more questions, pause until they are answered";
|
|
735
|
+
|
|
736
|
+
actionConfigFields.push({
|
|
737
|
+
label: "Form header",
|
|
738
|
+
sublabel: "Text shown to the user at the top of the form",
|
|
739
|
+
name: "form_header",
|
|
740
|
+
type: "String",
|
|
741
|
+
showIf: { wf_action_name: "UserForm" },
|
|
742
|
+
});
|
|
743
|
+
actionConfigFields.push({
|
|
744
|
+
label: "User ID",
|
|
745
|
+
name: "user_id_expression",
|
|
746
|
+
type: "String",
|
|
747
|
+
sublabel: "Optional. If blank assigned to user starting the workflow",
|
|
748
|
+
showIf: { wf_action_name: "UserForm" },
|
|
749
|
+
});
|
|
750
|
+
actionConfigFields.push({
|
|
751
|
+
label: "Resume at",
|
|
752
|
+
name: "resume_at",
|
|
753
|
+
sublabel:
|
|
754
|
+
"JavaScript expression for the time to resume. <code>moment</code> is in scope.",
|
|
755
|
+
type: "String",
|
|
756
|
+
showIf: { wf_action_name: "WaitUntil" },
|
|
757
|
+
});
|
|
758
|
+
actionConfigFields.push({
|
|
759
|
+
label: "Context values",
|
|
760
|
+
name: "ctx_values",
|
|
761
|
+
sublabel:
|
|
762
|
+
"JavaScript object expression for the variables to set. Example <code>{x: 5, y:y+1}</code> will set x to 5 and increment existing context variable y",
|
|
763
|
+
type: "String",
|
|
764
|
+
fieldview: "textarea",
|
|
765
|
+
class: "validate-expression",
|
|
766
|
+
default: "{}",
|
|
767
|
+
showIf: { wf_action_name: "SetContext" },
|
|
768
|
+
});
|
|
769
|
+
actionConfigFields.push({
|
|
770
|
+
label: "Output text",
|
|
771
|
+
name: "output_text",
|
|
772
|
+
sublabel:
|
|
773
|
+
"Message shown to the user. Can contain HTML tags and use interpolations {{ }} to access the context",
|
|
774
|
+
type: "String",
|
|
775
|
+
fieldview: "textarea",
|
|
776
|
+
showIf: { wf_action_name: "Output" },
|
|
777
|
+
});
|
|
778
|
+
actionConfigFields.push({
|
|
779
|
+
label: "Output expression",
|
|
780
|
+
name: "output_expr",
|
|
781
|
+
sublabel:
|
|
782
|
+
"JavaScript expression for the value to output. Typically the name of a variable",
|
|
783
|
+
type: "String",
|
|
784
|
+
class: "validate-expression",
|
|
785
|
+
showIf: { wf_action_name: "DataOutput" },
|
|
786
|
+
});
|
|
787
|
+
actionConfigFields.push({
|
|
788
|
+
label: "Markdown",
|
|
789
|
+
name: "markdown",
|
|
790
|
+
sublabel:
|
|
791
|
+
"The centents are markdown formatted and should be rendered to HTML",
|
|
792
|
+
type: "Bool",
|
|
793
|
+
showIf: { wf_action_name: "Output" },
|
|
794
|
+
});
|
|
795
|
+
actionConfigFields.push({
|
|
796
|
+
label: "Table",
|
|
797
|
+
name: "query_table",
|
|
798
|
+
type: "String",
|
|
799
|
+
required: true,
|
|
800
|
+
attributes: { options: (await Table.find()).map((t) => t.name) },
|
|
801
|
+
showIf: { wf_action_name: "TableQuery" },
|
|
802
|
+
});
|
|
803
|
+
actionConfigFields.push({
|
|
804
|
+
label: "Query",
|
|
805
|
+
name: "query_object",
|
|
806
|
+
sublabel: "Where object, example <code>{manager: 1}</code>",
|
|
807
|
+
type: "String",
|
|
808
|
+
required: true,
|
|
809
|
+
class: "validate-expression",
|
|
810
|
+
default: "{}",
|
|
811
|
+
showIf: { wf_action_name: "TableQuery" },
|
|
812
|
+
});
|
|
813
|
+
actionConfigFields.push({
|
|
814
|
+
label: "Variable",
|
|
815
|
+
name: "query_variable",
|
|
816
|
+
sublabel: "Context variable to write to query results to",
|
|
817
|
+
type: "String",
|
|
818
|
+
required: true,
|
|
819
|
+
validator: jsIdentifierValidator,
|
|
820
|
+
showIf: { wf_action_name: "TableQuery" },
|
|
821
|
+
});
|
|
822
|
+
actionConfigFields.push(
|
|
823
|
+
new FieldRepeat({
|
|
824
|
+
name: "user_form_questions",
|
|
825
|
+
showIf: { wf_action_name: "UserForm" },
|
|
826
|
+
fields: [
|
|
827
|
+
{
|
|
828
|
+
label: "Label",
|
|
829
|
+
name: "label",
|
|
830
|
+
type: "String",
|
|
831
|
+
sublabel:
|
|
832
|
+
"The text that will shown to the user above the input elements",
|
|
833
|
+
},
|
|
834
|
+
{
|
|
835
|
+
label: "Variable name",
|
|
836
|
+
name: "var_name",
|
|
837
|
+
type: "String",
|
|
838
|
+
sublabel:
|
|
839
|
+
"The answer will be set in the context with this variable name",
|
|
840
|
+
validator: jsIdentifierValidator,
|
|
841
|
+
},
|
|
842
|
+
{
|
|
843
|
+
label: "Input Type",
|
|
844
|
+
name: "qtype",
|
|
845
|
+
type: "String",
|
|
846
|
+
required: true,
|
|
847
|
+
attributes: {
|
|
848
|
+
options: [
|
|
849
|
+
"Yes/No",
|
|
850
|
+
"Checkbox",
|
|
851
|
+
"Free text",
|
|
852
|
+
"Multiple choice",
|
|
853
|
+
//"Multiple checks",
|
|
854
|
+
"Integer",
|
|
855
|
+
"Float",
|
|
856
|
+
//"File upload",
|
|
857
|
+
],
|
|
858
|
+
},
|
|
859
|
+
},
|
|
860
|
+
{
|
|
861
|
+
label: "Options",
|
|
862
|
+
name: "options",
|
|
863
|
+
type: "String",
|
|
864
|
+
sublabel: "Comma separated list of multiple choice options",
|
|
865
|
+
showIf: { qtype: ["Multiple choice", "Multiple checks"] },
|
|
866
|
+
},
|
|
867
|
+
],
|
|
868
|
+
})
|
|
869
|
+
);
|
|
870
|
+
|
|
871
|
+
const form = new Form({
|
|
872
|
+
action: addOnDoneRedirect(`/actions/stepedit/${trigger.id}`, req),
|
|
873
|
+
onChange: step_id ? "saveAndContinueIfValid(this)" : undefined,
|
|
874
|
+
submitLabel: step_id ? req.__("Done") : undefined,
|
|
875
|
+
additionalButtons: step_id
|
|
876
|
+
? [
|
|
877
|
+
{
|
|
878
|
+
label: req.__("Delete"),
|
|
879
|
+
class: "btn btn-outline-danger",
|
|
880
|
+
onclick: `ajax_post('/actions/delete-step/${+step_id}')`,
|
|
881
|
+
afterSave: true,
|
|
882
|
+
},
|
|
883
|
+
]
|
|
884
|
+
: undefined,
|
|
885
|
+
fields: [
|
|
886
|
+
{
|
|
887
|
+
input_type: "section_header",
|
|
888
|
+
label: req.__("Step settings"),
|
|
889
|
+
},
|
|
890
|
+
{
|
|
891
|
+
name: "wf_step_name",
|
|
892
|
+
label: req.__("Step name"),
|
|
893
|
+
type: "String",
|
|
894
|
+
required: true,
|
|
895
|
+
sublabel: "An identifier by which this step can be referred to.",
|
|
896
|
+
validator: jsIdentifierValidator,
|
|
897
|
+
},
|
|
898
|
+
{
|
|
899
|
+
name: "wf_initial_step",
|
|
900
|
+
label: req.__("Initial step"),
|
|
901
|
+
sublabel: "Is this the first step in the workflow?",
|
|
902
|
+
type: "Bool",
|
|
903
|
+
},
|
|
904
|
+
{
|
|
905
|
+
name: "wf_only_if",
|
|
906
|
+
label: req.__("Only if..."),
|
|
907
|
+
sublabel:
|
|
908
|
+
"Optional JavaScript expression based on the run context. If given, the chosen action will only be executed if evaluates to true",
|
|
909
|
+
type: "String",
|
|
910
|
+
},
|
|
911
|
+
{
|
|
912
|
+
name: "wf_next_step",
|
|
913
|
+
label: req.__("Next step"),
|
|
914
|
+
type: "String",
|
|
915
|
+
class: "validate-expression",
|
|
916
|
+
sublabel:
|
|
917
|
+
"Name of next step. Can be a JavaScript expression based on the run context. Blank if final step",
|
|
918
|
+
},
|
|
919
|
+
{
|
|
920
|
+
input_type: "section_header",
|
|
921
|
+
label: req.__("Action"),
|
|
922
|
+
},
|
|
923
|
+
{
|
|
924
|
+
name: "wf_action_name",
|
|
925
|
+
label: req.__("Action"),
|
|
926
|
+
type: "String",
|
|
927
|
+
required: true,
|
|
928
|
+
attributes: {
|
|
929
|
+
options: actionsNotRequiringRow,
|
|
930
|
+
explainers: actionExplainers,
|
|
931
|
+
},
|
|
932
|
+
},
|
|
933
|
+
{
|
|
934
|
+
input_type: "section_header",
|
|
935
|
+
label: req.__("Action settings"),
|
|
936
|
+
},
|
|
937
|
+
...actionConfigFields,
|
|
938
|
+
],
|
|
939
|
+
});
|
|
940
|
+
form.hidden("wf_step_id");
|
|
941
|
+
form.hidden("_after_step");
|
|
942
|
+
if (before_step) form.values.wf_next_step = before_step;
|
|
943
|
+
if (after_step) form.values._after_step = after_step;
|
|
944
|
+
if (step_id) {
|
|
945
|
+
const step = await WorkflowStep.findOne({ id: step_id });
|
|
946
|
+
if (!step) throw new Error("Step not found");
|
|
947
|
+
form.values = {
|
|
948
|
+
wf_step_id: step.id,
|
|
949
|
+
wf_step_name: step.name,
|
|
950
|
+
wf_initial_step: step.initial_step,
|
|
951
|
+
wf_only_if: step.only_if,
|
|
952
|
+
wf_action_name: step.action_name,
|
|
953
|
+
wf_next_step: step.next_step,
|
|
954
|
+
...step.configuration,
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
return form;
|
|
958
|
+
};
|
|
959
|
+
|
|
470
960
|
const getMultiStepForm = async (req, id, table) => {
|
|
471
961
|
let stateActions = getState().actions;
|
|
472
962
|
const stateActionKeys = Object.entries(stateActions)
|
|
@@ -579,7 +1069,35 @@ router.get(
|
|
|
579
1069
|
{ href: `/actions/testrun/${id}`, class: "ms-2" },
|
|
580
1070
|
req.__("Test run") + " »"
|
|
581
1071
|
);
|
|
582
|
-
if (trigger.action === "
|
|
1072
|
+
if (trigger.action === "Workflow") {
|
|
1073
|
+
const wfCfg = await getWorkflowConfig(req, id, table, trigger);
|
|
1074
|
+
send_events_page({
|
|
1075
|
+
res,
|
|
1076
|
+
req,
|
|
1077
|
+
active_sub: "Triggers",
|
|
1078
|
+
sub2_page: "Configure",
|
|
1079
|
+
page_title: req.__(`%s configuration`, trigger.name),
|
|
1080
|
+
headers: [
|
|
1081
|
+
{
|
|
1082
|
+
script: `/static_assets/${db.connectObj.version_tag}/mermaid.min.js`,
|
|
1083
|
+
},
|
|
1084
|
+
{
|
|
1085
|
+
headerTag: `<script type="module">mermaid.initialize({securityLevel: 'loose'${
|
|
1086
|
+
getState().getLightDarkMode(req.user) === "dark"
|
|
1087
|
+
? ",theme: 'dark',"
|
|
1088
|
+
: ""
|
|
1089
|
+
}});</script>`,
|
|
1090
|
+
},
|
|
1091
|
+
],
|
|
1092
|
+
contents: {
|
|
1093
|
+
type: "card",
|
|
1094
|
+
titleAjaxIndicator: true,
|
|
1095
|
+
title: req.__("Configure trigger %s", trigger.name),
|
|
1096
|
+
subtitle,
|
|
1097
|
+
contents: wfCfg,
|
|
1098
|
+
},
|
|
1099
|
+
});
|
|
1100
|
+
} else if (trigger.action === "Multi-step action") {
|
|
583
1101
|
const form = await getMultiStepForm(req, id, table);
|
|
584
1102
|
form.values = trigger.configuration;
|
|
585
1103
|
send_events_page({
|
|
@@ -725,6 +1243,11 @@ router.post(
|
|
|
725
1243
|
let form;
|
|
726
1244
|
if (trigger.action === "Multi-step action") {
|
|
727
1245
|
form = await getMultiStepForm(req, id, table);
|
|
1246
|
+
} else if (trigger.action === "Workflow") {
|
|
1247
|
+
form = new Form({
|
|
1248
|
+
action: `/actions/configure/${id}`,
|
|
1249
|
+
fields: [{ name: "save_traces", label: "Save traces", type: "Bool" }],
|
|
1250
|
+
});
|
|
728
1251
|
} else {
|
|
729
1252
|
const cfgFields = await getActionConfigFields(action, table, {
|
|
730
1253
|
mode: "trigger",
|
|
@@ -830,6 +1353,7 @@ router.get(
|
|
|
830
1353
|
table,
|
|
831
1354
|
row,
|
|
832
1355
|
req,
|
|
1356
|
+
interactive: true,
|
|
833
1357
|
...(row || {}),
|
|
834
1358
|
Table,
|
|
835
1359
|
user: req.user,
|
|
@@ -848,7 +1372,13 @@ router.get(
|
|
|
848
1372
|
? script(domReady(`common_done(${JSON.stringify(runres)})`))
|
|
849
1373
|
: ""
|
|
850
1374
|
);
|
|
851
|
-
|
|
1375
|
+
if (trigger.action === "Workflow")
|
|
1376
|
+
res.redirect(
|
|
1377
|
+
runres?.__wf_run_id
|
|
1378
|
+
? `/actions/run/${runres?.__wf_run_id}`
|
|
1379
|
+
: `/actions/runs/?trigger=${trigger.id}`
|
|
1380
|
+
);
|
|
1381
|
+
else res.redirect(`/actions/`);
|
|
852
1382
|
} else {
|
|
853
1383
|
send_events_page({
|
|
854
1384
|
res,
|
|
@@ -909,3 +1439,510 @@ router.post(
|
|
|
909
1439
|
res.redirect(`/actions`);
|
|
910
1440
|
})
|
|
911
1441
|
);
|
|
1442
|
+
|
|
1443
|
+
/**
|
|
1444
|
+
* @name post/clone/:id
|
|
1445
|
+
* @function
|
|
1446
|
+
* @memberof module:routes/actions~actionsRouter
|
|
1447
|
+
* @function
|
|
1448
|
+
*/
|
|
1449
|
+
router.get(
|
|
1450
|
+
"/stepedit/:trigger_id/:step_id?",
|
|
1451
|
+
isAdmin,
|
|
1452
|
+
error_catcher(async (req, res) => {
|
|
1453
|
+
const { trigger_id, step_id } = req.params;
|
|
1454
|
+
const { initial_step, after_step, before_step } = req.query;
|
|
1455
|
+
const trigger = await Trigger.findOne({ id: trigger_id });
|
|
1456
|
+
const form = await getWorkflowStepForm(
|
|
1457
|
+
trigger,
|
|
1458
|
+
req,
|
|
1459
|
+
step_id,
|
|
1460
|
+
after_step,
|
|
1461
|
+
before_step
|
|
1462
|
+
);
|
|
1463
|
+
|
|
1464
|
+
if (initial_step) form.values.wf_initial_step = true;
|
|
1465
|
+
if (!step_id) {
|
|
1466
|
+
const steps = await WorkflowStep.find({ trigger_id });
|
|
1467
|
+
const stepNames = new Set(steps.map((s) => s.name));
|
|
1468
|
+
let name_ix = steps.length + 1;
|
|
1469
|
+
while (stepNames.has(`step${name_ix}`)) name_ix += 1;
|
|
1470
|
+
form.values.wf_step_name = `step${name_ix}`;
|
|
1471
|
+
}
|
|
1472
|
+
send_events_page({
|
|
1473
|
+
res,
|
|
1474
|
+
req,
|
|
1475
|
+
active_sub: "Triggers",
|
|
1476
|
+
sub2_page: "Configure",
|
|
1477
|
+
page_title: req.__(`%s configuration`, trigger.name),
|
|
1478
|
+
contents: {
|
|
1479
|
+
type: "card",
|
|
1480
|
+
titleAjaxIndicator: true,
|
|
1481
|
+
title: req.__(
|
|
1482
|
+
"Configure trigger %s",
|
|
1483
|
+
a({ href: `/actions/configure/${trigger.id}` }, trigger.name)
|
|
1484
|
+
),
|
|
1485
|
+
contents: renderForm(form, req.csrfToken()),
|
|
1486
|
+
},
|
|
1487
|
+
});
|
|
1488
|
+
})
|
|
1489
|
+
);
|
|
1490
|
+
|
|
1491
|
+
router.post(
|
|
1492
|
+
"/stepedit/:trigger_id",
|
|
1493
|
+
isAdmin,
|
|
1494
|
+
error_catcher(async (req, res) => {
|
|
1495
|
+
const { trigger_id } = req.params;
|
|
1496
|
+
const trigger = await Trigger.findOne({ id: trigger_id });
|
|
1497
|
+
const form = await getWorkflowStepForm(trigger, req);
|
|
1498
|
+
form.validate(req.body);
|
|
1499
|
+
if (form.hasErrors) {
|
|
1500
|
+
if (req.xhr) {
|
|
1501
|
+
res.json({ error: form.errorSummary });
|
|
1502
|
+
} else
|
|
1503
|
+
send_events_page({
|
|
1504
|
+
res,
|
|
1505
|
+
req,
|
|
1506
|
+
active_sub: "Triggers",
|
|
1507
|
+
sub2_page: "Configure",
|
|
1508
|
+
page_title: req.__(`%s configuration`, trigger.name),
|
|
1509
|
+
contents: {
|
|
1510
|
+
type: "card",
|
|
1511
|
+
titleAjaxIndicator: true,
|
|
1512
|
+
title: req.__(
|
|
1513
|
+
"Configure trigger %s",
|
|
1514
|
+
a({ href: `/actions/configure/${trigger.id}` }, trigger.name)
|
|
1515
|
+
),
|
|
1516
|
+
contents: renderForm(form, req.csrfToken()),
|
|
1517
|
+
},
|
|
1518
|
+
});
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
const {
|
|
1522
|
+
wf_step_name,
|
|
1523
|
+
wf_action_name,
|
|
1524
|
+
wf_next_step,
|
|
1525
|
+
wf_initial_step,
|
|
1526
|
+
wf_only_if,
|
|
1527
|
+
wf_step_id,
|
|
1528
|
+
_after_step,
|
|
1529
|
+
...configuration
|
|
1530
|
+
} = form.values;
|
|
1531
|
+
Object.entries(configuration).forEach(([k, v]) => {
|
|
1532
|
+
if (v === null) delete configuration[k];
|
|
1533
|
+
});
|
|
1534
|
+
const step = {
|
|
1535
|
+
name: wf_step_name,
|
|
1536
|
+
action_name: wf_action_name,
|
|
1537
|
+
next_step: wf_next_step,
|
|
1538
|
+
only_if: wf_only_if,
|
|
1539
|
+
initial_step: wf_initial_step,
|
|
1540
|
+
trigger_id,
|
|
1541
|
+
configuration,
|
|
1542
|
+
};
|
|
1543
|
+
try {
|
|
1544
|
+
if (wf_step_id && wf_step_id !== "undefined") {
|
|
1545
|
+
const wfStep = new WorkflowStep({ id: wf_step_id, ...step });
|
|
1546
|
+
|
|
1547
|
+
await wfStep.update(step);
|
|
1548
|
+
if (req.xhr) res.json({ success: "ok" });
|
|
1549
|
+
else {
|
|
1550
|
+
req.flash("success", req.__("Step saved"));
|
|
1551
|
+
res.redirect(`/actions/configure/${step.trigger_id}`);
|
|
1552
|
+
}
|
|
1553
|
+
} else {
|
|
1554
|
+
//insert
|
|
1555
|
+
|
|
1556
|
+
const id = await WorkflowStep.create(step);
|
|
1557
|
+
if (req.xhr)
|
|
1558
|
+
res.json({ success: "ok", set_fields: { wf_step_id: id } });
|
|
1559
|
+
else {
|
|
1560
|
+
req.flash("success", req.__("Step saved"));
|
|
1561
|
+
res.redirect(`/actions/configure/${step.trigger_id}`);
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
if (_after_step) {
|
|
1565
|
+
const astep = await WorkflowStep.findOne({
|
|
1566
|
+
id: _after_step,
|
|
1567
|
+
trigger_id,
|
|
1568
|
+
});
|
|
1569
|
+
if (astep) await astep.update({ next_step: step.name });
|
|
1570
|
+
}
|
|
1571
|
+
} catch (e) {
|
|
1572
|
+
const emsg =
|
|
1573
|
+
e.message ===
|
|
1574
|
+
'duplicate key value violates unique constraint "workflow_steps_name_uniq"'
|
|
1575
|
+
? `A step with the name ${wf_step_name} already exists`
|
|
1576
|
+
: e.message;
|
|
1577
|
+
if (req.xhr) res.json({ error: emsg });
|
|
1578
|
+
else {
|
|
1579
|
+
req.flash("error", emsg);
|
|
1580
|
+
res.redirect(`/actions/configure/${step.trigger_id}`);
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
})
|
|
1584
|
+
);
|
|
1585
|
+
|
|
1586
|
+
router.post(
|
|
1587
|
+
"/delete-step/:step_id",
|
|
1588
|
+
isAdmin,
|
|
1589
|
+
error_catcher(async (req, res) => {
|
|
1590
|
+
const { step_id } = req.params;
|
|
1591
|
+
const step = await WorkflowStep.findOne({ id: step_id });
|
|
1592
|
+
await step.delete();
|
|
1593
|
+
res.json({ goto: `/actions/configure/${step.trigger_id}` });
|
|
1594
|
+
})
|
|
1595
|
+
);
|
|
1596
|
+
|
|
1597
|
+
router.get(
|
|
1598
|
+
"/runs",
|
|
1599
|
+
isAdmin,
|
|
1600
|
+
error_catcher(async (req, res) => {
|
|
1601
|
+
const trNames = {};
|
|
1602
|
+
const { _page, trigger } = req.query;
|
|
1603
|
+
for (const trig of await Trigger.find({ action: "Workflow" }))
|
|
1604
|
+
trNames[trig.id] = trig.name;
|
|
1605
|
+
const q = {};
|
|
1606
|
+
const selOpts = { orderBy: "started_at", orderDesc: true, limit: 20 };
|
|
1607
|
+
if (_page) selOpts.offset = 20 * (parseInt(_page) - 1);
|
|
1608
|
+
if (trigger) q.trigger_id = trigger;
|
|
1609
|
+
const runs = await WorkflowRun.find(q, selOpts);
|
|
1610
|
+
const count = await WorkflowRun.count(q);
|
|
1611
|
+
|
|
1612
|
+
const wfTable = mkTable(
|
|
1613
|
+
[
|
|
1614
|
+
{ label: "Trigger", key: (run) => trNames[run.trigger_id] },
|
|
1615
|
+
{ label: "Started", key: (run) => localeDateTime(run.started_at) },
|
|
1616
|
+
{
|
|
1617
|
+
label: "Updated",
|
|
1618
|
+
key: (run) => localeDateTime(run.status_updated_at),
|
|
1619
|
+
},
|
|
1620
|
+
{ label: "Status", key: "status" },
|
|
1621
|
+
{
|
|
1622
|
+
label: "",
|
|
1623
|
+
key: (run) => {
|
|
1624
|
+
switch (run.status) {
|
|
1625
|
+
case "Running":
|
|
1626
|
+
return run.current_step;
|
|
1627
|
+
case "Error":
|
|
1628
|
+
return run.error;
|
|
1629
|
+
case "Waiting":
|
|
1630
|
+
if (run.wait_info?.form || run.wait_info.output)
|
|
1631
|
+
return a(
|
|
1632
|
+
{ href: `/actions/fill-workflow-form/${run.id}` },
|
|
1633
|
+
run.wait_info.output ? "Show " : "Fill ",
|
|
1634
|
+
run.current_step
|
|
1635
|
+
);
|
|
1636
|
+
return run.current_step;
|
|
1637
|
+
default:
|
|
1638
|
+
return "";
|
|
1639
|
+
}
|
|
1640
|
+
},
|
|
1641
|
+
},
|
|
1642
|
+
],
|
|
1643
|
+
runs,
|
|
1644
|
+
{
|
|
1645
|
+
onRowSelect: (row) => `location.href='/actions/run/${row.id}'`,
|
|
1646
|
+
pagination: {
|
|
1647
|
+
current_page: parseInt(_page) || 1,
|
|
1648
|
+
pages: Math.ceil(count / 20),
|
|
1649
|
+
get_page_link: (n) => `gopage(${n}, 20)`,
|
|
1650
|
+
},
|
|
1651
|
+
}
|
|
1652
|
+
);
|
|
1653
|
+
send_events_page({
|
|
1654
|
+
res,
|
|
1655
|
+
req,
|
|
1656
|
+
active_sub: "Workflow runs",
|
|
1657
|
+
page_title: req.__(`Workflow runs`),
|
|
1658
|
+
contents: {
|
|
1659
|
+
type: "card",
|
|
1660
|
+
titleAjaxIndicator: true,
|
|
1661
|
+
title: req.__("Workflow runs"),
|
|
1662
|
+
contents: wfTable,
|
|
1663
|
+
},
|
|
1664
|
+
});
|
|
1665
|
+
})
|
|
1666
|
+
);
|
|
1667
|
+
|
|
1668
|
+
router.get(
|
|
1669
|
+
"/run/:id",
|
|
1670
|
+
isAdmin,
|
|
1671
|
+
error_catcher(async (req, res) => {
|
|
1672
|
+
const { id } = req.params;
|
|
1673
|
+
|
|
1674
|
+
const run = await WorkflowRun.findOne({ id });
|
|
1675
|
+
const trigger = await Trigger.findOne({ id: run.trigger_id });
|
|
1676
|
+
const traces = await WorkflowTrace.find(
|
|
1677
|
+
{ run_id: run.id },
|
|
1678
|
+
{ orderBy: "id" }
|
|
1679
|
+
);
|
|
1680
|
+
const traces_accordion_items = div(
|
|
1681
|
+
{ class: "accordion" },
|
|
1682
|
+
traces.map((trace, ix) =>
|
|
1683
|
+
div(
|
|
1684
|
+
{ class: "accordion-item" },
|
|
1685
|
+
|
|
1686
|
+
h2(
|
|
1687
|
+
{ class: "accordion-header", id: `trhead${ix}` },
|
|
1688
|
+
button(
|
|
1689
|
+
{
|
|
1690
|
+
class: ["accordion-button", "collapsed"],
|
|
1691
|
+
type: "button",
|
|
1692
|
+
|
|
1693
|
+
"data-bs-toggle": "collapse",
|
|
1694
|
+
"data-bs-target": `#trtab${ix}`,
|
|
1695
|
+
"aria-expanded": "false",
|
|
1696
|
+
"aria-controls": `trtab${ix}`,
|
|
1697
|
+
},
|
|
1698
|
+
`${ix + 1}: ${trace.step_name_run}`
|
|
1699
|
+
)
|
|
1700
|
+
),
|
|
1701
|
+
div(
|
|
1702
|
+
{
|
|
1703
|
+
class: ["accordion-collapse", "collapse"],
|
|
1704
|
+
id: `trtab${ix}`,
|
|
1705
|
+
"aria-labelledby": `trhead${ix}`,
|
|
1706
|
+
},
|
|
1707
|
+
div(
|
|
1708
|
+
{ class: ["accordion-body"] },
|
|
1709
|
+
table(
|
|
1710
|
+
{ class: "table table-condensed w-unset" },
|
|
1711
|
+
tbody(
|
|
1712
|
+
tr(
|
|
1713
|
+
th("Started at"),
|
|
1714
|
+
td(localeDateTime(trace.step_started_at))
|
|
1715
|
+
),
|
|
1716
|
+
tr(th("Elapsed"), td(trace.elapsed, "s")),
|
|
1717
|
+
tr(th("Run by user"), td(trace.user_id)),
|
|
1718
|
+
tr(th("Status"), td(trace.status)),
|
|
1719
|
+
trace.status === "Waiting"
|
|
1720
|
+
? tr(th("Waiting for"), td(JSON.stringify(trace.wait_info)))
|
|
1721
|
+
: null,
|
|
1722
|
+
tr(
|
|
1723
|
+
th("Context"),
|
|
1724
|
+
td(pre(text(JSON.stringify(trace.context, null, 2))))
|
|
1725
|
+
)
|
|
1726
|
+
)
|
|
1727
|
+
)
|
|
1728
|
+
)
|
|
1729
|
+
)
|
|
1730
|
+
)
|
|
1731
|
+
)
|
|
1732
|
+
);
|
|
1733
|
+
|
|
1734
|
+
send_events_page({
|
|
1735
|
+
res,
|
|
1736
|
+
req,
|
|
1737
|
+
active_sub: "Workflow runs",
|
|
1738
|
+
page_title: req.__(`Workflow runs`),
|
|
1739
|
+
sub2_page: trigger.name,
|
|
1740
|
+
contents: {
|
|
1741
|
+
above: [
|
|
1742
|
+
{
|
|
1743
|
+
type: "card",
|
|
1744
|
+
titleAjaxIndicator: true,
|
|
1745
|
+
title: req.__("Workflow run"),
|
|
1746
|
+
contents:
|
|
1747
|
+
table(
|
|
1748
|
+
{ class: "table table-condensed w-unset" },
|
|
1749
|
+
tbody(
|
|
1750
|
+
tr(th("Run ID"), td(run.id)),
|
|
1751
|
+
tr(
|
|
1752
|
+
th("Trigger"),
|
|
1753
|
+
td(
|
|
1754
|
+
a(
|
|
1755
|
+
{ href: `/actions/configure/${trigger.id}` },
|
|
1756
|
+
trigger.name
|
|
1757
|
+
)
|
|
1758
|
+
)
|
|
1759
|
+
),
|
|
1760
|
+
tr(th("Started at"), td(localeDateTime(run.started_at))),
|
|
1761
|
+
tr(th("Started by user"), td(run.started_by)),
|
|
1762
|
+
tr(th("Status"), td(run.status)),
|
|
1763
|
+
run.status === "Waiting"
|
|
1764
|
+
? tr(th("Waiting for"), td(JSON.stringify(run.wait_info)))
|
|
1765
|
+
: null,
|
|
1766
|
+
run.status === "Error"
|
|
1767
|
+
? tr(th("Error message"), td(run.error))
|
|
1768
|
+
: null,
|
|
1769
|
+
tr(
|
|
1770
|
+
th("Context"),
|
|
1771
|
+
td(pre(text(JSON.stringify(run.context, null, 2))))
|
|
1772
|
+
)
|
|
1773
|
+
)
|
|
1774
|
+
) + post_delete_btn("/actions/delete-run/" + run.id, req),
|
|
1775
|
+
},
|
|
1776
|
+
...(traces.length
|
|
1777
|
+
? [
|
|
1778
|
+
{
|
|
1779
|
+
type: "card",
|
|
1780
|
+
title: req.__("Step traces"),
|
|
1781
|
+
contents: traces_accordion_items,
|
|
1782
|
+
},
|
|
1783
|
+
]
|
|
1784
|
+
: []),
|
|
1785
|
+
],
|
|
1786
|
+
},
|
|
1787
|
+
});
|
|
1788
|
+
})
|
|
1789
|
+
);
|
|
1790
|
+
|
|
1791
|
+
router.post(
|
|
1792
|
+
"/delete-run/:id",
|
|
1793
|
+
isAdmin,
|
|
1794
|
+
error_catcher(async (req, res) => {
|
|
1795
|
+
const { id } = req.params;
|
|
1796
|
+
|
|
1797
|
+
const run = await WorkflowRun.findOne({ id });
|
|
1798
|
+
await run.delete();
|
|
1799
|
+
res.redirect("/actions/runs");
|
|
1800
|
+
})
|
|
1801
|
+
);
|
|
1802
|
+
|
|
1803
|
+
const getWorkflowStepUserForm = async (run, trigger, step, req) => {
|
|
1804
|
+
let blurb = run.wait_info.output || step.configuration?.form_header || "";
|
|
1805
|
+
if (run.wait_info.markdown && run.wait_info.output) blurb = md.render(blurb);
|
|
1806
|
+
const form = new Form({
|
|
1807
|
+
action: `/actions/fill-workflow-form/${run.id}`,
|
|
1808
|
+
submitLabel: run.wait_info.output ? req.__("OK") : req.__("Submit"),
|
|
1809
|
+
blurb,
|
|
1810
|
+
formStyle: run.wait_info.output || req.xhr ? "vert" : undefined,
|
|
1811
|
+
fields: await run.userFormFields(step),
|
|
1812
|
+
});
|
|
1813
|
+
return form;
|
|
1814
|
+
};
|
|
1815
|
+
|
|
1816
|
+
router.get(
|
|
1817
|
+
"/fill-workflow-form/:id",
|
|
1818
|
+
error_catcher(async (req, res) => {
|
|
1819
|
+
const { id } = req.params;
|
|
1820
|
+
|
|
1821
|
+
const run = await WorkflowRun.findOne({ id });
|
|
1822
|
+
|
|
1823
|
+
if (!run.user_allowed_to_fill_form(req.user)) {
|
|
1824
|
+
if (req.xhr) res.json({ error: "Not authorized" });
|
|
1825
|
+
else {
|
|
1826
|
+
req.flash("danger", req.__("Not authorized"));
|
|
1827
|
+
res.redirect("/");
|
|
1828
|
+
}
|
|
1829
|
+
return;
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
const trigger = await Trigger.findOne({ id: run.trigger_id });
|
|
1833
|
+
const step = await WorkflowStep.findOne({
|
|
1834
|
+
trigger_id: trigger.id,
|
|
1835
|
+
name: run.current_step,
|
|
1836
|
+
});
|
|
1837
|
+
|
|
1838
|
+
const form = await getWorkflowStepUserForm(run, trigger, step, req);
|
|
1839
|
+
if (req.xhr) form.xhrSubmit = true;
|
|
1840
|
+
const title = run.wait_info.output ? "Workflow output" : "Fill form";
|
|
1841
|
+
res.sendWrap(title, renderForm(form, req.csrfToken()));
|
|
1842
|
+
})
|
|
1843
|
+
);
|
|
1844
|
+
|
|
1845
|
+
router.post(
|
|
1846
|
+
"/fill-workflow-form/:id",
|
|
1847
|
+
error_catcher(async (req, res) => {
|
|
1848
|
+
const { id } = req.params;
|
|
1849
|
+
|
|
1850
|
+
const run = await WorkflowRun.findOne({ id });
|
|
1851
|
+
if (!run.user_allowed_to_fill_form(req.user)) {
|
|
1852
|
+
if (req.xhr) res.json({ error: "Not authorized" });
|
|
1853
|
+
else {
|
|
1854
|
+
req.flash("danger", req.__("Not authorized"));
|
|
1855
|
+
res.redirect("/");
|
|
1856
|
+
}
|
|
1857
|
+
return;
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
const trigger = await Trigger.findOne({ id: run.trigger_id });
|
|
1861
|
+
const step = await WorkflowStep.findOne({
|
|
1862
|
+
trigger_id: trigger.id,
|
|
1863
|
+
name: run.current_step,
|
|
1864
|
+
});
|
|
1865
|
+
|
|
1866
|
+
const form = await getWorkflowStepUserForm(run, trigger, step, req);
|
|
1867
|
+
form.validate(req.body);
|
|
1868
|
+
if (form.hasErrors) {
|
|
1869
|
+
const title = "Fill form";
|
|
1870
|
+
res.sendWrap(title, renderForm(form, req.csrfToken()));
|
|
1871
|
+
} else {
|
|
1872
|
+
await run.provide_form_input(form.values);
|
|
1873
|
+
const runres = await run.run({
|
|
1874
|
+
user: req.user,
|
|
1875
|
+
trace: trigger.configuration?.save_traces,
|
|
1876
|
+
interactive: true,
|
|
1877
|
+
});
|
|
1878
|
+
if (req.xhr) {
|
|
1879
|
+
const retDirs = await run.popReturnDirectives();
|
|
1880
|
+
|
|
1881
|
+
if (runres?.popup) retDirs.popup = runres.popup;
|
|
1882
|
+
res.json({ success: "ok", ...retDirs });
|
|
1883
|
+
} else {
|
|
1884
|
+
if (run.context.goto) res.redirect(run.context.goto);
|
|
1885
|
+
else res.redirect("/");
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
})
|
|
1889
|
+
);
|
|
1890
|
+
|
|
1891
|
+
router.post(
|
|
1892
|
+
"/resume-workflow/:id",
|
|
1893
|
+
error_catcher(async (req, res) => {
|
|
1894
|
+
const { id } = req.params;
|
|
1895
|
+
|
|
1896
|
+
const run = await WorkflowRun.findOne({ id });
|
|
1897
|
+
//TODO session if not logged in
|
|
1898
|
+
if (run.started_by !== req.user?.id) {
|
|
1899
|
+
if (req.xhr) res.json({ error: "Not authorized" });
|
|
1900
|
+
else {
|
|
1901
|
+
req.flash("danger", req.__("Not authorized"));
|
|
1902
|
+
res.redirect("/");
|
|
1903
|
+
}
|
|
1904
|
+
return;
|
|
1905
|
+
}
|
|
1906
|
+
const trigger = await Trigger.findOne({ id: run.trigger_id });
|
|
1907
|
+
const runResult = await run.run({
|
|
1908
|
+
user: req.user,
|
|
1909
|
+
interactive: true,
|
|
1910
|
+
trace: trigger.configuration?.save_traces,
|
|
1911
|
+
});
|
|
1912
|
+
if (req.xhr) {
|
|
1913
|
+
if (
|
|
1914
|
+
runResult &&
|
|
1915
|
+
typeof runResult === "object" &&
|
|
1916
|
+
Object.keys(runResult).length
|
|
1917
|
+
) {
|
|
1918
|
+
res.json({ success: "ok", ...runResult });
|
|
1919
|
+
return;
|
|
1920
|
+
}
|
|
1921
|
+
const retDirs = await run.popReturnDirectives();
|
|
1922
|
+
res.json({ success: "ok", ...retDirs });
|
|
1923
|
+
} else {
|
|
1924
|
+
if (run.context.goto) res.redirect(run.context.goto);
|
|
1925
|
+
else res.redirect("/");
|
|
1926
|
+
}
|
|
1927
|
+
})
|
|
1928
|
+
);
|
|
1929
|
+
|
|
1930
|
+
/*
|
|
1931
|
+
|
|
1932
|
+
WORKFLOWS TODO
|
|
1933
|
+
|
|
1934
|
+
help file to explain steps, and context
|
|
1935
|
+
|
|
1936
|
+
workflow actions: ForLoop, EndForLoop, ReadFile, WriteFile, APIResponse
|
|
1937
|
+
|
|
1938
|
+
Error handlers
|
|
1939
|
+
other triggers can be steps
|
|
1940
|
+
interactive workflows for not logged in
|
|
1941
|
+
show end node in diagram
|
|
1942
|
+
actions can declare which variables they inject into scope
|
|
1943
|
+
|
|
1944
|
+
show unconnected steps
|
|
1945
|
+
why is code not initialising
|
|
1946
|
+
drag and drop edges
|
|
1947
|
+
|
|
1948
|
+
*/
|