@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.
- package/help/Table constraints.tmd +23 -0
- package/help/Table formula constraint.tmd +43 -0
- package/help/index.js +45 -0
- package/load_plugins.js +4 -2
- package/locales/en.json +3 -1
- package/markup/expression_blurb.js +4 -127
- package/package.json +10 -8
- package/public/saltcorn-common.js +26 -12
- package/public/saltcorn.css +14 -0
- package/public/saltcorn.js +4 -4
- package/routes/admin.js +149 -19
- package/routes/api.js +1 -1
- package/routes/fields.js +2 -1
- package/routes/tables.js +18 -8
|
@@ -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 {
|
|
28
|
+
* @param {Table} table
|
|
145
29
|
* @param {object} req
|
|
146
30
|
* @returns {p[]}
|
|
147
31
|
*/
|
|
148
|
-
const expressionBlurb = (type, stored,
|
|
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.
|
|
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.
|
|
10
|
-
"@saltcorn/builder": "0.
|
|
11
|
-
"@saltcorn/data": "0.
|
|
12
|
-
"@saltcorn/admin-models": "0.
|
|
13
|
-
"@saltcorn/filemanager": "0.
|
|
14
|
-
"@saltcorn/markup": "0.
|
|
15
|
-
"@saltcorn/sbadmin2": "0.
|
|
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(
|
|
338
|
+
function get_form_record(e_in, select_labels) {
|
|
339
339
|
const rec = {};
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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({
|
package/public/saltcorn.css
CHANGED
|
@@ -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
|
+
}
|
package/public/saltcorn.js
CHANGED
|
@@ -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
|
-
{
|
|
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
|
-
{
|
|
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:
|
|
1577
|
-
|
|
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(
|
|
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:
|
|
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(
|
|
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:
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
|
2257
|
+
if (synchedTables.length > 0)
|
|
2152
2258
|
spawnParams.push("--synchedTables", ...synchedTables.map((tbl) => tbl));
|
|
2153
|
-
if (includedPlugins
|
|
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 ${
|
|
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
|
-
|
|
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,
|
|
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/${
|
|
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/${
|
|
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/${
|
|
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
|
|
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
|
|
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 {
|