@saltcorn/server 1.1.0 → 1.1.1-beta.1

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/locales/en.json CHANGED
@@ -1521,5 +1521,6 @@
1521
1521
  "Step settings": "Step settings",
1522
1522
  "Action settings": "Action settings",
1523
1523
  "Workflow": "Workflow",
1524
- "Previous runs": "Previous runs"
1524
+ "Previous runs": "Previous runs",
1525
+ "The workflow the user will be interacting with.": "The workflow the user will be interacting with."
1525
1526
  }
package/locales/pl.json CHANGED
@@ -1508,5 +1508,20 @@
1508
1508
  "Workflow runs": "Przepływ pracy jest uruchomiony",
1509
1509
  "Workflow run": "Uruchomienie przepływu pracy",
1510
1510
  "Share to enabled": "Udostępnij włączone",
1511
- "Enable the share to feature": "Włącz udostępnianie funkcji"
1511
+ "Enable the share to feature": "Włącz udostępnianie funkcji",
1512
+ "Allocate new row": "Przydziel nowy wiersz",
1513
+ "If the view is run without existing row, allocate a new row on load. Defaults must be set on all required fields.": "Jeśli widok jest uruchamiany bez istniejącego wiersza, przydziel nowy wiersz podczas ładowania. Domyślne wartości muszą być ustawione na wszystkich wymaganych polach.",
1514
+ "Step traces": "Ślady kroków",
1515
+ "Please enter a version in the format 'x.y.z' (e.g. 0.0.1 with numbers from 0 to 999) or leave it empty.": "Wprowadź wersję w formacie 'x.y.z' (np. 0.0.1 z liczbami od 0 do 999) lub pozostaw puste.",
1516
+ "Delete unchanged": "Usuń bez zmian",
1517
+ "Delete allocated row if there are no changes.": "Usuń przydzielony wiersz, jeśli nie wprowadzono żadnych zmian.",
1518
+ "Triggers on table": "Wyzwalacze na tabeli",
1519
+ "Please provide the keystore alias and password for the android build.": "Podaj alias i hasło do keystore dla kompilacji Android.",
1520
+ "Submit": "Prześlij",
1521
+ "OK": "OK",
1522
+ "Step settings": "Ustawienia kroku",
1523
+ "Action settings": "Ustawienia akcji",
1524
+ "Workflow": "Przepływ pracy",
1525
+ "Previous runs": "Poprzednie uruchomienia",
1526
+ "The workflow the user will be interacting with.": "Przepływ pracy, z którym użytkownik będzie wchodził w interakcję."
1512
1527
  }
package/package.json CHANGED
@@ -1,20 +1,20 @@
1
1
  {
2
2
  "name": "@saltcorn/server",
3
- "version": "1.1.0",
3
+ "version": "1.1.1-beta.1",
4
4
  "description": "Server app for Saltcorn, open-source no-code platform",
5
5
  "homepage": "https://saltcorn.com",
6
6
  "main": "index.js",
7
7
  "license": "MIT",
8
8
  "dependencies": {
9
9
  "@aws-sdk/client-s3": "^3.451.0",
10
- "@saltcorn/base-plugin": "1.1.0",
11
- "@saltcorn/builder": "1.1.0",
12
- "@saltcorn/data": "1.1.0",
13
- "@saltcorn/admin-models": "1.1.0",
14
- "@saltcorn/filemanager": "1.1.0",
15
- "@saltcorn/markup": "1.1.0",
16
- "@saltcorn/plugins-loader": "1.1.0",
17
- "@saltcorn/sbadmin2": "1.1.0",
10
+ "@saltcorn/base-plugin": "1.1.1-beta.1",
11
+ "@saltcorn/builder": "1.1.1-beta.1",
12
+ "@saltcorn/data": "1.1.1-beta.1",
13
+ "@saltcorn/admin-models": "1.1.1-beta.1",
14
+ "@saltcorn/filemanager": "1.1.1-beta.1",
15
+ "@saltcorn/markup": "1.1.1-beta.1",
16
+ "@saltcorn/plugins-loader": "1.1.1-beta.1",
17
+ "@saltcorn/sbadmin2": "1.1.1-beta.1",
18
18
  "@socket.io/cluster-adapter": "^0.2.1",
19
19
  "@socket.io/sticky": "^1.0.1",
20
20
  "adm-zip": "0.5.10",
@@ -469,6 +469,18 @@ div.unread-notify {
469
469
  color: inherit;
470
470
  }
471
471
 
472
+ i.add-btw-nodes {
473
+ cursor: pointer;
474
+ }
475
+
476
+ pre.mermaid g.node[class*=" wfstep"] {
477
+ cursor: pointer;
478
+ }
479
+
480
+ pre.mermaid g.node[class*=" wfadd"] {
481
+ cursor: pointer;
482
+ }
483
+
472
484
  .link-style {
473
485
  cursor: pointer;
474
486
  text-decoration: underline;
@@ -605,16 +617,18 @@ button.monospace-copy-btn {
605
617
  display: block;
606
618
  }
607
619
 
608
- i[class^="unicode-"], i[class*=" unicode-"] {
620
+ i[class^="unicode-"],
621
+ i[class*=" unicode-"] {
609
622
  font-style: normal;
610
623
  }
611
624
 
612
- .tabulator.table-dark:not(.thead-light) .tabulator-footer, .tabulator.table-dark:not(.thead-light) .tabulator-footer .tabulator-col {
625
+ .tabulator.table-dark:not(.thead-light) .tabulator-footer,
626
+ .tabulator.table-dark:not(.thead-light) .tabulator-footer .tabulator-col {
613
627
  background-color: #212529;
614
628
  border-color: #32383e;
615
629
  color: #fff;
616
630
  }
617
631
 
618
632
  .mobile-toast-margin {
619
- margin-bottom: 1.0rem
620
- }
633
+ margin-bottom: 1rem;
634
+ }
package/routes/actions.js CHANGED
@@ -11,7 +11,7 @@ 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
17
  const FieldRepeat = require("@saltcorn/data/models/fieldrepeat");
@@ -22,6 +22,8 @@ const WorkflowRun = require("@saltcorn/data/models/workflow_run");
22
22
  const WorkflowTrace = require("@saltcorn/data/models/workflow_trace");
23
23
  const Tag = require("@saltcorn/data/models/tag");
24
24
  const db = require("@saltcorn/data/db");
25
+ const MarkdownIt = require("markdown-it"),
26
+ md = new MarkdownIt();
25
27
 
26
28
  /**
27
29
  * @type {object}
@@ -493,7 +495,7 @@ router.post(
493
495
  function genWorkflowDiagram(steps) {
494
496
  const stepNames = steps.map((s) => s.name);
495
497
  const nodeLines = steps.map(
496
- (s) => ` ${s.name}["\`**${s.name}**
498
+ (s) => ` ${s.mmname}["\`**${s.name}**
497
499
  ${s.action_name}\`"]:::wfstep${s.id}`
498
500
  );
499
501
 
@@ -501,18 +503,39 @@ function genWorkflowDiagram(steps) {
501
503
  const linkLines = [];
502
504
  let step_ix = 0;
503
505
  for (const step of steps) {
504
- if (step.initial_step) linkLines.push(` _Start --> ${step.name}`);
505
- if (step.action_name === "ForLoop") {
506
+ if (step.initial_step) linkLines.push(` _Start --> ${step.mmname}`);
507
+ if (stepNames.includes(step.next_step)) {
506
508
  linkLines.push(
507
- ` ${step.name} --> ${step.configuration.for_loop_step_name}`
509
+ ` ${step.mmname}-- <i class="fas fa-plus add-btw-nodes btw-nodes-${step.id}-${step.next_step}"></i> ---${step.mmnext}`
508
510
  );
509
- } else if (stepNames.includes(step.next_step)) {
510
- linkLines.push(` ${step.name} --> ${step.next_step}`);
511
511
  } else if (step.next_step) {
512
+ let found = false;
512
513
  for (const otherStep of stepNames)
513
- if (step.next_step.includes(otherStep))
514
- linkLines.push(` ${step.name} --> ${otherStep}`);
514
+ if (step.next_step.includes(otherStep)) {
515
+ linkLines.push(
516
+ ` ${step.mmname} --> ${WorkflowStep.mmescape(otherStep)}`
517
+ );
518
+ found = true;
519
+ }
520
+ if (!found) {
521
+ linkLines.push(
522
+ ` ${step.mmname}-- <a href="/actions/stepedit/${step.trigger_id}/${step.id}">Error: missing next step in ${step.mmname}</a> ---_End_${step.mmname}`
523
+ );
524
+ nodeLines.push(
525
+ ` _End_${step.mmname}:::wfadd${step.id}@{ shape: circle, label: "<i class='fas fa-plus with-link'></i>" }`
526
+ );
527
+ }
528
+ } else if (!step.next_step) {
529
+ linkLines.push(` ${step.mmname} --> _End_${step.mmname}`);
530
+ nodeLines.push(
531
+ ` _End_${step.mmname}:::wfadd${step.id}@{ shape: circle, label: "<i class='fas fa-plus with-link'></i>" }`
532
+ );
515
533
  }
534
+ if (step.action_name === "ForLoop") {
535
+ linkLines.push(
536
+ ` ${step.mmname}-.->${WorkflowStep.mmescape(step.configuration.loop_body_initial_step)}`
537
+ );
538
+ }
516
539
  if (step.action_name === "EndForLoop") {
517
540
  // TODO this is not correct. improve.
518
541
  let forStep;
@@ -522,12 +545,20 @@ function genWorkflowDiagram(steps) {
522
545
  break;
523
546
  }
524
547
  }
525
- if (forStep) linkLines.push(` ${step.name} --> ${forStep.name}`);
548
+ if (forStep) linkLines.push(` ${step.mmname} --> ${forStep.mmname}`);
526
549
  }
527
550
  step_ix += 1;
528
551
  }
552
+ if (!steps.length || !steps.find((s) => s.initial_step)) {
553
+ linkLines.push(` _Start --> _End`);
554
+ nodeLines.push(
555
+ ` _End:::wfaddstart@{ shape: circle, label: "<i class='fas fa-plus with-link'></i>" }`
556
+ );
557
+ }
529
558
  const fc =
530
559
  "flowchart TD\n" + nodeLines.join("\n") + "\n" + linkLines.join("\n");
560
+ //console.log(fc);
561
+
531
562
  return fc;
532
563
  }
533
564
 
@@ -553,7 +584,29 @@ const getWorkflowConfig = async (req, id, table, trigger) => {
553
584
  ],
554
585
  });
555
586
  trigCfgForm.values = trigger.configuration;
587
+ let copilot_form = "";
588
+
589
+ if (getState().functions.copilot_generate_workflow) {
590
+ copilot_form = renderForm(
591
+ new Form({
592
+ action: `/actions/gen-copilot/${id}`,
593
+ values: { description: trigger.description || "" },
594
+ submitLabel: "Generate workflow with copilot",
595
+ formStyle: "vert",
596
+ fields: [
597
+ {
598
+ name: "description",
599
+ label: "Description",
600
+ type: "String",
601
+ fieldview: "textarea",
602
+ },
603
+ ],
604
+ }),
605
+ req.csrfToken()
606
+ );
607
+ }
556
608
  return (
609
+ copilot_form +
557
610
  pre({ class: "mermaid" }, genWorkflowDiagram(steps)) +
558
611
  script(
559
612
  { defer: "defer" },
@@ -561,13 +614,31 @@ const getWorkflowConfig = async (req, id, table, trigger) => {
561
614
  const ns = $("g.node");
562
615
  if(!ns.length) setTimeout(tryAddWFNodes, 200)
563
616
  else {
617
+ $("i.add-btw-nodes").on("click", (e)=>{
618
+ const $e = $(e.target || e);
619
+ const cls = $e.attr('class');
620
+ const idnext = cls.split(" ").find(c=>c.startsWith("btw-nodes-")).
621
+ substr(10);
622
+ const [idprev, nmnext] = idnext.split("-");
623
+ location.href = '/actions/stepedit/${trigger.id}?after_step='+idprev+'&before_step='+nmnext;
624
+ })
564
625
  $("g.node").on("click", (e)=>{
565
626
  const $e = $(e.target || e).closest("g.node")
566
627
  const cls = $e.attr('class')
567
- if(!cls || !cls.includes("wfstep")) return;
628
+ if(!cls) return;
629
+ //console.log(cls)
630
+ if(cls.includes("wfstep")) {
568
631
  const id = cls.split(" ").find(c=>c.startsWith("wfstep")).
569
632
  substr(6);
570
633
  location.href = '/actions/stepedit/${trigger.id}/'+id;
634
+ }
635
+ if(cls.includes("wfaddstart")) {
636
+ location.href = '/actions/stepedit/${trigger.id}?initial_step=true';
637
+ } else if(cls.includes("wfadd")) {
638
+ const id = cls.split(" ").find(c=>c.startsWith("wfadd")).
639
+ substr(5);
640
+ location.href = '/actions/stepedit/${trigger.id}?after_step='+id;
641
+ }
571
642
  //console.log($e.attr('class'), id)
572
643
  })
573
644
  }
@@ -579,7 +650,7 @@ window.addEventListener('DOMContentLoaded',tryAddWFNodes)`
579
650
  href: `/actions/stepedit/${trigger.id}${
580
651
  initial_step ? "" : "?initial_step=true"
581
652
  }`,
582
- class: "btn btn-primary",
653
+ class: "btn btn-secondary",
583
654
  },
584
655
  i({ class: "fas fa-plus me-2" }),
585
656
  "Add step"
@@ -595,17 +666,13 @@ window.addEventListener('DOMContentLoaded',tryAddWFNodes)`
595
666
  );
596
667
  };
597
668
 
598
- const jsIdentifierValidator = (s) => {
599
- if (!s) return "An identifier is required";
600
- if (s.includes(" ")) return "Spaces not allowd";
601
- let badc = "'#:/\\@()[]{}\"!%^&*-+*~<>,.?|"
602
- .split("")
603
- .find((c) => s.includes(c));
604
-
605
- if (badc) return `Character ${badc} not allowed`;
606
- };
607
-
608
- const getWorkflowStepForm = async (trigger, req, step_id) => {
669
+ const getWorkflowStepForm = async (
670
+ trigger,
671
+ req,
672
+ step_id,
673
+ after_step,
674
+ before_step
675
+ ) => {
609
676
  const table = trigger.table_id ? Table.findOne(trigger.table_id) : null;
610
677
  const actionExplainers = {};
611
678
 
@@ -626,30 +693,34 @@ const getWorkflowStepForm = async (trigger, req, step_id) => {
626
693
  });
627
694
 
628
695
  for (const field of cfgFields) {
629
- const cfgFld = {
630
- ...field,
631
- showIf: {
632
- wf_action_name: name,
633
- ...(field.showIf || {}),
634
- },
635
- };
696
+ let cfgFld;
697
+ if (field.isRepeat)
698
+ cfgFld = new FieldRepeat({
699
+ ...field,
700
+ showIf: {
701
+ wf_action_name: name,
702
+ ...(field.showIf || {}),
703
+ },
704
+ });
705
+ else
706
+ cfgFld = {
707
+ ...field,
708
+ showIf: {
709
+ wf_action_name: name,
710
+ ...(field.showIf || {}),
711
+ },
712
+ };
636
713
  if (cfgFld.input_type === "code") cfgFld.input_type = "textarea";
637
714
  actionConfigFields.push(cfgFld);
638
715
  }
639
716
  } catch {}
640
717
  }
718
+ const builtInActionExplainers = WorkflowStep.builtInActionExplainers();
641
719
  const actionsNotRequiringRow = Trigger.action_options({
642
720
  notRequireRow: true,
643
721
  noMultiStep: true,
644
722
  builtInLabel: "Workflow Actions",
645
- builtIns: [
646
- "SetContext",
647
- "TableQuery",
648
- "Output",
649
- "WaitUntil",
650
- "WaitNextTick",
651
- "UserForm",
652
- ],
723
+ builtIns: Object.keys(builtInActionExplainers),
653
724
  forWorkflow: true,
654
725
  });
655
726
  const triggers = Trigger.find({
@@ -658,138 +729,13 @@ const getWorkflowStepForm = async (trigger, req, step_id) => {
658
729
  triggers.forEach((tr) => {
659
730
  if (tr.description) actionExplainers[tr.name] = tr.description;
660
731
  });
661
- actionExplainers.SetContext = "Set variables in the context";
662
- actionExplainers.TableQuery = "Query a table into a variable in the context";
663
- actionExplainers.Output =
664
- "Display a message to the user. Pause workflow until the message is read.";
665
- actionExplainers.WaitUntil = "Pause until a time in the future";
666
- actionExplainers.WaitNextTick =
667
- "Pause until the next scheduler invocation (at most 5 minutes)";
668
- actionExplainers.UserForm =
669
- "Ask a user one or more questions, pause until they are answered";
670
-
671
- actionConfigFields.push({
672
- label: "Form header",
673
- sublabel: "Text shown to the user at the top of the form",
674
- name: "form_header",
675
- type: "String",
676
- showIf: { wf_action_name: "UserForm" },
677
- });
678
- actionConfigFields.push({
679
- label: "User ID",
680
- name: "user_id_expression",
681
- type: "String",
682
- sublabel: "Optional. If blank assigned to user starting the workflow",
683
- showIf: { wf_action_name: "UserForm" },
684
- });
685
- actionConfigFields.push({
686
- label: "Resume at",
687
- name: "resume_at",
688
- sublabel:
689
- "JavaScript expression for the time to resume. <code>moment</code> is in scope.",
690
- type: "String",
691
- showIf: { wf_action_name: "WaitUntil" },
692
- });
693
- actionConfigFields.push({
694
- label: "Context values",
695
- name: "ctx_values",
696
- sublabel:
697
- "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",
698
- type: "String",
699
- fieldview: "textarea",
700
- class: "validate-expression",
701
- default: "{}",
702
- showIf: { wf_action_name: "SetContext" },
703
- });
704
- actionConfigFields.push({
705
- label: "Output text",
706
- name: "output_text",
707
- sublabel:
708
- "Message shown to the user. Can contain HTML tags and use interpolations {{ }} to access the context",
709
- type: "String",
710
- fieldview: "textarea",
711
- showIf: { wf_action_name: "Output" },
712
- });
713
- actionConfigFields.push({
714
- label: "Table",
715
- name: "query_table",
716
- type: "String",
717
- required: true,
718
- attributes: { options: (await Table.find()).map((t) => t.name) },
719
- showIf: { wf_action_name: "TableQuery" },
720
- });
721
- actionConfigFields.push({
722
- label: "Query",
723
- name: "query_object",
724
- sublabel: "Where object, example <code>{manager: 1}</code>",
725
- type: "String",
726
- required: true,
727
- class: "validate-expression",
728
- default: "{}",
729
- showIf: { wf_action_name: "TableQuery" },
730
- });
731
- actionConfigFields.push({
732
- label: "Variable",
733
- name: "query_variable",
734
- sublabel: "Context variable to write to query results to",
735
- type: "String",
736
- required: true,
737
- validator: jsIdentifierValidator,
738
- showIf: { wf_action_name: "TableQuery" },
739
- });
740
- actionConfigFields.push(
741
- new FieldRepeat({
742
- name: "user_form_questions",
743
- showIf: { wf_action_name: "UserForm" },
744
- fields: [
745
- {
746
- label: "Label",
747
- name: "label",
748
- type: "String",
749
- sublabel:
750
- "The text that will shown to the user above the input elements",
751
- },
752
- {
753
- label: "Variable name",
754
- name: "var_name",
755
- type: "String",
756
- sublabel:
757
- "The answer will be set in the context with this variable name",
758
- validator: jsIdentifierValidator,
759
- },
760
- {
761
- label: "Input Type",
762
- name: "qtype",
763
- type: "String",
764
- required: true,
765
- attributes: {
766
- options: [
767
- "Yes/No",
768
- "Checkbox",
769
- "Free text",
770
- "Multiple choice",
771
- //"Multiple checks",
772
- "Integer",
773
- "Float",
774
- //"File upload",
775
- ],
776
- },
777
- },
778
- {
779
- label: "Options",
780
- name: "options",
781
- type: "String",
782
- sublabel: "Comma separated list of multiple choice options",
783
- showIf: { qtype: ["Multiple choice", "Multiple checks"] },
784
- },
785
- ],
786
- })
787
- );
732
+ Object.assign(actionExplainers, builtInActionExplainers);
733
+ actionConfigFields.push(...(await WorkflowStep.builtInActionConfigFields()));
788
734
 
789
735
  const form = new Form({
790
736
  action: addOnDoneRedirect(`/actions/stepedit/${trigger.id}`, req),
791
- onChange: "saveAndContinueIfValid(this)",
792
- submitLabel: req.__("Done"),
737
+ onChange: step_id ? "saveAndContinueIfValid(this)" : undefined,
738
+ submitLabel: step_id ? req.__("Done") : undefined,
793
739
  additionalButtons: step_id
794
740
  ? [
795
741
  {
@@ -856,6 +802,9 @@ const getWorkflowStepForm = async (trigger, req, step_id) => {
856
802
  ],
857
803
  });
858
804
  form.hidden("wf_step_id");
805
+ form.hidden("_after_step");
806
+ if (before_step) form.values.wf_next_step = before_step;
807
+ if (after_step) form.values._after_step = after_step;
859
808
  if (step_id) {
860
809
  const step = await WorkflowStep.findOne({ id: step_id });
861
810
  if (!step) throw new Error("Step not found");
@@ -1366,9 +1315,15 @@ router.get(
1366
1315
  isAdmin,
1367
1316
  error_catcher(async (req, res) => {
1368
1317
  const { trigger_id, step_id } = req.params;
1369
- const { initial_step, name } = req.query;
1318
+ const { initial_step, after_step, before_step } = req.query;
1370
1319
  const trigger = await Trigger.findOne({ id: trigger_id });
1371
- const form = await getWorkflowStepForm(trigger, req, step_id);
1320
+ const form = await getWorkflowStepForm(
1321
+ trigger,
1322
+ req,
1323
+ step_id,
1324
+ after_step,
1325
+ before_step
1326
+ );
1372
1327
 
1373
1328
  if (initial_step) form.values.wf_initial_step = true;
1374
1329
  if (!step_id) {
@@ -1434,6 +1389,7 @@ router.post(
1434
1389
  wf_initial_step,
1435
1390
  wf_only_if,
1436
1391
  wf_step_id,
1392
+ _after_step,
1437
1393
  ...configuration
1438
1394
  } = form.values;
1439
1395
  Object.entries(configuration).forEach(([k, v]) => {
@@ -1469,6 +1425,13 @@ router.post(
1469
1425
  res.redirect(`/actions/configure/${step.trigger_id}`);
1470
1426
  }
1471
1427
  }
1428
+ if (_after_step && _after_step !== "undefined") {
1429
+ const astep = await WorkflowStep.findOne({
1430
+ id: _after_step,
1431
+ trigger_id,
1432
+ });
1433
+ if (astep) await astep.update({ next_step: step.name });
1434
+ }
1472
1435
  } catch (e) {
1473
1436
  const emsg =
1474
1437
  e.message ===
@@ -1484,6 +1447,28 @@ router.post(
1484
1447
  })
1485
1448
  );
1486
1449
 
1450
+ router.post(
1451
+ "/gen-copilot/:trigger_id",
1452
+ isAdmin,
1453
+ error_catcher(async (req, res) => {
1454
+ const { trigger_id } = req.params;
1455
+ const trigger = await Trigger.findOne({ id: trigger_id });
1456
+ await WorkflowStep.deleteForTrigger(trigger.id);
1457
+ const description = req.body.description;
1458
+ await Trigger.update(trigger.id, { description });
1459
+ const steps = await getState().functions.copilot_generate_workflow.run(
1460
+ description,
1461
+ trigger.id
1462
+ );
1463
+ if (steps.length) steps[0].initial_step = true;
1464
+ for (const step of steps) {
1465
+ step.trigger_id = trigger.id;
1466
+ await WorkflowStep.create(step);
1467
+ }
1468
+ res.redirect(`/actions/configure/${trigger.id}`);
1469
+ })
1470
+ );
1471
+
1487
1472
  router.post(
1488
1473
  "/delete-step/:step_id",
1489
1474
  isAdmin,
@@ -1514,6 +1499,10 @@ router.get(
1514
1499
  [
1515
1500
  { label: "Trigger", key: (run) => trNames[run.trigger_id] },
1516
1501
  { label: "Started", key: (run) => localeDateTime(run.started_at) },
1502
+ {
1503
+ label: "Updated",
1504
+ key: (run) => localeDateTime(run.status_updated_at),
1505
+ },
1517
1506
  { label: "Status", key: "status" },
1518
1507
  {
1519
1508
  label: "",
@@ -1698,10 +1687,12 @@ router.post(
1698
1687
  );
1699
1688
 
1700
1689
  const getWorkflowStepUserForm = async (run, trigger, step, req) => {
1690
+ let blurb = run.wait_info.output || step.configuration?.form_header || "";
1691
+ if (run.wait_info.markdown && run.wait_info.output) blurb = md.render(blurb);
1701
1692
  const form = new Form({
1702
1693
  action: `/actions/fill-workflow-form/${run.id}`,
1703
1694
  submitLabel: run.wait_info.output ? req.__("OK") : req.__("Submit"),
1704
- blurb: run.wait_info.output || step.configuration?.form_header || "",
1695
+ blurb,
1705
1696
  formStyle: run.wait_info.output || req.xhr ? "vert" : undefined,
1706
1697
  fields: await run.userFormFields(step),
1707
1698
  });
package/routes/utils.js CHANGED
@@ -107,6 +107,7 @@ const setLanguage = (req, res, state) => {
107
107
  } else if (req.cookies?.lang) {
108
108
  req.setLocale(req.cookies?.lang);
109
109
  }
110
+ if (req.user) Object.freeze(req.user);
110
111
  set_custom_http_headers(res, req, state);
111
112
  };
112
113
 
@@ -194,6 +194,43 @@ describe("render view with slug", () => {
194
194
  });
195
195
  });
196
196
 
197
+ describe("frozen user object", () => {
198
+ it("should create view writing to user object", async () => {
199
+ const table = Table.findOne({ name: "books" });
200
+ const configuration = {
201
+ layout: {
202
+ type: "container",
203
+ style: {},
204
+ contents: {
205
+ type: "blank",
206
+ contents: "'userid='+user.id",
207
+ isFormula: {
208
+ text: true,
209
+ },
210
+ },
211
+ showIfFormula: "user.id=1",
212
+ },
213
+ columns: [],
214
+ };
215
+ await View.create({
216
+ table_id: table.id,
217
+ name: "ShowBookWriteUser",
218
+ viewtemplate: "Show",
219
+ configuration,
220
+ min_role: 100,
221
+ });
222
+ });
223
+ it("should run view setting user", async () => {
224
+ const loginCookie = await getStaffLoginCookie();
225
+
226
+ const app = await getApp({ disableCsrf: true });
227
+ await request(app)
228
+ .get("/view/ShowBookWriteUser?id=1")
229
+ .set("Cookie", loginCookie)
230
+ .expect(toInclude(">userid=2</div>"));
231
+ });
232
+ });
233
+
197
234
  describe("action row_variable", () => {
198
235
  const createFilterView = async ({
199
236
  configuration,