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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/routes/plugins.js CHANGED
@@ -7,13 +7,7 @@
7
7
 
8
8
  const Router = require("express-promise-router");
9
9
  const { isAdmin, error_catcher } = require("./utils.js");
10
- const {
11
- mkTable,
12
- renderForm,
13
- link,
14
- post_btn,
15
- post_delete_btn,
16
- } = require("@saltcorn/markup");
10
+ const { renderForm, link, post_btn } = require("@saltcorn/markup");
17
11
  const {
18
12
  getState,
19
13
  restart_tenant,
@@ -26,7 +20,6 @@ const { fetch_available_packs } = require("@saltcorn/admin-models/models/pack");
26
20
  const {
27
21
  upgrade_all_tenants_plugins,
28
22
  } = require("@saltcorn/admin-models/models/tenant");
29
- const { getConfig, setConfig } = require("@saltcorn/data/models/config");
30
23
  const db = require("@saltcorn/data/db");
31
24
  const {
32
25
  plugin_types_info_card,
@@ -37,7 +30,6 @@ const {
37
30
  const load_plugins = require("../load_plugins");
38
31
  const {
39
32
  h5,
40
- nbsp,
41
33
  a,
42
34
  div,
43
35
  span,
@@ -50,7 +42,11 @@ const {
50
42
  th,
51
43
  td,
52
44
  p,
53
- strong,
45
+ form,
46
+ select,
47
+ option,
48
+ input,
49
+ label,
54
50
  text,
55
51
  } = require("@saltcorn/markup/tags");
56
52
  const { search_bar } = require("@saltcorn/markup/helpers");
@@ -58,8 +54,9 @@ const fs = require("fs");
58
54
  const path = require("path");
59
55
  const { get_latest_npm_version } = require("@saltcorn/data/models/config");
60
56
  const { flash_restart } = require("../markup/admin.js");
61
- const { sleep } = require("@saltcorn/data/utils");
57
+ const { sleep, removeNonWordChars } = require("@saltcorn/data/utils");
62
58
  const { loadAllPlugins } = require("../load_plugins");
59
+ const npmFetch = require("npm-registry-fetch");
63
60
 
64
61
  /**
65
62
  * @type {object}
@@ -177,6 +174,7 @@ const get_store_items = async () => {
177
174
  has_theme: plugin.has_theme,
178
175
  has_auth: plugin.has_auth,
179
176
  unsafe: plugin.unsafe,
177
+ source: plugin.source,
180
178
  }))
181
179
  .filter((p) => !p.unsafe || isRoot || tenants_unsafe_plugins);
182
180
  const local_logins = installed_plugins
@@ -290,15 +288,19 @@ const store_item_html = (req) => (item) => ({
290
288
  div(
291
289
  !item.installed &&
292
290
  item.plugin &&
293
- post_btn(
294
- `/plugins/install/${encodeURIComponent(item.name)}`,
295
- req.__("Install"),
296
- req.csrfToken(),
297
- {
298
- klass: "store-install",
299
- small: true,
300
- onClick: "press_store_button(this)",
301
- }
291
+ div(
292
+ { class: "me-2 d-inline" },
293
+ post_btn(
294
+ `/plugins/install/${encodeURIComponent(item.name)}`,
295
+ req.__("Install"),
296
+ req.csrfToken(),
297
+ {
298
+ klass: "store-install",
299
+ small: true,
300
+ onClick: "press_store_button(this)",
301
+ formClass: "d-inline",
302
+ }
303
+ )
302
304
  ),
303
305
  !item.installed &&
304
306
  item.pack &&
@@ -526,8 +528,12 @@ const plugin_store_html = (items, req) => {
526
528
  contents: div(
527
529
  { class: "d-flex justify-content-between" },
528
530
  storeNavPills(req),
529
- div(search_bar("q", req.query.q || "",
530
- { placeHolder: req.__("Search for..."), stateField: "q" })),
531
+ div(
532
+ search_bar("q", req.query.q || "", {
533
+ placeHolder: req.__("Search for..."),
534
+ stateField: "q",
535
+ })
536
+ ),
531
537
  div(store_actions_dropdown(req))
532
538
  ),
533
539
  },
@@ -558,6 +564,105 @@ router.get(
558
564
  })
559
565
  );
560
566
 
567
+ router.get(
568
+ "/versions_dialog/:name",
569
+ isAdmin,
570
+ error_catcher(async (req, res) => {
571
+ const { name } = req.params;
572
+ const withoutOrg = name.replace(/^@saltcorn\//, "");
573
+ const plugin = await Plugin.store_by_name(decodeURIComponent(withoutOrg));
574
+ if (!plugin) {
575
+ getState().log(
576
+ 2,
577
+ `GET /versions_dialog${withoutOrg}: '${withoutOrg}' not found`
578
+ );
579
+ return res
580
+ .status(404)
581
+ .json({ error: req.__("Module '%s' not found", withoutOrg) });
582
+ } else {
583
+ try {
584
+ const pkgInfo = await npmFetch.json(
585
+ `https://registry.npmjs.org/${plugin.location}`
586
+ );
587
+ if (!pkgInfo?.versions)
588
+ throw new Error(req.__("Unable to fetch versions"));
589
+ res.set("Page-Title", req.__("%s versions", text(withoutOrg)));
590
+ const versions = Object.keys(pkgInfo.versions);
591
+ if (versions.length === 0) throw new Error(req.__("No versions found"));
592
+ let selected = null;
593
+ if (getState().plugins[plugin.name]) {
594
+ const mod = await load_plugins.requirePlugin(plugin);
595
+ if (mod) selected = mod.version;
596
+ }
597
+ if (!selected) selected = versions[versions.length - 1];
598
+ return res.send(
599
+ form(
600
+ {
601
+ action: `/plugins/install/${encodeURIComponent(name)}`,
602
+ method: "post",
603
+ },
604
+ input({ type: "hidden", name: "_csrf", value: req.csrfToken() }),
605
+ div(
606
+ { class: "form-group" },
607
+ label(
608
+ {
609
+ for: "version_select",
610
+ class: "form-label fw-bold",
611
+ },
612
+ req.__("Version")
613
+ ),
614
+ select(
615
+ {
616
+ id: "version_select",
617
+ class: "form-control form-select",
618
+ name: "version",
619
+ },
620
+ versions.map((version) =>
621
+ option({
622
+ id: `${version}_opt`,
623
+ value: version,
624
+ label: version,
625
+ selected: version === selected,
626
+ })
627
+ )
628
+ )
629
+ ),
630
+ div(
631
+ { class: "d-flex justify-content-end" },
632
+ button(
633
+ {
634
+ type: "button",
635
+ class: "btn btn-secondary me-2",
636
+ "data-bs-dismiss": "modal",
637
+ },
638
+ req.__("Close")
639
+ ),
640
+ button(
641
+ {
642
+ type: "submit",
643
+ class: "btn btn-primary",
644
+ onClick: "press_store_button(this)",
645
+ },
646
+ req.__("Install")
647
+ )
648
+ )
649
+ )
650
+ );
651
+ } catch (error) {
652
+ getState().log(
653
+ 2,
654
+ `GET /versions_dialog${withoutOrg}: ${
655
+ error.message || "unknown error"
656
+ }`
657
+ );
658
+ return res
659
+ .status(500)
660
+ .json({ error: error.message || "unknown error" });
661
+ }
662
+ }
663
+ })
664
+ );
665
+
561
666
  /**
562
667
  * @name get/configure/:name
563
668
  * @function
@@ -810,7 +915,12 @@ router.get(
810
915
  isAdmin,
811
916
  error_catcher(async (req, res) => {
812
917
  const { name } = req.params;
813
- const plugin_db = await Plugin.findOne({ name });
918
+ let plugin_db = await Plugin.findOne({ name });
919
+ if (!plugin_db) {
920
+ req.flash("warning", req.__("Module not found"));
921
+ res.redirect("/plugins");
922
+ return;
923
+ }
814
924
  const mod = await load_plugins.requirePlugin(plugin_db);
815
925
  const store_items = await get_store_items();
816
926
  const store_item = store_items.find((item) => item.name === name);
@@ -821,27 +931,49 @@ router.get(
821
931
  update_permitted &&
822
932
  (await get_latest_npm_version(plugin_db.location, 1000));
823
933
  const can_update = update_permitted && latest && mod.version !== latest;
934
+ const can_select_version = update_permitted && plugin_db.source === "npm";
824
935
  let pkgjson;
825
936
  if (mod.location && fs.existsSync(path.join(mod.location, "package.json")))
826
937
  pkgjson = require(path.join(mod.location, "package.json"));
827
-
828
- if (!plugin_db) {
829
- req.flash("warning", req.__("Module not found"));
830
- res.redirect("/plugins");
831
- return;
832
- }
938
+ const domId = `${removeNonWordChars(mod.name)}_store_version_btn`;
833
939
  const infoTable = table(
834
940
  tbody(
835
941
  tr(th(req.__("Package name")), td(mod.name)),
836
- tr(th(req.__("Package version")), td(mod.version)),
942
+ tr(
943
+ th(req.__("Package version")),
944
+ td(
945
+ span(
946
+ { style: "display: inline-block; min-width: 2.9rem;" },
947
+ mod.version
948
+ ),
949
+ can_select_version
950
+ ? a(
951
+ {
952
+ id: domId,
953
+ class: "store-install btn btn-sm btn-primary ms-2",
954
+ onClick: "press_store_button(this, true)",
955
+ href: `javascript:ajax_modal('/plugins/versions_dialog/${encodeURIComponent(
956
+ encodeURIComponent(plugin_db.name)
957
+ )}', { onOpen: () => { restore_old_button('${domId}'); }, onError: (res) => { selectVersionError(res, '${domId}') } });`,
958
+ },
959
+ req.__("install a different version")
960
+ )
961
+ : ""
962
+ )
963
+ ),
837
964
  tr(
838
965
  th(req.__("Latest version")),
839
966
  td(
840
- latest || "",
967
+ span(
968
+ { style: "display: inline-block; min-width: 2.9rem;" },
969
+ latest || ""
970
+ ),
841
971
  can_update
842
972
  ? a(
843
973
  {
844
- href: `/plugins/upgrade-plugin/${plugin_db.name}`,
974
+ href: `/plugins/upgrade-plugin/${encodeURIComponent(
975
+ plugin_db.name
976
+ )}`,
845
977
  class: "btn btn-primary btn-sm ms-2",
846
978
  },
847
979
  req.__("Upgrade")
@@ -979,7 +1111,7 @@ router.get(
979
1111
  await plugin.upgrade_version((p, f) => load_plugins.loadPlugin(p, f));
980
1112
  req.flash("success", req.__(`Module up-to-date`));
981
1113
 
982
- res.redirect(`/plugins/info/${plugin.name}`);
1114
+ res.redirect(`/plugins/info/${encodeURIComponent(plugin.name)}`);
983
1115
  })
984
1116
  );
985
1117
 
@@ -1068,11 +1200,25 @@ router.post(
1068
1200
  isAdmin,
1069
1201
  error_catcher(async (req, res) => {
1070
1202
  const { name } = req.params;
1203
+ const { version } = req.body;
1071
1204
  const tenants_unsafe_plugins = getRootState().getConfig(
1072
1205
  "tenants_unsafe_plugins",
1073
1206
  false
1074
1207
  );
1075
- const plugin = await Plugin.store_by_name(decodeURIComponent(name));
1208
+ // when a version is specified, either update the db row or use the plugin from the store
1209
+ // when no version is specified, allways use the plugin from the store
1210
+ let plugin = null;
1211
+ if (version) {
1212
+ plugin = await Plugin.findOne({ name: name });
1213
+ if (plugin) plugin.version = version;
1214
+ }
1215
+ if (!plugin) {
1216
+ plugin = await Plugin.store_by_name(decodeURIComponent(name));
1217
+ if (plugin) {
1218
+ delete plugin.id;
1219
+ if (version) plugin.version = version;
1220
+ }
1221
+ }
1076
1222
  if (!plugin) {
1077
1223
  req.flash(
1078
1224
  "error",
@@ -1081,6 +1227,11 @@ router.post(
1081
1227
  res.redirect(`/plugins`);
1082
1228
  return;
1083
1229
  }
1230
+ let forceReInstall =
1231
+ version !== undefined ||
1232
+ (plugin.source === "npm" && plugin.version === "latest");
1233
+ if (version) plugin.version = version;
1234
+
1084
1235
  const isRoot = db.getTenantSchema() === db.connectObj.default_schema;
1085
1236
  if (!isRoot && plugin.unsafe && !tenants_unsafe_plugins) {
1086
1237
  req.flash(
@@ -1090,8 +1241,7 @@ router.post(
1090
1241
  res.redirect(`/plugins`);
1091
1242
  return;
1092
1243
  }
1093
- delete plugin.id;
1094
- await load_plugins.loadAndSaveNewPlugin(plugin);
1244
+ await load_plugins.loadAndSaveNewPlugin(plugin, forceReInstall);
1095
1245
  const plugin_module = getState().plugins[name];
1096
1246
  if (plugin_module && plugin_module.configuration_workflow) {
1097
1247
  const plugin_db = await Plugin.findOne({ name });
package/routes/tables.js CHANGED
@@ -163,7 +163,7 @@ const tableForm = async (table, req) => {
163
163
  {
164
164
  label: req.__("Version history"),
165
165
  sublabel: req.__(
166
- "Version history allows to track table data changes"
166
+ "Track table data changes over time"
167
167
  ),
168
168
  name: "versioned",
169
169
  type: "Bool",
@@ -235,7 +235,8 @@ router.get(
235
235
  name: "provider_name",
236
236
  input_type: "select",
237
237
  options: [
238
- req.__("Database table"),
238
+ // Due to packages/saltcorn-markup/helpers.ts#L45 (select_options replaces label if o.value === "")
239
+ {label:req.__("Database table"), value:'-'},
239
240
  ...table_provider_names,
240
241
  ],
241
242
  required: true,
@@ -1066,7 +1067,7 @@ router.post(
1066
1067
  res.redirect(`/table/new`);
1067
1068
  } else if (
1068
1069
  rest.provider_name &&
1069
- rest.provider_name !== "Database table"
1070
+ rest.provider_name !== "-"
1070
1071
  ) {
1071
1072
  const table = await Table.create(name, rest);
1072
1073
  res.redirect(`/table/provider-cfg/${table.id}`);
@@ -147,6 +147,10 @@ const viewForm = async (req, tableOptions, roles, pages, values) => {
147
147
  sublabel: req.__(
148
148
  "The view pattern sets the foundation of how the view relates to the table and the behaviour of the view"
149
149
  ),
150
+ help: {
151
+ topic: "View patterns",
152
+ context: {},
153
+ },
150
154
  options: Object.keys(getState().viewtemplates),
151
155
  attributes: {
152
156
  explainers: mapObjectValues(
@@ -209,7 +213,7 @@ const viewForm = async (req, tableOptions, roles, pages, values) => {
209
213
  }),
210
214
  {
211
215
  name: "popup_width",
212
- label: req.__("Column width"),
216
+ label: req.__("Popup width"),
213
217
  type: "Integer",
214
218
  tab: "Popup settings",
215
219
  parent_field: "attributes",
@@ -232,6 +236,9 @@ const viewForm = async (req, tableOptions, roles, pages, values) => {
232
236
  label: req.__("Save indicator"),
233
237
  type: "Bool",
234
238
  parent_field: "attributes",
239
+ sublabel: req.__(
240
+ "Show an icon in the title bar to indicate when form data is being saved"
241
+ ),
235
242
  tab: "Popup settings",
236
243
  },
237
244
  {