@saltcorn/server 1.1.1-beta.0 → 1.1.1-beta.2

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/routes/actions.js CHANGED
@@ -11,9 +11,13 @@ const {
11
11
  addOnDoneRedirect,
12
12
  is_relative_url,
13
13
  } = require("./utils.js");
14
- const { ppVal } = require("@saltcorn/data/utils");
14
+ const { ppVal, jsIdentifierValidator } = require("@saltcorn/data/utils");
15
15
  const { getState } = require("@saltcorn/data/db/state");
16
16
  const Trigger = require("@saltcorn/data/models/trigger");
17
+ const View = require("@saltcorn/data/models/view");
18
+ const {
19
+ getForm,
20
+ } = require("@saltcorn/data/base-plugin/viewtemplates/viewable_fields");
17
21
  const FieldRepeat = require("@saltcorn/data/models/fieldrepeat");
18
22
  const { getTriggerList } = require("./common_lists");
19
23
  const TagEntry = require("@saltcorn/data/models/tag_entry");
@@ -441,6 +445,10 @@ router.post(
441
445
  const tr = await Trigger.create(form.values);
442
446
  id = tr.id;
443
447
  }
448
+ Trigger.emitEvent("AppChange", `Trigger ${form.values.name}`, req.user, {
449
+ entity_type: "Trigger",
450
+ entity_name: form.values.name,
451
+ });
444
452
  res.redirect(addOnDoneRedirect(`/actions/configure/${id}`, req));
445
453
  }
446
454
  })
@@ -482,6 +490,10 @@ router.post(
482
490
  ...form.values.configuration,
483
491
  };
484
492
  await Trigger.update(trigger.id, form.values); //{configuration: form.values});
493
+ Trigger.emitEvent("AppChange", `Trigger ${trigger.name}`, req.user, {
494
+ entity_type: "Trigger",
495
+ entity_name: trigger.name,
496
+ });
485
497
  if (req.xhr) {
486
498
  res.json({ success: "ok" });
487
499
  return;
@@ -495,7 +507,7 @@ router.post(
495
507
  function genWorkflowDiagram(steps) {
496
508
  const stepNames = steps.map((s) => s.name);
497
509
  const nodeLines = steps.map(
498
- (s) => ` ${s.name}["\`**${s.name}**
510
+ (s) => ` ${s.mmname}["\`**${s.name}**
499
511
  ${s.action_name}\`"]:::wfstep${s.id}`
500
512
  );
501
513
 
@@ -503,34 +515,39 @@ function genWorkflowDiagram(steps) {
503
515
  const linkLines = [];
504
516
  let step_ix = 0;
505
517
  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)) {
518
+ if (step.initial_step) linkLines.push(` _Start --> ${step.mmname}`);
519
+ if (stepNames.includes(step.next_step)) {
512
520
  linkLines.push(
513
- ` ${step.name}-- <i class="fas fa-plus add-btw-nodes btw-nodes-${step.id}-${step.next_step}"></i> ---${step.next_step}`
521
+ ` ${step.mmname}-- <i class="fas fa-plus add-btw-nodes btw-nodes-${step.id}-${step.next_step}"></i> ---${step.mmnext}`
514
522
  );
515
523
  } else if (step.next_step) {
516
524
  let found = false;
517
525
  for (const otherStep of stepNames)
518
526
  if (step.next_step.includes(otherStep)) {
519
- linkLines.push(` ${step.name} --> ${otherStep}`);
527
+ linkLines.push(
528
+ ` ${step.mmname} --> ${WorkflowStep.mmescape(otherStep)}`
529
+ );
520
530
  found = true;
521
531
  }
522
532
  if (!found) {
523
533
  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}`
534
+ ` ${step.mmname}-- <a href="/actions/stepedit/${step.trigger_id}/${step.id}">Error: missing next step in ${step.mmname}</a> ---_End_${step.mmname}`
525
535
  );
526
536
  nodeLines.push(
527
- ` _End_${step.name}:::wfadd${step.id}@{ shape: circle, label: "<i class='fas fa-plus with-link'></i>" }`
537
+ ` _End_${step.mmname}:::wfadd${step.id}@{ shape: circle, label: "<i class='fas fa-plus with-link'></i>" }`
528
538
  );
529
539
  }
530
540
  } else if (!step.next_step) {
531
- linkLines.push(` ${step.name} --> _End_${step.name}`);
541
+ linkLines.push(` ${step.mmname} --> _End_${step.mmname}`);
532
542
  nodeLines.push(
533
- ` _End_${step.name}:::wfadd${step.id}@{ shape: circle, label: "<i class='fas fa-plus with-link'></i>" }`
543
+ ` _End_${step.mmname}:::wfadd${step.id}@{ shape: circle, label: "<i class='fas fa-plus with-link'></i>" }`
544
+ );
545
+ }
546
+ if (step.action_name === "ForLoop") {
547
+ linkLines.push(
548
+ ` ${step.mmname}-.->${WorkflowStep.mmescape(
549
+ step.configuration.loop_body_initial_step
550
+ )}`
534
551
  );
535
552
  }
536
553
  if (step.action_name === "EndForLoop") {
@@ -542,7 +559,7 @@ function genWorkflowDiagram(steps) {
542
559
  break;
543
560
  }
544
561
  }
545
- if (forStep) linkLines.push(` ${step.name} --> ${forStep.name}`);
562
+ if (forStep) linkLines.push(` ${step.mmname} --> ${forStep.mmname}`);
546
563
  }
547
564
  step_ix += 1;
548
565
  }
@@ -581,7 +598,29 @@ const getWorkflowConfig = async (req, id, table, trigger) => {
581
598
  ],
582
599
  });
583
600
  trigCfgForm.values = trigger.configuration;
601
+ let copilot_form = "";
602
+
603
+ if (getState().functions.copilot_generate_workflow) {
604
+ copilot_form = renderForm(
605
+ new Form({
606
+ action: `/actions/gen-copilot/${id}`,
607
+ values: { description: trigger.description || "" },
608
+ submitLabel: "Generate workflow with copilot",
609
+ formStyle: "vert",
610
+ fields: [
611
+ {
612
+ name: "description",
613
+ label: "Description",
614
+ type: "String",
615
+ fieldview: "textarea",
616
+ },
617
+ ],
618
+ }),
619
+ req.csrfToken()
620
+ );
621
+ }
584
622
  return (
623
+ copilot_form +
585
624
  pre({ class: "mermaid" }, genWorkflowDiagram(steps)) +
586
625
  script(
587
626
  { defer: "defer" },
@@ -641,16 +680,6 @@ window.addEventListener('DOMContentLoaded',tryAddWFNodes)`
641
680
  );
642
681
  };
643
682
 
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
683
  const getWorkflowStepForm = async (
655
684
  trigger,
656
685
  req,
@@ -696,23 +725,26 @@ const getWorkflowStepForm = async (
696
725
  },
697
726
  };
698
727
  if (cfgFld.input_type === "code") cfgFld.input_type = "textarea";
699
- actionConfigFields.push(cfgFld);
700
728
  }
701
729
  } catch {}
702
730
  }
731
+ actionConfigFields.push({
732
+ label: "Subcontext",
733
+ name: "subcontext",
734
+ type: "String",
735
+ sublabel:
736
+ "Optional. A key on the current workflow's context, the values of which will be the called workflow's context.",
737
+ showIf: {
738
+ wf_action_name: Trigger.find({ action: "Workflow" }).map((wf) => wf.name),
739
+ },
740
+ });
741
+
742
+ const builtInActionExplainers = WorkflowStep.builtInActionExplainers();
703
743
  const actionsNotRequiringRow = Trigger.action_options({
704
744
  notRequireRow: true,
705
745
  noMultiStep: true,
706
746
  builtInLabel: "Workflow Actions",
707
- builtIns: [
708
- "SetContext",
709
- "TableQuery",
710
- "Output",
711
- "DataOutput",
712
- "WaitUntil",
713
- "WaitNextTick",
714
- "UserForm",
715
- ],
747
+ builtIns: Object.keys(builtInActionExplainers),
716
748
  forWorkflow: true,
717
749
  });
718
750
  const triggers = Trigger.find({
@@ -721,151 +753,9 @@ const getWorkflowStepForm = async (
721
753
  triggers.forEach((tr) => {
722
754
  if (tr.description) actionExplainers[tr.name] = tr.description;
723
755
  });
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
- });
756
+ Object.assign(actionExplainers, builtInActionExplainers);
822
757
  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
- })
758
+ ...(await WorkflowStep.builtInActionConfigFields({ trigger }))
869
759
  );
870
760
 
871
761
  const form = new Form({
@@ -1278,6 +1168,10 @@ router.post(
1278
1168
  await Trigger.update(trigger.id, {
1279
1169
  configuration: { ...trigger.configuration, ...form.values },
1280
1170
  });
1171
+ Trigger.emitEvent("AppChange", `Trigger ${trigger.name}`, req.user, {
1172
+ entity_type: "Trigger",
1173
+ entity_name: trigger.name,
1174
+ });
1281
1175
  if (req.xhr) {
1282
1176
  res.json({ success: "ok" });
1283
1177
  return;
@@ -1304,6 +1198,10 @@ router.post(
1304
1198
  error_catcher(async (req, res) => {
1305
1199
  const { id } = req.params;
1306
1200
  const trigger = await Trigger.findOne({ id });
1201
+ Trigger.emitEvent("AppChange", `Trigger ${trigger.name}`, req.user, {
1202
+ entity_type: "Trigger",
1203
+ entity_name: trigger.name,
1204
+ });
1307
1205
  await trigger.delete();
1308
1206
  res.redirect(`/actions/`);
1309
1207
  })
@@ -1432,6 +1330,10 @@ router.post(
1432
1330
  const { id } = req.params;
1433
1331
  const trig = await Trigger.findOne({ id });
1434
1332
  const newtrig = await trig.clone();
1333
+ Trigger.emitEvent("AppChange", `Trigger ${newtrig.name}`, req.user, {
1334
+ entity_type: "Trigger",
1335
+ entity_name: newtrig.name,
1336
+ });
1435
1337
  req.flash(
1436
1338
  "success",
1437
1339
  req.__("Trigger %s duplicated as %s", trig.name, newtrig.name)
@@ -1561,7 +1463,11 @@ router.post(
1561
1463
  res.redirect(`/actions/configure/${step.trigger_id}`);
1562
1464
  }
1563
1465
  }
1564
- if (_after_step) {
1466
+ Trigger.emitEvent("AppChange", `Trigger ${trigger.name}`, req.user, {
1467
+ entity_type: "Trigger",
1468
+ entity_name: trigger.name,
1469
+ });
1470
+ if (_after_step && _after_step !== "undefined") {
1565
1471
  const astep = await WorkflowStep.findOne({
1566
1472
  id: _after_step,
1567
1473
  trigger_id,
@@ -1583,6 +1489,32 @@ router.post(
1583
1489
  })
1584
1490
  );
1585
1491
 
1492
+ router.post(
1493
+ "/gen-copilot/:trigger_id",
1494
+ isAdmin,
1495
+ error_catcher(async (req, res) => {
1496
+ const { trigger_id } = req.params;
1497
+ const trigger = await Trigger.findOne({ id: trigger_id });
1498
+ await WorkflowStep.deleteForTrigger(trigger.id);
1499
+ const description = req.body.description;
1500
+ await Trigger.update(trigger.id, { description });
1501
+ const steps = await getState().functions.copilot_generate_workflow.run(
1502
+ description,
1503
+ trigger.id
1504
+ );
1505
+ if (steps.length) steps[0].initial_step = true;
1506
+ for (const step of steps) {
1507
+ step.trigger_id = trigger.id;
1508
+ await WorkflowStep.create(step);
1509
+ }
1510
+ Trigger.emitEvent("AppChange", `Trigger ${trigger.name}`, req.user, {
1511
+ entity_type: "Trigger",
1512
+ entity_name: trigger.name,
1513
+ });
1514
+ res.redirect(`/actions/configure/${trigger.id}`);
1515
+ })
1516
+ );
1517
+
1586
1518
  router.post(
1587
1519
  "/delete-step/:step_id",
1588
1520
  isAdmin,
@@ -1801,6 +1733,21 @@ router.post(
1801
1733
  );
1802
1734
 
1803
1735
  const getWorkflowStepUserForm = async (run, trigger, step, req) => {
1736
+ if (step.action_name === "EditViewForm") {
1737
+ const view = View.findOne({ name: step.configuration.edit_view });
1738
+ const table = Table.findOne({ id: view.table_id });
1739
+ const form = await getForm(
1740
+ table,
1741
+ view.name,
1742
+ view.configuration.columns,
1743
+ view.configuration.layout,
1744
+ null,
1745
+ req
1746
+ );
1747
+ form.action = `/actions/fill-workflow-form/${run.id}`;
1748
+ return form;
1749
+ }
1750
+
1804
1751
  let blurb = run.wait_info.output || step.configuration?.form_header || "";
1805
1752
  if (run.wait_info.markdown && run.wait_info.output) blurb = md.render(blurb);
1806
1753
  const form = new Form({
@@ -1933,12 +1880,10 @@ WORKFLOWS TODO
1933
1880
 
1934
1881
  help file to explain steps, and context
1935
1882
 
1936
- workflow actions: ForLoop, EndForLoop, ReadFile, WriteFile, APIResponse
1883
+ workflow actions: Stop, RunEditView, ReadFile, WriteFile, APIResponse
1937
1884
 
1938
- Error handlers
1939
1885
  other triggers can be steps
1940
1886
  interactive workflows for not logged in
1941
- show end node in diagram
1942
1887
  actions can declare which variables they inject into scope
1943
1888
 
1944
1889
  show unconnected steps