@saltcorn/server 0.9.5-beta.2 → 0.9.5-beta.20

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.
@@ -196,7 +196,7 @@ const pageBuilderData = async (req, context) => {
196
196
  f.required = false;
197
197
  if (f.type && f.type.name === "Bool") f.fieldview = "tristate";
198
198
 
199
- await f.fill_fkey_options(true);
199
+ //await f.fill_fkey_options(true);
200
200
  fixed_state_fields[view.name].push(f);
201
201
  if (table.name === "users" && f.primary_key)
202
202
  fixed_state_fields[view.name].push(
package/routes/plugins.js CHANGED
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  const Router = require("express-promise-router");
9
- const { isAdmin, error_catcher } = require("./utils.js");
9
+ const { isAdmin, loggedIn, error_catcher } = require("./utils.js");
10
10
  const { renderForm, link, post_btn } = require("@saltcorn/markup");
11
11
  const {
12
12
  getState,
@@ -16,6 +16,7 @@ const {
16
16
  const Form = require("@saltcorn/data/models/form");
17
17
  const Field = require("@saltcorn/data/models/field");
18
18
  const Plugin = require("@saltcorn/data/models/plugin");
19
+ const User = require("@saltcorn/data/models/user");
19
20
  const { fetch_available_packs } = require("@saltcorn/admin-models/models/pack");
20
21
  const {
21
22
  upgrade_all_tenants_plugins,
@@ -57,6 +58,7 @@ const { flash_restart } = require("../markup/admin.js");
57
58
  const { sleep, removeNonWordChars } = require("@saltcorn/data/utils");
58
59
  const { loadAllPlugins } = require("../load_plugins");
59
60
  const npmFetch = require("npm-registry-fetch");
61
+ const PluginInstaller = require("@saltcorn/plugins-loader/plugin_installer");
60
62
 
61
63
  /**
62
64
  * @type {object}
@@ -545,6 +547,16 @@ const plugin_store_html = (items, req) => {
545
547
  };
546
548
  };
547
549
 
550
+ const flash_relogin = (req, exposedConfigs) => {
551
+ req.flash(
552
+ "warning",
553
+ req.__(
554
+ "To see changes for '%s' in show-if-formulas, users need to relogin",
555
+ exposedConfigs.join(", ")
556
+ )
557
+ );
558
+ };
559
+
548
560
  /**
549
561
  * @name get
550
562
  * @function
@@ -696,7 +708,12 @@ router.get(
696
708
  label: "Reload page to see changes",
697
709
  id: "btnReloadNow",
698
710
  class: "btn btn-outline-secondary",
699
- onclick: "location.reload()",
711
+ onclick: `if (window.savingViewConfig)
712
+ notifyAlert({
713
+ type: 'danger',
714
+ text: 'Still saving, please wait',
715
+ });
716
+ else location.reload();`,
700
717
  },
701
718
  ];
702
719
  wfres.renderForm.onChange = `${
@@ -704,25 +721,31 @@ router.get(
704
721
  };$('#btnReloadNow').removeClass('btn-outline-secondary').addClass('btn-secondary')`;
705
722
  }
706
723
 
707
- res.sendWrap(req.__(`Configure %s Plugin`, plugin.name), {
708
- above: [
709
- {
710
- type: "breadcrumbs",
711
- crumbs: [
712
- { text: req.__("Settings"), href: "/settings" },
713
- { text: req.__("Module store"), href: "/plugins" },
714
- { text: plugin.name },
715
- ],
716
- },
717
- {
718
- type: "card",
719
- class: "mt-0",
720
- title: req.__(`Configure %s Plugin`, plugin.name),
721
- titleAjaxIndicator: true,
722
- contents: renderForm(wfres.renderForm, req.csrfToken()),
723
- },
724
- ],
725
- });
724
+ res.sendWrap(
725
+ {
726
+ title: req.__(`Configure %s Plugin`, plugin.name),
727
+ headers: wfres.renderForm?.additionalHeaders || [],
728
+ },
729
+ {
730
+ above: [
731
+ {
732
+ type: "breadcrumbs",
733
+ crumbs: [
734
+ { text: req.__("Settings"), href: "/settings" },
735
+ { text: req.__("Module store"), href: "/plugins" },
736
+ { text: plugin.name },
737
+ ],
738
+ },
739
+ {
740
+ type: "card",
741
+ class: "mt-0",
742
+ title: req.__(`Configure %s Plugin`, plugin.name),
743
+ titleAjaxIndicator: true,
744
+ contents: renderForm(wfres.renderForm, req.csrfToken()),
745
+ },
746
+ ],
747
+ }
748
+ );
726
749
  })
727
750
  );
728
751
 
@@ -769,17 +792,21 @@ router.post(
769
792
  contents: renderForm(wfres.renderForm, req.csrfToken()),
770
793
  });
771
794
  } else {
772
- plugin.configuration = wfres;
795
+ const newCfg = wfres.cleanup ? wfres.context : wfres;
796
+ plugin.configuration = newCfg;
773
797
  await plugin.upsert();
774
798
  await load_plugins.loadPlugin(plugin);
775
799
  const instore = await Plugin.store_plugins_available();
776
800
  const store_plugin = instore.find((p) => p.name === plugin.name);
777
801
  if (store_plugin && store_plugin.has_auth) flash_restart(req);
802
+ if (module.exposed_configs?.length > 0)
803
+ flash_relogin(req, module.exposed_configs);
778
804
  getState().processSend({
779
805
  refresh_plugin_cfg: plugin.name,
780
806
  tenant: db.getTenantSchema(),
781
807
  });
782
808
  if (module.layout) await sleep(500); // Allow other workers to reload this plugin
809
+ if (wfres.cleanup) await wfres.cleanup();
783
810
  res.redirect("/plugins");
784
811
  }
785
812
  })
@@ -798,22 +825,223 @@ router.post(
798
825
  const flow = module.configuration_workflow();
799
826
  const step = await flow.singleStepForm(req.body, req);
800
827
  if (step?.renderForm) {
801
- if (!step.renderForm.hasErrors) {
828
+ if (step.renderForm.hasErrors || step.savingErrors)
829
+ res.status(400).send(step.savingErrors || "Error");
830
+ else {
802
831
  plugin.configuration = {
803
832
  ...plugin.configuration,
804
833
  ...step.renderForm.values,
805
834
  };
806
835
  await plugin.upsert();
807
836
  await load_plugins.loadPlugin(plugin);
808
- getState().processSend({
809
- refresh_plugin_cfg: plugin.name,
810
- tenant: db.getTenantSchema(),
811
- });
812
- res.json({ success: "ok" });
813
837
  }
838
+ getState().processSend({
839
+ refresh_plugin_cfg: plugin.name,
840
+ tenant: db.getTenantSchema(),
841
+ });
842
+ res.json({ success: "ok" });
814
843
  }
815
844
  })
816
845
  );
846
+
847
+ router.get(
848
+ "/user_configure/:name",
849
+ loggedIn,
850
+ error_catcher(async (req, res) => {
851
+ const user = await User.findOne({ id: req.user?.id });
852
+ if (!user) {
853
+ req.flash("error", req.__("Not authorized"));
854
+ return res.redirect("/");
855
+ }
856
+ const { name } = req.params;
857
+ const plugin = await Plugin.findOne({ name: decodeURIComponent(name) });
858
+ if (!plugin) {
859
+ req.flash("warning", req.__("Module not found"));
860
+ return res.redirect("/auth/settings");
861
+ }
862
+ let module = getState().plugins[plugin.name];
863
+ if (!module) {
864
+ module = getState().plugins[getState().plugin_module_names[plugin.name]];
865
+ }
866
+ const form = await module.user_config_form({
867
+ ...(plugin.configuration || {}),
868
+ ...(user._attributes?.layout?.config || {}),
869
+ });
870
+ form.action = `/plugins/user_configure/${encodeURIComponent(plugin.name)}`;
871
+ form.onChange = `applyViewConfig(this, '/plugins/user_saveconfig/${encodeURIComponent(
872
+ name
873
+ )}', null, event);$('#btnReloadNow').removeClass('btn-outline-secondary').addClass('btn-secondary')`;
874
+
875
+ form.additionalButtons = [
876
+ {
877
+ label: "Reload page to see changes",
878
+ id: "btnReloadNow",
879
+ class: "btn btn-outline-secondary",
880
+ onclick: "location.reload();",
881
+ },
882
+ ];
883
+ form.submitLabel = req.__("Finish") + " »";
884
+ res.sendWrap(
885
+ {
886
+ title: req.__(`Configure %s Plugin for %s`, plugin.name, user.email),
887
+ headers: form.additionalHeaders || [],
888
+ },
889
+ {
890
+ above: [
891
+ {
892
+ type: "breadcrumbs",
893
+ crumbs: [
894
+ { text: req.__("Settings"), href: "/settings" },
895
+ { text: req.__("Module store"), href: "/plugins" },
896
+ { text: plugin.name },
897
+ ],
898
+ },
899
+ {
900
+ type: "card",
901
+ class: "mt-0",
902
+ title: req.__(`Configure %s Plugin`, plugin.name),
903
+ titleAjaxIndicator: true,
904
+ contents: renderForm(form, req.csrfToken()),
905
+ },
906
+ ],
907
+ }
908
+ );
909
+ })
910
+ );
911
+
912
+ router.post(
913
+ "/user_configure/:name",
914
+ loggedIn,
915
+ error_catcher(async (req, res) => {
916
+ const user = await User.findOne({ id: req.user?.id });
917
+ if (!user) {
918
+ req.flash("error", req.__("Not authorized"));
919
+ return res.redirect("/");
920
+ }
921
+ const { name } = req.params;
922
+ const plugin = await Plugin.findOne({ name: decodeURIComponent(name) });
923
+ let module = getState().plugins[plugin.name];
924
+ if (!module) {
925
+ module = getState().plugins[getState().plugin_module_names[plugin.name]];
926
+ }
927
+ const form = await module.user_config_form({
928
+ ...(plugin.configuration || {}),
929
+ ...(user._attributes?.layout?.config || {}),
930
+ });
931
+ const valResult = form.validate(req.body);
932
+ if (form.hasErrors) {
933
+ req.flash("warning", req.__("An error occurred"));
934
+ return res.sendWrap(
935
+ req.__(`Configure %s Plugin for %s`, plugin.name, user.email),
936
+ renderForm(form, req.csrfToken())
937
+ );
938
+ }
939
+ const values = valResult.success;
940
+ values.is_user_config = true;
941
+ const userAttrs = user._attributes ? { ...user._attributes } : {};
942
+ userAttrs.layout = {
943
+ plugin: plugin.name,
944
+ config: values,
945
+ };
946
+ await user.update({ _attributes: userAttrs });
947
+ getState().userLayouts[req.user.email] = module.layout({
948
+ ...(plugin.configuration ? plugin.configuration : {}),
949
+ ...values,
950
+ });
951
+ const sessionUser = req.session?.passport?.user;
952
+ if (sessionUser) {
953
+ const pluginName = module.plugin_name;
954
+ if (sessionUser.attributes) {
955
+ const oldAttrs = sessionUser.attributes[pluginName] || {};
956
+ sessionUser.attributes[pluginName] = { ...oldAttrs, ...values };
957
+ } else sessionUser.attributes = { [pluginName]: values };
958
+ }
959
+ getState().processSend({
960
+ refresh_plugin_cfg: plugin.name,
961
+ tenant: db.getTenantSchema(),
962
+ });
963
+ if (module.layout) await sleep(500); // Allow other workers to reload this plugin
964
+ res.redirect("/auth/settings");
965
+ })
966
+ );
967
+
968
+ router.post(
969
+ "/user_saveconfig/:name",
970
+ loggedIn,
971
+ error_catcher(async (req, res) => {
972
+ const user = await User.findOne({ id: req.user?.id });
973
+ if (!user) return res.status(401).json({ error: req.__("Not authorized") });
974
+ const { name } = req.params;
975
+ const plugin = await Plugin.findOne({ name: decodeURIComponent(name) });
976
+ let module = getState().plugins[plugin.name];
977
+ if (!module) {
978
+ module = getState().plugins[getState().plugin_module_names[plugin.name]];
979
+ }
980
+ const form = await module.user_config_form({
981
+ ...(plugin.configuration || {}),
982
+ ...(user._attributes?.layout?.config || {}),
983
+ });
984
+ const valResult = form.validate(req.body);
985
+ if (form.hasErrors) {
986
+ return res.status(400).json({ error: req.__("An error occured") });
987
+ }
988
+ const values = valResult.success;
989
+ values.is_user_config = true;
990
+ const userAttrs = user._attributes ? { ...user._attributes } : {};
991
+ userAttrs.layout = {
992
+ plugin: plugin.name,
993
+ config: values,
994
+ };
995
+ await user.update({ _attributes: userAttrs });
996
+ getState().userLayouts[req.user.email] = module.layout(
997
+ userAttrs.layout.config
998
+ );
999
+ const sessionUser = req.session?.passport?.user;
1000
+ if (sessionUser) {
1001
+ const pluginName = module.plugin_name;
1002
+ if (sessionUser.attributes) {
1003
+ const oldAttrs = sessionUser.attributes[pluginName] || {};
1004
+ sessionUser.attributes[pluginName] = { ...oldAttrs, ...values };
1005
+ } else sessionUser.attributes = { [pluginName]: values };
1006
+ }
1007
+ getState().processSend({
1008
+ refresh_plugin_cfg: plugin.name,
1009
+ tenant: db.getTenantSchema(),
1010
+ });
1011
+ res.json({ success: "ok" });
1012
+ })
1013
+ );
1014
+
1015
+ router.post(
1016
+ "/remove_user_layout",
1017
+ loggedIn,
1018
+ error_catcher(async (req, res) => {
1019
+ const user = await User.findOne({ id: req.user.id });
1020
+ if (!user) {
1021
+ return res.status(401).json({ error: req.__("Not authorized") });
1022
+ } else if (user._attributes?.layout) {
1023
+ const userAttrs = { ...user._attributes };
1024
+ const plugin = userAttrs.layout.plugin;
1025
+ delete userAttrs.layout;
1026
+ await user.update({ _attributes: userAttrs });
1027
+ getState().userLayouts[req.user.email] = null;
1028
+ let module = getState().plugins[plugin];
1029
+ if (!module) {
1030
+ module = getState().plugins[getState().plugin_module_names[plugin]];
1031
+ }
1032
+ const pluginName = module.plugin_name;
1033
+ const sessionUser = req.session?.passport?.user;
1034
+ if (sessionUser?.attributes[pluginName])
1035
+ sessionUser.attributes[pluginName] = {};
1036
+ getState().processSend({
1037
+ refresh_plugin_cfg: plugin,
1038
+ tenant: db.getTenantSchema(),
1039
+ });
1040
+ }
1041
+ res.json({ success: "ok", reload_page: true });
1042
+ })
1043
+ );
1044
+
817
1045
  /**
818
1046
  * @name get/new
819
1047
  * @function
@@ -1178,6 +1406,7 @@ router.post(
1178
1406
  getState().getConfig("development_mode", false)
1179
1407
  ) {
1180
1408
  await plugin.delete();
1409
+ await new PluginInstaller(plugin).remove();
1181
1410
  req.flash("success", req.__(`Module %s removed.`, plugin.name));
1182
1411
  } else {
1183
1412
  req.flash(
@@ -1241,7 +1470,12 @@ router.post(
1241
1470
  res.redirect(`/plugins`);
1242
1471
  return;
1243
1472
  }
1244
- await load_plugins.loadAndSaveNewPlugin(plugin, forceReInstall);
1473
+ const msgs = await load_plugins.loadAndSaveNewPlugin(
1474
+ plugin,
1475
+ forceReInstall,
1476
+ undefined,
1477
+ req.__
1478
+ );
1245
1479
  const plugin_module = getState().plugins[name];
1246
1480
  await sleep(1000); // Allow other workers to load this plugin
1247
1481
  await getState().refresh_views();
@@ -1255,9 +1489,11 @@ router.post(
1255
1489
  plugin_db.name
1256
1490
  )
1257
1491
  );
1492
+ if (msgs?.length > 0) req.flash("warning", msgs.join("<br>"));
1258
1493
  res.redirect(`/plugins/configure/${plugin_db.name}`);
1259
1494
  } else {
1260
1495
  req.flash("success", req.__(`Module %s installed`, plugin.name));
1496
+ if (msgs?.length > 0) req.flash("warning", msgs.join("<br>"));
1261
1497
  res.redirect(`/plugins`);
1262
1498
  }
1263
1499
  })
package/routes/search.js CHANGED
@@ -52,6 +52,12 @@ const searchConfigForm = (tables, views, req) => {
52
52
  attributes: { options: ok_views.map((v) => v.name).join() },
53
53
  });
54
54
  }
55
+ fields.push({
56
+ name: "search_table_description",
57
+ label: req.__("Description header"),
58
+ sublabel: req.__("Use table description instead of name as header"),
59
+ type: "Bool",
60
+ });
55
61
  const blurb1 = req.__(
56
62
  `Choose views for <a href="/search">search results</a> for each table.<br/>Set to blank to omit table from global search.`
57
63
  );
@@ -84,7 +90,11 @@ router.get(
84
90
  const views = await View.find({}, { orderBy: "name" });
85
91
  const tables = await Table.find();
86
92
  const form = searchConfigForm(tables, views, req);
87
- form.values = getState().getConfig("globalSearch");
93
+ form.values = getState().getConfig("globalSearch", {});
94
+ form.values.search_table_description = getState().getConfig(
95
+ "search_table_description",
96
+ false
97
+ );
88
98
  send_infoarch_page({
89
99
  res,
90
100
  req,
@@ -116,6 +126,13 @@ router.post(
116
126
  const result = form.validate(req.body);
117
127
 
118
128
  if (result.success) {
129
+ const search_table_description =
130
+ !!result.success.search_table_description;
131
+ await getState().setConfig(
132
+ "search_table_description",
133
+ search_table_description
134
+ );
135
+ delete result.success.search_table_description;
119
136
  await getState().setConfig("globalSearch", result.success);
120
137
  if (!req.xhr) res.redirect("/search/config");
121
138
  else res.json({ success: "ok" });
@@ -175,6 +192,10 @@ const runSearch = async ({ q, _page, table }, req, res) => {
175
192
  res.redirect("/");
176
193
  return;
177
194
  }
195
+ const search_table_description = getState().getConfig(
196
+ "search_table_description",
197
+ false
198
+ );
178
199
  const current_page = parseInt(_page) || 1;
179
200
  const offset = (current_page - 1) * page_size;
180
201
  let resp = [];
@@ -184,6 +205,11 @@ const runSearch = async ({ q, _page, table }, req, res) => {
184
205
  if (!viewName || viewName === "") continue;
185
206
  tablesConfigured += 1;
186
207
  if (table && tableName !== table) continue;
208
+ let sectionHeader = tableName;
209
+ if (search_table_description) {
210
+ sectionHeader =
211
+ Table.findOne({ name: tableName })?.description || tableName;
212
+ }
187
213
  const view = await View.findOne({ name: viewName });
188
214
  if (!view)
189
215
  throw new InvalidConfiguration(
@@ -209,7 +235,7 @@ const runSearch = async ({ q, _page, table }, req, res) => {
209
235
  tablesWithResults.push(tableName);
210
236
  resp.push({
211
237
  type: "card",
212
- title: span({ id: tableName }, tableName),
238
+ title: span({ id: tableName }, sectionHeader),
213
239
  contents: vresps.map((vr) => vr.html).join("<hr>") + paginate,
214
240
  });
215
241
  }
package/routes/tables.js CHANGED
@@ -167,6 +167,10 @@ const tableForm = async (table, req) => {
167
167
  label: req.__("Version history"),
168
168
  sublabel: req.__("Track table data changes over time"),
169
169
  name: "versioned",
170
+ attributes: {
171
+ onChange:
172
+ "if(!this.checked && !confirm('Are you sure? This will delete all history')) {this.checked = true; return false}",
173
+ },
170
174
  type: "Bool",
171
175
  },
172
176
  ...(table.name === "users"
package/serve.js CHANGED
@@ -105,7 +105,7 @@ const initMaster = async ({ disableMigrate }, useClusterAdaptor = true) => {
105
105
  // migrate database
106
106
  if (!disableMigrate) await migrate(db.connectObj.default_schema, true);
107
107
  // load all plugins
108
- await loadAllPlugins();
108
+ await loadAllPlugins(true);
109
109
  // switch on sql logging - but it was initiated before???
110
110
  if (getState().getConfig("log_sql", false)) db.set_sql_logging();
111
111
  if (db.is_it_multi_tenant()) {
@@ -30,7 +30,17 @@ const prepHtmlFiles = async () => {
30
30
  const html = `<html><head><title>Landing page</title></head><body><h1>${content}</h1></body></html>`;
31
31
  if (!existsSync(scFolder)) await File.new_folder(folder);
32
32
  if (!existsSync(join(scFolder, name))) {
33
- return await File.from_contents(name, "text/html", html, 1, 1, folder);
33
+ const file = await File.from_contents(
34
+ name,
35
+ "text/html",
36
+ html,
37
+ 1,
38
+ 1,
39
+ folder
40
+ );
41
+ file.location = File.absPathToServePath(file.location);
42
+
43
+ return file;
34
44
  } else {
35
45
  const file = await File.from_file_on_disk(name, scFolder);
36
46
  fs.writeFileSync(file.location, html);