@saltcorn/server 0.9.5-beta.14 → 0.9.5-beta.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/auth/routes.js CHANGED
@@ -45,6 +45,7 @@ const {
45
45
  p,
46
46
  script,
47
47
  domReady,
48
+ button,
48
49
  } = require("@saltcorn/markup/tags");
49
50
  const {
50
51
  available_languages,
@@ -1362,6 +1363,57 @@ const userSettings = async ({ req, res, pwform, user }) => {
1362
1363
  ),
1363
1364
  ],
1364
1365
  };
1366
+ let themeCfgCard;
1367
+ const layoutPlugin = getState().getLayoutPlugin(user);
1368
+ const modNames = getState().plugin_module_names;
1369
+ const pluginName = layoutPlugin.plugin_name;
1370
+ let safeName = pluginName;
1371
+ for (const [k, v] of Object.entries(modNames)) {
1372
+ if (v === pluginName) safeName = k;
1373
+ }
1374
+
1375
+ const hasUserConfigs =
1376
+ layoutPlugin.user_config_form &&
1377
+ (await layoutPlugin.user_config_form(
1378
+ getState().plugin_cfgs[pluginName] || {}
1379
+ )) !== null;
1380
+ themeCfgCard = {
1381
+ type: "card",
1382
+ title: req.__("Layout"),
1383
+ contents: [
1384
+ div(
1385
+ hasUserConfigs
1386
+ ? req.__("Adjust the the theme for this user")
1387
+ : req.__("The current theme has no user specific settings")
1388
+ ),
1389
+ hasUserConfigs
1390
+ ? div(
1391
+ {
1392
+ class: "mt-4",
1393
+ },
1394
+
1395
+ a(
1396
+ {
1397
+ class: "btn btn-primary",
1398
+ role: "button",
1399
+ href: `/plugins/user_configure/${encodeURIComponent(safeName)}`,
1400
+ title: req.__("Configure theme"),
1401
+ },
1402
+ req.__("Configure")
1403
+ ),
1404
+ button(
1405
+ {
1406
+ class: "btn btn-primary ms-2",
1407
+ onclick: "ajax_post('/plugins/remove_user_layout')",
1408
+ title: req.__("Remove all user specific theme settings"),
1409
+ },
1410
+ req.__("Reset")
1411
+ )
1412
+ )
1413
+ : "",
1414
+ ],
1415
+ };
1416
+
1365
1417
  return {
1366
1418
  above: [
1367
1419
  {
@@ -1434,6 +1486,7 @@ const userSettings = async ({ req, res, pwform, user }) => {
1434
1486
  ]
1435
1487
  : []),
1436
1488
  ...(apikeycard ? [apikeycard] : []),
1489
+ ...(themeCfgCard ? [themeCfgCard] : []),
1437
1490
  ],
1438
1491
  };
1439
1492
  };
package/load_plugins.js CHANGED
@@ -35,6 +35,10 @@ const loadPlugin = async (plugin, force) => {
35
35
  res.name
36
36
  );
37
37
  } catch (error) {
38
+ getState().log(
39
+ 3,
40
+ `Error loading plugin ${plugin.name}: ${error.message || error}`
41
+ );
38
42
  if (force) {
39
43
  // remove the install dir and try again
40
44
  await loader.remove();
@@ -48,6 +52,7 @@ const loadPlugin = async (plugin, force) => {
48
52
  );
49
53
  }
50
54
  }
55
+ if (res.plugin_module.user_config_form) getState().refreshUserLayouts();
51
56
  if (res.plugin_module.onLoad) {
52
57
  try {
53
58
  await res.plugin_module.onLoad(plugin.configuration);
@@ -83,6 +88,7 @@ const loadAllPlugins = async (force) => {
83
88
  console.error(e);
84
89
  }
85
90
  }
91
+ await getState().refreshUserLayouts();
86
92
  await getState().refresh(true);
87
93
  };
88
94
 
package/locales/en.json CHANGED
@@ -1394,5 +1394,10 @@
1394
1394
  "The plugin was corrupted and had to be repaired. We recommend restarting your server.": "The plugin was corrupted and had to be repaired. We recommend restarting your server.",
1395
1395
  "%s code page": "%s code page",
1396
1396
  "Constants and function code": "Constants and function code",
1397
- "Delete code page": "Delete code page"
1397
+ "Delete code page": "Delete code page",
1398
+ "Adjust the the theme for this user": "Adjust the the theme for this user",
1399
+ "Configure theme": "Configure theme",
1400
+ "Remove all user specific theme settings": "Remove all user specific theme settings",
1401
+ "Configure %s Plugin for %s": "Configure %s Plugin for %s",
1402
+ "The current theme has no user specific settings": "The current theme has no user specific settings"
1398
1403
  }
package/package.json CHANGED
@@ -1,20 +1,20 @@
1
1
  {
2
2
  "name": "@saltcorn/server",
3
- "version": "0.9.5-beta.14",
3
+ "version": "0.9.5-beta.15",
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": "0.9.5-beta.14",
11
- "@saltcorn/builder": "0.9.5-beta.14",
12
- "@saltcorn/data": "0.9.5-beta.14",
13
- "@saltcorn/admin-models": "0.9.5-beta.14",
14
- "@saltcorn/filemanager": "0.9.5-beta.14",
15
- "@saltcorn/markup": "0.9.5-beta.14",
16
- "@saltcorn/plugins-loader": "0.9.5-beta.14",
17
- "@saltcorn/sbadmin2": "0.9.5-beta.14",
10
+ "@saltcorn/base-plugin": "0.9.5-beta.15",
11
+ "@saltcorn/builder": "0.9.5-beta.15",
12
+ "@saltcorn/data": "0.9.5-beta.15",
13
+ "@saltcorn/admin-models": "0.9.5-beta.15",
14
+ "@saltcorn/filemanager": "0.9.5-beta.15",
15
+ "@saltcorn/markup": "0.9.5-beta.15",
16
+ "@saltcorn/plugins-loader": "0.9.5-beta.15",
17
+ "@saltcorn/sbadmin2": "0.9.5-beta.15",
18
18
  "@socket.io/cluster-adapter": "^0.2.1",
19
19
  "@socket.io/sticky": "^1.0.1",
20
20
  "adm-zip": "0.5.10",
@@ -1184,8 +1184,10 @@ async function common_done(res, viewname, isWeb = true) {
1184
1184
  await handle(res.notify_success, (text) =>
1185
1185
  notifyAlert({ type: "success", text: text })
1186
1186
  );
1187
- if (res.set_fields && viewname) {
1188
- const form = $(`form[data-viewname="${viewname}"]`);
1187
+ if (res.set_fields && (viewname || res.set_fields._viewname)) {
1188
+ const form = $(
1189
+ `form[data-viewname="${res.set_fields._viewname || viewname}"]`
1190
+ );
1189
1191
  if (form.length === 0 && set_state_fields) {
1190
1192
  // assume this is a filter
1191
1193
  set_state_fields(
@@ -1195,6 +1197,7 @@ async function common_done(res, viewname, isWeb = true) {
1195
1197
  );
1196
1198
  } else {
1197
1199
  Object.keys(res.set_fields).forEach((k) => {
1200
+ if (k === "_viewname") return;
1198
1201
  const input = form.find(
1199
1202
  `input[name=${k}], textarea[name=${k}], select[name=${k}]`
1200
1203
  );
@@ -522,6 +522,7 @@ function applyViewConfig(e, url, k, event) {
522
522
  cfg[item.name] = item.value;
523
523
  });
524
524
  ajax_indicator(true, e);
525
+ window.savingViewConfig = true;
525
526
  $.ajax(url, {
526
527
  type: "POST",
527
528
  dataType: "json",
@@ -531,9 +532,11 @@ function applyViewConfig(e, url, k, event) {
531
532
  },
532
533
  data: JSON.stringify(cfg),
533
534
  error: function (request) {
535
+ window.savingViewConfig = false;
534
536
  ajax_indicate_error(e, request);
535
537
  },
536
538
  success: function (res) {
539
+ window.savingViewConfig = false;
537
540
  ajax_indicator(false);
538
541
  k && k(res);
539
542
  !k && updateViewPreview();
@@ -16,6 +16,7 @@ const { eachTenant } = require("@saltcorn/admin-models/models/tenant");
16
16
  const relevantPackages = [
17
17
  "db-common",
18
18
  "common-code",
19
+ "plugins-loader",
19
20
  "postgres",
20
21
  "saltcorn-data",
21
22
  "saltcorn-builder",
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,
@@ -697,7 +698,12 @@ router.get(
697
698
  label: "Reload page to see changes",
698
699
  id: "btnReloadNow",
699
700
  class: "btn btn-outline-secondary",
700
- onclick: "location.reload()",
701
+ onclick: `if (window.savingViewConfig)
702
+ notifyAlert({
703
+ type: 'danger',
704
+ text: 'Still saving, please wait',
705
+ });
706
+ else location.reload();`,
701
707
  },
702
708
  ];
703
709
  wfres.renderForm.onChange = `${
@@ -705,25 +711,31 @@ router.get(
705
711
  };$('#btnReloadNow').removeClass('btn-outline-secondary').addClass('btn-secondary')`;
706
712
  }
707
713
 
708
- res.sendWrap(req.__(`Configure %s Plugin`, plugin.name), {
709
- above: [
710
- {
711
- type: "breadcrumbs",
712
- crumbs: [
713
- { text: req.__("Settings"), href: "/settings" },
714
- { text: req.__("Module store"), href: "/plugins" },
715
- { text: plugin.name },
716
- ],
717
- },
718
- {
719
- type: "card",
720
- class: "mt-0",
721
- title: req.__(`Configure %s Plugin`, plugin.name),
722
- titleAjaxIndicator: true,
723
- contents: renderForm(wfres.renderForm, req.csrfToken()),
724
- },
725
- ],
726
- });
714
+ res.sendWrap(
715
+ {
716
+ title: req.__(`Configure %s Plugin`, plugin.name),
717
+ headers: wfres.renderForm?.additionalHeaders || [],
718
+ },
719
+ {
720
+ above: [
721
+ {
722
+ type: "breadcrumbs",
723
+ crumbs: [
724
+ { text: req.__("Settings"), href: "/settings" },
725
+ { text: req.__("Module store"), href: "/plugins" },
726
+ { text: plugin.name },
727
+ ],
728
+ },
729
+ {
730
+ type: "card",
731
+ class: "mt-0",
732
+ title: req.__(`Configure %s Plugin`, plugin.name),
733
+ titleAjaxIndicator: true,
734
+ contents: renderForm(wfres.renderForm, req.csrfToken()),
735
+ },
736
+ ],
737
+ }
738
+ );
727
739
  })
728
740
  );
729
741
 
@@ -770,7 +782,8 @@ router.post(
770
782
  contents: renderForm(wfres.renderForm, req.csrfToken()),
771
783
  });
772
784
  } else {
773
- plugin.configuration = wfres;
785
+ const newCfg = wfres.cleanup ? wfres.context : wfres;
786
+ plugin.configuration = newCfg;
774
787
  await plugin.upsert();
775
788
  await load_plugins.loadPlugin(plugin);
776
789
  const instore = await Plugin.store_plugins_available();
@@ -781,6 +794,7 @@ router.post(
781
794
  tenant: db.getTenantSchema(),
782
795
  });
783
796
  if (module.layout) await sleep(500); // Allow other workers to reload this plugin
797
+ if (wfres.cleanup) await wfres.cleanup();
784
798
  res.redirect("/plugins");
785
799
  }
786
800
  })
@@ -799,22 +813,199 @@ router.post(
799
813
  const flow = module.configuration_workflow();
800
814
  const step = await flow.singleStepForm(req.body, req);
801
815
  if (step?.renderForm) {
802
- if (!step.renderForm.hasErrors) {
816
+ if (step.renderForm.hasErrors || step.savingErrors)
817
+ res.status(400).send(step.savingErrors || "Error");
818
+ else {
803
819
  plugin.configuration = {
804
820
  ...plugin.configuration,
805
821
  ...step.renderForm.values,
806
822
  };
807
823
  await plugin.upsert();
808
824
  await load_plugins.loadPlugin(plugin);
809
- getState().processSend({
810
- refresh_plugin_cfg: plugin.name,
811
- tenant: db.getTenantSchema(),
812
- });
813
- res.json({ success: "ok" });
814
825
  }
826
+ getState().processSend({
827
+ refresh_plugin_cfg: plugin.name,
828
+ tenant: db.getTenantSchema(),
829
+ });
830
+ res.json({ success: "ok" });
815
831
  }
816
832
  })
817
833
  );
834
+
835
+ router.get(
836
+ "/user_configure/:name",
837
+ loggedIn,
838
+ error_catcher(async (req, res) => {
839
+ const user = await User.findOne({ id: req.user?.id });
840
+ if (!user) {
841
+ req.flash("error", req.__("Not authorized"));
842
+ return res.redirect("/");
843
+ }
844
+ const { name } = req.params;
845
+ const plugin = await Plugin.findOne({ name: decodeURIComponent(name) });
846
+ if (!plugin) {
847
+ req.flash("warning", req.__("Module not found"));
848
+ return res.redirect("/auth/settings");
849
+ }
850
+ let module = getState().plugins[plugin.name];
851
+ if (!module) {
852
+ module = getState().plugins[getState().plugin_module_names[plugin.name]];
853
+ }
854
+ const form = await module.user_config_form({
855
+ ...(plugin.configuration || {}),
856
+ ...(user._attributes?.layout?.config || {}),
857
+ });
858
+ form.action = `/plugins/user_configure/${encodeURIComponent(plugin.name)}`;
859
+ form.onChange = `applyViewConfig(this, '/plugins/user_saveconfig/${encodeURIComponent(
860
+ name
861
+ )}', null, event);$('#btnReloadNow').removeClass('btn-outline-secondary').addClass('btn-secondary')`;
862
+
863
+ form.additionalButtons = [
864
+ {
865
+ label: "Reload page to see changes",
866
+ id: "btnReloadNow",
867
+ class: "btn btn-outline-secondary",
868
+ onclick: "location.reload();",
869
+ },
870
+ ];
871
+ form.submitLabel = req.__("Finish") + " »";
872
+ res.sendWrap(
873
+ {
874
+ title: req.__(`Configure %s Plugin for %s`, plugin.name, user.email),
875
+ headers: form.additionalHeaders || [],
876
+ },
877
+ {
878
+ above: [
879
+ {
880
+ type: "breadcrumbs",
881
+ crumbs: [
882
+ { text: req.__("Settings"), href: "/settings" },
883
+ { text: req.__("Module store"), href: "/plugins" },
884
+ { text: plugin.name },
885
+ ],
886
+ },
887
+ {
888
+ type: "card",
889
+ class: "mt-0",
890
+ title: req.__(`Configure %s Plugin`, plugin.name),
891
+ titleAjaxIndicator: true,
892
+ contents: renderForm(form, req.csrfToken()),
893
+ },
894
+ ],
895
+ }
896
+ );
897
+ })
898
+ );
899
+
900
+ router.post(
901
+ "/user_configure/:name",
902
+ loggedIn,
903
+ error_catcher(async (req, res) => {
904
+ const user = await User.findOne({ id: req.user?.id });
905
+ if (!user) {
906
+ req.flash("error", req.__("Not authorized"));
907
+ return res.redirect("/");
908
+ }
909
+ const { name } = req.params;
910
+ const plugin = await Plugin.findOne({ name: decodeURIComponent(name) });
911
+ let module = getState().plugins[plugin.name];
912
+ if (!module) {
913
+ module = getState().plugins[getState().plugin_module_names[plugin.name]];
914
+ }
915
+ const form = await module.user_config_form({
916
+ ...(plugin.configuration || {}),
917
+ ...(user._attributes?.layout?.config || {}),
918
+ });
919
+ const valResult = form.validate(req.body);
920
+ if (form.hasErrors) {
921
+ req.flash("warning", req.__("An error occurred"));
922
+ return res.sendWrap(
923
+ req.__(`Configure %s Plugin for %s`, plugin.name, user.email),
924
+ renderForm(form, req.csrfToken())
925
+ );
926
+ }
927
+ const values = valResult.success;
928
+ values.is_user_config = true;
929
+ const userAttrs = user._attributes ? { ...user._attributes } : {};
930
+ userAttrs.layout = {
931
+ plugin: plugin.name,
932
+ config: values,
933
+ };
934
+ await user.update({ _attributes: userAttrs });
935
+ getState().userLayouts[req.user.email] = module.layout({
936
+ ...(plugin.configuration ? plugin.configuration : {}),
937
+ ...values,
938
+ });
939
+ getState().processSend({
940
+ refresh_plugin_cfg: plugin.name,
941
+ tenant: db.getTenantSchema(),
942
+ });
943
+ if (module.layout) await sleep(500); // Allow other workers to reload this plugin
944
+ res.redirect("/auth/settings");
945
+ })
946
+ );
947
+
948
+ router.post(
949
+ "/user_saveconfig/:name",
950
+ loggedIn,
951
+ error_catcher(async (req, res) => {
952
+ const user = await User.findOne({ id: req.user?.id });
953
+ if (!user) return res.status(401).json({ error: req.__("Not authorized") });
954
+ const { name } = req.params;
955
+ const plugin = await Plugin.findOne({ name: decodeURIComponent(name) });
956
+ let module = getState().plugins[plugin.name];
957
+ if (!module) {
958
+ module = getState().plugins[getState().plugin_module_names[plugin.name]];
959
+ }
960
+ const form = await module.user_config_form({
961
+ ...(plugin.configuration || {}),
962
+ ...(user._attributes?.layout?.config || {}),
963
+ });
964
+ const valResult = form.validate(req.body);
965
+ if (form.hasErrors) {
966
+ return res.status(400).json({ error: req.__("An error occured") });
967
+ }
968
+ const values = valResult.success;
969
+ values.is_user_config = true;
970
+ const userAttrs = user._attributes ? { ...user._attributes } : {};
971
+ userAttrs.layout = {
972
+ plugin: plugin.name,
973
+ config: values,
974
+ };
975
+ await user.update({ _attributes: userAttrs });
976
+ getState().userLayouts[req.user.email] = module.layout(
977
+ userAttrs.layout.config
978
+ );
979
+ getState().processSend({
980
+ refresh_plugin_cfg: plugin.name,
981
+ tenant: db.getTenantSchema(),
982
+ });
983
+ res.json({ success: "ok" });
984
+ })
985
+ );
986
+
987
+ router.post(
988
+ "/remove_user_layout",
989
+ loggedIn,
990
+ error_catcher(async (req, res) => {
991
+ const user = await User.findOne({ id: req.user.id });
992
+ if (!user) {
993
+ return res.status(401).json({ error: req.__("Not authorized") });
994
+ } else if (user._attributes?.layout) {
995
+ const userAttrs = { ...user._attributes };
996
+ const plugin = userAttrs.layout.plugin;
997
+ delete userAttrs.layout;
998
+ await user.update({ _attributes: userAttrs });
999
+ getState().userLayouts[req.user.email] = null;
1000
+ getState().processSend({
1001
+ refresh_plugin_cfg: plugin,
1002
+ tenant: db.getTenantSchema(),
1003
+ });
1004
+ }
1005
+ res.json({ success: "ok", reload_page: true });
1006
+ })
1007
+ );
1008
+
818
1009
  /**
819
1010
  * @name get/new
820
1011
  * @function