@saltcorn/server 0.9.1-beta.0 → 0.9.1-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/auth/admin.js +1 -1
- package/auth/routes.js +4 -6
- package/help/API actions.tmd +20 -1
- package/help/Event types.tmd +78 -0
- package/help/Extra state formula.tmd +12 -0
- package/help/JavaScript action code.tmd +1 -14
- package/locales/en.json +11 -1
- package/package.json +13 -13
- package/public/saltcorn-common.js +135 -54
- package/public/saltcorn.css +1 -0
- package/public/saltcorn.js +2 -11
- package/routes/actions.js +2 -1
- package/routes/api.js +10 -2
- package/routes/fields.js +28 -1
- package/routes/homepage.js +86 -12
- package/routes/page.js +27 -21
- package/routes/pageedit.js +185 -26
- package/routes/utils.js +36 -0
- package/routes/view.js +8 -7
- package/s3storage.js +6 -5
- package/tests/fields.test.js +37 -1
- package/tests/page.test.js +123 -1
- package/tests/table.test.js +5 -1
- package/tests/view.test.js +97 -0
- package/wrapper.js +8 -1
package/auth/admin.js
CHANGED
package/auth/routes.js
CHANGED
|
@@ -877,17 +877,17 @@ router.post(
|
|
|
877
877
|
|
|
878
878
|
const unsuitableEmailPassword = async (urecord) => {
|
|
879
879
|
const { email, password, passwordRepeat } = urecord;
|
|
880
|
-
if (
|
|
880
|
+
if (email == "" || !password) {
|
|
881
881
|
req.flash("danger", req.__("E-mail and password required"));
|
|
882
882
|
res.redirect("/auth/signup");
|
|
883
883
|
return true;
|
|
884
884
|
}
|
|
885
|
-
if (email.length > 127) {
|
|
885
|
+
if (email && email.length > 127) {
|
|
886
886
|
req.flash("danger", req.__("E-mail too long"));
|
|
887
887
|
res.redirect("/auth/signup");
|
|
888
888
|
return true;
|
|
889
889
|
}
|
|
890
|
-
if (!User.valid_email(email)) {
|
|
890
|
+
if (email && !User.valid_email(email)) {
|
|
891
891
|
req.flash("danger", req.__("Not a valid e-mail address"));
|
|
892
892
|
res.redirect("/auth/signup");
|
|
893
893
|
return true;
|
|
@@ -905,9 +905,7 @@ router.post(
|
|
|
905
905
|
res.redirect("/auth/signup");
|
|
906
906
|
return true;
|
|
907
907
|
}
|
|
908
|
-
|
|
909
|
-
const us = await User.find({ email });
|
|
910
|
-
if (us.length > 0) {
|
|
908
|
+
if (await User.matches_existing_user(urecord)) {
|
|
911
909
|
req.flash("danger", req.__("Account already exists"));
|
|
912
910
|
res.redirect("/auth/signup");
|
|
913
911
|
return true;
|
package/help/API actions.tmd
CHANGED
|
@@ -9,4 +9,23 @@ If you make a GET request, the query string values
|
|
|
9
9
|
are accessible as a JSON object using the `row` variable (e.g. in a `run_js_code`).
|
|
10
10
|
|
|
11
11
|
If you return a value from the action, it will be available in the `data`
|
|
12
|
-
subfield of the JSON body in the HTTP response
|
|
12
|
+
subfield of the JSON body in the HTTP response
|
|
13
|
+
|
|
14
|
+
### Redirection as response
|
|
15
|
+
|
|
16
|
+
Normally API call will return a JSON object with a true/false success field
|
|
17
|
+
and the response from the action, if any, in the data field. You may instead
|
|
18
|
+
require the API call to redirect to a different URL. You can do this in two
|
|
19
|
+
different ways.
|
|
20
|
+
|
|
21
|
+
**Static redirect**: if you set the `scgotourl` HTTP header, the response will
|
|
22
|
+
redirect to this URL.
|
|
23
|
+
|
|
24
|
+
**Dynamic redirect**: The redirect URL can also come from the action. If this
|
|
25
|
+
returns an object with the `goto` field (e.g.
|
|
26
|
+
`return {goto: "https://myapp.com/view/ShowTheThing?id"+newid}`)
|
|
27
|
+
that is normally returned in the JSON post body so it can be processed by client code.
|
|
28
|
+
You can instead request that this is processed server-side, by (a) setting a
|
|
29
|
+
`_process_result` field to true in the row data, i.e. JSON body in POST and query
|
|
30
|
+
string in GET or (b) setting the `scprocessresults` HTTP header. This will cause the
|
|
31
|
+
API call to return an HTTP redirect with the `goto` value.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
The event type for triggers determines when the chosen action should be run.
|
|
2
|
+
The different event types options come from a variety of the types and sources.
|
|
3
|
+
|
|
4
|
+
These events also form the basis of the event log. Use the log settings to enable or disable
|
|
5
|
+
recording of the occurrence of events.
|
|
6
|
+
|
|
7
|
+
## Database events
|
|
8
|
+
|
|
9
|
+
These conditions are triggered by changes to rows in tables. Together with the event type
|
|
10
|
+
a specific table is chosen. The individual conditions are:
|
|
11
|
+
|
|
12
|
+
**Insert**: run the action when a new row is inserted in the table. This is a good choice
|
|
13
|
+
when a table itself represents actions to be carried out; for instance a table of
|
|
14
|
+
outbound emails would have a trigger with When = Insert and Action = send_email
|
|
15
|
+
|
|
16
|
+
**Update**: run this action when changes are made to an existing row. The old row can
|
|
17
|
+
be accessed with the `old_row` variable.
|
|
18
|
+
|
|
19
|
+
**Validate**: run before inserts or updates. If the action returns `error` (for example,
|
|
20
|
+
`run_js_code` code: `return {error: "Invalid row"}`), the insert/update is aborted. If the
|
|
21
|
+
trigger returns `set_fields` (for example, `run_js_code` code: `return {set_fields: {full_name: "Unknown"}}`)
|
|
22
|
+
these values are inserted in row.
|
|
23
|
+
|
|
24
|
+
Guaranteed to run before any Insert or Update triggers
|
|
25
|
+
|
|
26
|
+
**Delete**: run this action when a row is deleted
|
|
27
|
+
|
|
28
|
+
## Periodic events
|
|
29
|
+
|
|
30
|
+
These triggers are run periodically at different times.
|
|
31
|
+
|
|
32
|
+
**Weekly**: run this once a week.
|
|
33
|
+
|
|
34
|
+
**Daily**: run this once a day.
|
|
35
|
+
|
|
36
|
+
**Hourly**: run this once an hour.
|
|
37
|
+
|
|
38
|
+
**Often**: run this every 5 minutes.
|
|
39
|
+
|
|
40
|
+
## User-based events
|
|
41
|
+
|
|
42
|
+
**PageLoad**: run this whenever a page or view is loaded. If you set up the event log to
|
|
43
|
+
record these events you can use this as a basis for an analytics system.
|
|
44
|
+
|
|
45
|
+
**Login**: run this whenever a user log in successfully
|
|
46
|
+
|
|
47
|
+
**LoginFailed**: run this whenever a user login failed
|
|
48
|
+
|
|
49
|
+
**UserVerified**: run this when a user is verified, if an appropriate module for
|
|
50
|
+
user verification is enabled.
|
|
51
|
+
|
|
52
|
+
## System-based events
|
|
53
|
+
|
|
54
|
+
**Error**: run this whenever an error occurs
|
|
55
|
+
|
|
56
|
+
**Startup**: run this whenever this saltcorn process initializes.
|
|
57
|
+
|
|
58
|
+
## Other events
|
|
59
|
+
|
|
60
|
+
**Never**: this trigger is never run on its own. However triggers that are marked as never
|
|
61
|
+
can be chosen as the target action for a button in the UI. Use this if you have a complex
|
|
62
|
+
configuration for an action that needs to be run in response to a button click, or if you
|
|
63
|
+
have a configuration that needs to be reused between two different buttons in two different
|
|
64
|
+
views. You can also use this to switch off a trigger that is running on a different event
|
|
65
|
+
type without deleting it.
|
|
66
|
+
|
|
67
|
+
**API call**: this trigger can be run in response to an inbound API call. To see the URL
|
|
68
|
+
and further help, click the help icon next to the "API call" label in the trigger list.
|
|
69
|
+
|
|
70
|
+
## Custom events
|
|
71
|
+
|
|
72
|
+
You can create your own event type which can then be triggered with an emit_event action
|
|
73
|
+
or the `emitEvent` call in a run js code action
|
|
74
|
+
|
|
75
|
+
## Events supplied by modules
|
|
76
|
+
|
|
77
|
+
Modules can provide new event types. For instance the mqtt module provides an event
|
|
78
|
+
type based on receiving new messages.
|
|
@@ -33,6 +33,9 @@ must be enclosed in quotes (single, double or
|
|
|
33
33
|
[backtick](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals))).
|
|
34
34
|
You can also as the value use a more complex JavaScript expression.
|
|
35
35
|
|
|
36
|
+
For examples of how you can use these values for greater than/less than,
|
|
37
|
+
or-conditions or case insensitivity, see [here](https://saltcorn.github.io/saltcorn/classes/_saltcorn_data.models.Table-1.html#md:querying-table-rows).
|
|
38
|
+
|
|
36
39
|
You can refer to the logged in user using the variable name user.
|
|
37
40
|
This is an object and you must use the dot to access user fields,
|
|
38
41
|
e.g. user.id for the logged in users id. A very common scenario
|
|
@@ -60,3 +63,12 @@ in this case the table {{srcTable.name}}:
|
|
|
60
63
|
|
|
61
64
|
{{# } }}
|
|
62
65
|
|
|
66
|
+
The state values from the query string can also be accessed, by preceeding the
|
|
67
|
+
variable name by a dollar sign (in pages and Show views). For instance if your URL ends in `?id=45&foo=1`,
|
|
68
|
+
you can access the value of `foo` (here 1) as `$foo`. In Filter views, there is no
|
|
69
|
+
underlying row and the purpose of the view is fundamentally to manipulate the state
|
|
70
|
+
so the state in extra state formulae is accessed without dollar sign; in the
|
|
71
|
+
previous example `foo` instead of `$foo`.
|
|
72
|
+
|
|
73
|
+
Use `session_id` to refer to the current session ID string (unique for the user's session
|
|
74
|
+
whether the are logged in or not).
|
|
@@ -87,20 +87,7 @@ Example: `return { error: "Invalid command!" }`
|
|
|
87
87
|
|
|
88
88
|
If this is triggered by an Edit view with the SubmitWithAjax,
|
|
89
89
|
halt navigation and stay on page. This can be used for complex validation logic,
|
|
90
|
-
|
|
91
|
-
may also need to clear the returned id in order to allow the user to continue editing.
|
|
92
|
-
|
|
93
|
-
Example:
|
|
94
|
-
|
|
95
|
-
```
|
|
96
|
-
if(amount>cash_on_hand) {
|
|
97
|
-
await table.deleteRows({ id })
|
|
98
|
-
return {
|
|
99
|
-
error: "Invalid order!",
|
|
100
|
-
id: null
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
```
|
|
90
|
+
when added as a Validate trigger.
|
|
104
91
|
|
|
105
92
|
#### `goto`
|
|
106
93
|
|
package/locales/en.json
CHANGED
|
@@ -1273,5 +1273,15 @@
|
|
|
1273
1273
|
"Body size limit (Kb)": "Body size limit (Kb)",
|
|
1274
1274
|
"Maximum request body size in kilobytes": "Maximum request body size in kilobytes",
|
|
1275
1275
|
"URL encoded size limit (Kb)": "URL encoded size limit (Kb)",
|
|
1276
|
-
"Maximum URL encoded request size in kilobytes": "Maximum URL encoded request size in kilobytes"
|
|
1276
|
+
"Maximum URL encoded request size in kilobytes": "Maximum URL encoded request size in kilobytes",
|
|
1277
|
+
"HTML file": "HTML file",
|
|
1278
|
+
"HTML file to use as page content": "HTML file to use as page content",
|
|
1279
|
+
"Offline mode: cannot load file": "Offline mode: cannot load file",
|
|
1280
|
+
"None - use drag and drop builder": "None - use drag and drop builder",
|
|
1281
|
+
"Do not pick or compare time": "Do not pick or compare time",
|
|
1282
|
+
"Installed theme": "Installed theme",
|
|
1283
|
+
"Theme for role": "Theme for role",
|
|
1284
|
+
"Set theme for each user role »": "Set theme for each user role »",
|
|
1285
|
+
"Available themes": "Available themes",
|
|
1286
|
+
"Install more themes »": "Install more themes »"
|
|
1277
1287
|
}
|
package/package.json
CHANGED
|
@@ -1,28 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saltcorn/server",
|
|
3
|
-
"version": "0.9.1-beta.
|
|
3
|
+
"version": "0.9.1-beta.10",
|
|
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
|
-
"@
|
|
10
|
-
"@saltcorn/
|
|
11
|
-
"@saltcorn/
|
|
12
|
-
"@saltcorn/
|
|
13
|
-
"@saltcorn/
|
|
14
|
-
"@saltcorn/
|
|
15
|
-
"@saltcorn/
|
|
9
|
+
"@aws-sdk/client-s3": "^3.451.0",
|
|
10
|
+
"@saltcorn/base-plugin": "0.9.1-beta.10",
|
|
11
|
+
"@saltcorn/builder": "0.9.1-beta.10",
|
|
12
|
+
"@saltcorn/data": "0.9.1-beta.10",
|
|
13
|
+
"@saltcorn/admin-models": "0.9.1-beta.10",
|
|
14
|
+
"@saltcorn/filemanager": "0.9.1-beta.10",
|
|
15
|
+
"@saltcorn/markup": "0.9.1-beta.10",
|
|
16
|
+
"@saltcorn/sbadmin2": "0.9.1-beta.10",
|
|
16
17
|
"@socket.io/cluster-adapter": "^0.2.1",
|
|
17
18
|
"@socket.io/sticky": "^1.0.1",
|
|
18
19
|
"adm-zip": "0.5.10",
|
|
19
|
-
"aws-sdk": "^2.1386.0",
|
|
20
20
|
"connect-flash": "^0.1.1",
|
|
21
21
|
"connect-pg-simple": "^6.1.0",
|
|
22
22
|
"content-disposition": "^0.5.3",
|
|
23
23
|
"contractis": "^0.1.0",
|
|
24
24
|
"cookie-parser": "^1.4.4",
|
|
25
25
|
"cookie-session": "^1.4.0",
|
|
26
|
+
"cors": "2.8.5",
|
|
26
27
|
"csurf": "^1.11.0",
|
|
27
28
|
"csv-stringify": "^5.5.0",
|
|
28
29
|
"express": "^4.17.1",
|
|
@@ -32,7 +33,6 @@
|
|
|
32
33
|
"express-session": "^1.17.1",
|
|
33
34
|
"greenlock": "^4.0.4",
|
|
34
35
|
"greenlock-express": "^4.0.3",
|
|
35
|
-
"underscore": "1.13.6",
|
|
36
36
|
"helmet": "^3.23.3",
|
|
37
37
|
"i18n": "^0.15.1",
|
|
38
38
|
"imapflow": "1.0.123",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"markdown-it": "^13.0.2",
|
|
42
42
|
"moment": "^2.29.4",
|
|
43
43
|
"multer": "1.4.5-lts.1",
|
|
44
|
-
"multer-s3": "^
|
|
44
|
+
"multer-s3": "^3.0.1",
|
|
45
45
|
"node-fetch": "2.6.9",
|
|
46
46
|
"node-watch": "^0.7.2",
|
|
47
47
|
"notp": "2.0.3",
|
|
@@ -59,8 +59,8 @@
|
|
|
59
59
|
"systeminformation": "^5.21.7",
|
|
60
60
|
"thirty-two": "1.0.2",
|
|
61
61
|
"tmp-promise": "^3.0.2",
|
|
62
|
-
"
|
|
63
|
-
"
|
|
62
|
+
"underscore": "1.13.6",
|
|
63
|
+
"uuid": "^8.2.0"
|
|
64
64
|
},
|
|
65
65
|
"optionalDependencies": {
|
|
66
66
|
"connect-sqlite3": "^0.9.11",
|
|
@@ -50,6 +50,11 @@ const nubBy = (prop, xs) => {
|
|
|
50
50
|
return true;
|
|
51
51
|
});
|
|
52
52
|
};
|
|
53
|
+
|
|
54
|
+
function valid_js_var_name(s) {
|
|
55
|
+
if (!s) return false;
|
|
56
|
+
return !!s.match(/^[a-zA-Z_$][a-zA-Z_$0-9]*$/);
|
|
57
|
+
}
|
|
53
58
|
function apply_showif() {
|
|
54
59
|
const isNode = typeof parent?.saltcorn?.data?.state === "undefined";
|
|
55
60
|
$("[data-show-if]").each(function (ix, element) {
|
|
@@ -83,7 +88,7 @@ function apply_showif() {
|
|
|
83
88
|
const e = $(element);
|
|
84
89
|
const rec = get_form_record(e);
|
|
85
90
|
const href = new Function(
|
|
86
|
-
`{${Object.keys(rec).join(",")}}`,
|
|
91
|
+
`{${Object.keys(rec).filter(valid_js_var_name).join(",")}}`,
|
|
87
92
|
"return " + e.attr("data-dyn-href")
|
|
88
93
|
)(rec);
|
|
89
94
|
e.attr("href", href);
|
|
@@ -155,7 +160,7 @@ function apply_showif() {
|
|
|
155
160
|
e.prop("data-fetch-options-current-set", qs);
|
|
156
161
|
const toAppend = [];
|
|
157
162
|
if (!dynwhere.required)
|
|
158
|
-
toAppend.push({ label: dynwhere.neutral_label || "" });
|
|
163
|
+
toAppend.push({ label: dynwhere.neutral_label || "", value: "" });
|
|
159
164
|
let currentDataOption = undefined;
|
|
160
165
|
const dataOptions = [];
|
|
161
166
|
//console.log(success);
|
|
@@ -191,7 +196,7 @@ function apply_showif() {
|
|
|
191
196
|
.map(
|
|
192
197
|
({ label, value, selected }) =>
|
|
193
198
|
`<option${selected ? ` selected` : ""}${
|
|
194
|
-
value ? ` value="${value}"` : ""
|
|
199
|
+
typeof value !== "undefined" ? ` value="${value}"` : ""
|
|
195
200
|
}>${label || ""}</option>`
|
|
196
201
|
)
|
|
197
202
|
.join("")
|
|
@@ -285,9 +290,13 @@ function apply_showif() {
|
|
|
285
290
|
.closest(".form-namespace")
|
|
286
291
|
.find("input[name=_columndef]");
|
|
287
292
|
try {
|
|
288
|
-
const
|
|
289
|
-
def
|
|
290
|
-
|
|
293
|
+
const defval = $def.val();
|
|
294
|
+
const def =
|
|
295
|
+
typeof defval === "undefined" ? undefined : JSON.parse(defval);
|
|
296
|
+
if (def) {
|
|
297
|
+
def[k] = v;
|
|
298
|
+
$def.val(JSON.stringify(def));
|
|
299
|
+
}
|
|
291
300
|
} catch (e) {
|
|
292
301
|
console.error("Invalid json", e);
|
|
293
302
|
}
|
|
@@ -381,24 +390,46 @@ function get_form_record(e_in, select_labels) {
|
|
|
381
390
|
? $(`form[data-viewname=${e_in.viewname}]`)
|
|
382
391
|
: e_in.closest(".form-namespace");
|
|
383
392
|
|
|
393
|
+
const form = $(e).closest("form");
|
|
394
|
+
|
|
395
|
+
const rowVals = form.attr("data-row-values");
|
|
396
|
+
if (rowVals)
|
|
397
|
+
try {
|
|
398
|
+
const initRow = JSON.parse(decodeURIComponent(rowVals));
|
|
399
|
+
Object.assign(rec, initRow);
|
|
400
|
+
} catch (error) {
|
|
401
|
+
console.error(error);
|
|
402
|
+
}
|
|
403
|
+
|
|
384
404
|
e.find("input[name],select[name],textarea[name]").each(function () {
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
405
|
+
const $this = $(this);
|
|
406
|
+
const name = $this.attr("data-fieldname") || $this.attr("name");
|
|
407
|
+
if (select_labels && $this.prop("tagName").toLowerCase() === "select")
|
|
408
|
+
rec[name] = $this.find("option:selected").text();
|
|
409
|
+
else if ($this.prop("type") === "checkbox")
|
|
410
|
+
rec[name] = $this.prop("checked");
|
|
411
|
+
else if ($this.prop("type") === "radio" && !$this.prop("checked")) {
|
|
412
|
+
//do nothing
|
|
413
|
+
} else rec[name] = $this.val();
|
|
391
414
|
});
|
|
392
415
|
return rec;
|
|
393
416
|
}
|
|
394
417
|
function showIfFormulaInputs(e, fml) {
|
|
395
418
|
const rec = get_form_record(e);
|
|
419
|
+
if (window._sc_loglevel > 4)
|
|
420
|
+
console.log(`show if fml ${fml} form_record`, rec);
|
|
396
421
|
try {
|
|
397
|
-
return new Function(
|
|
398
|
-
|
|
399
|
-
|
|
422
|
+
return new Function(
|
|
423
|
+
"row",
|
|
424
|
+
`{${Object.keys(rec).join(",")}}`,
|
|
425
|
+
"return " + fml
|
|
426
|
+
)(rec, rec);
|
|
400
427
|
} catch (e) {
|
|
401
|
-
throw new Error(
|
|
428
|
+
throw new Error(
|
|
429
|
+
`Error in evaluating showIf formula ${fml} with values ${JSON.stringify(
|
|
430
|
+
rec
|
|
431
|
+
)}: ${e.message}`
|
|
432
|
+
);
|
|
402
433
|
}
|
|
403
434
|
}
|
|
404
435
|
|
|
@@ -579,14 +610,17 @@ function initialize_page() {
|
|
|
579
610
|
schema = JSON.parse(decodeURIComponent(schema));
|
|
580
611
|
}
|
|
581
612
|
if (type === "Date") {
|
|
582
|
-
console.log("timeelsems", $(this).find("span.current time"));
|
|
613
|
+
//console.log("timeelsems", $(this).find("span.current time"));
|
|
583
614
|
current =
|
|
584
615
|
$(this).attr("data-inline-edit-current") ||
|
|
585
616
|
$(this).find("span.current time").attr("datetime"); // ||
|
|
586
617
|
//$(this).children("span.current").html();
|
|
587
618
|
}
|
|
588
|
-
|
|
619
|
+
if (type === "Bool") {
|
|
620
|
+
current = current === "true";
|
|
621
|
+
}
|
|
589
622
|
var is_key = type?.startsWith("Key:");
|
|
623
|
+
const resetHtml = this.outerHTML;
|
|
590
624
|
const opts = encodeURIComponent(
|
|
591
625
|
JSON.stringify({
|
|
592
626
|
url,
|
|
@@ -597,6 +631,7 @@ function initialize_page() {
|
|
|
597
631
|
type,
|
|
598
632
|
is_key,
|
|
599
633
|
schema,
|
|
634
|
+
resetHtml,
|
|
600
635
|
...(decimalPlaces ? { decimalPlaces } : {}),
|
|
601
636
|
})
|
|
602
637
|
);
|
|
@@ -649,7 +684,11 @@ function initialize_page() {
|
|
|
649
684
|
: ""
|
|
650
685
|
}
|
|
651
686
|
<input type="${
|
|
652
|
-
type === "Integer" || type === "Float"
|
|
687
|
+
type === "Integer" || type === "Float"
|
|
688
|
+
? "number"
|
|
689
|
+
: type === "Bool"
|
|
690
|
+
? "checkbox"
|
|
691
|
+
: "text"
|
|
653
692
|
}" ${
|
|
654
693
|
type === "Float"
|
|
655
694
|
? `step="${
|
|
@@ -660,7 +699,13 @@ function initialize_page() {
|
|
|
660
699
|
: "any"
|
|
661
700
|
}"`
|
|
662
701
|
: ""
|
|
663
|
-
} name="${key}"
|
|
702
|
+
} name="${key}" ${
|
|
703
|
+
type === "Bool"
|
|
704
|
+
? current
|
|
705
|
+
? "checked"
|
|
706
|
+
: ""
|
|
707
|
+
: `value="${escapeHtml(current)}"`
|
|
708
|
+
}>
|
|
664
709
|
<button type="submit" class="btn btn-sm btn-primary">OK</button>
|
|
665
710
|
<button onclick="cancel_inline_edit(event, '${opts}')" type="button" class="btn btn-sm btn-danger"><i class="fas fa-times"></i></button>
|
|
666
711
|
</form>`
|
|
@@ -722,12 +767,37 @@ function initialize_page() {
|
|
|
722
767
|
$(el).addClass("codemirror-enabled");
|
|
723
768
|
cm.on(
|
|
724
769
|
"change",
|
|
725
|
-
$.debounce(
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
770
|
+
$.debounce(
|
|
771
|
+
(cm1) => {
|
|
772
|
+
cm1.save();
|
|
773
|
+
if ($(el).hasClass("validate-statements")) {
|
|
774
|
+
try {
|
|
775
|
+
let AsyncFunction = Object.getPrototypeOf(
|
|
776
|
+
async function () {}
|
|
777
|
+
).constructor;
|
|
778
|
+
AsyncFunction(cm.getValue());
|
|
779
|
+
$(el).closest("form").trigger("change");
|
|
780
|
+
} catch (e) {
|
|
781
|
+
const form = $(el).closest("form");
|
|
782
|
+
const errorArea = form.parent().find(".full-form-error");
|
|
783
|
+
if (errorArea.length) errorArea.text(e.message);
|
|
784
|
+
else
|
|
785
|
+
form
|
|
786
|
+
.parent()
|
|
787
|
+
.append(
|
|
788
|
+
`<p class="text-danger full-form-error">${e.message}</p>`
|
|
789
|
+
);
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
} else {
|
|
793
|
+
cm1.save();
|
|
794
|
+
$(el).closest("form").trigger("change");
|
|
795
|
+
}
|
|
796
|
+
},
|
|
797
|
+
500,
|
|
798
|
+
null,
|
|
799
|
+
true
|
|
800
|
+
)
|
|
731
801
|
);
|
|
732
802
|
});
|
|
733
803
|
}, 100);
|
|
@@ -777,33 +847,7 @@ function cancel_inline_edit(e, opts1) {
|
|
|
777
847
|
const isNode = typeof parent?.saltcorn?.data?.state === "undefined";
|
|
778
848
|
var opts = JSON.parse(decodeURIComponent(opts1 || "") || "{}");
|
|
779
849
|
var form = $(e.target).closest("form");
|
|
780
|
-
|
|
781
|
-
if (opts.schema) {
|
|
782
|
-
json_fk_opt = form.find(`option[value="${opts.current}"]`).text();
|
|
783
|
-
}
|
|
784
|
-
form.replaceWith(`<div
|
|
785
|
-
data-inline-edit-field="${opts.key}"
|
|
786
|
-
${opts.ajax ? `data-inline-edit-ajax="true"` : ""}
|
|
787
|
-
${opts.type ? `data-inline-edit-type="${opts.type}"` : ""}
|
|
788
|
-
${opts.current ? `data-inline-edit-current="${opts.current}"` : ""}
|
|
789
|
-
${
|
|
790
|
-
opts.current_label
|
|
791
|
-
? `data-inline-edit-current-label="${opts.current_label}"`
|
|
792
|
-
: ""
|
|
793
|
-
}
|
|
794
|
-
${
|
|
795
|
-
opts.schema
|
|
796
|
-
? `data-inline-edit-schema="${encodeURIComponent(
|
|
797
|
-
JSON.stringify(opts.schema)
|
|
798
|
-
)}"`
|
|
799
|
-
: ""
|
|
800
|
-
}
|
|
801
|
-
data-inline-edit-dest-url="${opts.url}">
|
|
802
|
-
<span class="current">${
|
|
803
|
-
json_fk_opt || opts.current_label || opts.current
|
|
804
|
-
}</span>
|
|
805
|
-
<i class="editicon ${!isNode ? "visible" : ""} fas fa-edit ms-1"></i>
|
|
806
|
-
</div>`);
|
|
850
|
+
form.replaceWith(opts.resetHtml);
|
|
807
851
|
initialize_page();
|
|
808
852
|
}
|
|
809
853
|
|
|
@@ -811,7 +855,8 @@ function inline_submit_success(e, form, opts) {
|
|
|
811
855
|
const isNode = typeof parent?.saltcorn?.data?.state === "undefined";
|
|
812
856
|
const formDataArray = form.serializeArray();
|
|
813
857
|
if (opts) {
|
|
814
|
-
let
|
|
858
|
+
let fdEntry = formDataArray.find((f) => f.name == opts.key);
|
|
859
|
+
let rawVal = opts.type === "Bool" ? !!fdEntry : fdEntry.value;
|
|
815
860
|
let val =
|
|
816
861
|
opts.is_key || (opts.schema && opts.schema.type.startsWith("Key to "))
|
|
817
862
|
? form.find("select").find("option:selected").text()
|
|
@@ -846,9 +891,13 @@ function inline_submit_success(e, form, opts) {
|
|
|
846
891
|
function inline_ajax_submit(e, opts1) {
|
|
847
892
|
var opts = JSON.parse(decodeURIComponent(opts1 || "") || "{}");
|
|
848
893
|
e.preventDefault();
|
|
894
|
+
|
|
849
895
|
var form = $(e.target).closest("form");
|
|
850
896
|
var form_data = form.serialize();
|
|
851
897
|
var url = form.attr("action");
|
|
898
|
+
if (opts.type === "Bool" && !form_data.includes(`${opts.key}=on`)) {
|
|
899
|
+
form_data += `&${opts.key}=off`;
|
|
900
|
+
}
|
|
852
901
|
$.ajax(url, {
|
|
853
902
|
type: "POST",
|
|
854
903
|
headers: {
|
|
@@ -1357,3 +1406,35 @@ function check_saltcorn_notifications() {
|
|
|
1357
1406
|
}
|
|
1358
1407
|
});
|
|
1359
1408
|
}
|
|
1409
|
+
|
|
1410
|
+
function disable_inactive_tab_inputs(id) {
|
|
1411
|
+
setTimeout(() => {
|
|
1412
|
+
const isAccordion = $(`#${id}`).hasClass("accordion");
|
|
1413
|
+
const iterElem = isAccordion
|
|
1414
|
+
? `#${id} div.accordion-item .accordion-button`
|
|
1415
|
+
: `#${id} li a`;
|
|
1416
|
+
$(iterElem).each(function () {
|
|
1417
|
+
const isActive = isAccordion
|
|
1418
|
+
? !$(this).hasClass("collapsed")
|
|
1419
|
+
: $(this).hasClass("active");
|
|
1420
|
+
const target = isAccordion
|
|
1421
|
+
? $(this).attr("data-bs-target")
|
|
1422
|
+
: $(this).attr("href");
|
|
1423
|
+
if (isActive) {
|
|
1424
|
+
//activate previously disabled
|
|
1425
|
+
$(target)
|
|
1426
|
+
.find("[disabled-by-tab]")
|
|
1427
|
+
.prop("disabled", false)
|
|
1428
|
+
.removeAttr("disabled-by-tab");
|
|
1429
|
+
} else {
|
|
1430
|
+
//disable all input
|
|
1431
|
+
$(target)
|
|
1432
|
+
.find(
|
|
1433
|
+
"input:not(:disabled), textarea:not(:disabled), button:not(:disabled), select:not(:disabled)"
|
|
1434
|
+
)
|
|
1435
|
+
.prop("disabled", true)
|
|
1436
|
+
.attr("disabled-by-tab", "1");
|
|
1437
|
+
}
|
|
1438
|
+
});
|
|
1439
|
+
}, 100);
|
|
1440
|
+
}
|
package/public/saltcorn.css
CHANGED
package/public/saltcorn.js
CHANGED
|
@@ -360,7 +360,7 @@ function submitWithAjax(e) {
|
|
|
360
360
|
saveAndContinue(e, (res) => {
|
|
361
361
|
if (res && res.responseJSON && res.responseJSON.url_when_done)
|
|
362
362
|
window.location.href = res.responseJSON.url_when_done;
|
|
363
|
-
if (res && res.responseJSON && res.responseJSON.error)
|
|
363
|
+
if (res && res.responseJSON && res.responseJSON.error && res.status < 300)
|
|
364
364
|
notifyAlert({ type: "danger", text: res.responseJSON.error });
|
|
365
365
|
});
|
|
366
366
|
}
|
|
@@ -397,16 +397,7 @@ function saveAndContinue(e, k) {
|
|
|
397
397
|
error: function (request) {
|
|
398
398
|
var ct = request.getResponseHeader("content-type") || "";
|
|
399
399
|
if (ct.startsWith && ct.startsWith("application/json")) {
|
|
400
|
-
|
|
401
|
-
if (errorArea.length) {
|
|
402
|
-
errorArea.text(request.responseJSON.error);
|
|
403
|
-
} else {
|
|
404
|
-
form
|
|
405
|
-
.parent()
|
|
406
|
-
.append(
|
|
407
|
-
`<p class="text-danger full-form-error">${request.responseJSON.error}</p>`
|
|
408
|
-
);
|
|
409
|
-
}
|
|
400
|
+
notifyAlert({ type: "danger", text: request.responseJSON.error });
|
|
410
401
|
} else {
|
|
411
402
|
$("#page-inner-content").html(request.responseText);
|
|
412
403
|
initialize_page();
|
package/routes/actions.js
CHANGED
|
@@ -145,7 +145,7 @@ const triggerForm = async (req, trigger) => {
|
|
|
145
145
|
.filter(([k, v]) => v.hasChannel)
|
|
146
146
|
.map(([k, v]) => k);
|
|
147
147
|
const allActions = actions.map((t) => t.name);
|
|
148
|
-
const table_triggers = ["Insert", "Update", "Delete"];
|
|
148
|
+
const table_triggers = ["Insert", "Update", "Delete", "Validate"];
|
|
149
149
|
const action_options = {};
|
|
150
150
|
const actionsNotRequiringRow = actions
|
|
151
151
|
.filter((a) => !a.requireRow)
|
|
@@ -171,6 +171,7 @@ const triggerForm = async (req, trigger) => {
|
|
|
171
171
|
required: true,
|
|
172
172
|
options: Trigger.when_options.map((t) => ({ value: t, label: t })),
|
|
173
173
|
sublabel: req.__("Event type which runs the trigger"),
|
|
174
|
+
help: { topic: "Event types" },
|
|
174
175
|
attributes: {
|
|
175
176
|
explainers: {
|
|
176
177
|
Often: req.__("Every 5 minutes"),
|
package/routes/api.js
CHANGED
|
@@ -358,14 +358,22 @@ router.all(
|
|
|
358
358
|
if (accessAllowed(req, user, trigger)) {
|
|
359
359
|
try {
|
|
360
360
|
const action = getState().actions[trigger.action];
|
|
361
|
+
const row = req.method === "GET" ? req.query : req.body;
|
|
361
362
|
const resp = await action.run({
|
|
362
363
|
configuration: trigger.configuration,
|
|
363
364
|
body: req.body,
|
|
364
|
-
row
|
|
365
|
+
row,
|
|
365
366
|
req,
|
|
366
367
|
user: user || req.user,
|
|
367
368
|
});
|
|
368
|
-
|
|
369
|
+
if (
|
|
370
|
+
(row._process_result || req.headers?.scprocessresults) &&
|
|
371
|
+
resp?.goto
|
|
372
|
+
)
|
|
373
|
+
res.redirect(resp.goto);
|
|
374
|
+
else if (req.headers?.scgotourl)
|
|
375
|
+
res.redirect(req.headers?.scgotourl);
|
|
376
|
+
else res.json({ success: true, data: resp });
|
|
369
377
|
} catch (e) {
|
|
370
378
|
Crash.create(e, req);
|
|
371
379
|
res.status(400).json({ success: false, error: e.message });
|