@saltcorn/server 0.8.9 → 0.9.0-beta.1

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.
@@ -0,0 +1,23 @@
1
+ Table constraints in Saltcorn are used to impose constraints on the data that
2
+ can be held in individual rows or to alter the performance characteristics of
3
+ the table.
4
+
5
+ There are three types of table constraints:
6
+
7
+ A __unique constraint__ can be used to impose a restriction that a combination
8
+ of fields is unique
9
+ across the whole table. For instance, if you have a table for people and you
10
+ mark `name` and `age` as jointly unique using a unique constraint then you cannot
11
+ have two rows with the same name and age; but you could have two rows with the
12
+ same name.
13
+
14
+ A __formula constraint__ is used to impose restrictions on the content of
15
+ each row which can use several Fields. For instance if you have from and to
16
+ date fields, you can use the formula constraint to ensure that the to date is
17
+ always after the from date.
18
+
19
+ An __index__ does not put any limitations or constraints
20
+ on the table data. It adds an index to the table to accelerate queries from
21
+ individual fields, at the expense of using more disk space. You should put an
22
+ index on fields in large tables that are frequently queried either through
23
+ filters or aggregations.
@@ -0,0 +1,43 @@
1
+ The constraint formula should be a JavaScript expression which evaluates to a
2
+ boolean (true or false). If the formula evaluates to false then the row is not
3
+ valid and will not be accepted in the table. If you are editing an existing row
4
+ then the edit will not be accepted. The supplied error message text will be shown
5
+ to the user. 
6
+
7
+ Some examples:
8
+
9
+ {{# for (const fml of table.getFormulaExamples("Bool")) { }} * `{{ fml}}`
10
+ {{# } }}
11
+
12
+ This formula can use any of the fields in table {{ table.name }} as variables:
13
+
14
+ | Field | Variable name | Type |
15
+ | ----- | ------------- | ---- |
16
+ {{# for (const field of table.fields) { }} | {{ field.label }} | `{{ field.name }}` | {{ field.pretty_type }} |
17
+ {{# } }}
18
+
19
+
20
+
21
+
22
+ {{# if(table.fields.some(f=>f.is_fkey)) { }}
23
+
24
+ You can also use join fields on related tables. These are accessed with the dot object access
25
+ notation, in the form:
26
+
27
+ ```
28
+ {key field}.{field on referenced table}
29
+ ```
30
+
31
+ The first-order join fields you can use in the constraint formula are:
32
+
33
+ {{# for (const field of table.fields.filter(f=>f.is_fkey && f.reftable_name)) { }}
34
+ {{# const reftable = Table.findOne( field.reftable_name); }}
35
+ * Through {{field.label}} key field: {{ reftable.fields.map(jf=>`\`${field.name}.${jf.name}\``).join(", ") }}
36
+
37
+
38
+ {{# } }}
39
+
40
+ If you use join fields, Saltcorn is not able to create a constraint in the SQL database. In that case, it will not check existing
41
+ rows, and it will also not be able to enforce constraints involving the primary key value on newly created rows.
42
+
43
+ {{# } }}
package/help/index.js ADDED
@@ -0,0 +1,45 @@
1
+ const Table = require("@saltcorn/data/models/table");
2
+ const File = require("@saltcorn/data/models/file");
3
+ const _ = require("underscore");
4
+ const fs = require("fs").promises;
5
+ const MarkdownIt = require("markdown-it"),
6
+ md = new MarkdownIt();
7
+
8
+ const { pre } = require("@saltcorn/markup/tags");
9
+ const path = require("path");
10
+
11
+ const get_md_file = async (topic) => {
12
+ try {
13
+ const fp = path.join(__dirname, `${File.normalise(topic)}.tmd`);
14
+ const fileBuf = await fs.readFile(fp);
15
+ return fileBuf.toString();
16
+ } catch (e) {
17
+ return false;
18
+ }
19
+ };
20
+
21
+ md.renderer.rules.table_open = function (tokens, idx) {
22
+ return '<table class="help-md">';
23
+ };
24
+
25
+ const get_help_markup = async (topic, query, req) => {
26
+ try {
27
+ const context = { user: req.user, Table };
28
+ if (query.table) {
29
+ context.table = Table.findOne({ name: query.table });
30
+ }
31
+ const mdTemplate = await get_md_file(topic);
32
+ if (!mdTemplate) return { markup: "Topic not found" };
33
+ const template = _.template(mdTemplate, {
34
+ evaluate: /\{\{#(.+?)\}\}/g,
35
+ interpolate: /\{\{([^#].+?)\}\}/g,
36
+ });
37
+ const mdTopic = template(context);
38
+ const markup = md.render(mdTopic);
39
+ return { markup };
40
+ } catch (e) {
41
+ return { markup: pre(e.toString()) };
42
+ }
43
+ };
44
+
45
+ module.exports = { get_help_markup };
package/load_plugins.js CHANGED
@@ -176,9 +176,10 @@ const loadAllPlugins = async () => {
176
176
  * @param plugin
177
177
  * @param force
178
178
  * @param noSignalOrDB
179
+ * @param manager - optional plugin manager
179
180
  * @returns {Promise<void>}
180
181
  */
181
- const loadAndSaveNewPlugin = async (plugin, force, noSignalOrDB) => {
182
+ const loadAndSaveNewPlugin = async (plugin, force, noSignalOrDB, manager) => {
182
183
  const tenants_unsafe_plugins = getRootState().getConfig(
183
184
  "tenants_unsafe_plugins",
184
185
  false
@@ -200,7 +201,8 @@ const loadAndSaveNewPlugin = async (plugin, force, noSignalOrDB) => {
200
201
  }
201
202
  const { version, plugin_module, location } = await requirePlugin(
202
203
  plugin,
203
- force
204
+ force,
205
+ manager,
204
206
  );
205
207
 
206
208
  // install dependecies
package/locales/en.json CHANGED
@@ -1250,5 +1250,7 @@
1250
1250
  "New user view": "New user view",
1251
1251
  "A view to show to new users, to finalise registration (if Edit) or as a welcome view": "A view to show to new users, to finalise registration (if Edit) or as a welcome view",
1252
1252
  "View decoration": "View decoration",
1253
- "Title formula": "Title formula"
1253
+ "Title formula": "Title formula",
1254
+ "Show if true": "Show if true",
1255
+ "Formula. Leave blank to always show": "Formula. Leave blank to always show"
1254
1256
  }
@@ -22,144 +22,21 @@ const toJsType = (type) =>
22
22
  Color: "string",
23
23
  }[type] || type);
24
24
 
25
- /**
26
- * @param {*} type
27
- * @param {object[]} fields
28
- * @returns {string[]}
29
- */
30
- const intExamples = (type, fields) => {
31
- const boolFields = fields.filter((f) => f.type && f.type.name === "Bool");
32
- const intFields = fields.filter((f) => f.type && f.type.name === "Integer");
33
- const exs = ["3"];
34
- if (boolFields.length > 0) {
35
- const b = is.one_of(boolFields).generate();
36
- exs.push(`${b.name} ? 6 : 9`);
37
- }
38
- if (intFields.length > 0) {
39
- const b = is.one_of(intFields).generate();
40
- exs.push(`${b.name} + 5`);
41
- }
42
- return exs;
43
- };
44
-
45
- /**
46
- * @param {*} type
47
- * @param {object[]} fields
48
- * @returns {string[]}
49
- */
50
- const colorExamples = (type, fields) => {
51
- const boolFields = fields.filter((f) => f.type && f.type.name === "Bool");
52
- const exs = [`"#06ab6d1"`];
53
- if (boolFields.length > 0) {
54
- const b = is.one_of(boolFields).generate();
55
- exs.push(`${b.name} ? "#000000" : "#ffffff"`);
56
- }
57
- return exs;
58
- };
59
-
60
- /**
61
- * @param {*} type
62
- * @param {object[]} fields
63
- * @returns {string[]}
64
- */
65
- const stringExamples = (type, fields) => {
66
- const boolFields = fields.filter((f) => f.type && f.type.name === "Bool");
67
- const strFields = fields.filter((f) => f.type && f.type.name === "String");
68
- const exs = [`"Hello world!"`];
69
- if (boolFields.length > 0) {
70
- const b = is.one_of(boolFields).generate();
71
- exs.push(`${b.name} ? "Squish" : "Squash"`);
72
- }
73
- if (strFields.length > 0) {
74
- const b = is.one_of(strFields).generate();
75
- exs.push(`${b.name}.toUpperCase()`);
76
- }
77
- return exs;
78
- };
79
-
80
- /**
81
- * @param {*} type
82
- * @param {object[]} fields
83
- * @returns {string[]}
84
- */
85
- const floatExamples = (type, fields) => {
86
- const boolFields = fields.filter((f) => f.type && f.type.name === "Bool");
87
- const numFields = fields.filter(
88
- (f) => f.type && (f.type.name === "Integer" || f.type.name === "Float")
89
- );
90
- const exs = ["3.14"];
91
- if (boolFields.length > 0) {
92
- const b = is.one_of(boolFields).generate();
93
- exs.push(`${b.name} ? 2.78 : 99`);
94
- }
95
- if (numFields.length > 0) {
96
- const b = is.one_of(numFields).generate();
97
- exs.push(`Math.pow(${b.name}, 2)`);
98
- }
99
- if (numFields.length > 1) {
100
- const n1 = numFields[0];
101
- const n2 = numFields[1];
102
- exs.push(
103
- `${n1.name}>${n2.name} ? Math.sqrt(${n1.name}) : ${n1.name}*${n2.name}`
104
- );
105
- }
106
- return exs;
107
- };
108
-
109
- /**
110
- * @param {*} type
111
- * @param {object[]} fields
112
- * @returns {string[]}
113
- */
114
- const boolExamples = (type, fields) => {
115
- const boolFields = fields.filter((f) => f.type && f.type.name === "Bool");
116
- const numFields = fields.filter(
117
- (f) => (f.type && f.type.name === "Integer") || f.type.name === "Float"
118
- );
119
- const exs = ["true"];
120
- if (boolFields.length > 0) {
121
- const b = is.one_of(boolFields).generate();
122
- exs.push(`!${b.name}`);
123
- }
124
- if (boolFields.length > 1) {
125
- const b1 = boolFields[0];
126
- const b2 = boolFields[1];
127
- exs.push(`${b1.name} && ${b2.name}`);
128
- }
129
- if (numFields.length > 0) {
130
- const b = is.one_of(numFields).generate();
131
- exs.push(`${b.name}<3`);
132
- }
133
- if (numFields.length > 1) {
134
- const n1 = numFields[0];
135
- const n2 = numFields[1];
136
- exs.push(`${n1.name}>${n2.name}`);
137
- }
138
- return exs;
139
- };
140
-
141
25
  /**
142
26
  * @param {string} type
143
27
  * @param {*} stored
144
- * @param {object[]} allFields
28
+ * @param {Table} table
145
29
  * @param {object} req
146
30
  * @returns {p[]}
147
31
  */
148
- const expressionBlurb = (type, stored, allFields, req) => {
32
+ const expressionBlurb = (type, stored, table, req) => {
33
+ const allFields = table.fields;
149
34
  const fields = allFields.filter((f) => !f.calculated);
150
35
  const funs = getState().functions;
151
36
  const funNames = Object.entries(funs)
152
37
  .filter(([k, v]) => !(!stored && v.isAsync))
153
38
  .map(([k, v]) => k);
154
- const examples = (
155
- {
156
- Integer: () => intExamples(type, fields),
157
- Float: () => floatExamples(type, fields),
158
- Bool: () => boolExamples(type, fields),
159
- Color: () => colorExamples(type, fields),
160
- String: () => stringExamples(type, fields),
161
- }[type] || (() => [])
162
- )();
39
+ const examples = table.getFormulaExamples(type);
163
40
  return [
164
41
  p(
165
42
  req.__(
package/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@saltcorn/server",
3
- "version": "0.8.9",
3
+ "version": "0.9.0-beta.1",
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
- "@saltcorn/base-plugin": "0.8.9",
10
- "@saltcorn/builder": "0.8.9",
11
- "@saltcorn/data": "0.8.9",
12
- "@saltcorn/admin-models": "0.8.9",
13
- "@saltcorn/filemanager": "0.8.9",
14
- "@saltcorn/markup": "0.8.9",
15
- "@saltcorn/sbadmin2": "0.8.9",
9
+ "@saltcorn/base-plugin": "0.9.0-beta.1",
10
+ "@saltcorn/builder": "0.9.0-beta.1",
11
+ "@saltcorn/data": "0.9.0-beta.1",
12
+ "@saltcorn/admin-models": "0.9.0-beta.1",
13
+ "@saltcorn/filemanager": "0.9.0-beta.1",
14
+ "@saltcorn/markup": "0.9.0-beta.1",
15
+ "@saltcorn/sbadmin2": "0.9.0-beta.1",
16
16
  "@socket.io/cluster-adapter": "^0.2.1",
17
17
  "@socket.io/sticky": "^1.0.1",
18
18
  "adm-zip": "0.5.10",
@@ -32,11 +32,13 @@
32
32
  "express-session": "^1.17.1",
33
33
  "greenlock": "^4.0.4",
34
34
  "greenlock-express": "^4.0.3",
35
+ "underscore": "1.13.6",
35
36
  "helmet": "^3.23.3",
36
37
  "i18n": "^0.15.1",
37
38
  "imapflow": "1.0.123",
38
39
  "jsonwebtoken": "^9.0.0",
39
40
  "live-plugin-manager": "^0.17.1",
41
+ "markdown-it": "^13.0.2",
40
42
  "moment": "^2.29.4",
41
43
  "multer": "1.4.5-lts.1",
42
44
  "multer-s3": "^2.10.0",
@@ -335,18 +335,21 @@ function splitTargetMatch(elemValue, target, keySpec) {
335
335
  return elemValueShort === target;
336
336
  }
337
337
 
338
- function get_form_record(e, select_labels) {
338
+ function get_form_record(e_in, select_labels) {
339
339
  const rec = {};
340
- e.closest(".form-namespace")
341
- .find("input[name],select[name]")
342
- .each(function () {
343
- const name = $(this).attr("data-fieldname") || $(this).attr("name");
344
- if (select_labels && $(this).prop("tagName").toLowerCase() === "select")
345
- rec[name] = $(this).find("option:selected").text();
346
- else if ($(this).prop("type") === "checkbox")
347
- rec[name] = $(this).prop("checked");
348
- else rec[name] = $(this).val();
349
- });
340
+
341
+ const e = e_in.viewname
342
+ ? $(`form[data-viewname=${e_in.viewname}]`)
343
+ : e_in.closest(".form-namespace");
344
+
345
+ e.find("input[name],select[name],textarea[name]").each(function () {
346
+ const name = $(this).attr("data-fieldname") || $(this).attr("name");
347
+ if (select_labels && $(this).prop("tagName").toLowerCase() === "select")
348
+ rec[name] = $(this).find("option:selected").text();
349
+ else if ($(this).prop("type") === "checkbox")
350
+ rec[name] = $(this).prop("checked");
351
+ else rec[name] = $(this).val();
352
+ });
350
353
  return rec;
351
354
  }
352
355
  function showIfFormulaInputs(e, fml) {
@@ -953,7 +956,7 @@ function press_store_button(clicked) {
953
956
  $(btn).html('<i class="fas fa-spinner fa-spin"></i>').width(width);
954
957
  }
955
958
 
956
- function common_done(res, isWeb = true) {
959
+ function common_done(res, viewname, isWeb = true) {
957
960
  const handle = (element, fn) => {
958
961
  if (Array.isArray(element)) for (const current of element) fn(current);
959
962
  else fn(element);
@@ -982,6 +985,17 @@ function common_done(res, isWeb = true) {
982
985
  });
983
986
  });
984
987
  }
988
+ if (res.set_fields && viewname) {
989
+ Object.keys(res.set_fields).forEach((k) => {
990
+ const form = $(`form[data-viewname=${viewname}]`);
991
+ const input = form.find(
992
+ `input[name=${k}], textarea[name=${k}], select[name=${k}]`
993
+ );
994
+ if (input.attr("type") === "checkbox")
995
+ input.prop("checked", res.set_fields[k]);
996
+ else input.val(res.set_fields[k]);
997
+ });
998
+ }
985
999
  if (res.goto && !isWeb)
986
1000
  // TODO ch
987
1001
  notifyAlert({
@@ -472,3 +472,17 @@ div.unread-notify {
472
472
  cursor: pointer;
473
473
  text-decoration: underline;
474
474
  }
475
+
476
+ table.help-md thead {
477
+ border-bottom: 1px solid black;
478
+ }
479
+
480
+ table.help-md {
481
+ margin-bottom: 1em;
482
+ }
483
+
484
+ table.help-md td:nth-child(2),
485
+ table.help-md th:nth-child(2) {
486
+ padding-left: 10px;
487
+ padding-right: 10px;
488
+ }
@@ -198,8 +198,8 @@ function clear_state(omit_fields_str, e) {
198
198
  pjax_to(newUrl, e);
199
199
  }
200
200
 
201
- function ajax_done(res) {
202
- common_done(res);
201
+ function ajax_done(res, viewname) {
202
+ common_done(res, viewname);
203
203
  }
204
204
 
205
205
  function view_post(viewname, route, data, onDone, sendState) {
@@ -220,7 +220,7 @@ function view_post(viewname, route, data, onDone, sendState) {
220
220
  })
221
221
  .done(function (res) {
222
222
  if (onDone) onDone(res);
223
- ajax_done(res);
223
+ ajax_done(res, viewname);
224
224
  })
225
225
  .fail(function (res) {
226
226
  notifyAlert({ type: "danger", text: res.responseText });
@@ -491,7 +491,7 @@ function ajaxSubmitForm(e) {
491
491
  var no_reload = $("#scmodal").hasClass("no-submit-reload");
492
492
  $("#scmodal").modal("hide");
493
493
  if (!no_reload) location.reload();
494
- else common_done(res);
494
+ else common_done(res, form.attr("data-viewname"));
495
495
  },
496
496
  error: function (request) {
497
497
  var title = request.getResponseHeader("Page-Title");
package/routes/admin.js CHANGED
@@ -56,6 +56,7 @@ const {
56
56
  li,
57
57
  ol,
58
58
  script,
59
+ text,
59
60
  domReady,
60
61
  } = require("@saltcorn/markup/tags");
61
62
  const db = require("@saltcorn/data/db");
@@ -104,6 +105,7 @@ const Page = require("@saltcorn/data/models/page");
104
105
  const { getSafeSaltcornCmd } = require("@saltcorn/data/utils");
105
106
  const stream = require("stream");
106
107
  const Crash = require("@saltcorn/data/models/crash");
108
+ const { get_help_markup } = require("../help/index.js");
107
109
 
108
110
  const router = new Router();
109
111
  module.exports = router;
@@ -240,6 +242,22 @@ router.get(
240
242
  })
241
243
  );
242
244
 
245
+ /**
246
+ * @name get/send-test-email
247
+ * @function
248
+ * @memberof module:routes/admin~routes/adminRouter
249
+ */
250
+ router.get(
251
+ "/help/:topic",
252
+ isAdmin,
253
+ error_catcher(async (req, res) => {
254
+ const { topic } = req.params;
255
+ const { markup } = await get_help_markup(topic, req.query, req);
256
+
257
+ res.sendWrap(`Help: ${topic}`, { above: [markup] });
258
+ })
259
+ );
260
+
243
261
  /**
244
262
  * @name get/backup
245
263
  * @function
@@ -538,6 +556,7 @@ router.get(
538
556
  error_catcher(async (req, res) => {
539
557
  const { type, name } = req.params;
540
558
  const snaps = await Snapshot.entity_history(type, name);
559
+ res.set("Page-Title", `Restore ${text(name)}`);
541
560
  res.send(
542
561
  mkTable(
543
562
  [
@@ -1495,6 +1514,8 @@ router.get(
1495
1514
  const plugins = (await Plugin.find()).filter(
1496
1515
  (plugin) => ["base", "sbadmin2"].indexOf(plugin.name) < 0
1497
1516
  );
1517
+ const builderSettings =
1518
+ getState().getConfig("mobile_builder_settings") || {};
1498
1519
  send_admin_page({
1499
1520
  res,
1500
1521
  req,
@@ -1555,7 +1576,14 @@ router.get(
1555
1576
  onClick: "showEntrySelect('view')",
1556
1577
  },
1557
1578
  div(
1558
- { class: "nav-link active", id: "viewNavLinkID" },
1579
+ {
1580
+ class: `nav-link ${
1581
+ !builderSettings.entryPointType || builderSettings.entryPointType === "view"
1582
+ ? "active"
1583
+ : ""
1584
+ }`,
1585
+ id: "viewNavLinkID",
1586
+ },
1559
1587
  req.__("View")
1560
1588
  )
1561
1589
  ),
@@ -1565,7 +1593,14 @@ router.get(
1565
1593
  onClick: "showEntrySelect('page')",
1566
1594
  },
1567
1595
  div(
1568
- { class: "nav-link", id: "pageNavLinkID" },
1596
+ {
1597
+ class: `nav-link ${
1598
+ builderSettings.entryPointType === "page"
1599
+ ? "active"
1600
+ : ""
1601
+ }`,
1602
+ id: "pageNavLinkID",
1603
+ },
1569
1604
  req.__("Page")
1570
1605
  )
1571
1606
  )
@@ -1573,25 +1608,54 @@ router.get(
1573
1608
  // select entry-view
1574
1609
  select(
1575
1610
  {
1576
- class: "form-select",
1577
- name: "entryPoint",
1611
+ class: `form-select ${
1612
+ builderSettings.entryPointType === "page"
1613
+ ? "d-none"
1614
+ : ""
1615
+ }`,
1616
+ ...(!builderSettings.entryPointType || builderSettings.entryPointType === "view"
1617
+ ? { name: "entryPoint" }
1618
+ : {}),
1578
1619
  id: "viewInputID",
1579
1620
  },
1580
1621
  views
1581
1622
  .map((view) =>
1582
- option({ value: view.name }, view.name)
1623
+ option(
1624
+ {
1625
+ value: view.name,
1626
+ selected:
1627
+ builderSettings.entryPointType === "view" &&
1628
+ builderSettings.entryPoint === view.name,
1629
+ },
1630
+ view.name
1631
+ )
1583
1632
  )
1584
1633
  .join(",")
1585
1634
  ),
1586
1635
  // select entry-page
1587
1636
  select(
1588
1637
  {
1589
- class: "form-select d-none",
1638
+ class: `form-select ${
1639
+ !builderSettings.entryPointType || builderSettings.entryPointType === "view"
1640
+ ? "d-none"
1641
+ : ""
1642
+ }`,
1643
+ ...(builderSettings.entryPointType === "page"
1644
+ ? { name: "entryPoint" }
1645
+ : {}),
1590
1646
  id: "pageInputID",
1591
1647
  },
1592
1648
  pages
1593
1649
  .map((page) =>
1594
- option({ value: page.name }, page.name)
1650
+ option(
1651
+ {
1652
+ value: page.name,
1653
+ selected:
1654
+ builderSettings.entryPointType === "page" &&
1655
+ builderSettings.entryPoint === page.name,
1656
+ },
1657
+ page.name
1658
+ )
1595
1659
  )
1596
1660
  .join("")
1597
1661
  )
@@ -1612,6 +1676,7 @@ router.get(
1612
1676
  name: "androidPlatform",
1613
1677
  id: "androidCheckboxId",
1614
1678
  onClick: "toggle_android_platform()",
1679
+ checked: builderSettings.androidPlatform === "on",
1615
1680
  })
1616
1681
  )
1617
1682
  ),
@@ -1626,6 +1691,7 @@ router.get(
1626
1691
  class: "form-check-input",
1627
1692
  name: "iOSPlatform",
1628
1693
  id: "iOSCheckboxId",
1694
+ checked: builderSettings.iOSPlatform === "on",
1629
1695
  })
1630
1696
  )
1631
1697
  )
@@ -1639,7 +1705,8 @@ router.get(
1639
1705
  class: "form-check-input",
1640
1706
  name: "useDocker",
1641
1707
  id: "dockerCheckboxId",
1642
- hidden: true,
1708
+ hidden: builderSettings.androidPlatform !== "on",
1709
+ checked: builderSettings.useDocker === "on",
1643
1710
  })
1644
1711
  )
1645
1712
  ),
@@ -1661,6 +1728,7 @@ router.get(
1661
1728
  name: "appName",
1662
1729
  id: "appNameInputId",
1663
1730
  placeholder: "SaltcornMobileApp",
1731
+ value: builderSettings.appName || "",
1664
1732
  })
1665
1733
  )
1666
1734
  ),
@@ -1682,6 +1750,7 @@ router.get(
1682
1750
  name: "appVersion",
1683
1751
  id: "appVersionInputId",
1684
1752
  placeholder: "1.0.0",
1753
+ value: builderSettings.appVersion || "",
1685
1754
  })
1686
1755
  )
1687
1756
  ),
@@ -1702,6 +1771,7 @@ router.get(
1702
1771
  class: "form-control",
1703
1772
  name: "serverURL",
1704
1773
  id: "serverURLInputId",
1774
+ value: builderSettings.serverURL || "",
1705
1775
  placeholder: getState().getConfig("base_url") || "",
1706
1776
  })
1707
1777
  )
@@ -1727,7 +1797,14 @@ router.get(
1727
1797
  [
1728
1798
  option({ value: "" }, ""),
1729
1799
  ...images.map((image) =>
1730
- option({ value: image.location }, image.filename)
1800
+ option(
1801
+ {
1802
+ value: image.location,
1803
+ selected:
1804
+ builderSettings.appIcon === image.location,
1805
+ },
1806
+ image.filename
1807
+ )
1731
1808
  ),
1732
1809
  ].join("")
1733
1810
  )
@@ -1753,7 +1830,14 @@ router.get(
1753
1830
  [
1754
1831
  option({ value: "" }, ""),
1755
1832
  ...pages.map((page) =>
1756
- option({ value: page.name }, page.name)
1833
+ option(
1834
+ {
1835
+ value: page.name,
1836
+ selected:
1837
+ builderSettings.splashPage === page.name,
1838
+ },
1839
+ page.name
1840
+ )
1757
1841
  ),
1758
1842
  ].join("")
1759
1843
  )
@@ -1769,8 +1853,7 @@ router.get(
1769
1853
  id: "autoPublLoginId",
1770
1854
  class: "form-check-input me-2",
1771
1855
  name: "autoPublicLogin",
1772
- value: "autoPublicLogin",
1773
- checked: false,
1856
+ checked: builderSettings.autoPublicLogin === "on",
1774
1857
  }),
1775
1858
  label(
1776
1859
  {
@@ -1791,9 +1874,8 @@ router.get(
1791
1874
  id: "offlineModeBoxId",
1792
1875
  class: "form-check-input me-2",
1793
1876
  name: "allowOfflineMode",
1794
- value: "allowOfflineMode",
1795
1877
  onClick: "toggle_tbl_sync()",
1796
- checked: true,
1878
+ checked: builderSettings.allowOfflineMode === "on",
1797
1879
  }),
1798
1880
  label(
1799
1881
  {
@@ -1809,6 +1891,7 @@ router.get(
1809
1891
  {
1810
1892
  id: "tblSyncSelectorId",
1811
1893
  class: "row pb-3",
1894
+ hidden: builderSettings.allowOfflineMode !== "on",
1812
1895
  },
1813
1896
  div(
1814
1897
  label(
@@ -1845,6 +1928,12 @@ router.get(
1845
1928
  id: `${table.name}_unsynched_opt`,
1846
1929
  value: table.name,
1847
1930
  label: table.name,
1931
+ hidden:
1932
+ builderSettings.synchedTables?.indexOf(
1933
+ table.name
1934
+ ) >= 0
1935
+ ? true
1936
+ : false,
1848
1937
  })
1849
1938
  )
1850
1939
  )
@@ -1889,7 +1978,12 @@ router.get(
1889
1978
  id: `${table.name}_synched_opt`,
1890
1979
  value: table.name,
1891
1980
  label: table.name,
1892
- hidden: "true",
1981
+ hidden:
1982
+ builderSettings.synchedTables?.indexOf(
1983
+ table.name
1984
+ ) >= 0
1985
+ ? false
1986
+ : true,
1893
1987
  })
1894
1988
  )
1895
1989
  )
@@ -1935,7 +2029,12 @@ router.get(
1935
2029
  id: `${plugin.name}_excluded_opt`,
1936
2030
  value: plugin.name,
1937
2031
  label: plugin.name,
1938
- hidden: "true",
2032
+ hidden:
2033
+ builderSettings.excludedPlugins?.indexOf(
2034
+ plugin.name
2035
+ ) >= 0
2036
+ ? false
2037
+ : true,
1939
2038
  })
1940
2039
  )
1941
2040
  )
@@ -1980,7 +2079,12 @@ router.get(
1980
2079
  id: `${plugin.name}_included_opt`,
1981
2080
  value: plugin.name,
1982
2081
  label: plugin.name,
1983
- // hidden: "true",
2082
+ hidden:
2083
+ builderSettings.excludedPlugins?.indexOf(
2084
+ plugin.name
2085
+ ) >= 0
2086
+ ? true
2087
+ : false,
1984
2088
  })
1985
2089
  )
1986
2090
  )
@@ -2097,6 +2201,8 @@ router.post(
2097
2201
  synchedTables,
2098
2202
  includedPlugins,
2099
2203
  } = req.body;
2204
+ if (!includedPlugins) includedPlugins = [];
2205
+ if (!synchedTables) synchedTables = [];
2100
2206
  if (!androidPlatform && !iOSPlatform) {
2101
2207
  return res.json({
2102
2208
  error: req.__("Please select at least one platform (android or iOS)."),
@@ -2148,9 +2254,9 @@ router.post(
2148
2254
  if (splashPage) spawnParams.push("--splashPage", splashPage);
2149
2255
  if (allowOfflineMode) spawnParams.push("--allowOfflineMode");
2150
2256
  if (autoPublicLogin) spawnParams.push("--autoPublicLogin");
2151
- if (synchedTables?.length > 0)
2257
+ if (synchedTables.length > 0)
2152
2258
  spawnParams.push("--synchedTables", ...synchedTables.map((tbl) => tbl));
2153
- if (includedPlugins?.length > 0)
2259
+ if (includedPlugins.length > 0)
2154
2260
  spawnParams.push(
2155
2261
  "--includedPlugins",
2156
2262
  ...includedPlugins.map((pluginName) => pluginName)
@@ -2161,6 +2267,30 @@ router.post(
2161
2267
  ) {
2162
2268
  spawnParams.push("--tenantAppName", db.getTenantSchema());
2163
2269
  }
2270
+ const excludedPlugins = (await Plugin.find())
2271
+ .filter(
2272
+ (plugin) =>
2273
+ ["base", "sbadmin2"].indexOf(plugin.name) < 0 &&
2274
+ includedPlugins.indexOf(plugin.name) < 0
2275
+ )
2276
+ .map((plugin) => plugin.name);
2277
+ await getState().setConfig("mobile_builder_settings", {
2278
+ entryPoint,
2279
+ entryPointType,
2280
+ androidPlatform,
2281
+ iOSPlatform,
2282
+ useDocker,
2283
+ appName,
2284
+ appVersion,
2285
+ appIcon,
2286
+ serverURL,
2287
+ splashPage,
2288
+ autoPublicLogin,
2289
+ allowOfflineMode,
2290
+ synchedTables: synchedTables,
2291
+ includedPlugins: includedPlugins,
2292
+ excludedPlugins,
2293
+ });
2164
2294
  // end http call, return the out directory name
2165
2295
  // the gui polls for results
2166
2296
  res.json({ build_dir_name: outDirName });
package/routes/api.js CHANGED
@@ -140,7 +140,7 @@ router.post(
140
140
  const view = await View.findOne({ name: viewName });
141
141
  const db = require("@saltcorn/data/db");
142
142
  if (!view) {
143
- getState().log(3, `API viewQuery ${view.name} not found`);
143
+ getState().log(3, `API viewQuery ${viewName} not found`);
144
144
  res.status(404).json({
145
145
  error: req.__("View %s not found", viewName),
146
146
  view: viewName,
package/routes/fields.js CHANGED
@@ -165,6 +165,7 @@ const fieldForm = async (req, fkey_opts, existing_names, id, hasData) => {
165
165
  name: "protected",
166
166
  sublabel: req.__("Set role to access"),
167
167
  type: "Bool",
168
+ showIf: { calculated: false },
168
169
  }),
169
170
  {
170
171
  label: req.__("Minimum role to write"),
@@ -460,7 +461,7 @@ const fieldFlow = (req) =>
460
461
  html: expressionBlurb(
461
462
  context.type,
462
463
  context.stored,
463
- fields,
464
+ table,
464
465
  req
465
466
  ),
466
467
  },
package/routes/tables.js CHANGED
@@ -1378,6 +1378,12 @@ router.get(
1378
1378
  link(`/table/add-constraint/${id}/Formula`, req.__("Formula")),
1379
1379
  " | ",
1380
1380
  link(`/table/add-constraint/${id}/Index`, req.__("Index")),
1381
+ a(
1382
+ {
1383
+ href: `javascript:ajax_modal('/admin/help/Table%20constraints?table=${table.name}')`,
1384
+ },
1385
+ i({ class: "fas fa-question-circle ms-1" })
1386
+ ),
1381
1387
  ],
1382
1388
  },
1383
1389
  ],
@@ -1392,11 +1398,11 @@ router.get(
1392
1398
  * @param {object[]} fields
1393
1399
  * @returns {Form}
1394
1400
  */
1395
- const constraintForm = (req, table_id, fields, type) => {
1401
+ const constraintForm = (req, table, fields, type) => {
1396
1402
  switch (type) {
1397
1403
  case "Formula":
1398
1404
  return new Form({
1399
- action: `/table/add-constraint/${table_id}/${type}`,
1405
+ action: `/table/add-constraint/${table.id}/${type}`,
1400
1406
 
1401
1407
  fields: [
1402
1408
  {
@@ -1405,6 +1411,10 @@ const constraintForm = (req, table_id, fields, type) => {
1405
1411
  validator: expressionValidator,
1406
1412
  type: "String",
1407
1413
  class: "validate-expression",
1414
+ help: {
1415
+ topic: "Table formula constraint",
1416
+ context: { table: table.name },
1417
+ },
1408
1418
  sublabel:
1409
1419
  req.__(
1410
1420
  "Formula must evaluate to true for valid rows. In scope: "
@@ -1417,14 +1427,14 @@ const constraintForm = (req, table_id, fields, type) => {
1417
1427
  {
1418
1428
  name: "errormsg",
1419
1429
  label: "Error message",
1420
- sublabel: "Shown the user if formula is false",
1430
+ sublabel: "Shown to the user if formula is false",
1421
1431
  type: "String",
1422
1432
  },
1423
1433
  ],
1424
1434
  });
1425
1435
  case "Unique":
1426
1436
  return new Form({
1427
- action: `/table/add-constraint/${table_id}/${type}`,
1437
+ action: `/table/add-constraint/${table.id}/${type}`,
1428
1438
  blurb: req.__(
1429
1439
  "Tick the boxes for the fields that should be jointly unique"
1430
1440
  ),
@@ -1437,14 +1447,14 @@ const constraintForm = (req, table_id, fields, type) => {
1437
1447
  {
1438
1448
  name: "errormsg",
1439
1449
  label: "Error message",
1440
- sublabel: "Shown the user if joint uniqueness is violated",
1450
+ sublabel: "Shown to the user if joint uniqueness is violated",
1441
1451
  type: "String",
1442
1452
  },
1443
1453
  ],
1444
1454
  });
1445
1455
  case "Index":
1446
1456
  return new Form({
1447
- action: `/table/add-constraint/${table_id}/${type}`,
1457
+ action: `/table/add-constraint/${table.id}/${type}`,
1448
1458
  blurb: req.__(
1449
1459
  "Choose the field to be indexed. This make searching the table faster."
1450
1460
  ),
@@ -1483,7 +1493,7 @@ router.get(
1483
1493
  return;
1484
1494
  }
1485
1495
  const fields = table.getFields();
1486
- const form = constraintForm(req, table.id, fields, type);
1496
+ const form = constraintForm(req, table, fields, type);
1487
1497
  res.sendWrap(req.__(`Add constraint to %s`, table.name), {
1488
1498
  above: [
1489
1499
  {
@@ -1527,7 +1537,7 @@ router.post(
1527
1537
  return;
1528
1538
  }
1529
1539
  const fields = table.getFields();
1530
- const form = constraintForm(req, table.id, fields, type);
1540
+ const form = constraintForm(req, table, fields, type);
1531
1541
  form.validate(req.body);
1532
1542
  if (form.hasErrors) req.flash("error", req.__("An error occurred"));
1533
1543
  else {