@saltcorn/server 1.1.1-beta.1 → 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.
@@ -55,6 +55,8 @@ user verification is enabled.
55
55
 
56
56
  **Startup**: run this whenever this saltcorn process initializes. 
57
57
 
58
+ **AppChange**: the application build (views, pages, triggers etc.) changed
59
+
58
60
  ## Other events
59
61
 
60
62
  **Never**: this trigger is never run on its own. However triggers that are marked as never
package/locales/en.json CHANGED
@@ -1520,7 +1520,13 @@
1520
1520
  "OK": "OK",
1521
1521
  "Step settings": "Step settings",
1522
1522
  "Action settings": "Action settings",
1523
+ "Keystore file is not applied for debug builds.": "Keystore file is not applied for debug builds.",
1523
1524
  "Workflow": "Workflow",
1524
1525
  "Previous runs": "Previous runs",
1525
- "The workflow the user will be interacting with.": "The workflow the user will be interacting with."
1526
+ "The workflow the user will be interacting with.": "The workflow the user will be interacting with.",
1527
+ "Delete old workflow runs with status after days": "Delete old workflow runs with status after days",
1528
+ "Finished": "Finished",
1529
+ "Error": "Error",
1530
+ "Waiting": "Waiting",
1531
+ "Running": "Running"
1526
1532
  }
package/package.json CHANGED
@@ -1,20 +1,20 @@
1
1
  {
2
2
  "name": "@saltcorn/server",
3
- "version": "1.1.1-beta.1",
3
+ "version": "1.1.1-beta.2",
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.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",
10
+ "@saltcorn/base-plugin": "1.1.1-beta.2",
11
+ "@saltcorn/builder": "1.1.1-beta.2",
12
+ "@saltcorn/data": "1.1.1-beta.2",
13
+ "@saltcorn/admin-models": "1.1.1-beta.2",
14
+ "@saltcorn/filemanager": "1.1.1-beta.2",
15
+ "@saltcorn/markup": "1.1.1-beta.2",
16
+ "@saltcorn/plugins-loader": "1.1.1-beta.2",
17
+ "@saltcorn/sbadmin2": "1.1.1-beta.2",
18
18
  "@socket.io/cluster-adapter": "^0.2.1",
19
19
  "@socket.io/sticky": "^1.0.1",
20
20
  "adm-zip": "0.5.10",
@@ -65,7 +65,7 @@ function updateQueryStringParameters(uri1, kvs) {
65
65
  return uri;
66
66
  }
67
67
 
68
- function removeQueryStringParameter(uri1, key) {
68
+ function removeQueryStringParameter(uri1, key, value) {
69
69
  let hash = "";
70
70
  let uri = uri1;
71
71
  if (uri && uri.includes("#")) {
@@ -73,8 +73,12 @@ function removeQueryStringParameter(uri1, key) {
73
73
  hash = "#" + uris[1];
74
74
  uri = uris[0];
75
75
  }
76
-
77
- var re = new RegExp("([?&])" + key + "=.*?(&|$)", "gi");
76
+ let re;
77
+ if (value) {
78
+ re = new RegExp("([?&])" + key + "=" + value + "?(&|$)", "gi");
79
+ } else {
80
+ re = new RegExp("([?&])" + key + "=.*?(&|$)", "gi");
81
+ }
78
82
  if (uri.match(re)) {
79
83
  uri = uri.replace(re, "$1" + "$2");
80
84
  }
@@ -84,6 +88,28 @@ function removeQueryStringParameter(uri1, key) {
84
88
  return uri + hash;
85
89
  }
86
90
 
91
+ function addQueryStringParameter(uri1, key, value) {
92
+ let hash = "";
93
+ let uri = uri1;
94
+ if (uri && uri.includes("#")) {
95
+ let uris = uri1.split("#");
96
+ hash = "#" + uris[1];
97
+ uri = uris[0];
98
+ }
99
+ var re = new RegExp("([?&])" + key + "=" + value + "?(&|$)", "gi");
100
+ if (uri.match(re)) return uri1;
101
+
102
+ var separator = uri.indexOf("?") !== -1 ? "&" : "?";
103
+ if (Array.isArray(value))
104
+ return (
105
+ uri +
106
+ separator +
107
+ value.map((val) => key + "=" + encodeURIComponent(val)).join("&") +
108
+ hash
109
+ );
110
+ else return uri + separator + key + "=" + encodeURIComponent(value) + hash;
111
+ }
112
+
87
113
  function select_id(id, e) {
88
114
  pjax_to(updateQueryStringParameter(get_current_state_url(e), "id", id), e);
89
115
  }
@@ -96,10 +122,10 @@ function check_state_field(that, e) {
96
122
  const checked = that.checked;
97
123
  const name = that.name;
98
124
  const value = encodeURIComponent(that.value);
99
- var separator = get_current_state_url(e).indexOf("?") !== -1 ? "&" : "?";
100
125
  let dest;
101
- if (checked) dest = get_current_state_url(e) + `${separator}${name}=${value}`;
102
- else dest = get_current_state_url(e).replace(`${name}=${value}`, "");
126
+ if (checked)
127
+ dest = addQueryStringParameter(get_current_state_url(e), name, value);
128
+ else dest = removeQueryStringParameter(get_current_state_url(e), name, value);
103
129
  pjax_to(dest.replace("&&", "&").replace("?&", "?"), e);
104
130
  }
105
131
 
@@ -591,7 +617,7 @@ function updateViewPreview() {
591
617
  function ajaxSubmitForm(e, force_no_reload, event) {
592
618
  var form = $(e).closest("form");
593
619
  var url = form.attr("action");
594
- if(event) event.preventDefault();
620
+ if (event) event.preventDefault();
595
621
  $.ajax(url, {
596
622
  type: "POST",
597
623
  headers: {
@@ -606,10 +632,10 @@ function ajaxSubmitForm(e, force_no_reload, event) {
606
632
  "data-on-close-reload-view"
607
633
  );
608
634
  $("#scmodal").modal("hide");
609
- if (!force_no_reload && on_close_reload_view) {
635
+ if (on_close_reload_view) {
610
636
  const viewE = $(`[data-sc-embed-viewname="${on_close_reload_view}"]`);
611
637
  if (viewE.length) reload_embedded_view(on_close_reload_view);
612
- else location.reload();
638
+ else if (!force_no_reload) location.reload();
613
639
  } else if (!force_no_reload && !no_reload) location.reload();
614
640
  else common_done(res, form.attr("data-viewname"));
615
641
  },
@@ -859,10 +885,16 @@ function builderMenuChanged(e) {
859
885
  });
860
886
  }
861
887
 
862
- function poll_mobile_build_finished(outDirName, pollCount, orginalBtnHtml) {
888
+ function poll_mobile_build_finished(
889
+ outDirName,
890
+ buildDir,
891
+ mode,
892
+ pollCount,
893
+ orginalBtnHtml
894
+ ) {
863
895
  $.ajax("/admin/build-mobile-app/finished", {
864
896
  type: "GET",
865
- data: { build_dir: outDirName },
897
+ data: { out_dir_name: outDirName, mode: mode },
866
898
  success: function (res) {
867
899
  if (!res.finished) {
868
900
  if (pollCount >= 100) {
@@ -873,14 +905,45 @@ function poll_mobile_build_finished(outDirName, pollCount, orginalBtnHtml) {
873
905
  });
874
906
  } else {
875
907
  setTimeout(() => {
876
- poll_mobile_build_finished(outDirName, ++pollCount, orginalBtnHtml);
908
+ poll_mobile_build_finished(
909
+ outDirName,
910
+ buildDir,
911
+ mode,
912
+ ++pollCount,
913
+ orginalBtnHtml
914
+ );
877
915
  }, 5000);
878
916
  }
879
917
  } else {
880
918
  href_to(
881
- `build-mobile-app/result?build_dir_name=${encodeURIComponent(
919
+ `/admin/build-mobile-app/result?out_dir_name=${encodeURIComponent(
882
920
  outDirName
883
- )}`
921
+ )}&build_dir=${encodeURIComponent(buildDir)}&mode=${mode}`
922
+ );
923
+ }
924
+ },
925
+ });
926
+ }
927
+
928
+ function finish_mobile_app(button, outDirName, buildDir) {
929
+ $.ajax("/admin/build-mobile-app/finish", {
930
+ type: "POST",
931
+ headers: {
932
+ "CSRF-Token": _sc_globalCsrf,
933
+ },
934
+ data: { out_dir_name: outDirName, build_dir: buildDir },
935
+ success: function (data) {
936
+ if (data.success) {
937
+ notifyAlert("Finishing the app, please wait.", true);
938
+ for (const msg of data.msgs || []) notifyAlert(msg);
939
+ const orginalBtnHtml = $("#finishMobileAppBtnId").html();
940
+ press_store_button(button);
941
+ poll_mobile_build_finished(
942
+ outDirName,
943
+ buildDir,
944
+ "finish",
945
+ 0,
946
+ orginalBtnHtml
884
947
  );
885
948
  }
886
949
  },
@@ -938,11 +1001,18 @@ function build_mobile_app(button) {
938
1001
  ajax_post("/admin/build-mobile-app", {
939
1002
  data: params,
940
1003
  success: (data) => {
941
- if (data.build_dir_name) {
942
- handleMessages();
1004
+ if (data.out_dir_name && data.build_dir) {
1005
+ notifyAlert("Building the app, please wait.", true);
1006
+ for (const msg of data.msgs || []) notifyAlert(msg);
943
1007
  const orginalBtnHtml = $("#buildMobileAppBtnId").html();
944
1008
  press_store_button(button);
945
- poll_mobile_build_finished(data.build_dir_name, 0, orginalBtnHtml);
1009
+ poll_mobile_build_finished(
1010
+ data.out_dir_name,
1011
+ data.build_dir,
1012
+ data.mode,
1013
+ 0,
1014
+ orginalBtnHtml
1015
+ );
946
1016
  }
947
1017
  },
948
1018
  });
package/routes/actions.js CHANGED
@@ -14,6 +14,10 @@ const {
14
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;
@@ -533,9 +545,11 @@ function genWorkflowDiagram(steps) {
533
545
  }
534
546
  if (step.action_name === "ForLoop") {
535
547
  linkLines.push(
536
- ` ${step.mmname}-.->${WorkflowStep.mmescape(step.configuration.loop_body_initial_step)}`
548
+ ` ${step.mmname}-.->${WorkflowStep.mmescape(
549
+ step.configuration.loop_body_initial_step
550
+ )}`
537
551
  );
538
- }
552
+ }
539
553
  if (step.action_name === "EndForLoop") {
540
554
  // TODO this is not correct. improve.
541
555
  let forStep;
@@ -711,10 +725,20 @@ const getWorkflowStepForm = async (
711
725
  },
712
726
  };
713
727
  if (cfgFld.input_type === "code") cfgFld.input_type = "textarea";
714
- actionConfigFields.push(cfgFld);
715
728
  }
716
729
  } catch {}
717
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
+
718
742
  const builtInActionExplainers = WorkflowStep.builtInActionExplainers();
719
743
  const actionsNotRequiringRow = Trigger.action_options({
720
744
  notRequireRow: true,
@@ -730,7 +754,9 @@ const getWorkflowStepForm = async (
730
754
  if (tr.description) actionExplainers[tr.name] = tr.description;
731
755
  });
732
756
  Object.assign(actionExplainers, builtInActionExplainers);
733
- actionConfigFields.push(...(await WorkflowStep.builtInActionConfigFields()));
757
+ actionConfigFields.push(
758
+ ...(await WorkflowStep.builtInActionConfigFields({ trigger }))
759
+ );
734
760
 
735
761
  const form = new Form({
736
762
  action: addOnDoneRedirect(`/actions/stepedit/${trigger.id}`, req),
@@ -1142,6 +1168,10 @@ router.post(
1142
1168
  await Trigger.update(trigger.id, {
1143
1169
  configuration: { ...trigger.configuration, ...form.values },
1144
1170
  });
1171
+ Trigger.emitEvent("AppChange", `Trigger ${trigger.name}`, req.user, {
1172
+ entity_type: "Trigger",
1173
+ entity_name: trigger.name,
1174
+ });
1145
1175
  if (req.xhr) {
1146
1176
  res.json({ success: "ok" });
1147
1177
  return;
@@ -1168,6 +1198,10 @@ router.post(
1168
1198
  error_catcher(async (req, res) => {
1169
1199
  const { id } = req.params;
1170
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
+ });
1171
1205
  await trigger.delete();
1172
1206
  res.redirect(`/actions/`);
1173
1207
  })
@@ -1296,6 +1330,10 @@ router.post(
1296
1330
  const { id } = req.params;
1297
1331
  const trig = await Trigger.findOne({ id });
1298
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
+ });
1299
1337
  req.flash(
1300
1338
  "success",
1301
1339
  req.__("Trigger %s duplicated as %s", trig.name, newtrig.name)
@@ -1425,6 +1463,10 @@ router.post(
1425
1463
  res.redirect(`/actions/configure/${step.trigger_id}`);
1426
1464
  }
1427
1465
  }
1466
+ Trigger.emitEvent("AppChange", `Trigger ${trigger.name}`, req.user, {
1467
+ entity_type: "Trigger",
1468
+ entity_name: trigger.name,
1469
+ });
1428
1470
  if (_after_step && _after_step !== "undefined") {
1429
1471
  const astep = await WorkflowStep.findOne({
1430
1472
  id: _after_step,
@@ -1465,6 +1507,10 @@ router.post(
1465
1507
  step.trigger_id = trigger.id;
1466
1508
  await WorkflowStep.create(step);
1467
1509
  }
1510
+ Trigger.emitEvent("AppChange", `Trigger ${trigger.name}`, req.user, {
1511
+ entity_type: "Trigger",
1512
+ entity_name: trigger.name,
1513
+ });
1468
1514
  res.redirect(`/actions/configure/${trigger.id}`);
1469
1515
  })
1470
1516
  );
@@ -1687,6 +1733,21 @@ router.post(
1687
1733
  );
1688
1734
 
1689
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
+
1690
1751
  let blurb = run.wait_info.output || step.configuration?.form_header || "";
1691
1752
  if (run.wait_info.markdown && run.wait_info.output) blurb = md.render(blurb);
1692
1753
  const form = new Form({
@@ -1819,12 +1880,10 @@ WORKFLOWS TODO
1819
1880
 
1820
1881
  help file to explain steps, and context
1821
1882
 
1822
- workflow actions: ForLoop, EndForLoop, ReadFile, WriteFile, APIResponse
1883
+ workflow actions: Stop, RunEditView, ReadFile, WriteFile, APIResponse
1823
1884
 
1824
- Error handlers
1825
1885
  other triggers can be steps
1826
1886
  interactive workflows for not logged in
1827
- show end node in diagram
1828
1887
  actions can declare which variables they inject into scope
1829
1888
 
1830
1889
  show unconnected steps
package/routes/admin.js CHANGED
@@ -19,6 +19,7 @@ const Plugin = require("@saltcorn/data/models/plugin");
19
19
  const File = require("@saltcorn/data/models/file");
20
20
  const { spawn, exec } = require("child_process");
21
21
  const User = require("@saltcorn/data/models/user");
22
+ const Trigger = require("@saltcorn/data/models/trigger");
22
23
  const path = require("path");
23
24
  const { X509Certificate } = require("crypto");
24
25
  const { getAllTenants } = require("@saltcorn/admin-models/models/tenant");
@@ -146,6 +147,24 @@ const app_files_table = (files, buildDirName, req) =>
146
147
  ],
147
148
  files
148
149
  );
150
+ const intermediate_build_result = (outDirName, buildDir, req) => {
151
+ return div(
152
+ h3("Intermediate build result"),
153
+ div(
154
+ button(
155
+ {
156
+ id: "finishMobileAppBtnId",
157
+ type: "button",
158
+ onClick: `finish_mobile_app(this, '${outDirName}', '${buildDir}');`,
159
+ class: "btn btn-warning",
160
+ },
161
+ i({ class: "fas fa-hammer pe-2" }),
162
+
163
+ req.__("Finish the build")
164
+ )
165
+ )
166
+ );
167
+ };
149
168
 
150
169
  admin_config_route({
151
170
  router,
@@ -1993,9 +2012,6 @@ const buildDialogScript = (capacitorBuilderAvailable, isSbadmin2) =>
1993
2012
  $("#entryPointTypeID").attr("value", type);
1994
2013
  }
1995
2014
 
1996
- function handleMessages() {
1997
- notifyAlert("Building the app, please wait.", true)
1998
- }
1999
2015
  const versionPattern = /^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$/;
2000
2016
  ${domReady(`
2001
2017
  const versionInput = document.getElementById('appVersionInputId');
@@ -3077,6 +3093,48 @@ router.get(
3077
3093
  )
3078
3094
  )
3079
3095
  )
3096
+ // Share Extension provisioning profile
3097
+ // disabled for now
3098
+ // div(
3099
+ // { class: "row pb-3" },
3100
+ // div(
3101
+ // { class: "col-sm-8" },
3102
+ // label(
3103
+ // {
3104
+ // for: "shareProvisioningProfileInputId",
3105
+ // class: "form-label fw-bold",
3106
+ // },
3107
+ // req.__("Share Extension Provisioning Profile"),
3108
+ // a(
3109
+ // {
3110
+ // href: "javascript:ajax_modal('/admin/help/Provisioning Profile?')",
3111
+ // },
3112
+ // i({ class: "fas fa-question-circle ps-1" })
3113
+ // )
3114
+ // ),
3115
+ // select(
3116
+ // {
3117
+ // class: "form-select",
3118
+ // name: "shareProvisioningProfile",
3119
+ // id: "shareProvisioningProfileInputId",
3120
+ // },
3121
+ // [
3122
+ // option({ value: "" }, ""),
3123
+ // ...provisioningFiles.map((file) =>
3124
+ // option(
3125
+ // {
3126
+ // value: file.location,
3127
+ // selected:
3128
+ // builderSettings.shareProvisioningProfile ===
3129
+ // file.location,
3130
+ // },
3131
+ // file.filename
3132
+ // )
3133
+ // ),
3134
+ // ].join("")
3135
+ // )
3136
+ // )
3137
+ // )
3080
3138
  )
3081
3139
  )
3082
3140
  ),
@@ -3100,15 +3158,13 @@ router.get(
3100
3158
  })
3101
3159
  );
3102
3160
 
3103
- const checkFiles = async (outDir, fileNames) => {
3161
+ const checkFiles = async (outDirName, fileNames) => {
3104
3162
  const rootFolder = await File.rootFolder();
3105
- const mobile_app_dir = path.join(rootFolder.location, "mobile_app", outDir);
3163
+ const outDir = path.join(rootFolder.location, "mobile_app", outDirName);
3106
3164
  const unsafeFiles = await Promise.all(
3107
3165
  fs
3108
- .readdirSync(mobile_app_dir)
3109
- .map(
3110
- async (outFile) => await File.from_file_on_disk(outFile, mobile_app_dir)
3111
- )
3166
+ .readdirSync(outDir)
3167
+ .map(async (outFile) => await File.from_file_on_disk(outFile, outDir))
3112
3168
  );
3113
3169
  const entries = unsafeFiles
3114
3170
  .filter(
@@ -3127,9 +3183,18 @@ router.get(
3127
3183
  "/build-mobile-app/finished",
3128
3184
  isAdmin,
3129
3185
  error_catcher(async (req, res) => {
3130
- const { build_dir } = req.query;
3186
+ const { out_dir_name, mode } = req.query;
3187
+ const stepDesc =
3188
+ mode === "prepare"
3189
+ ? "_prepare_step"
3190
+ : mode === "finish"
3191
+ ? "_finish_step"
3192
+ : "";
3131
3193
  res.json({
3132
- finished: await checkFiles(build_dir, ["logs.txt", "error_logs.txt"]),
3194
+ finished: await checkFiles(out_dir_name, [
3195
+ `logs${stepDesc}.txt`,
3196
+ `error_logs${stepDesc}.txt`,
3197
+ ]),
3133
3198
  });
3134
3199
  })
3135
3200
  );
@@ -3164,8 +3229,8 @@ router.get(
3164
3229
  "/build-mobile-app/result",
3165
3230
  isAdmin,
3166
3231
  error_catcher(async (req, res) => {
3167
- const { build_dir_name } = req.query;
3168
- if (!validateBuildDirName(build_dir_name)) {
3232
+ const { out_dir_name, build_dir, mode } = req.query;
3233
+ if (!validateBuildDirName(out_dir_name)) {
3169
3234
  return res.sendWrap(req.__(`Admin`), {
3170
3235
  above: [
3171
3236
  {
@@ -3177,11 +3242,7 @@ router.get(
3177
3242
  });
3178
3243
  }
3179
3244
  const rootFolder = await File.rootFolder();
3180
- const buildDir = path.join(
3181
- rootFolder.location,
3182
- "mobile_app",
3183
- build_dir_name
3184
- );
3245
+ const buildDir = path.join(rootFolder.location, "mobile_app", out_dir_name);
3185
3246
  if (!validateBuildDir(buildDir, rootFolder.location)) {
3186
3247
  return res.sendWrap(req.__(`Admin`), {
3187
3248
  above: [
@@ -3199,7 +3260,15 @@ router.get(
3199
3260
  .readdirSync(buildDir)
3200
3261
  .map(async (outFile) => await File.from_file_on_disk(outFile, buildDir))
3201
3262
  );
3202
- const resultMsg = files.find((file) => file.filename === "logs.txt")
3263
+ const stepDesc =
3264
+ mode === "prepare"
3265
+ ? "_prepare_step"
3266
+ : mode === "finish"
3267
+ ? "_finish_step"
3268
+ : "";
3269
+ const resultMsg = files.find(
3270
+ (file) => file.filename === `logs${stepDesc}.txt`
3271
+ )
3203
3272
  ? req.__("The build was successfully")
3204
3273
  : req.__("Unable to build the app");
3205
3274
  res.sendWrap(req.__(`Admin`), {
@@ -3209,11 +3278,98 @@ router.get(
3209
3278
  title: req.__("Build Result"),
3210
3279
  contents: div(resultMsg),
3211
3280
  },
3212
- files.length > 0 ? app_files_table(files, build_dir_name, req) : "",
3281
+ files.length > 0 ? app_files_table(files, out_dir_name, req) : "",
3282
+ mode === "prepare"
3283
+ ? intermediate_build_result(out_dir_name, build_dir, req)
3284
+ : "",
3213
3285
  ],
3214
3286
  });
3215
3287
  })
3216
3288
  );
3289
+
3290
+ router.post(
3291
+ "/build-mobile-app/finish",
3292
+ isAdmin,
3293
+ error_catcher(async (req, res) => {
3294
+ const { out_dir_name, build_dir } = req.body;
3295
+ const content = await fs.promises.readFile(
3296
+ path.join(build_dir, "spawnParams.json")
3297
+ );
3298
+ const spawnParams = JSON.parse(content);
3299
+ const rootFolder = await File.rootFolder();
3300
+ const outDirFullPath = path.join(
3301
+ rootFolder.location,
3302
+ "mobile_app",
3303
+ out_dir_name
3304
+ );
3305
+ res.json({
3306
+ success: true,
3307
+ });
3308
+ const child = spawn(
3309
+ getSafeSaltcornCmd(),
3310
+ [...spawnParams, "-m", "finish"],
3311
+ {
3312
+ stdio: ["ignore", "pipe", "pipe"],
3313
+ cwd: ".",
3314
+ }
3315
+ );
3316
+ const childOutputs = [];
3317
+ child.stdout.on("data", (data) => {
3318
+ const outMsg = data.toString();
3319
+ getState().log(5, outMsg);
3320
+ if (data) childOutputs.push(outMsg);
3321
+ });
3322
+ child.stderr.on("data", (data) => {
3323
+ const errMsg = data ? data.toString() : req.__("An error occurred");
3324
+ getState().log(5, errMsg);
3325
+ childOutputs.push(errMsg);
3326
+ });
3327
+ child.on("exit", async (exitCode, signal) => {
3328
+ const logFile =
3329
+ exitCode === 0 ? "logs_finish_step.txt" : "error_logs_finish_step.txt";
3330
+ try {
3331
+ const exitMsg = childOutputs.join("\n");
3332
+ await fs.promises.writeFile(
3333
+ path.join(outDirFullPath, logFile),
3334
+ exitMsg
3335
+ );
3336
+ await File.set_xattr_of_existing_file(
3337
+ logFile,
3338
+ outDirFullPath,
3339
+ req.user
3340
+ );
3341
+ } catch (error) {
3342
+ console.log(`unable to write '${logFile}' to '${outDirFullPath}'`);
3343
+ console.log(error);
3344
+ }
3345
+ });
3346
+ child.on("error", (msg) => {
3347
+ const message = msg.message ? msg.message : msg.code;
3348
+ const stack = msg.stack ? msg.stack : "";
3349
+ const logFile = "error_logs.txt";
3350
+ const errMsg = [message, stack].join("\n");
3351
+ getState().log(5, msg);
3352
+ fs.writeFile(
3353
+ path.join(outDirFullPath, "error_logs.txt"),
3354
+ errMsg,
3355
+ async (error) => {
3356
+ if (error) {
3357
+ console.log(`unable to write logFile to '${outDirFullPath}'`);
3358
+ console.log(error);
3359
+ } else {
3360
+ // no transaction, '/build-mobile-app/finished' filters for valid attributes
3361
+ await File.set_xattr_of_existing_file(
3362
+ logFile,
3363
+ outDirFullPath,
3364
+ req.user
3365
+ );
3366
+ }
3367
+ }
3368
+ );
3369
+ });
3370
+ })
3371
+ );
3372
+
3217
3373
  /**
3218
3374
  * Do Build Mobile App
3219
3375
  */
@@ -3222,6 +3378,8 @@ router.post(
3222
3378
  isAdmin,
3223
3379
  error_catcher(async (req, res) => {
3224
3380
  getState().log(2, `starting mobile build: ${JSON.stringify(req.body)}`);
3381
+ const msgs = [];
3382
+ let mode = "full";
3225
3383
  let {
3226
3384
  entryPoint,
3227
3385
  entryPointType,
@@ -3239,11 +3397,27 @@ router.post(
3239
3397
  synchedTables,
3240
3398
  includedPlugins,
3241
3399
  provisioningProfile,
3400
+ shareProvisioningProfile,
3242
3401
  buildType,
3243
3402
  keystoreFile,
3244
3403
  keystoreAlias,
3245
3404
  keystorePassword,
3246
3405
  } = req.body;
3406
+ // const receiveShareTriggers = Trigger.find({
3407
+ // when_trigger: "ReceiveMobileShareData",
3408
+ // });
3409
+ // disabeling share to support for now
3410
+ let allowShareTo = false; // receiveShareTriggers.length > 0;
3411
+ if (allowShareTo && iOSPlatform && !shareProvisioningProfile) {
3412
+ allowShareTo = false;
3413
+ msgs.push({
3414
+ type: "warning",
3415
+ text: req.__(
3416
+ "A ReceiveMobileShareData trigger exists, but no Share Extension Provisioning Profile is provided. " +
3417
+ "Building without share to support."
3418
+ ),
3419
+ });
3420
+ }
3247
3421
  if (!includedPlugins) includedPlugins = [];
3248
3422
  if (!synchedTables) synchedTables = [];
3249
3423
  if (!entryPoint) {
@@ -3279,14 +3453,26 @@ router.post(
3279
3453
  ),
3280
3454
  });
3281
3455
  }
3282
- if (iOSPlatform && !provisioningProfile) {
3283
- return res.json({
3284
- error: req.__(
3285
- "Please provide a Provisioning Profile for the iOS build."
3286
- ),
3456
+ if (iOSPlatform) {
3457
+ if (!provisioningProfile)
3458
+ return res.json({
3459
+ error: req.__(
3460
+ "Please provide a Provisioning Profile for the iOS build."
3461
+ ),
3462
+ });
3463
+ }
3464
+ if (buildType === "debug" && keystoreFile) {
3465
+ msgs.push({
3466
+ type: "warning",
3467
+ text: req.__("Keystore file is not applied for debug builds."),
3287
3468
  });
3288
3469
  }
3289
- if (keystoreFile && (!keystoreAlias || !keystorePassword)) {
3470
+
3471
+ if (
3472
+ buildType === "release" &&
3473
+ keystoreFile &&
3474
+ (!keystoreAlias || !keystorePassword)
3475
+ ) {
3290
3476
  return res.json({
3291
3477
  error: req.__(
3292
3478
  "Please provide the keystore alias and password for the android build."
@@ -3294,8 +3480,9 @@ router.post(
3294
3480
  });
3295
3481
  }
3296
3482
  const outDirName = `build_${new Date().valueOf()}`;
3483
+ const buildDir = `${os.userInfo().homedir}/mobile_app_build`;
3297
3484
  const rootFolder = await File.rootFolder();
3298
- const buildDir = path.join(rootFolder.location, "mobile_app", outDirName);
3485
+ const outDir = path.join(rootFolder.location, "mobile_app", outDirName);
3299
3486
  await File.new_folder(outDirName, "/mobile_app");
3300
3487
  const spawnParams = [
3301
3488
  "build-app",
@@ -3304,9 +3491,9 @@ router.post(
3304
3491
  "-t",
3305
3492
  entryPointType === "pagegroup" ? "page" : entryPointType,
3306
3493
  "-c",
3307
- buildDir,
3494
+ outDir,
3308
3495
  "-b",
3309
- `${os.userInfo().homedir}/mobile_app_build`,
3496
+ buildDir,
3310
3497
  "-u",
3311
3498
  req.user.email, // ensured by isAdmin
3312
3499
  ];
@@ -3319,6 +3506,13 @@ router.post(
3319
3506
  "--provisioningProfile",
3320
3507
  provisioningProfile
3321
3508
  );
3509
+ if (allowShareTo) {
3510
+ mode = "prepare";
3511
+ spawnParams.push(
3512
+ "--shareExtensionProvisioningProfile",
3513
+ shareProvisioningProfile
3514
+ );
3515
+ }
3322
3516
  }
3323
3517
  if (appName) spawnParams.push("--appName", appName);
3324
3518
  if (appId) spawnParams.push("--appId", appId);
@@ -3327,6 +3521,7 @@ router.post(
3327
3521
  if (serverURL) spawnParams.push("-s", serverURL);
3328
3522
  if (splashPage) spawnParams.push("--splashPage", splashPage);
3329
3523
  if (allowOfflineMode) spawnParams.push("--allowOfflineMode");
3524
+ if (allowShareTo) spawnParams.push("--allowShareTo");
3330
3525
  if (autoPublicLogin) spawnParams.push("--autoPublicLogin");
3331
3526
  if (synchedTables.length > 0)
3332
3527
  spawnParams.push("--synchedTables", ...synchedTables.map((tbl) => tbl));
@@ -3348,10 +3543,15 @@ router.post(
3348
3543
  spawnParams.push("--androidKeyStoreAlias", keystoreAlias);
3349
3544
  if (keystorePassword)
3350
3545
  spawnParams.push("--androidKeystorePassword", keystorePassword);
3351
- // end http call, return the out directory name
3546
+ // end http call, return the out directory name, the build directory path and the mode
3352
3547
  // the gui polls for results
3353
- res.json({ build_dir_name: outDirName });
3354
- const child = spawn(getSafeSaltcornCmd(), spawnParams, {
3548
+ res.json({
3549
+ out_dir_name: outDirName,
3550
+ build_dir: buildDir,
3551
+ mode: mode,
3552
+ msgs,
3553
+ });
3554
+ const child = spawn(getSafeSaltcornCmd(), [...spawnParams, "-m", mode], {
3355
3555
  stdio: ["ignore", "pipe", "pipe"],
3356
3556
  cwd: ".",
3357
3557
  });
@@ -3366,18 +3566,29 @@ router.post(
3366
3566
  getState().log(5, errMsg);
3367
3567
  childOutputs.push(errMsg);
3368
3568
  });
3369
- child.on("exit", (exitCode, signal) => {
3370
- const logFile = exitCode === 0 ? "logs.txt" : "error_logs.txt";
3371
- const exitMsg = childOutputs.join("\n");
3372
- fs.writeFile(path.join(buildDir, logFile), exitMsg, async (error) => {
3373
- if (error) {
3374
- console.log(`unable to write '${logFile}' to '${buildDir}'`);
3569
+ child.on("exit", async (exitCode, signal) => {
3570
+ if (mode === "prepare" && exitCode === 0) {
3571
+ try {
3572
+ fs.promises.writeFile(
3573
+ path.join(buildDir, "spawnParams.json"),
3574
+ JSON.stringify(spawnParams)
3575
+ );
3576
+ } catch (error) {
3577
+ console.log(`unable to write spawnParams to '${buildDir}'`);
3375
3578
  console.log(error);
3376
- } else {
3377
- // no transaction, '/build-mobile-app/finished' filters for valid attributes
3378
- await File.set_xattr_of_existing_file(logFile, buildDir, req.user);
3379
3579
  }
3380
- });
3580
+ }
3581
+ const stepDesc = mode === "prepare" ? "_prepare_step" : "";
3582
+ const logFile =
3583
+ exitCode === 0 ? `logs${stepDesc}.txt` : `error_logs${stepDesc}.txt`;
3584
+ try {
3585
+ const exitMsg = childOutputs.join("\n");
3586
+ await fs.promises.writeFile(path.join(outDir, logFile), exitMsg);
3587
+ await File.set_xattr_of_existing_file(logFile, outDir, req.user);
3588
+ } catch (error) {
3589
+ console.log(`unable to write '${logFile}' to '${outDir}'`);
3590
+ console.log(error);
3591
+ }
3381
3592
  });
3382
3593
  child.on("error", (msg) => {
3383
3594
  const message = msg.message ? msg.message : msg.code;
@@ -3386,15 +3597,15 @@ router.post(
3386
3597
  const errMsg = [message, stack].join("\n");
3387
3598
  getState().log(5, msg);
3388
3599
  fs.writeFile(
3389
- path.join(buildDir, "error_logs.txt"),
3600
+ path.join(outDir, "error_logs.txt"),
3390
3601
  errMsg,
3391
3602
  async (error) => {
3392
3603
  if (error) {
3393
- console.log(`unable to write logFile to '${buildDir}'`);
3604
+ console.log(`unable to write logFile to '${outDir}'`);
3394
3605
  console.log(error);
3395
3606
  } else {
3396
3607
  // no transaction, '/build-mobile-app/finished' filters for valid attributes
3397
- await File.set_xattr_of_existing_file(logFile, buildDir, req.user);
3608
+ await File.set_xattr_of_existing_file(logFile, outDir, req.user);
3398
3609
  }
3399
3610
  }
3400
3611
  );
@@ -3462,7 +3673,7 @@ router.post(
3462
3673
  .filter(
3463
3674
  (plugin) =>
3464
3675
  ["base", "sbadmin2"].indexOf(plugin.name) < 0 &&
3465
- newCfg.includedPlugins.indexOf(plugin.name) < 0
3676
+ (newCfg.includedPlugins || []).indexOf(plugin.name) < 0
3466
3677
  )
3467
3678
  .map((plugin) => plugin.name);
3468
3679
  newCfg.excludedPlugins = excludedPlugins;
package/routes/fields.js CHANGED
@@ -12,6 +12,7 @@ const { getState } = require("@saltcorn/data/db/state");
12
12
  const { renderForm } = require("@saltcorn/markup");
13
13
  const Field = require("@saltcorn/data/models/field");
14
14
  const Table = require("@saltcorn/data/models/table");
15
+ const Trigger = require("@saltcorn/data/models/trigger");
15
16
  const Form = require("@saltcorn/data/models/form");
16
17
  const Workflow = require("@saltcorn/data/models/workflow");
17
18
  const User = require("@saltcorn/data/models/user");
@@ -306,6 +307,10 @@ const fieldFlow = (req) =>
306
307
  }
307
308
 
308
309
  await field.update(fldRow);
310
+ Trigger.emitEvent("AppChange", `Field ${fldRow.name}`, req.user, {
311
+ entity_type: "Field",
312
+ entity_name: fldRow.name || fldRow.label,
313
+ });
309
314
  } catch (e) {
310
315
  return {
311
316
  redirect: `/table/${context.table_id}`,
@@ -315,6 +320,10 @@ const fieldFlow = (req) =>
315
320
  } else {
316
321
  try {
317
322
  await Field.create(fldRow);
323
+ Trigger.emitEvent("AppChange", `Field ${fldRow.name}`, req.user, {
324
+ entity_type: "Field",
325
+ entity_name: fldRow.name || fldRow.label,
326
+ });
318
327
  } catch (e) {
319
328
  return {
320
329
  redirect: `/table/${context.table_id}`,
package/routes/menu.js CHANGED
@@ -571,6 +571,7 @@ router.post(
571
571
  const new_menu = req.body;
572
572
  const menu_items = jQMEtoMenu(new_menu);
573
573
  await save_menu_items(menu_items);
574
+ Trigger.emitEvent("AppChange", `Menu`, req.user, {});
574
575
 
575
576
  res.json({ success: true });
576
577
  })
@@ -199,23 +199,31 @@ router.post(
199
199
  error_catcher(async (req, res) => {
200
200
  const role = req.user?.role_id || 100;
201
201
  if (role === 100) {
202
- req.flash("error", req.__("You must be logged in to share"));
203
- res.redirect("/auth/login");
202
+ const msg = req.__("You must be logged in to share");
203
+ if (!req.smr) {
204
+ req.flash("error", msg);
205
+ res.redirect("/auth/login");
206
+ } else res.json({ error: msg });
204
207
  } else if (!getState().getConfig("pwa_share_to_enabled", false)) {
205
- req.flash("error", req.__("Sharing not enabled"));
206
- res.redirect("/");
208
+ const msg = req.__("Sharing not enabled");
209
+ if (!req.smr) {
210
+ req.flash("error", msg);
211
+ res.redirect("/");
212
+ } else res.json({ error: msg });
207
213
  } else {
208
214
  Trigger.emitEvent("ReceiveMobileShareData", null, req.user, {
209
215
  row: req.body,
210
216
  });
211
- req.flash(
212
- "success",
213
- req.__(
214
- "Shared: %s",
215
- req.body.title || req.body.text || req.body.url || ""
216
- )
217
- );
218
- res.status(303).redirect("/");
217
+ if (!req.smr) {
218
+ req.flash(
219
+ "success",
220
+ req.__(
221
+ "Shared: %s",
222
+ req.body.title || req.body.text || req.body.url || ""
223
+ )
224
+ );
225
+ res.status(303).redirect("/");
226
+ } else res.json({ success: "ok" });
219
227
  }
220
228
  })
221
229
  );
package/routes/page.js CHANGED
@@ -68,13 +68,13 @@ const runPage = async (page, req, res, tic) => {
68
68
  no_menu: page.attributes?.no_menu,
69
69
  requestFluidLayout: page.attributes?.request_fluid_layout,
70
70
  } || `${page.name} page`,
71
- add_edit_bar({
71
+ req.smr ? contents : add_edit_bar({
72
72
  role,
73
73
  title: page.name,
74
74
  what: req.__("Page"),
75
75
  url: `/pageedit/edit/${encodeURIComponent(page.name)}`,
76
76
  contents,
77
- })
77
+ }),
78
78
  );
79
79
  } else {
80
80
  getState().log(2, `Page ${page.name} not authorized`);
@@ -146,7 +146,9 @@ const pagePropertiesForm = async (req, isNew) => {
146
146
  {
147
147
  name: "request_fluid_layout",
148
148
  label: req.__("Fluid layout"),
149
- sublabel: req.__("Request fluid layout from theme for a wider display for this page"),
149
+ sublabel: req.__(
150
+ "Request fluid layout from theme for a wider display for this page"
151
+ ),
150
152
  type: "Bool",
151
153
  },
152
154
  ],
@@ -503,12 +505,21 @@ router.post(
503
505
  pageRow.layout = {};
504
506
  }
505
507
  await Page.update(+id, pageRow);
508
+ Trigger.emitEvent("AppChange", `Page ${dbPage.name}`, req.user, {
509
+ entity_type: "Page",
510
+ entity_name: dbPage.name,
511
+ });
506
512
  if (req.xhr) res.json({ success: "ok" });
507
513
  else res.redirect(`/pageedit/`);
508
514
  } else {
509
515
  if (!pageRow.layout) pageRow.layout = {};
510
516
  if (!pageRow.fixed_states) pageRow.fixed_states = {};
511
517
  await Page.create(pageRow);
518
+ Trigger.emitEvent("AppChange", `Page ${pageRow.name}`, req.user, {
519
+ entity_type: "Page",
520
+ entity_name: pageRow.name,
521
+ });
522
+
512
523
  if (!html_file)
513
524
  res.redirect(
514
525
  addOnDoneRedirect(`/pageedit/edit/${pageRow.name}`, req)
@@ -679,6 +690,10 @@ router.post(
679
690
  await Page.update(page.id, {
680
691
  layout: decodeURIComponent(req.body.layout),
681
692
  });
693
+ Trigger.emitEvent("AppChange", `Page ${page.name}`, req.user, {
694
+ entity_type: "Page",
695
+ entity_name: page.name,
696
+ });
682
697
  req.flash("success", req.__(`Page %s saved`, pagename));
683
698
  res.redirect(redirectTarget);
684
699
  } else if (req.body.code) {
@@ -687,6 +702,10 @@ router.post(
687
702
  const file = await File.findOne(page.html_file);
688
703
  if (!file) throw new Error(req.__("File not found"));
689
704
  await fsp.writeFile(file.location, req.body.code);
705
+ Trigger.emitEvent("AppChange", `Page ${page.name}`, req.user, {
706
+ entity_type: "Page",
707
+ entity_name: page.name,
708
+ });
690
709
  if (!req.xhr) {
691
710
  req.flash("success", req.__(`Page %s saved`, pagename));
692
711
  res.redirect(redirectTarget);
@@ -723,6 +742,11 @@ router.post(
723
742
 
724
743
  if (id && req.body.layout) {
725
744
  await Page.update(+id, { layout: req.body.layout });
745
+ const page = await Page.findOne({ id });
746
+ Trigger.emitEvent("AppChange", `Page ${page.name}`, req.user, {
747
+ entity_type: "Page",
748
+ entity_name: page.name,
749
+ });
726
750
  res.json({
727
751
  success: "ok",
728
752
  });
@@ -744,6 +768,10 @@ router.post(
744
768
  error_catcher(async (req, res) => {
745
769
  const { id } = req.params;
746
770
  const page = await Page.findOne({ id });
771
+ Trigger.emitEvent("AppChange", `Page ${page.name}`, req.user, {
772
+ entity_type: "Page",
773
+ entity_name: page.name,
774
+ });
747
775
  await page.delete();
748
776
  req.flash("success", req.__(`Page deleted`));
749
777
  res.redirect(`/pageedit`);
@@ -796,6 +824,7 @@ router.post(
796
824
  min_role: page.min_role,
797
825
  pagename: page.name,
798
826
  });
827
+ Trigger.emitEvent("AppChange", `Menu`, req.user, {});
799
828
  req.flash(
800
829
  "success",
801
830
  req.__(
@@ -820,6 +849,10 @@ router.post(
820
849
  const { id } = req.params;
821
850
  const page = await Page.findOne({ id });
822
851
  const newpage = await page.clone();
852
+ Trigger.emitEvent("AppChange", `Page ${newpage.name}`, req.user, {
853
+ entity_type: "Page",
854
+ entity_name: newpage.name,
855
+ });
823
856
  req.flash(
824
857
  "success",
825
858
  req.__("Page %s duplicated as %s", page.name, newpage.name)
package/routes/utils.js CHANGED
@@ -23,6 +23,7 @@ const Crash = require("@saltcorn/data/models/crash");
23
23
  const File = require("@saltcorn/data/models/file");
24
24
  const User = require("@saltcorn/data/models/user");
25
25
  const Page = require("@saltcorn/data/models/page");
26
+ const Trigger = require("@saltcorn/data/models/trigger");
26
27
  const si = require("systeminformation");
27
28
  const {
28
29
  config_fields_form,
@@ -426,6 +427,9 @@ const admin_config_route = ({
426
427
  const restart_required = check_if_restart_required(form, req);
427
428
 
428
429
  await save_config_from_form(form);
430
+ Trigger.emitEvent("AppChange", `Config`, req.user, {
431
+ config_keys: Object.keys(form.values),
432
+ });
429
433
  if (!req.xhr) {
430
434
  if (restart_required) {
431
435
  flash_restart(req);
package/routes/view.js CHANGED
@@ -139,18 +139,20 @@ router.get(
139
139
  : contents0;
140
140
  res.sendWrap(
141
141
  title,
142
- add_edit_bar({
143
- role,
144
- title: view.name,
145
- what: req.__("View"),
146
- url: `/viewedit/edit/${encodeURIComponent(view.name)}`,
147
- cfgUrl: `/viewedit/config/${encodeURIComponent(view.name)}`,
148
- contents,
149
- req,
150
- view,
151
- viewtemplate: view.viewtemplate,
152
- table: view.table_id || view.exttable_name,
153
- })
142
+ !req.smr
143
+ ? add_edit_bar({
144
+ role,
145
+ title: view.name,
146
+ what: req.__("View"),
147
+ url: `/viewedit/edit/${encodeURIComponent(view.name)}`,
148
+ cfgUrl: `/viewedit/config/${encodeURIComponent(view.name)}`,
149
+ contents,
150
+ req,
151
+ view,
152
+ viewtemplate: view.viewtemplate,
153
+ table: view.table_id || view.exttable_name,
154
+ })
155
+ : contents
154
156
  );
155
157
  }
156
158
  })
@@ -27,6 +27,7 @@ const Table = require("@saltcorn/data/models/table");
27
27
  const View = require("@saltcorn/data/models/view");
28
28
  const Workflow = require("@saltcorn/data/models/workflow");
29
29
  const User = require("@saltcorn/data/models/user");
30
+ const Trigger = require("@saltcorn/data/models/trigger");
30
31
  const Page = require("@saltcorn/data/models/page");
31
32
  const File = require("@saltcorn/data/models/file");
32
33
  const Tag = require("@saltcorn/data/models/tag");
@@ -551,6 +552,10 @@ router.post(
551
552
  //console.log(v);
552
553
  await View.create(v);
553
554
  }
555
+ Trigger.emitEvent("AppChange", `View ${v.name}`, req.user, {
556
+ entity_type: "View",
557
+ entity_name: v.name,
558
+ });
554
559
  if (req.xhr) res.json({ success: "ok" });
555
560
  else
556
561
  res.redirect(
@@ -733,6 +738,10 @@ router.post(
733
738
  };
734
739
  else newcfg = { ...view.configuration, ...context };
735
740
  await View.update({ configuration: newcfg }, view.id);
741
+ Trigger.emitEvent("AppChange", `View ${view.name}`, req.user, {
742
+ entity_type: "View",
743
+ entity_name: view.name,
744
+ });
736
745
  };
737
746
  const wfres = await configFlow.run(req.body, req);
738
747
 
@@ -761,6 +770,7 @@ router.post(
761
770
  min_role: view.min_role,
762
771
  viewname: view.name,
763
772
  });
773
+ Trigger.emitEvent("AppChange", `Menu`, req.user, {});
764
774
  req.flash(
765
775
  "success",
766
776
  req.__(
@@ -790,6 +800,10 @@ router.post(
790
800
  const { id } = req.params;
791
801
  const view = await View.findOne({ id });
792
802
  const newview = await view.clone();
803
+ Trigger.emitEvent("AppChange", `View ${newview.name}`, req.user, {
804
+ entity_type: "View",
805
+ entity_name: newview.name,
806
+ });
793
807
  req.flash(
794
808
  "success",
795
809
  req.__("View %s duplicated as %s", view.name, newview.name)
@@ -841,6 +855,10 @@ router.post(
841
855
  const exview = await View.findOne({ id });
842
856
  let newcfg = { ...exview.configuration, ...req.body };
843
857
  await View.update({ configuration: newcfg }, +id);
858
+ Trigger.emitEvent("AppChange", `View ${exview.name}`, req.user, {
859
+ entity_type: "View",
860
+ entity_name: exview.name,
861
+ });
844
862
  res.json({ success: "ok" });
845
863
  } else {
846
864
  res.json({ error: req.__("Unable to save: No view") });
@@ -879,6 +897,10 @@ router.post(
879
897
  };
880
898
  else newcfg = { ...view.configuration, ...step.renderForm.values };
881
899
  await View.update({ configuration: newcfg }, view.id);
900
+ Trigger.emitEvent("AppChange", `View ${view.name}`, req.user, {
901
+ entity_type: "View",
902
+ entity_name: view.name,
903
+ });
882
904
  res.json({ success: "ok" });
883
905
  } else {
884
906
  res.json({ error: step.renderForm.errorSummary });
@@ -906,6 +928,10 @@ router.post(
906
928
  const role = req.body.role;
907
929
  await View.update({ min_role: role }, +id);
908
930
  const view = await View.findOne({ id });
931
+ Trigger.emitEvent("AppChange", `View ${view.name}`, req.user, {
932
+ entity_type: "View",
933
+ entity_name: view.name,
934
+ });
909
935
  const roles = await User.get_roles();
910
936
  const roleRow = roles.find((r) => r.id === +role);
911
937
  const message =
@@ -90,6 +90,25 @@ test("updateQueryStringParameter hash", () => {
90
90
  "/foo?name=Bar#Baz"
91
91
  );
92
92
  });
93
+ test("addQueryStringParameter", () => {
94
+ expect(addQueryStringParameter("/foo", "age", 43)).toBe(
95
+ "/foo?age=43"
96
+ );
97
+ expect(addQueryStringParameter("/foo?age=43", "age", 44)).toBe(
98
+ "/foo?age=43&age=44"
99
+ );
100
+ expect(addQueryStringParameter("/foo?age=43", "age", 43)).toBe(
101
+ "/foo?age=43"
102
+ );
103
+ });
104
+ test("addQueryStringParameter hash", () => {
105
+ expect(addQueryStringParameter("/foo#baz", "age", 43)).toBe(
106
+ "/foo?age=43#baz"
107
+ );
108
+ expect(addQueryStringParameter("/foo?age=43#baz", "age", 44)).toBe(
109
+ "/foo?age=43&age=44#baz"
110
+ );
111
+ });
93
112
  test("unique_field_from_rows test", () => {
94
113
  $("body").append(`<input id="mkuniq6" value="bar"></div>`);
95
114
  unique_field_from_rows(