@saltcorn/server 0.9.5-beta.1 → 0.9.5-beta.10

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/admin.js CHANGED
@@ -107,6 +107,7 @@ const { getSafeSaltcornCmd } = require("@saltcorn/data/utils");
107
107
  const stream = require("stream");
108
108
  const Crash = require("@saltcorn/data/models/crash");
109
109
  const { get_help_markup } = require("../help/index.js");
110
+ const Docker = require("dockerode");
110
111
 
111
112
  const router = new Router();
112
113
  module.exports = router;
@@ -273,6 +274,8 @@ router.get(
273
274
  const aBackupFilePrefixForm = backupFilePrefixForm(req);
274
275
  aBackupFilePrefixForm.values.backup_file_prefix =
275
276
  getState().getConfig("backup_file_prefix");
277
+ aBackupFilePrefixForm.values.backup_history =
278
+ getState().getConfig("backup_history");
276
279
  //
277
280
  const backupForm = autoBackupForm(req);
278
281
  backupForm.values.auto_backup_frequency = getState().getConfig(
@@ -673,6 +676,13 @@ const backupFilePrefixForm = (req) =>
673
676
  sublabel: req.__("Backup file prefix"),
674
677
  default: "sc-backup-",
675
678
  },
679
+ {
680
+ type: "Bool",
681
+ label: req.__("History"),
682
+ name: "backup_history",
683
+ sublabel: req.__("Include table history in backup"),
684
+ default: true,
685
+ },
676
686
  ],
677
687
  });
678
688
 
@@ -1104,6 +1114,28 @@ router.post(
1104
1114
  })
1105
1115
  );
1106
1116
 
1117
+ const pullCordovaBuilder = (req, res) => {
1118
+ const child = spawn("docker", ["pull", "saltcorn/cordova-builder"], {
1119
+ stdio: ["ignore", "pipe", "pipe"],
1120
+ });
1121
+ return new Promise((resolve, reject) => {
1122
+ child.stdout.on("data", (data) => {
1123
+ res.write(data);
1124
+ });
1125
+ child.stderr?.on("data", (data) => {
1126
+ res.write(data);
1127
+ });
1128
+ child.on("exit", function (code, signal) {
1129
+ resolve(code);
1130
+ });
1131
+ child.on("error", (msg) => {
1132
+ const message = msg.message ? msg.message : msg.code;
1133
+ res.write(req.__("Error: ") + message + "\n");
1134
+ resolve(msg.code);
1135
+ });
1136
+ });
1137
+ };
1138
+
1107
1139
  /**
1108
1140
  * Do Upgrade
1109
1141
  * @name post/upgrade
@@ -1132,7 +1164,14 @@ router.post(
1132
1164
  child.stderr?.on("data", (data) => {
1133
1165
  res.write(data);
1134
1166
  });
1135
- child.on("exit", function (code, signal) {
1167
+ child.on("exit", async function (code, signal) {
1168
+ if (code === 0) {
1169
+ res.write(
1170
+ req.__("Pulling the cordova-builder docker image...") + "\n"
1171
+ );
1172
+ const pullCode = await pullCordovaBuilder(req, res);
1173
+ res.write(req.__("Pull done with code %s", pullCode) + "\n");
1174
+ }
1136
1175
  res.end(
1137
1176
  req.__(
1138
1177
  `Upgrade done (if it was available) with code ${code}.\n\nPress the BACK button in your browser, then RELOAD the page.`
@@ -1481,8 +1520,9 @@ router.get(
1481
1520
  });
1482
1521
  })
1483
1522
  );
1484
- const buildDialogScript = () => {
1523
+ const buildDialogScript = (cordovaBuilderAvailable) => {
1485
1524
  return `<script>
1525
+ var cordovaBuilderAvailable = ${cordovaBuilderAvailable};
1486
1526
  function showEntrySelect(type) {
1487
1527
  for( const currentType of ["view", "page", "pagegroup"]) {
1488
1528
  const tab = $('#' + currentType + 'NavLinkID');
@@ -1519,6 +1559,17 @@ const buildDialogScript = () => {
1519
1559
  }
1520
1560
  </script>`;
1521
1561
  };
1562
+
1563
+ const imageAvailable = async () => {
1564
+ try {
1565
+ const image = new Docker().getImage("saltcorn/cordova-builder");
1566
+ await image.inspect();
1567
+ return true;
1568
+ } catch (e) {
1569
+ return false;
1570
+ }
1571
+ };
1572
+
1522
1573
  /**
1523
1574
  * Build mobile app
1524
1575
  */
@@ -1538,13 +1589,14 @@ router.get(
1538
1589
  );
1539
1590
  const builderSettings =
1540
1591
  getState().getConfig("mobile_builder_settings") || {};
1592
+ const dockerAvailable = await imageAvailable();
1541
1593
  send_admin_page({
1542
1594
  res,
1543
1595
  req,
1544
1596
  active_sub: "Mobile app",
1545
1597
  headers: [
1546
1598
  {
1547
- headerTag: buildDialogScript(),
1599
+ headerTag: buildDialogScript(dockerAvailable),
1548
1600
  },
1549
1601
  ],
1550
1602
  contents: {
@@ -2165,6 +2217,56 @@ router.get(
2165
2217
  )
2166
2218
  )
2167
2219
  )
2220
+ ),
2221
+ div(
2222
+ { class: "row pb-3 pt-3" },
2223
+ div(
2224
+ label(
2225
+ { class: "form-label fw-bold" },
2226
+ req.__("Cordova builder") +
2227
+ a(
2228
+ {
2229
+ href: "javascript:ajax_modal('/admin/help/Cordova Builder?')",
2230
+ },
2231
+ i({ class: "fas fa-question-circle ps-1" })
2232
+ )
2233
+ )
2234
+ ),
2235
+ div(
2236
+ { class: "col-sm-4" },
2237
+ div(
2238
+ {
2239
+ id: "dockerBuilderStatusId",
2240
+ class: "",
2241
+ },
2242
+ dockerAvailable
2243
+ ? span(
2244
+ req.__("installed"),
2245
+ i({ class: "ps-2 fas fa-check text-success" })
2246
+ )
2247
+ : span(
2248
+ req.__("not available"),
2249
+ i({ class: "ps-2 fas fa-times text-danger" })
2250
+ )
2251
+ )
2252
+ ),
2253
+ div(
2254
+ { class: "col-sm-4" },
2255
+ button(
2256
+ {
2257
+ id: "pullCordovaBtnId",
2258
+ type: "button",
2259
+ onClick: `pull_cordova_builder(this);`,
2260
+ class: "btn btn-warning",
2261
+ },
2262
+ req.__("pull")
2263
+ ),
2264
+ span(
2265
+ { role: "button", onClick: "check_cordova_builder()" },
2266
+ span({ class: "ps-3" }, req.__("refresh")),
2267
+ i({ class: "ps-2 fas fa-undo" })
2268
+ )
2269
+ )
2168
2270
  )
2169
2271
  ),
2170
2272
  button(
@@ -2419,6 +2521,48 @@ router.post(
2419
2521
  })
2420
2522
  );
2421
2523
 
2524
+ router.post(
2525
+ "/mobile-app/pull-cordova-builder",
2526
+ isAdmin,
2527
+ error_catcher(async (req, res) => {
2528
+ const state = getState();
2529
+ const child = spawn(
2530
+ "docker",
2531
+ ["image", "pull", "saltcorn/cordova-builder:latest"],
2532
+ {
2533
+ stdio: ["ignore", "pipe", "pipe"],
2534
+ cwd: ".",
2535
+ }
2536
+ );
2537
+ child.stdout.on("data", (data) => {
2538
+ state.log(5, data.toString());
2539
+ });
2540
+ child.stderr.on("data", (data) => {
2541
+ state.log(1, data.toString());
2542
+ });
2543
+ child.on("exit", (exitCode, signal) => {
2544
+ state.log(
2545
+ 2,
2546
+ `"pull cordova-builder exit with code: ${exitCode} and signal: ${signal}`
2547
+ );
2548
+ });
2549
+ child.on("error", (msg) => {
2550
+ state.log(1, `pull cordova-builder error: ${msg}`);
2551
+ });
2552
+
2553
+ res.json({});
2554
+ })
2555
+ );
2556
+
2557
+ router.get(
2558
+ "/mobile-app/check-cordova-builder",
2559
+ isAdmin,
2560
+ error_catcher(async (req, res) => {
2561
+ const installed = await imageAvailable();
2562
+ res.json({ installed });
2563
+ })
2564
+ );
2565
+
2422
2566
  /**
2423
2567
  * Do Clear All
2424
2568
  * @function
@@ -2511,6 +2655,8 @@ router.post(
2511
2655
  await getState().refresh();
2512
2656
  }
2513
2657
  if (form.values.users) {
2658
+ await db.deleteWhere("_sc_notifications");
2659
+
2514
2660
  const users1 = Table.findOne({ name: "users" });
2515
2661
  const userfields1 = await users1.getFields();
2516
2662
 
@@ -226,7 +226,7 @@ const tagsDropdown = (tags, altHeader) =>
226
226
  { class: "dropdown" },
227
227
  div(
228
228
  {
229
- class: "link-style",
229
+ class: "link-style text-nowrap",
230
230
  "data-boundary": "viewport",
231
231
  type: "button",
232
232
  id: "tagsselector",
package/routes/fields.js CHANGED
@@ -994,13 +994,15 @@ router.post(
994
994
  if (oldRow) {
995
995
  const value = oldRow[kpath[kpath.length - 1]];
996
996
  //TODO run fieldview
997
- res.send(
998
- typeof value === "string"
999
- ? value
1000
- : value?.toString
1001
- ? value.toString()
1002
- : `${value}`
1003
- );
997
+ if (value === null || typeof value === "undefined") res.send("");
998
+ else
999
+ res.send(
1000
+ typeof value === "string"
1001
+ ? value
1002
+ : value?.toString
1003
+ ? value.toString()
1004
+ : `${value}`
1005
+ );
1004
1006
  return;
1005
1007
  }
1006
1008
  }
@@ -547,7 +547,7 @@ const no_views_logged_in = async (req, res) => {
547
547
  * @returns {Promise<boolean>}
548
548
  */
549
549
  const get_config_response = async (role_id, res, req) => {
550
- const wrap = async (contents, homeCfg, title, description) => {
550
+ const wrap = async (contents, homeCfg, title, description, no_menu) => {
551
551
  if (contents.html_file) await sendHtmlFile(req, res, contents.html_file);
552
552
  else
553
553
  res.sendWrap(
@@ -555,6 +555,7 @@ const get_config_response = async (role_id, res, req) => {
555
555
  title: title || "",
556
556
  description: description || "",
557
557
  bodyClass: "page_" + db.sqlsanitize(homeCfg),
558
+ no_menu,
558
559
  },
559
560
  contents
560
561
  );
@@ -574,7 +575,8 @@ const get_config_response = async (role_id, res, req) => {
574
575
  await db_page.run(req.query, { res, req }),
575
576
  homeCfg,
576
577
  db_page.title,
577
- db_page.description
578
+ db_page.description,
579
+ db_page.attributes?.no_menu
578
580
  );
579
581
  else {
580
582
  const group = PageGroup.findOne({ name: homeCfg });
@@ -587,7 +589,8 @@ const get_config_response = async (role_id, res, req) => {
587
589
  await eligible.run(req.query, { res, req }),
588
590
  homeCfg,
589
591
  eligible.title,
590
- eligible.description
592
+ eligible.description,
593
+ eligible.attributes?.no_menu
591
594
  );
592
595
  } else wrap(req.__("%s has no eligible page", group.name), homeCfg);
593
596
  } else res.redirect(homeCfg);
package/routes/plugins.js CHANGED
@@ -57,6 +57,7 @@ const { flash_restart } = require("../markup/admin.js");
57
57
  const { sleep, removeNonWordChars } = require("@saltcorn/data/utils");
58
58
  const { loadAllPlugins } = require("../load_plugins");
59
59
  const npmFetch = require("npm-registry-fetch");
60
+ const PluginInstaller = require("@saltcorn/plugins-loader/plugin_installer");
60
61
 
61
62
  /**
62
63
  * @type {object}
@@ -1178,6 +1179,7 @@ router.post(
1178
1179
  getState().getConfig("development_mode", false)
1179
1180
  ) {
1180
1181
  await plugin.delete();
1182
+ await new PluginInstaller(plugin).remove();
1181
1183
  req.flash("success", req.__(`Module %s removed.`, plugin.name));
1182
1184
  } else {
1183
1185
  req.flash(
@@ -1241,7 +1243,12 @@ router.post(
1241
1243
  res.redirect(`/plugins`);
1242
1244
  return;
1243
1245
  }
1244
- await load_plugins.loadAndSaveNewPlugin(plugin, forceReInstall);
1246
+ const msgs = await load_plugins.loadAndSaveNewPlugin(
1247
+ plugin,
1248
+ forceReInstall,
1249
+ undefined,
1250
+ req.__
1251
+ );
1245
1252
  const plugin_module = getState().plugins[name];
1246
1253
  await sleep(1000); // Allow other workers to load this plugin
1247
1254
  await getState().refresh_views();
@@ -1255,9 +1262,11 @@ router.post(
1255
1262
  plugin_db.name
1256
1263
  )
1257
1264
  );
1265
+ if (msgs?.length > 0) req.flash("warning", msgs.join("<br>"));
1258
1266
  res.redirect(`/plugins/configure/${plugin_db.name}`);
1259
1267
  } else {
1260
1268
  req.flash("success", req.__(`Module %s installed`, plugin.name));
1269
+ if (msgs?.length > 0) req.flash("warning", msgs.join("<br>"));
1261
1270
  res.redirect(`/plugins`);
1262
1271
  }
1263
1272
  })
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);