@saltcorn/server 0.9.1-beta.1 → 0.9.1-beta.11
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 +7 -0
- package/help/Extra state formula.tmd +12 -0
- package/help/JavaScript action code.tmd +1 -14
- package/locales/en.json +7 -1
- package/package.json +13 -13
- package/public/dayjs.min.js +3 -1
- package/public/saltcorn-common.js +111 -23
- package/public/saltcorn.css +1 -0
- package/public/saltcorn.js +1 -1
- package/routes/actions.js +1 -1
- package/routes/api.js +10 -2
- package/routes/common_lists.js +1 -5
- package/routes/fields.js +28 -1
- package/routes/homepage.js +75 -2
- package/routes/page.js +8 -6
- package/routes/pageedit.js +135 -23
- package/routes/view.js +8 -7
- package/s3storage.js +6 -5
- package/tests/fields.test.js +37 -1
- package/tests/page.test.js +37 -8
- 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.
|
package/help/Event types.tmd
CHANGED
|
@@ -16,6 +16,13 @@ outbound emails would have a trigger with When = Insert and Action = send_email
|
|
|
16
16
|
**Update**: run this action when changes are made to an existing row. The old row can
|
|
17
17
|
be accessed with the `old_row` variable.
|
|
18
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
|
+
|
|
19
26
|
**Delete**: run this action when a row is deleted
|
|
20
27
|
|
|
21
28
|
## Periodic events
|
|
@@ -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
|
@@ -1277,5 +1277,11 @@
|
|
|
1277
1277
|
"HTML file": "HTML file",
|
|
1278
1278
|
"HTML file to use as page content": "HTML file to use as page content",
|
|
1279
1279
|
"Offline mode: cannot load file": "Offline mode: cannot load file",
|
|
1280
|
-
"None - use drag and drop builder": "None - use drag and drop builder"
|
|
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 »"
|
|
1281
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.11",
|
|
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.11",
|
|
11
|
+
"@saltcorn/builder": "0.9.1-beta.11",
|
|
12
|
+
"@saltcorn/data": "0.9.1-beta.11",
|
|
13
|
+
"@saltcorn/admin-models": "0.9.1-beta.11",
|
|
14
|
+
"@saltcorn/filemanager": "0.9.1-beta.11",
|
|
15
|
+
"@saltcorn/markup": "0.9.1-beta.11",
|
|
16
|
+
"@saltcorn/sbadmin2": "0.9.1-beta.11",
|
|
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",
|
package/public/dayjs.min.js
CHANGED
|
@@ -1 +1,3 @@
|
|
|
1
|
-
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).dayjs=e()}(this,(function(){"use strict";var t=1e3,e=6e4,n=36e5,r="millisecond",i="second",s="minute",u="hour",a="day",o="week",f="month",h="quarter",c="year",d="date",l="Invalid Date",$=/^(\d{4})[-/]?(\d{1,2})?[-/]?(\d{0,2})[Tt\s]*(\d{1,2})?:?(\d{1,2})?:?(\d{1,2})?[.:]?(\d+)?$/,y=/\[([^\]]+)]|Y{1,4}|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSS/g,M={name:"en",weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),ordinal:function(t){var e=["th","st","nd","rd"],n=t%100;return"["+t+(e[(n-20)%10]||e[n]||e[0])+"]"}},m=function(t,e,n){var r=String(t);return!r||r.length>=e?t:""+Array(e+1-r.length).join(n)+t},v={s:m,z:function(t){var e=-t.utcOffset(),n=Math.abs(e),r=Math.floor(n/60),i=n%60;return(e<=0?"+":"-")+m(r,2,"0")+":"+m(i,2,"0")},m:function t(e,n){if(e.date()<n.date())return-t(n,e);var r=12*(n.year()-e.year())+(n.month()-e.month()),i=e.clone().add(r,f),s=n-i<0,u=e.clone().add(r+(s?-1:1),f);return+(-(r+(n-i)/(s?i-u:u-i))||0)},a:function(t){return t<0?Math.ceil(t)||0:Math.floor(t)},p:function(t){return{M:f,y:c,w:o,d:a,D:d,h:u,m:s,s:i,ms:r,Q:h}[t]||String(t||"").toLowerCase().replace(/s$/,"")},u:function(t){return void 0===t}},g="en",D={};D[g]=M;var p=function(t){return t instanceof _},S=function t(e,n,r){var i;if(!e)return g;if("string"==typeof e){var s=e.toLowerCase();D[s]&&(i=s),n&&(D[s]=n,i=s);var u=e.split("-");if(!i&&u.length>1)return t(u[0])}else{var a=e.name;D[a]=e,i=a}return!r&&i&&(g=i),i||!r&&g},w=function(t,e){if(p(t))return t.clone();var n="object"==typeof e?e:{};return n.date=t,n.args=arguments,new _(n)},O=v;O.l=S,O.i=p,O.w=function(t,e){return w(t,{locale:e.$L,utc:e.$u,x:e.$x,$offset:e.$offset})};var _=function(){function M(t){this.$L=S(t.locale,null,!0),this.parse(t)}var m=M.prototype;return m.parse=function(t){this.$d=function(t){var e=t.date,n=t.utc;if(null===e)return new Date(NaN);if(O.u(e))return new Date;if(e instanceof Date)return new Date(e);if("string"==typeof e&&!/Z$/i.test(e)){var r=e.match($);if(r){var i=r[2]-1||0,s=(r[7]||"0").substring(0,3);return n?new Date(Date.UTC(r[1],i,r[3]||1,r[4]||0,r[5]||0,r[6]||0,s)):new Date(r[1],i,r[3]||1,r[4]||0,r[5]||0,r[6]||0,s)}}return new Date(e)}(t),this.$x=t.x||{},this.init()},m.init=function(){var t=this.$d;this.$y=t.getFullYear(),this.$M=t.getMonth(),this.$D=t.getDate(),this.$W=t.getDay(),this.$H=t.getHours(),this.$m=t.getMinutes(),this.$s=t.getSeconds(),this.$ms=t.getMilliseconds()},m.$utils=function(){return O},m.isValid=function(){return!(this.$d.toString()===l)},m.isSame=function(t,e){var n=w(t);return this.startOf(e)<=n&&n<=this.endOf(e)},m.isAfter=function(t,e){return w(t)<this.startOf(e)},m.isBefore=function(t,e){return this.endOf(e)<w(t)},m.$g=function(t,e,n){return O.u(t)?this[e]:this.set(n,t)},m.unix=function(){return Math.floor(this.valueOf()/1e3)},m.valueOf=function(){return this.$d.getTime()},m.startOf=function(t,e){var n=this,r=!!O.u(e)||e,h=O.p(t),l=function(t,e){var i=O.w(n.$u?Date.UTC(n.$y,e,t):new Date(n.$y,e,t),n);return r?i:i.endOf(a)},$=function(t,e){return O.w(n.toDate()[t].apply(n.toDate("s"),(r?[0,0,0,0]:[23,59,59,999]).slice(e)),n)},y=this.$W,M=this.$M,m=this.$D,v="set"+(this.$u?"UTC":"");switch(h){case c:return r?l(1,0):l(31,11);case f:return r?l(1,M):l(0,M+1);case o:var g=this.$locale().weekStart||0,D=(y<g?y+7:y)-g;return l(r?m-D:m+(6-D),M);case a:case d:return $(v+"Hours",0);case u:return $(v+"Minutes",1);case s:return $(v+"Seconds",2);case i:return $(v+"Milliseconds",3);default:return this.clone()}},m.endOf=function(t){return this.startOf(t,!1)},m.$set=function(t,e){var n,o=O.p(t),h="set"+(this.$u?"UTC":""),l=(n={},n[a]=h+"Date",n[d]=h+"Date",n[f]=h+"Month",n[c]=h+"FullYear",n[u]=h+"Hours",n[s]=h+"Minutes",n[i]=h+"Seconds",n[r]=h+"Milliseconds",n)[o],$=o===a?this.$D+(e-this.$W):e;if(o===f||o===c){var y=this.clone().set(d,1);y.$d[l]($),y.init(),this.$d=y.set(d,Math.min(this.$D,y.daysInMonth())).$d}else l&&this.$d[l]($);return this.init(),this},m.set=function(t,e){return this.clone().$set(t,e)},m.get=function(t){return this[O.p(t)]()},m.add=function(r,h){var d,l=this;r=Number(r);var $=O.p(h),y=function(t){var e=w(l);return O.w(e.date(e.date()+Math.round(t*r)),l)};if($===f)return this.set(f,this.$M+r);if($===c)return this.set(c,this.$y+r);if($===a)return y(1);if($===o)return y(7);var M=(d={},d[s]=e,d[u]=n,d[i]=t,d)[$]||1,m=this.$d.getTime()+r*M;return O.w(m,this)},m.subtract=function(t,e){return this.add(-1*t,e)},m.format=function(t){var e=this,n=this.$locale();if(!this.isValid())return n.invalidDate||l;var r=t||"YYYY-MM-DDTHH:mm:ssZ",i=O.z(this),s=this.$H,u=this.$m,a=this.$M,o=n.weekdays,f=n.months,h=function(t,n,i,s){return t&&(t[n]||t(e,r))||i[n].slice(0,s)},c=function(t){return O.s(s%12||12,t,"0")},d=n.meridiem||function(t,e,n){var r=t<12?"AM":"PM";return n?r.toLowerCase():r},$={YY:String(this.$y).slice(-2),YYYY:this.$y,M:a+1,MM:O.s(a+1,2,"0"),MMM:h(n.monthsShort,a,f,3),MMMM:h(f,a),D:this.$D,DD:O.s(this.$D,2,"0"),d:String(this.$W),dd:h(n.weekdaysMin,this.$W,o,2),ddd:h(n.weekdaysShort,this.$W,o,3),dddd:o[this.$W],H:String(s),HH:O.s(s,2,"0"),h:c(1),hh:c(2),a:d(s,u,!0),A:d(s,u,!1),m:String(u),mm:O.s(u,2,"0"),s:String(this.$s),ss:O.s(this.$s,2,"0"),SSS:O.s(this.$ms,3,"0"),Z:i};return r.replace(y,(function(t,e){return e||$[t]||i.replace(":","")}))},m.utcOffset=function(){return 15*-Math.round(this.$d.getTimezoneOffset()/15)},m.diff=function(r,d,l){var $,y=O.p(d),M=w(r),m=(M.utcOffset()-this.utcOffset())*e,v=this-M,g=O.m(this,M);return g=($={},$[c]=g/12,$[f]=g,$[h]=g/3,$[o]=(v-m)/6048e5,$[a]=(v-m)/864e5,$[u]=v/n,$[s]=v/e,$[i]=v/t,$)[y]||v,l?g:O.a(g)},m.daysInMonth=function(){return this.endOf(f).$D},m.$locale=function(){return D[this.$L]},m.locale=function(t,e){if(!t)return this.$L;var n=this.clone(),r=S(t,e,!0);return r&&(n.$L=r),n},m.clone=function(){return O.w(this.$d,this)},m.toDate=function(){return new Date(this.valueOf())},m.toJSON=function(){return this.isValid()?this.toISOString():null},m.toISOString=function(){return this.$d.toISOString()},m.toString=function(){return this.$d.toUTCString()},M}(),T=_.prototype;return w.prototype=T,[["$ms",r],["$s",i],["$m",s],["$H",u],["$W",a],["$M",f],["$y",c],["$D",d]].forEach((function(t){T[t[1]]=function(e){return this.$g(e,t[0],t[1])}})),w.extend=function(t,e){return t.$i||(t(e,_,w),t.$i=!0),w},w.locale=S,w.isDayjs=p,w.unix=function(t){return w(1e3*t)},w.en=D[g],w.Ls=D,w.p={},w}));
|
|
1
|
+
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).dayjs=e()}(this,(function(){"use strict";var t=1e3,e=6e4,n=36e5,r="millisecond",i="second",s="minute",u="hour",a="day",o="week",f="month",h="quarter",c="year",d="date",l="Invalid Date",$=/^(\d{4})[-/]?(\d{1,2})?[-/]?(\d{0,2})[Tt\s]*(\d{1,2})?:?(\d{1,2})?:?(\d{1,2})?[.:]?(\d+)?$/,y=/\[([^\]]+)]|Y{1,4}|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSS/g,M={name:"en",weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),ordinal:function(t){var e=["th","st","nd","rd"],n=t%100;return"["+t+(e[(n-20)%10]||e[n]||e[0])+"]"}},m=function(t,e,n){var r=String(t);return!r||r.length>=e?t:""+Array(e+1-r.length).join(n)+t},v={s:m,z:function(t){var e=-t.utcOffset(),n=Math.abs(e),r=Math.floor(n/60),i=n%60;return(e<=0?"+":"-")+m(r,2,"0")+":"+m(i,2,"0")},m:function t(e,n){if(e.date()<n.date())return-t(n,e);var r=12*(n.year()-e.year())+(n.month()-e.month()),i=e.clone().add(r,f),s=n-i<0,u=e.clone().add(r+(s?-1:1),f);return+(-(r+(n-i)/(s?i-u:u-i))||0)},a:function(t){return t<0?Math.ceil(t)||0:Math.floor(t)},p:function(t){return{M:f,y:c,w:o,d:a,D:d,h:u,m:s,s:i,ms:r,Q:h}[t]||String(t||"").toLowerCase().replace(/s$/,"")},u:function(t){return void 0===t}},g="en",D={};D[g]=M;var p=function(t){return t instanceof _},S=function t(e,n,r){var i;if(!e)return g;if("string"==typeof e){var s=e.toLowerCase();D[s]&&(i=s),n&&(D[s]=n,i=s);var u=e.split("-");if(!i&&u.length>1)return t(u[0])}else{var a=e.name;D[a]=e,i=a}return!r&&i&&(g=i),i||!r&&g},w=function(t,e){if(p(t))return t.clone();var n="object"==typeof e?e:{};return n.date=t,n.args=arguments,new _(n)},O=v;O.l=S,O.i=p,O.w=function(t,e){return w(t,{locale:e.$L,utc:e.$u,x:e.$x,$offset:e.$offset})};var _=function(){function M(t){this.$L=S(t.locale,null,!0),this.parse(t)}var m=M.prototype;return m.parse=function(t){this.$d=function(t){var e=t.date,n=t.utc;if(null===e)return new Date(NaN);if(O.u(e))return new Date;if(e instanceof Date)return new Date(e);if("string"==typeof e&&!/Z$/i.test(e)){var r=e.match($);if(r){var i=r[2]-1||0,s=(r[7]||"0").substring(0,3);return n?new Date(Date.UTC(r[1],i,r[3]||1,r[4]||0,r[5]||0,r[6]||0,s)):new Date(r[1],i,r[3]||1,r[4]||0,r[5]||0,r[6]||0,s)}}return new Date(e)}(t),this.$x=t.x||{},this.init()},m.init=function(){var t=this.$d;this.$y=t.getFullYear(),this.$M=t.getMonth(),this.$D=t.getDate(),this.$W=t.getDay(),this.$H=t.getHours(),this.$m=t.getMinutes(),this.$s=t.getSeconds(),this.$ms=t.getMilliseconds()},m.$utils=function(){return O},m.isValid=function(){return!(this.$d.toString()===l)},m.isSame=function(t,e){var n=w(t);return this.startOf(e)<=n&&n<=this.endOf(e)},m.isAfter=function(t,e){return w(t)<this.startOf(e)},m.isBefore=function(t,e){return this.endOf(e)<w(t)},m.$g=function(t,e,n){return O.u(t)?this[e]:this.set(n,t)},m.unix=function(){return Math.floor(this.valueOf()/1e3)},m.valueOf=function(){return this.$d.getTime()},m.startOf=function(t,e){var n=this,r=!!O.u(e)||e,h=O.p(t),l=function(t,e){var i=O.w(n.$u?Date.UTC(n.$y,e,t):new Date(n.$y,e,t),n);return r?i:i.endOf(a)},$=function(t,e){return O.w(n.toDate()[t].apply(n.toDate("s"),(r?[0,0,0,0]:[23,59,59,999]).slice(e)),n)},y=this.$W,M=this.$M,m=this.$D,v="set"+(this.$u?"UTC":"");switch(h){case c:return r?l(1,0):l(31,11);case f:return r?l(1,M):l(0,M+1);case o:var g=this.$locale().weekStart||0,D=(y<g?y+7:y)-g;return l(r?m-D:m+(6-D),M);case a:case d:return $(v+"Hours",0);case u:return $(v+"Minutes",1);case s:return $(v+"Seconds",2);case i:return $(v+"Milliseconds",3);default:return this.clone()}},m.endOf=function(t){return this.startOf(t,!1)},m.$set=function(t,e){var n,o=O.p(t),h="set"+(this.$u?"UTC":""),l=(n={},n[a]=h+"Date",n[d]=h+"Date",n[f]=h+"Month",n[c]=h+"FullYear",n[u]=h+"Hours",n[s]=h+"Minutes",n[i]=h+"Seconds",n[r]=h+"Milliseconds",n)[o],$=o===a?this.$D+(e-this.$W):e;if(o===f||o===c){var y=this.clone().set(d,1);y.$d[l]($),y.init(),this.$d=y.set(d,Math.min(this.$D,y.daysInMonth())).$d}else l&&this.$d[l]($);return this.init(),this},m.set=function(t,e){return this.clone().$set(t,e)},m.get=function(t){return this[O.p(t)]()},m.add=function(r,h){var d,l=this;r=Number(r);var $=O.p(h),y=function(t){var e=w(l);return O.w(e.date(e.date()+Math.round(t*r)),l)};if($===f)return this.set(f,this.$M+r);if($===c)return this.set(c,this.$y+r);if($===a)return y(1);if($===o)return y(7);var M=(d={},d[s]=e,d[u]=n,d[i]=t,d)[$]||1,m=this.$d.getTime()+r*M;return O.w(m,this)},m.subtract=function(t,e){return this.add(-1*t,e)},m.format=function(t){var e=this,n=this.$locale();if(!this.isValid())return n.invalidDate||l;var r=t||"YYYY-MM-DDTHH:mm:ssZ",i=O.z(this),s=this.$H,u=this.$m,a=this.$M,o=n.weekdays,f=n.months,h=function(t,n,i,s){return t&&(t[n]||t(e,r))||i[n].slice(0,s)},c=function(t){return O.s(s%12||12,t,"0")},d=n.meridiem||function(t,e,n){var r=t<12?"AM":"PM";return n?r.toLowerCase():r},$={YY:String(this.$y).slice(-2),YYYY:this.$y,M:a+1,MM:O.s(a+1,2,"0"),MMM:h(n.monthsShort,a,f,3),MMMM:h(f,a),D:this.$D,DD:O.s(this.$D,2,"0"),d:String(this.$W),dd:h(n.weekdaysMin,this.$W,o,2),ddd:h(n.weekdaysShort,this.$W,o,3),dddd:o[this.$W],H:String(s),HH:O.s(s,2,"0"),h:c(1),hh:c(2),a:d(s,u,!0),A:d(s,u,!1),m:String(u),mm:O.s(u,2,"0"),s:String(this.$s),ss:O.s(this.$s,2,"0"),SSS:O.s(this.$ms,3,"0"),Z:i};return r.replace(y,(function(t,e){return e||$[t]||i.replace(":","")}))},m.utcOffset=function(){return 15*-Math.round(this.$d.getTimezoneOffset()/15)},m.diff=function(r,d,l){var $,y=O.p(d),M=w(r),m=(M.utcOffset()-this.utcOffset())*e,v=this-M,g=O.m(this,M);return g=($={},$[c]=g/12,$[f]=g,$[h]=g/3,$[o]=(v-m)/6048e5,$[a]=(v-m)/864e5,$[u]=v/n,$[s]=v/e,$[i]=v/t,$)[y]||v,l?g:O.a(g)},m.daysInMonth=function(){return this.endOf(f).$D},m.$locale=function(){return D[this.$L]},m.locale=function(t,e){if(!t)return this.$L;var n=this.clone(),r=S(t,e,!0);return r&&(n.$L=r),n},m.clone=function(){return O.w(this.$d,this)},m.toDate=function(){return new Date(this.valueOf())},m.toJSON=function(){return this.isValid()?this.toISOString():null},m.toISOString=function(){return this.$d.toISOString()},m.toString=function(){return this.$d.toUTCString()},M}(),T=_.prototype;return w.prototype=T,[["$ms",r],["$s",i],["$m",s],["$H",u],["$W",a],["$M",f],["$y",c],["$D",d]].forEach((function(t){T[t[1]]=function(e){return this.$g(e,t[0],t[1])}})),w.extend=function(t,e){return t.$i||(t(e,_,w),t.$i=!0),w},w.locale=S,w.isDayjs=p,w.unix=function(t){return w(1e3*t)},w.en=D[g],w.Ls=D,w.p={},w}));
|
|
2
|
+
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).dayjs_plugin_advancedFormat=t()}(this,(function(){"use strict";return function(e,t){var r=t.prototype,n=r.format;r.format=function(e){var t=this,r=this.$locale();if(!this.isValid())return n.bind(this)(e);var s=this.$utils(),a=(e||"YYYY-MM-DDTHH:mm:ssZ").replace(/\[([^\]]+)]|Q|wo|ww|w|WW|W|zzz|z|gggg|GGGG|Do|X|x|k{1,2}|S/g,(function(e){switch(e){case"Q":return Math.ceil((t.$M+1)/3);case"Do":return r.ordinal(t.$D);case"gggg":return t.weekYear();case"GGGG":return t.isoWeekYear();case"wo":return r.ordinal(t.week(),"W");case"w":case"ww":return s.s(t.week(),"w"===e?1:2,"0");case"W":case"WW":return s.s(t.isoWeek(),"W"===e?1:2,"0");case"k":case"kk":return s.s(String(0===t.$H?24:t.$H),"k"===e?1:2,"0");case"X":return Math.floor(t.$d.getTime()/1e3);case"x":return t.$d.getTime();case"z":return"["+t.offsetName()+"]";case"zzz":return"["+t.offsetName("long")+"]";default:return e}}));return n.bind(this)(a)}}}));
|
|
3
|
+
dayjs.extend(window.dayjs_plugin_advancedFormat)
|
|
@@ -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,7 +610,7 @@ 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"); // ||
|
|
@@ -736,12 +767,37 @@ function initialize_page() {
|
|
|
736
767
|
$(el).addClass("codemirror-enabled");
|
|
737
768
|
cm.on(
|
|
738
769
|
"change",
|
|
739
|
-
$.debounce(
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
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
|
+
)
|
|
745
801
|
);
|
|
746
802
|
});
|
|
747
803
|
}, 100);
|
|
@@ -1350,3 +1406,35 @@ function check_saltcorn_notifications() {
|
|
|
1350
1406
|
}
|
|
1351
1407
|
});
|
|
1352
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
|
}
|
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)
|
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 });
|
package/routes/common_lists.js
CHANGED
|
@@ -344,11 +344,7 @@ const getPageList = (rows, roles, req, { tagId, domId, showList } = {}) => {
|
|
|
344
344
|
},
|
|
345
345
|
{
|
|
346
346
|
label: req.__("Edit"),
|
|
347
|
-
key: (r) =>
|
|
348
|
-
link(
|
|
349
|
-
`/pageedit/${!r.html_file ? "edit" : "edit-properties"}/${r.name}`,
|
|
350
|
-
req.__(!r.html_file ? "Edit" : "Edit properties")
|
|
351
|
-
),
|
|
347
|
+
key: (r) => link(`/pageedit/edit/${r.name}`, req.__("Edit")),
|
|
352
348
|
},
|
|
353
349
|
!tagId
|
|
354
350
|
? {
|
package/routes/fields.js
CHANGED
|
@@ -996,6 +996,30 @@ router.post(
|
|
|
996
996
|
result = row[field.name];
|
|
997
997
|
} else if (field.stored) {
|
|
998
998
|
const f = get_async_expression_function(formula, fields);
|
|
999
|
+
//are there join fields in formula?
|
|
1000
|
+
const joinFields = {};
|
|
1001
|
+
add_free_variables_to_joinfields(
|
|
1002
|
+
freeVariables(formula),
|
|
1003
|
+
joinFields,
|
|
1004
|
+
table.fields
|
|
1005
|
+
);
|
|
1006
|
+
for (const { target, ref, through, rename_object } of Object.values(
|
|
1007
|
+
joinFields
|
|
1008
|
+
)) {
|
|
1009
|
+
const jf = table.getField(ref);
|
|
1010
|
+
const jtable = Table.findOne(jf.reftable_name);
|
|
1011
|
+
const jrow = await jtable.getRow({ [jtable.pk_name]: row[ref] });
|
|
1012
|
+
row[ref] = jrow;
|
|
1013
|
+
if (through) {
|
|
1014
|
+
const jf2 = jtable.getField(through);
|
|
1015
|
+
const jtable2 = Table.findOne(jf2.reftable_name);
|
|
1016
|
+
const jrow2 = await jtable2.getRow({
|
|
1017
|
+
[jtable2.pk_name]: jrow[through],
|
|
1018
|
+
});
|
|
1019
|
+
row[ref][through] = jrow2;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
|
|
999
1023
|
result = await f(row);
|
|
1000
1024
|
} else {
|
|
1001
1025
|
const f = get_expression_function(formula, fields);
|
|
@@ -1145,7 +1169,10 @@ router.post(
|
|
|
1145
1169
|
}
|
|
1146
1170
|
|
|
1147
1171
|
const field = table.getField(fieldName);
|
|
1148
|
-
|
|
1172
|
+
if (!field) {
|
|
1173
|
+
res.send("");
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1149
1176
|
const fieldViewConfigForms = await calcfldViewConfig([field], false, 0);
|
|
1150
1177
|
const formFields = fieldViewConfigForms[field.name][fv_name];
|
|
1151
1178
|
if (!formFields) {
|
package/routes/homepage.js
CHANGED
|
@@ -12,8 +12,9 @@ const View = require("@saltcorn/data/models/view");
|
|
|
12
12
|
const User = require("@saltcorn/data/models/user");
|
|
13
13
|
const File = require("@saltcorn/data/models/file");
|
|
14
14
|
const Page = require("@saltcorn/data/models/page");
|
|
15
|
+
const Plugin = require("@saltcorn/data/models/plugin");
|
|
15
16
|
const { link, mkTable } = require("@saltcorn/markup");
|
|
16
|
-
const { div, a, p, i } = require("@saltcorn/markup/tags");
|
|
17
|
+
const { div, a, p, i, h5, span } = require("@saltcorn/markup/tags");
|
|
17
18
|
const Table = require("@saltcorn/data/models/table");
|
|
18
19
|
const { get_cached_packs } = require("@saltcorn/admin-models/models/pack");
|
|
19
20
|
// const { restore_backup } = require("../markup/admin");
|
|
@@ -310,10 +311,80 @@ const packTab = (req, packlist) =>
|
|
|
310
311
|
{ noHeader: true }
|
|
311
312
|
),
|
|
312
313
|
a(
|
|
313
|
-
{ href: `/plugins?set=packs`, class: "btn btn-primary" },
|
|
314
|
+
{ href: `/plugins?set=packs`, class: "btn btn-sm btn-primary" },
|
|
314
315
|
req.__("Go to pack store »")
|
|
315
316
|
)
|
|
316
317
|
);
|
|
318
|
+
|
|
319
|
+
const themeCard = (req, roleMap) => {
|
|
320
|
+
const state_layouts = getState().layouts;
|
|
321
|
+
const state_layout_names = Object.keys(state_layouts);
|
|
322
|
+
const layout_by_role = getState().getConfig("layout_by_role");
|
|
323
|
+
const used_layout_by_role = {};
|
|
324
|
+
Object.keys(roleMap).forEach((role_id) => {
|
|
325
|
+
used_layout_by_role[role_id] =
|
|
326
|
+
layout_by_role[role_id] ||
|
|
327
|
+
state_layout_names[state_layout_names.length - 1];
|
|
328
|
+
});
|
|
329
|
+
const themes_available = Plugin.get_cached_plugins().filter(
|
|
330
|
+
(p) => p.has_theme && !state_layout_names.includes(p.name)
|
|
331
|
+
);
|
|
332
|
+
const layouts = Object.entries(getState().layouts)
|
|
333
|
+
.filter(([nm, v]) => nm !== "emergency")
|
|
334
|
+
.map(([name, layout]) => {
|
|
335
|
+
let plugin = getState().plugins[name];
|
|
336
|
+
const for_role = Object.entries(used_layout_by_role)
|
|
337
|
+
.filter(([role, rname]) => rname === name)
|
|
338
|
+
.map(([role, rname]) =>
|
|
339
|
+
span({ class: "badge bg-info" }, roleMap[role])
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
name,
|
|
344
|
+
layout,
|
|
345
|
+
plugin,
|
|
346
|
+
for_role,
|
|
347
|
+
edit_cfg_link: plugin?.configuration_workflow
|
|
348
|
+
? a(
|
|
349
|
+
{
|
|
350
|
+
href: `/plugins/configure/${encodeURIComponent(name)}`,
|
|
351
|
+
},
|
|
352
|
+
i({ class: "fa fa-cog ms-2" })
|
|
353
|
+
)
|
|
354
|
+
: "",
|
|
355
|
+
};
|
|
356
|
+
});
|
|
357
|
+
const show_installable = themes_available.length > 0 || layouts.length == 1;
|
|
358
|
+
return div(
|
|
359
|
+
{ class: "pb-3 pt-2 pe-4" },
|
|
360
|
+
mkTable(
|
|
361
|
+
[
|
|
362
|
+
{
|
|
363
|
+
label: req.__("Installed theme"),
|
|
364
|
+
key: ({ name, edit_cfg_link }) => `${name}${edit_cfg_link}`,
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
label: req.__("Theme for role"),
|
|
368
|
+
key: ({ for_role }) => for_role.join(" "),
|
|
369
|
+
},
|
|
370
|
+
],
|
|
371
|
+
layouts
|
|
372
|
+
),
|
|
373
|
+
a({ href: "/roleadmin" }, req.__("Set theme for each user role »")),
|
|
374
|
+
show_installable && h5({ class: "mt-2" }, req.__("Available themes")),
|
|
375
|
+
show_installable &&
|
|
376
|
+
div(
|
|
377
|
+
themes_available
|
|
378
|
+
.map((p) => span({ class: "badge bg-secondary" }, p.name))
|
|
379
|
+
.join(" ")
|
|
380
|
+
),
|
|
381
|
+
show_installable &&
|
|
382
|
+
a(
|
|
383
|
+
{ href: `/plugins?set=themes`, class: "mt-2" },
|
|
384
|
+
req.__("Install more themes »")
|
|
385
|
+
)
|
|
386
|
+
);
|
|
387
|
+
};
|
|
317
388
|
/**
|
|
318
389
|
* Help Card
|
|
319
390
|
* @param req
|
|
@@ -412,10 +483,12 @@ const welcome_page = async (req) => {
|
|
|
412
483
|
users.length > 4
|
|
413
484
|
? {
|
|
414
485
|
Users: await usersTab(req, users, roleMap),
|
|
486
|
+
Theme: themeCard(req, roleMap),
|
|
415
487
|
Help: helpCard(req),
|
|
416
488
|
}
|
|
417
489
|
: {
|
|
418
490
|
Help: helpCard(req),
|
|
491
|
+
Theme: themeCard(req, roleMap),
|
|
419
492
|
Users: await usersTab(req, users, roleMap),
|
|
420
493
|
},
|
|
421
494
|
},
|
package/routes/page.js
CHANGED
|
@@ -16,6 +16,7 @@ const {
|
|
|
16
16
|
isAdmin,
|
|
17
17
|
sendHtmlFile,
|
|
18
18
|
} = require("../routes/utils.js");
|
|
19
|
+
const { isTest } = require("@saltcorn/data/utils");
|
|
19
20
|
const { add_edit_bar } = require("../markup/admin.js");
|
|
20
21
|
const { traverseSync } = require("@saltcorn/data/models/layout");
|
|
21
22
|
const { run_action_column } = require("@saltcorn/data/plugin-helper");
|
|
@@ -52,12 +53,13 @@ router.get(
|
|
|
52
53
|
const title = scan_for_page_title(contents, db_page.title);
|
|
53
54
|
const tock = new Date();
|
|
54
55
|
const ms = tock.getTime() - tic.getTime();
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
if (!isTest())
|
|
57
|
+
Trigger.emitEvent("PageLoad", null, req.user, {
|
|
58
|
+
text: req.__("Page '%s' was loaded", pagename),
|
|
59
|
+
type: "page",
|
|
60
|
+
name: pagename,
|
|
61
|
+
render_time: ms,
|
|
62
|
+
});
|
|
61
63
|
if (contents.html_file) await sendHtmlFile(req, res, contents.html_file);
|
|
62
64
|
else
|
|
63
65
|
res.sendWrap(
|
package/routes/pageedit.js
CHANGED
|
@@ -9,7 +9,7 @@ const View = require("@saltcorn/data/models/view");
|
|
|
9
9
|
const Field = require("@saltcorn/data/models/field");
|
|
10
10
|
const Table = require("@saltcorn/data/models/table");
|
|
11
11
|
const Page = require("@saltcorn/data/models/page");
|
|
12
|
-
const { div, a } = require("@saltcorn/markup/tags");
|
|
12
|
+
const { div, a, iframe, script } = require("@saltcorn/markup/tags");
|
|
13
13
|
const { getState } = require("@saltcorn/data/db/state");
|
|
14
14
|
const User = require("@saltcorn/data/models/user");
|
|
15
15
|
const Workflow = require("@saltcorn/data/models/workflow");
|
|
@@ -41,6 +41,7 @@ const {
|
|
|
41
41
|
const { getActionConfigFields } = require("@saltcorn/data/plugin-helper");
|
|
42
42
|
const Library = require("@saltcorn/data/models/library");
|
|
43
43
|
const path = require("path");
|
|
44
|
+
const fsp = require("fs").promises;
|
|
44
45
|
|
|
45
46
|
/**
|
|
46
47
|
* @type {object}
|
|
@@ -431,6 +432,116 @@ router.post(
|
|
|
431
432
|
);
|
|
432
433
|
|
|
433
434
|
/**
|
|
435
|
+
* open the builder
|
|
436
|
+
* @param {*} req
|
|
437
|
+
* @param {*} res
|
|
438
|
+
* @param {*} page
|
|
439
|
+
*/
|
|
440
|
+
const getEditNormalPage = async (req, res, page) => {
|
|
441
|
+
// set fixed states in page directly for legacy builds
|
|
442
|
+
traverseSync(page.layout, {
|
|
443
|
+
view(s) {
|
|
444
|
+
if (s.state === "fixed" && !s.configuration) {
|
|
445
|
+
const fs = page.fixed_states[s.name];
|
|
446
|
+
if (fs) s.configuration = fs;
|
|
447
|
+
}
|
|
448
|
+
},
|
|
449
|
+
});
|
|
450
|
+
const options = await pageBuilderData(req, page);
|
|
451
|
+
const builderData = {
|
|
452
|
+
options,
|
|
453
|
+
context: page,
|
|
454
|
+
layout: page.layout,
|
|
455
|
+
mode: "page",
|
|
456
|
+
version_tag: db.connectObj.version_tag,
|
|
457
|
+
};
|
|
458
|
+
res.sendWrap(
|
|
459
|
+
req.__(`%s configuration`, page.name),
|
|
460
|
+
wrap(renderBuilder(builderData, req.csrfToken()), true, req, page)
|
|
461
|
+
);
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* open a file editor with an iframe preview
|
|
466
|
+
* @param {*} req
|
|
467
|
+
* @param {*} res
|
|
468
|
+
* @param {*} page
|
|
469
|
+
*/
|
|
470
|
+
const getEditPageWithHtmlFile = async (req, res, page) => {
|
|
471
|
+
const htmlFile = page.html_file;
|
|
472
|
+
const iframeId = "page_preview_iframe";
|
|
473
|
+
const updateBttnId = "addnUpdBtn";
|
|
474
|
+
const file = await File.findOne(htmlFile);
|
|
475
|
+
if (!file) {
|
|
476
|
+
req.flash("error", req.__("File not found"));
|
|
477
|
+
return res.redirect(`/pageedit`);
|
|
478
|
+
}
|
|
479
|
+
const editForm = new Form({
|
|
480
|
+
action: `/pageedit/edit/${encodeURIComponent(page.name)}`,
|
|
481
|
+
fields: [
|
|
482
|
+
{
|
|
483
|
+
name: "code",
|
|
484
|
+
form_name: "code",
|
|
485
|
+
label: "Code",
|
|
486
|
+
input_type: "code",
|
|
487
|
+
attributes: { mode: "text/html" },
|
|
488
|
+
validator(s) {
|
|
489
|
+
return true;
|
|
490
|
+
},
|
|
491
|
+
},
|
|
492
|
+
],
|
|
493
|
+
values: {
|
|
494
|
+
code: await fsp.readFile(file.location, "utf8"),
|
|
495
|
+
},
|
|
496
|
+
onChange: `document.getElementById('${updateBttnId}').disabled = false;`,
|
|
497
|
+
additionalButtons: [
|
|
498
|
+
{
|
|
499
|
+
label: req.__("Update"),
|
|
500
|
+
id: updateBttnId,
|
|
501
|
+
class: "btn btn-primary",
|
|
502
|
+
onclick: `saveAndContinue(this, () => {
|
|
503
|
+
document.getElementById('${iframeId}').contentWindow.location.reload();
|
|
504
|
+
document.getElementById('${updateBttnId}').disabled = true;
|
|
505
|
+
})`,
|
|
506
|
+
disabled: true,
|
|
507
|
+
},
|
|
508
|
+
],
|
|
509
|
+
submitLabel: req.__("Finish") + " »",
|
|
510
|
+
});
|
|
511
|
+
res.sendWrap(req.__("Edit %s", page.title), {
|
|
512
|
+
above: [
|
|
513
|
+
{
|
|
514
|
+
type: "card",
|
|
515
|
+
title: "Edit",
|
|
516
|
+
titleAjaxIndicator: true,
|
|
517
|
+
contents: [renderForm(editForm, req.csrfToken())],
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
type: "card",
|
|
521
|
+
title: "Preview",
|
|
522
|
+
contents: [
|
|
523
|
+
iframe({
|
|
524
|
+
id: iframeId,
|
|
525
|
+
src: `/files/serve/${encodeURIComponent(htmlFile)}`,
|
|
526
|
+
}),
|
|
527
|
+
script(`
|
|
528
|
+
const iframe = document.getElementById("${iframeId}");
|
|
529
|
+
iframe.onload = () => {
|
|
530
|
+
const _iframe = document.getElementById("${iframeId}");
|
|
531
|
+
if (_iframe.contentWindow.document.body) {
|
|
532
|
+
_iframe.width = _iframe.contentWindow.document.body.scrollWidth;
|
|
533
|
+
_iframe.height = _iframe.contentWindow.document.body.scrollHeight;
|
|
534
|
+
}
|
|
535
|
+
}`),
|
|
536
|
+
],
|
|
537
|
+
},
|
|
538
|
+
],
|
|
539
|
+
});
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* for normal pages, open the builder
|
|
544
|
+
* for pages with a fixed html file, open a file editor with an iframe preview
|
|
434
545
|
* @name get/edit/:pagename
|
|
435
546
|
* @function
|
|
436
547
|
* @memberof module:routes/pageedit~pageeditRouter
|
|
@@ -446,27 +557,8 @@ router.get(
|
|
|
446
557
|
req.flash("error", req.__(`Page %s not found`, pagename));
|
|
447
558
|
res.redirect(`/pageedit`);
|
|
448
559
|
} else {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
view(s) {
|
|
452
|
-
if (s.state === "fixed" && !s.configuration) {
|
|
453
|
-
const fs = page.fixed_states[s.name];
|
|
454
|
-
if (fs) s.configuration = fs;
|
|
455
|
-
}
|
|
456
|
-
},
|
|
457
|
-
});
|
|
458
|
-
const options = await pageBuilderData(req, page);
|
|
459
|
-
const builderData = {
|
|
460
|
-
options,
|
|
461
|
-
context: page,
|
|
462
|
-
layout: page.layout,
|
|
463
|
-
mode: "page",
|
|
464
|
-
version_tag: db.connectObj.version_tag,
|
|
465
|
-
};
|
|
466
|
-
res.sendWrap(
|
|
467
|
-
req.__(`%s configuration`, page.name),
|
|
468
|
-
wrap(renderBuilder(builderData, req.csrfToken()), true, req, page)
|
|
469
|
-
);
|
|
560
|
+
if (!page.html_file) await getEditNormalPage(req, res, page);
|
|
561
|
+
else await getEditPageWithHtmlFile(req, res, page);
|
|
470
562
|
}
|
|
471
563
|
})
|
|
472
564
|
);
|
|
@@ -495,13 +587,33 @@ router.post(
|
|
|
495
587
|
await Page.update(page.id, {
|
|
496
588
|
layout: decodeURIComponent(req.body.layout),
|
|
497
589
|
});
|
|
498
|
-
|
|
499
590
|
req.flash("success", req.__(`Page %s saved`, pagename));
|
|
500
591
|
res.redirect(redirectTarget);
|
|
592
|
+
} else if (req.body.code) {
|
|
593
|
+
try {
|
|
594
|
+
if (!page.html_file) throw new Error(req.__("File not found"));
|
|
595
|
+
const file = await File.findOne(page.html_file);
|
|
596
|
+
if (!file) throw new Error(req.__("File not found"));
|
|
597
|
+
await fsp.writeFile(file.location, req.body.code);
|
|
598
|
+
if (!req.xhr) {
|
|
599
|
+
req.flash("success", req.__(`Page %s saved`, pagename));
|
|
600
|
+
res.redirect(redirectTarget);
|
|
601
|
+
} else res.json({ okay: true });
|
|
602
|
+
} catch (error) {
|
|
603
|
+
getState().log(2, `POST /edit/${pagename}: '${error.message}'`);
|
|
604
|
+
req.flash(
|
|
605
|
+
"error",
|
|
606
|
+
`${req.__("Error")}: ${error.message || req.__("An error occurred")}`
|
|
607
|
+
);
|
|
608
|
+
if (!req.xhr) res.redirect(redirectTarget);
|
|
609
|
+
else res.json({ error: error.message });
|
|
610
|
+
}
|
|
501
611
|
} else {
|
|
612
|
+
getState().log(2, `POST /edit/${pagename}: '${req.body}'`);
|
|
502
613
|
req.flash("error", req.__(`Error processing page`));
|
|
503
614
|
res.redirect(redirectTarget);
|
|
504
615
|
}
|
|
616
|
+
getState().log(5, `POST /edit/${pagename}: Success`);
|
|
505
617
|
})
|
|
506
618
|
);
|
|
507
619
|
|
package/routes/view.js
CHANGED
|
@@ -18,7 +18,7 @@ const {
|
|
|
18
18
|
setTenant,
|
|
19
19
|
} = require("../routes/utils.js");
|
|
20
20
|
const { add_edit_bar } = require("../markup/admin.js");
|
|
21
|
-
const { InvalidConfiguration } = require("@saltcorn/data/utils");
|
|
21
|
+
const { InvalidConfiguration, isTest } = require("@saltcorn/data/utils");
|
|
22
22
|
const { getState } = require("@saltcorn/data/db/state");
|
|
23
23
|
|
|
24
24
|
/**
|
|
@@ -88,12 +88,13 @@ router.get(
|
|
|
88
88
|
res.set("SaltcornModalLinkOut", `true`);
|
|
89
89
|
const tock = new Date();
|
|
90
90
|
const ms = tock.getTime() - tic.getTime();
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
91
|
+
if (!isTest())
|
|
92
|
+
Trigger.emitEvent("PageLoad", null, req.user, {
|
|
93
|
+
text: req.__("View '%s' was loaded", viewname),
|
|
94
|
+
type: "view",
|
|
95
|
+
name: viewname,
|
|
96
|
+
render_time: ms,
|
|
97
|
+
});
|
|
97
98
|
if (typeof contents === "object" && contents.goto)
|
|
98
99
|
res.redirect(contents.goto);
|
|
99
100
|
else
|
package/s3storage.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
require("aws-sdk/
|
|
2
|
-
const aws = require("aws-sdk");
|
|
1
|
+
const { S3 } = require("@aws-sdk/client-s3");
|
|
3
2
|
const multer = require("multer");
|
|
4
3
|
const multerS3 = require("multer-s3");
|
|
5
4
|
const { getState } = require("@saltcorn/data/db/state");
|
|
@@ -8,9 +7,11 @@ const { v4: uuidv4 } = require("uuid");
|
|
|
8
7
|
const contentDisposition = require("content-disposition");
|
|
9
8
|
const fs = require("fs");
|
|
10
9
|
function createS3Client() {
|
|
11
|
-
return new
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
return new S3({
|
|
11
|
+
credentials: {
|
|
12
|
+
secretAccessKey: getState().getConfig("storage_s3_access_secret"),
|
|
13
|
+
accessKeyId: getState().getConfig("storage_s3_access_key"),
|
|
14
|
+
},
|
|
14
15
|
region: getState().getConfig("storage_s3_region"),
|
|
15
16
|
endpoint: getState().getConfig("storage_s3_endpoint"),
|
|
16
17
|
});
|
package/tests/fields.test.js
CHANGED
|
@@ -364,7 +364,43 @@ describe("Field Endpoints", () => {
|
|
|
364
364
|
await request(app)
|
|
365
365
|
.post("/field/show-calculated/books/pagesp1/show")
|
|
366
366
|
.set("Cookie", loginCookie)
|
|
367
|
-
.expect((r) => +r.body >
|
|
367
|
+
.expect((r) => +r.body > 2);
|
|
368
|
+
});
|
|
369
|
+
it("should show calculated with single joinfield", async () => {
|
|
370
|
+
const loginCookie = await getAdminLoginCookie();
|
|
371
|
+
const table = Table.findOne({ name: "patients" });
|
|
372
|
+
await Field.create({
|
|
373
|
+
table,
|
|
374
|
+
label: "pagesp1",
|
|
375
|
+
type: "Integer",
|
|
376
|
+
calculated: true,
|
|
377
|
+
stored: true,
|
|
378
|
+
expression: "favbook.pages+1",
|
|
379
|
+
});
|
|
380
|
+
const app = await getApp({ disableCsrf: true });
|
|
381
|
+
|
|
382
|
+
await request(app)
|
|
383
|
+
.post("/field/show-calculated/patients/pagesp1/show")
|
|
384
|
+
.set("Cookie", loginCookie)
|
|
385
|
+
.expect((r) => +r.body > 2);
|
|
386
|
+
});
|
|
387
|
+
it("should show calculated with double joinfield", async () => {
|
|
388
|
+
const loginCookie = await getAdminLoginCookie();
|
|
389
|
+
const table = Table.findOne({ name: "readings" });
|
|
390
|
+
await Field.create({
|
|
391
|
+
table,
|
|
392
|
+
label: "pagesp1",
|
|
393
|
+
type: "Integer",
|
|
394
|
+
calculated: true,
|
|
395
|
+
stored: true,
|
|
396
|
+
expression: "patient_id.favbook.pages+1",
|
|
397
|
+
});
|
|
398
|
+
const app = await getApp({ disableCsrf: true });
|
|
399
|
+
|
|
400
|
+
await request(app)
|
|
401
|
+
.post("/field/show-calculated/readings/pagesp1/show")
|
|
402
|
+
.set("Cookie", loginCookie)
|
|
403
|
+
.expect((r) => +r.body > 2);
|
|
368
404
|
});
|
|
369
405
|
});
|
|
370
406
|
|
package/tests/page.test.js
CHANGED
|
@@ -16,6 +16,7 @@ const Page = require("@saltcorn/data/models/page");
|
|
|
16
16
|
const File = require("@saltcorn/data/models/file");
|
|
17
17
|
const { existsSync } = require("fs");
|
|
18
18
|
const { join } = require("path");
|
|
19
|
+
const fs = require("fs");
|
|
19
20
|
|
|
20
21
|
let htmlFile = null;
|
|
21
22
|
|
|
@@ -26,24 +27,20 @@ const prepHtmlFiles = async () => {
|
|
|
26
27
|
db.getTenantSchema(),
|
|
27
28
|
folder
|
|
28
29
|
);
|
|
30
|
+
const html = `<html><head><title>Landing page</title></head><body><h1>${content}</h1></body></html>`;
|
|
29
31
|
if (!existsSync(scFolder)) await File.new_folder(folder);
|
|
30
32
|
if (!existsSync(join(scFolder, name))) {
|
|
31
|
-
return await File.from_contents(
|
|
32
|
-
name,
|
|
33
|
-
"text/html",
|
|
34
|
-
`<html><head><title>Landing page</title></head><body><h1>${content}</h1></body></html>`,
|
|
35
|
-
1,
|
|
36
|
-
1,
|
|
37
|
-
folder
|
|
38
|
-
);
|
|
33
|
+
return await File.from_contents(name, "text/html", html, 1, 1, folder);
|
|
39
34
|
} else {
|
|
40
35
|
const file = await File.from_file_on_disk(name, scFolder);
|
|
36
|
+
fs.writeFileSync(file.location, html);
|
|
41
37
|
file.location = File.absPathToServePath(file.location);
|
|
42
38
|
return file;
|
|
43
39
|
}
|
|
44
40
|
};
|
|
45
41
|
htmlFile = await createFile("/", "fixed_page.html", "Land here");
|
|
46
42
|
await createFile("/subfolder", "fixed_page2.html", "Or Land here");
|
|
43
|
+
await createFile("/", "test.html", "page with fixed html");
|
|
47
44
|
};
|
|
48
45
|
|
|
49
46
|
beforeAll(async () => {
|
|
@@ -135,6 +132,11 @@ describe("page create", () => {
|
|
|
135
132
|
|
|
136
133
|
it("does not find the html file for staff or public", async () => {
|
|
137
134
|
const app = await getApp({ disableCsrf: true });
|
|
135
|
+
await request(app)
|
|
136
|
+
.post(`/files/setrole/fixed_page.html`)
|
|
137
|
+
.set("Cookie", await getAdminLoginCookie())
|
|
138
|
+
.send("role=1")
|
|
139
|
+
.expect(toRedirect("/files?dir=."));
|
|
138
140
|
const loginCookie = await getStaffLoginCookie();
|
|
139
141
|
await request(app)
|
|
140
142
|
.get("/page/new_page_with_html_file")
|
|
@@ -247,6 +249,33 @@ describe("pageedit", () => {
|
|
|
247
249
|
.set("Cookie", loginCookie)
|
|
248
250
|
.expect(toInclude("<script>builder.renderBuilder"));
|
|
249
251
|
});
|
|
252
|
+
it("show editor with html file", async () => {
|
|
253
|
+
const app = await getApp({ disableCsrf: true });
|
|
254
|
+
const loginCookie = await getAdminLoginCookie();
|
|
255
|
+
await request(app)
|
|
256
|
+
.get("/pageedit/edit/page_with_html_file")
|
|
257
|
+
.set("Cookie", loginCookie)
|
|
258
|
+
.expect(toInclude(`<textarea mode="text/html"`));
|
|
259
|
+
});
|
|
260
|
+
it("edit editor with html file", async () => {
|
|
261
|
+
const app = await getApp({ disableCsrf: true });
|
|
262
|
+
const loginCookie = await getAdminLoginCookie();
|
|
263
|
+
const newHtml =
|
|
264
|
+
"<html><head><title>title</title></head><body><h1>new html</h1></body</html>";
|
|
265
|
+
await request(app)
|
|
266
|
+
.get("/page/page_with_html_file")
|
|
267
|
+
.set("Cookie", loginCookie)
|
|
268
|
+
.expect(toInclude("page with fixed html"));
|
|
269
|
+
await request(app)
|
|
270
|
+
.post("/pageedit/edit/page_with_html_file")
|
|
271
|
+
.set("Cookie", loginCookie)
|
|
272
|
+
.send("code=" + encodeURIComponent(newHtml))
|
|
273
|
+
.expect(toRedirect("/pageedit"));
|
|
274
|
+
await request(app)
|
|
275
|
+
.get("/page/page_with_html_file")
|
|
276
|
+
.set("Cookie", loginCookie)
|
|
277
|
+
.expect(toInclude("new html"));
|
|
278
|
+
});
|
|
250
279
|
|
|
251
280
|
it("sets root page", async () => {
|
|
252
281
|
const app = await getApp({ disableCsrf: true });
|
package/tests/table.test.js
CHANGED
|
@@ -17,8 +17,12 @@ const db = require("@saltcorn/data/db");
|
|
|
17
17
|
const User = require("@saltcorn/data/models/user");
|
|
18
18
|
const { plugin_with_routes } = require("@saltcorn/data/tests/mocks");
|
|
19
19
|
const { getState } = require("@saltcorn/data/db/state");
|
|
20
|
+
const { sleep } = require("@saltcorn/data/utils");
|
|
20
21
|
|
|
21
|
-
afterAll(
|
|
22
|
+
afterAll(async () => {
|
|
23
|
+
await sleep(200);
|
|
24
|
+
await db.close();
|
|
25
|
+
});
|
|
22
26
|
beforeAll(async () => {
|
|
23
27
|
await resetToFixtures();
|
|
24
28
|
});
|
package/tests/view.test.js
CHANGED
|
@@ -17,6 +17,9 @@ const View = require("@saltcorn/data/models/view");
|
|
|
17
17
|
const Table = require("@saltcorn/data/models/table");
|
|
18
18
|
|
|
19
19
|
const { plugin_with_routes } = require("@saltcorn/data/tests/mocks");
|
|
20
|
+
const {
|
|
21
|
+
prepareArtistsAlbumRelation,
|
|
22
|
+
} = require("@saltcorn/data/tests/common_helpers");
|
|
20
23
|
|
|
21
24
|
afterAll(db.close);
|
|
22
25
|
beforeAll(async () => {
|
|
@@ -586,3 +589,97 @@ describe("inbound relations", () => {
|
|
|
586
589
|
.expect(toNotInclude("Content of post CPost C"));
|
|
587
590
|
});
|
|
588
591
|
});
|
|
592
|
+
|
|
593
|
+
describe("many to many relations", () => {
|
|
594
|
+
beforeAll(async () => {
|
|
595
|
+
await prepareArtistsAlbumRelation();
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
it("artist_plays_on_album", async () => {
|
|
599
|
+
const app = await getApp({ disableCsrf: true });
|
|
600
|
+
const loginCookie = await getAdminLoginCookie();
|
|
601
|
+
await request(app)
|
|
602
|
+
.get("/view/show_artist?id=1")
|
|
603
|
+
.set("Cookie", loginCookie)
|
|
604
|
+
.expect(toInclude("album A"))
|
|
605
|
+
.expect(toInclude("album B"));
|
|
606
|
+
|
|
607
|
+
await request(app)
|
|
608
|
+
.get("/view/show_artist?id=2")
|
|
609
|
+
.set("Cookie", loginCookie)
|
|
610
|
+
.expect(toInclude("album A"))
|
|
611
|
+
.expect(toNotInclude("album B"));
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
it("albums feed with query", async () => {
|
|
615
|
+
const app = await getApp({ disableCsrf: true });
|
|
616
|
+
const loginCookie = await getAdminLoginCookie();
|
|
617
|
+
|
|
618
|
+
const queryObj_1 = {
|
|
619
|
+
relation: ".artists.artist_plays_on_album$artist.album",
|
|
620
|
+
srcId: 1,
|
|
621
|
+
};
|
|
622
|
+
await request(app)
|
|
623
|
+
.get(
|
|
624
|
+
`/view/albums_feed?_inbound_relation_path_=${encodeURIComponent(
|
|
625
|
+
JSON.stringify(queryObj_1)
|
|
626
|
+
)}`
|
|
627
|
+
)
|
|
628
|
+
.set("Cookie", loginCookie)
|
|
629
|
+
.expect(toInclude("album A"))
|
|
630
|
+
.expect(toInclude("album B"));
|
|
631
|
+
|
|
632
|
+
const queryObj_2 = {
|
|
633
|
+
relation: ".artists.artist_plays_on_album$artist.album",
|
|
634
|
+
srcId: 2,
|
|
635
|
+
};
|
|
636
|
+
await request(app)
|
|
637
|
+
.get(
|
|
638
|
+
`/view/albums_feed?_inbound_relation_path_=${encodeURIComponent(
|
|
639
|
+
JSON.stringify(queryObj_2)
|
|
640
|
+
)}`
|
|
641
|
+
)
|
|
642
|
+
.set("Cookie", loginCookie)
|
|
643
|
+
.expect(toInclude("album A"))
|
|
644
|
+
.expect(toNotInclude("album B"));
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
it("fan_club feed with query", async () => {
|
|
648
|
+
const app = await getApp({ disableCsrf: true });
|
|
649
|
+
const loginCookie = await getAdminLoginCookie();
|
|
650
|
+
|
|
651
|
+
const queryObj_1 = {
|
|
652
|
+
relation:
|
|
653
|
+
".pressing_job.album.artist_plays_on_album$album.artist.fan_club$artist",
|
|
654
|
+
srcId: 1,
|
|
655
|
+
};
|
|
656
|
+
await request(app)
|
|
657
|
+
.get(
|
|
658
|
+
`/view/fan_club_feed?_inbound_relation_path_=${encodeURIComponent(
|
|
659
|
+
JSON.stringify(queryObj_1)
|
|
660
|
+
)}`
|
|
661
|
+
)
|
|
662
|
+
.set("Cookie", loginCookie)
|
|
663
|
+
.expect(toInclude("crazy fan club"))
|
|
664
|
+
.expect(toInclude("another club"))
|
|
665
|
+
.expect(toInclude("fan club"))
|
|
666
|
+
.expect(toInclude("fan club official"));
|
|
667
|
+
|
|
668
|
+
const queryObj_2 = {
|
|
669
|
+
relation:
|
|
670
|
+
".pressing_job.album.artist_plays_on_album$album.artist.fan_club$artist",
|
|
671
|
+
srcId: 2,
|
|
672
|
+
};
|
|
673
|
+
await request(app)
|
|
674
|
+
.get(
|
|
675
|
+
`/view/fan_club_feed?_inbound_relation_path_=${encodeURIComponent(
|
|
676
|
+
JSON.stringify(queryObj_2)
|
|
677
|
+
)}`
|
|
678
|
+
)
|
|
679
|
+
.set("Cookie", loginCookie)
|
|
680
|
+
.expect(toInclude("crazy fan club"))
|
|
681
|
+
.expect(toNotInclude("another club"))
|
|
682
|
+
.expect(toInclude("fan club"))
|
|
683
|
+
.expect(toInclude("fan club official"));
|
|
684
|
+
});
|
|
685
|
+
});
|
package/wrapper.js
CHANGED
|
@@ -316,7 +316,14 @@ module.exports = (version_tag) =>
|
|
|
316
316
|
if (req.xhr) {
|
|
317
317
|
const renderToHtml = layout.renderBody
|
|
318
318
|
? (h, role, req) =>
|
|
319
|
-
layout.renderBody({
|
|
319
|
+
layout.renderBody({
|
|
320
|
+
title,
|
|
321
|
+
body: h,
|
|
322
|
+
role,
|
|
323
|
+
alerts,
|
|
324
|
+
req,
|
|
325
|
+
hints: layout.hints,
|
|
326
|
+
})
|
|
320
327
|
: defaultRenderToHtml;
|
|
321
328
|
res.header(
|
|
322
329
|
"Cache-Control",
|