@saltcorn/server 0.9.0-beta.0 → 0.9.0-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/app.js +58 -6
- package/auth/routes.js +16 -20
- package/errors.js +15 -4
- package/help/Actions.tmd +9 -0
- package/help/Extra state formula.tmd +62 -0
- package/help/Field views.tmd +22 -0
- package/help/JavaScript action code.tmd +161 -0
- package/help/Table formula constraint.tmd +6 -4
- package/help/View patterns.tmd +35 -0
- package/help/Where formula.tmd +30 -0
- package/help/index.js +19 -10
- package/load_plugins.js +4 -2
- package/locales/da.json +709 -709
- package/locales/de.json +1049 -1049
- package/locales/en.json +18 -2
- package/locales/pl.json +1155 -1155
- package/locales/ru.json +1101 -1101
- package/locales/si.json +1196 -1196
- package/locales/uk.json +1168 -1168
- package/locales/zh.json +886 -886
- package/package.json +10 -9
- package/public/saltcorn-builder.css +4 -0
- package/public/saltcorn-common.js +85 -17
- package/public/saltcorn.css +14 -0
- package/public/saltcorn.js +33 -7
- package/routes/actions.js +5 -3
- package/routes/admin.js +146 -20
- package/routes/fields.js +15 -3
- package/routes/menu.js +1 -1
- package/routes/packs.js +134 -9
- package/routes/plugins.js +186 -36
- package/routes/sync.js +4 -1
- package/routes/tables.js +4 -3
- package/routes/viewedit.js +21 -1
- package/tests/admin.test.js +2 -2
- package/tests/sync.test.js +140 -6
package/app.js
CHANGED
|
@@ -69,6 +69,39 @@ const disabledCsurf = (req, res, next) => {
|
|
|
69
69
|
next();
|
|
70
70
|
};
|
|
71
71
|
|
|
72
|
+
const noCsrfLookup = (state) => {
|
|
73
|
+
if (!state.plugin_routes) return null;
|
|
74
|
+
else {
|
|
75
|
+
const result = new Set();
|
|
76
|
+
for (const [plugin, routes] of Object.entries(state.plugin_routes)) {
|
|
77
|
+
for (const url of routes
|
|
78
|
+
.filter((r) => r.noCsrf === true)
|
|
79
|
+
.map((r) => r.url)) {
|
|
80
|
+
result.add(url);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const prepPluginRouter = (pluginRoutes) => {
|
|
88
|
+
const router = express.Router();
|
|
89
|
+
for (const [plugin, routes] of Object.entries(pluginRoutes)) {
|
|
90
|
+
for (const route of routes) {
|
|
91
|
+
switch (route.method) {
|
|
92
|
+
case "post":
|
|
93
|
+
router.post(route.url, error_catcher(route.callback));
|
|
94
|
+
break;
|
|
95
|
+
case "get":
|
|
96
|
+
default:
|
|
97
|
+
router.get(route.url, error_catcher(route.callback));
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return router;
|
|
103
|
+
};
|
|
104
|
+
|
|
72
105
|
// todo console.log app instance info when app stxarts - avoid to show secrets (password, etc)
|
|
73
106
|
|
|
74
107
|
/**
|
|
@@ -95,17 +128,23 @@ const getApp = async (opts = {}) => {
|
|
|
95
128
|
app.use(helmet());
|
|
96
129
|
// TODO ch find a better solution
|
|
97
130
|
app.use(cors());
|
|
131
|
+
const bodyLimit = getState().getConfig("body_limit");
|
|
98
132
|
app.use(
|
|
99
133
|
express.json({
|
|
100
|
-
limit: "5mb",
|
|
134
|
+
limit: bodyLimit ? `${bodyLimit}kb` : "5mb",
|
|
101
135
|
verify: (req, res, buf) => {
|
|
102
136
|
req.rawBody = buf;
|
|
103
137
|
},
|
|
104
138
|
})
|
|
105
139
|
);
|
|
106
|
-
|
|
140
|
+
const urlencodedLimit = getState().getConfig("url_encoded_limit");
|
|
141
|
+
// extended url encoding in use
|
|
107
142
|
app.use(
|
|
108
|
-
express.urlencoded({
|
|
143
|
+
express.urlencoded({
|
|
144
|
+
limit: urlencodedLimit ? `${urlencodedLimit}kb` : "5mb",
|
|
145
|
+
extended: true,
|
|
146
|
+
parameterLimit: 50000,
|
|
147
|
+
})
|
|
109
148
|
);
|
|
110
149
|
|
|
111
150
|
// cookies
|
|
@@ -300,21 +339,34 @@ const getApp = async (opts = {}) => {
|
|
|
300
339
|
app.use("/scapi", scapi);
|
|
301
340
|
|
|
302
341
|
const csurf = csrf();
|
|
303
|
-
|
|
342
|
+
let noCsrf = null;
|
|
343
|
+
if (!opts.disableCsrf) {
|
|
344
|
+
noCsrf = noCsrfLookup(getState());
|
|
304
345
|
app.use(function (req, res, next) {
|
|
305
346
|
if (
|
|
347
|
+
noCsrf?.has(req.url) ||
|
|
306
348
|
(req.smr &&
|
|
307
349
|
(req.url.startsWith("/api/") ||
|
|
308
350
|
req.url === "/auth/login-with/jwt" ||
|
|
309
351
|
req.url === "/auth/signup")) ||
|
|
310
|
-
jwt_extractor(req)
|
|
352
|
+
jwt_extractor(req) ||
|
|
353
|
+
req.url === "/auth/callback/saml"
|
|
311
354
|
)
|
|
312
355
|
return disabledCsurf(req, res, next);
|
|
313
356
|
csurf(req, res, next);
|
|
314
357
|
});
|
|
315
|
-
else app.use(disabledCsurf);
|
|
358
|
+
} else app.use(disabledCsurf);
|
|
316
359
|
|
|
317
360
|
mountRoutes(app);
|
|
361
|
+
// mount plugin router with a callback for changes
|
|
362
|
+
let pluginRouter = prepPluginRouter(getState().plugin_routes || {});
|
|
363
|
+
getState().routesChangedCb = () => {
|
|
364
|
+
pluginRouter = prepPluginRouter(getState().plugin_routes || {});
|
|
365
|
+
noCsrf = noCsrfLookup(getState());
|
|
366
|
+
};
|
|
367
|
+
app.use((req, res, next) => {
|
|
368
|
+
pluginRouter(req, res, next);
|
|
369
|
+
});
|
|
318
370
|
// set tenant homepage as / root
|
|
319
371
|
app.get("/", error_catcher(homepage));
|
|
320
372
|
// /robots.txt
|
package/auth/routes.js
CHANGED
|
@@ -228,7 +228,7 @@ const loginWithJwt = async (email, password, saltcornApp, res, req) => {
|
|
|
228
228
|
const loginFn = async () => {
|
|
229
229
|
const publicUserLink = getState().getConfig("public_user_link");
|
|
230
230
|
const jwt_secret = db.connectObj.jwt_secret;
|
|
231
|
-
if (email && password) {
|
|
231
|
+
if (email !== undefined && password !== undefined) {
|
|
232
232
|
// with credentials
|
|
233
233
|
const user = await User.findOne({ email });
|
|
234
234
|
if (user && user.checkPassword(password)) {
|
|
@@ -1202,25 +1202,21 @@ const loginCallback = (req, res) => () => {
|
|
|
1202
1202
|
}
|
|
1203
1203
|
};
|
|
1204
1204
|
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
);
|
|
1221
|
-
}
|
|
1222
|
-
})
|
|
1223
|
-
);
|
|
1205
|
+
const callbackFn = async (req, res, next) => {
|
|
1206
|
+
const { method } = req.params;
|
|
1207
|
+
const auth = getState().auth_methods[method];
|
|
1208
|
+
if (auth) {
|
|
1209
|
+
passport.authenticate(method, { failureRedirect: "/auth/login" })(
|
|
1210
|
+
req,
|
|
1211
|
+
res,
|
|
1212
|
+
loginCallback(req, res)
|
|
1213
|
+
);
|
|
1214
|
+
}
|
|
1215
|
+
};
|
|
1216
|
+
|
|
1217
|
+
router.get("/callback/:method", error_catcher(callbackFn));
|
|
1218
|
+
|
|
1219
|
+
router.post("/callback/:method", error_catcher(callbackFn));
|
|
1224
1220
|
|
|
1225
1221
|
/**
|
|
1226
1222
|
* @param {object} req
|
package/errors.js
CHANGED
|
@@ -46,10 +46,10 @@ module.exports =
|
|
|
46
46
|
? text(err.message)
|
|
47
47
|
: req.__("An error occurred")
|
|
48
48
|
);
|
|
49
|
-
} else
|
|
50
|
-
res
|
|
51
|
-
|
|
52
|
-
.sendWrap(
|
|
49
|
+
} else {
|
|
50
|
+
const _res = res.status(code);
|
|
51
|
+
if (_res.sendWrap)
|
|
52
|
+
_res.sendWrap(
|
|
53
53
|
req.__(headline),
|
|
54
54
|
devmode ? pre(text(err.stack)) : h3(req.__(headline)),
|
|
55
55
|
role === 1 && !devmode ? pre(text(err.message)) : "",
|
|
@@ -61,4 +61,15 @@ module.exports =
|
|
|
61
61
|
)
|
|
62
62
|
: ""
|
|
63
63
|
);
|
|
64
|
+
else
|
|
65
|
+
_res.send(
|
|
66
|
+
`<h2>${
|
|
67
|
+
err.message
|
|
68
|
+
? err.message
|
|
69
|
+
: req.__
|
|
70
|
+
? req.__("An error occurred")
|
|
71
|
+
: "An error occurred"
|
|
72
|
+
}</h2>`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
64
75
|
};
|
package/help/Actions.tmd
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
The action chosen will determine what happens when the trigger occurs.
|
|
2
|
+
After you have chosen an action, there will be further configuration of that action.
|
|
3
|
+
|
|
4
|
+
The available actions, some of which are only accessible when the trigger is
|
|
5
|
+
table-related, are:
|
|
6
|
+
|
|
7
|
+
{{# for (const [name, action] of Object.entries(scState.actions)) { }}
|
|
8
|
+
* `{{name}}`: {{action.description||""}}
|
|
9
|
+
{{# } }}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{{# const srcTable = Table.findOne({ name: query.srcTable }) }}
|
|
2
|
+
{{# const view = View.findOne({ name: query.view_name }) }}
|
|
3
|
+
{{# const destTable = Table.findOne({ id: view.table_id }) }}
|
|
4
|
+
|
|
5
|
+
The extra state formula when embedding or linking to a view is used
|
|
6
|
+
to influence the row that will be included in that views display,
|
|
7
|
+
in addition to any selection based on a chosen relationship.
|
|
8
|
+
|
|
9
|
+
The formula takes the form of a JavaScript object where the object
|
|
10
|
+
keys are fields (variable names) on the table of the embedded view,
|
|
11
|
+
and the values are the JavaScript expressions for those fields which set the criteria
|
|
12
|
+
for the rows to be included. For example, if you would like to display
|
|
13
|
+
all rows where the Active field is true you would use this as the extra
|
|
14
|
+
state formula: `{ active: true }`. This is an object where the only
|
|
15
|
+
key is `active` and its value is `true`. You can have more than one
|
|
16
|
+
key, for example:
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
{ active: true, age: {gt: 21}, manager: user.id }
|
|
20
|
+
```
|
|
21
|
+
(See below for using the `user` variable and greater-than filters)
|
|
22
|
+
|
|
23
|
+
The keys you can use are the variable names on the {{destTable.name}} table:
|
|
24
|
+
|
|
25
|
+
| Field | Variable name | Type |
|
|
26
|
+
| ----- | ------------- | ---- |
|
|
27
|
+
{{# for (const field of destTable.fields) { }} | {{ field.label }} | `{{ field.name }}` | {{ field.pretty_type }} |
|
|
28
|
+
{{# } }}
|
|
29
|
+
|
|
30
|
+
In the value expressions (to the right of the colon) you can use
|
|
31
|
+
literals (e.g. 17 for numbers or true for booleans; string literals
|
|
32
|
+
must be enclosed in quotes (single, double or
|
|
33
|
+
[backtick](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals))).
|
|
34
|
+
You can also as the value use a more complex JavaScript expression.
|
|
35
|
+
|
|
36
|
+
You can refer to the logged in user using the variable name user.
|
|
37
|
+
This is an object and you must use the dot to access user fields,
|
|
38
|
+
e.g. user.id for the logged in users id. A very common scenario
|
|
39
|
+
is to restrict rows to show to those that belong to the current user.
|
|
40
|
+
If you have a key to users field called `theUser`, you would use this
|
|
41
|
+
as the extra state formula: `{theUser: user.id}`
|
|
42
|
+
|
|
43
|
+
The user object has other fields than the primary key identifier.
|
|
44
|
+
For your login session the user object is:
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
{{JSON.stringify(user, null, 2)}}
|
|
48
|
+
```
|
|
49
|
+
{{# const userkey = oneOf(Object.keys(user)) }}
|
|
50
|
+
|
|
51
|
+
For example to use the `{{userkey}}` field on the user object, write `user.{{userkey}}`
|
|
52
|
+
{{# if(srcTable) { }}
|
|
53
|
+
You can also use variables from the currently displayed row in the value expressions,
|
|
54
|
+
in this case the table {{srcTable.name}}:
|
|
55
|
+
|
|
56
|
+
| Field | Variable name | Type |
|
|
57
|
+
| ----- | ------------- | ---- |
|
|
58
|
+
{{# for (const field of srcTable.fields) { }} | {{ field.label }} | `{{ field.name }}` | {{ field.pretty_type }} |
|
|
59
|
+
{{# } }}
|
|
60
|
+
|
|
61
|
+
{{# } }}
|
|
62
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{{# const table = Table.findOne({ name: query.table_name }) }}
|
|
2
|
+
{{# const mode = query.mode }}
|
|
3
|
+
{{# const field = table.getField(query.field_name) }}
|
|
4
|
+
{{# const fvsObj = field.type === "Key" ? scState.keyFieldviews : field.type === "File" ? scState.fileviews : field.type?.fieldviews || {} }}
|
|
5
|
+
{{# const allFVs = Object.entries(fvsObj) }}
|
|
6
|
+
{{# const myFVs = mode==='filter' ? allFVs.filter(([nm,fv])=>fv.isFilter || fv.isEdit) : mode==='edit' ? allFVs.filter(([nm,fv])=>!fv.isFilter || fv.isEdit) : mode==='show'||mode==='list' ? allFVs.filter(([nm,fv])=>!fv.isFilter && !fv.isEdit) : allFVs }}
|
|
7
|
+
|
|
8
|
+
The field view determines how a field value is shown to the user and how the
|
|
9
|
+
user can interact with (or edit, in Edit views) that value. The available
|
|
10
|
+
field views are determined by the field type and the view pattern.
|
|
11
|
+
|
|
12
|
+
Some field views have configuration options which can be used to further
|
|
13
|
+
customize how the value is displayed or how the user interacts with it.
|
|
14
|
+
This configuration options will appear in the field settings below the
|
|
15
|
+
field view choice.
|
|
16
|
+
|
|
17
|
+
In the table {{ table.name }} the field {{ field.label }} of type
|
|
18
|
+
{{ field.pretty_type }} has the following available field views:
|
|
19
|
+
|
|
20
|
+
{{# for (const [name, fv] of myFVs) { }}
|
|
21
|
+
* {{name}}{{mode==='edit' && !fv.isEdit? ` [Not editable]`:''}}: {{fv.description||undefined}}
|
|
22
|
+
{{# } }}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
Here you can enter the code that needs to run when this trigger occurs.
|
|
2
|
+
The action can manipulate rows in the database, manipulate files, interact
|
|
3
|
+
with remote APIs, or issue directives for the user's display.
|
|
4
|
+
|
|
5
|
+
Your code can use await at the top level, and should do so whenever calling
|
|
6
|
+
database queries or other aynchronous code (see example below)
|
|
7
|
+
|
|
8
|
+
The variable `table` is the associated table (if any; note lowercase). If you want to access a different table,
|
|
9
|
+
use the `Table` variable (note uppercase) to access the Table class of tables (see
|
|
10
|
+
[documentation for Table class](https://saltcorn.github.io/saltcorn/classes/_saltcorn_data.models.Table-1.html))
|
|
11
|
+
|
|
12
|
+
Example:
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
await table.insertRow({name: "Alex", age: 43})
|
|
16
|
+
const otherTable = Table.findOne({name: "Orders"})
|
|
17
|
+
await otherTable.deleteRows({id: order})
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
In addition to `table` and `Table`, you can use other functions/variables:
|
|
21
|
+
|
|
22
|
+
#### `console`
|
|
23
|
+
|
|
24
|
+
Use this to print to the terminal.
|
|
25
|
+
|
|
26
|
+
Example: `console.log("Hello world")`
|
|
27
|
+
|
|
28
|
+
#### `Actions`
|
|
29
|
+
|
|
30
|
+
Use `Actions.{ACTION NAME}` to run an action.
|
|
31
|
+
|
|
32
|
+
Your available action names are: {{ Object.keys(scState.actions).join(", ") }}
|
|
33
|
+
|
|
34
|
+
Example:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
await Actions.set_user_language({language: "fr"})
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
#### `sleep`
|
|
41
|
+
|
|
42
|
+
A small utility function to sleep for certain number of milliseconds. Use this with await
|
|
43
|
+
|
|
44
|
+
Example: `await sleep(1000)`
|
|
45
|
+
|
|
46
|
+
#### `require`
|
|
47
|
+
|
|
48
|
+
Use require to access NPM packages listed under your [Development settings](/admin/dev)
|
|
49
|
+
|
|
50
|
+
Example: `const _ = require("underscore")`
|
|
51
|
+
|
|
52
|
+
#### `fetch` and `fetchJSON`
|
|
53
|
+
|
|
54
|
+
Use these to make HTTP API calls. `fetch` is the standard JavaScript `fetch` (provided by
|
|
55
|
+
[node-fetch](https://www.npmjs.com/package/node-fetch#common-usage)). `fetchJSON` performs a fetch
|
|
56
|
+
and then reads its reponse to JSON
|
|
57
|
+
|
|
58
|
+
Example:
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
const response = await fetch('https://api.github.com/users/github');
|
|
62
|
+
const data = await response.json();
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
which is the same as
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
const data = await fetchJSON('https://api.github.com/users/github');
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Return directives
|
|
72
|
+
|
|
73
|
+
Your code can with its return value give directives to the current page.
|
|
74
|
+
Valid return values are:
|
|
75
|
+
|
|
76
|
+
#### `notify`
|
|
77
|
+
|
|
78
|
+
Send a pop-up notification indicating success to the user
|
|
79
|
+
|
|
80
|
+
Example: `return { notify: "Order completed!" }`
|
|
81
|
+
|
|
82
|
+
#### `error`
|
|
83
|
+
|
|
84
|
+
Send a pop-up notification indicating error to the user.
|
|
85
|
+
|
|
86
|
+
Example: `return { error: "Invalid command!" }`
|
|
87
|
+
|
|
88
|
+
If this is triggered by an Edit view with the SubmitWithAjax,
|
|
89
|
+
halt navigation and stay on page. This can be used for complex validation logic,
|
|
90
|
+
When added as an Insert or Update trigger. If you delete the inserted row, You
|
|
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
|
+
```
|
|
104
|
+
|
|
105
|
+
#### `goto`
|
|
106
|
+
|
|
107
|
+
Navigate to a different URL:
|
|
108
|
+
|
|
109
|
+
Example: `return { goto: "https://saltcorn.com" }`
|
|
110
|
+
|
|
111
|
+
Add `target: "_blank"` to open in a new tab.
|
|
112
|
+
|
|
113
|
+
#### `reload_page`
|
|
114
|
+
|
|
115
|
+
Request a page reload with the existing URL.
|
|
116
|
+
|
|
117
|
+
Example: `return { reload_page: true }`
|
|
118
|
+
|
|
119
|
+
#### `popup`
|
|
120
|
+
|
|
121
|
+
Open a URL in a popup:
|
|
122
|
+
|
|
123
|
+
Example:
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
return { popup: `/view/Orders?id=${parent}` }
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
#### `download`
|
|
130
|
+
|
|
131
|
+
Download a file to the client browser.
|
|
132
|
+
|
|
133
|
+
Example:
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
return { download: {
|
|
137
|
+
mimetype: "text/csv",
|
|
138
|
+
blob: filecontents
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
#### `set_fields`
|
|
144
|
+
|
|
145
|
+
If triggered from an edit view, set fields dynamically in the form. The
|
|
146
|
+
value should be an object with keys that are field variable names.
|
|
147
|
+
|
|
148
|
+
Example:
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
return { set_fields: {
|
|
152
|
+
zidentifier: `${name.toUpperCase()}-${id}`
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
#### `eval_js`
|
|
158
|
+
|
|
159
|
+
Execute JavaScript in the browser.
|
|
160
|
+
|
|
161
|
+
Example: `return { eval_js: 'alert("Hello world")' }`
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
{{# const table = Table.findOne({ name: query.table }) }}
|
|
2
|
+
|
|
1
3
|
The constraint formula should be a JavaScript expression which evaluates to a
|
|
2
4
|
boolean (true or false). If the formula evaluates to false then the row is not
|
|
3
5
|
valid and will not be accepted in the table. If you are editing an existing row
|
|
@@ -6,14 +8,14 @@ to the user.
|
|
|
6
8
|
|
|
7
9
|
Some examples:
|
|
8
10
|
|
|
9
|
-
{{# for (const fml of table.getFormulaExamples("Bool")) { }} * `{{
|
|
11
|
+
{{# for (const fml of table.getFormulaExamples("Bool")) { }} * `{{ fml}}`
|
|
10
12
|
{{# } }}
|
|
11
13
|
|
|
12
|
-
This formula can use any of the fields in table {{
|
|
14
|
+
This formula can use any of the fields in table {{ table.name }} as variables:
|
|
13
15
|
|
|
14
16
|
| Field | Variable name | Type |
|
|
15
17
|
| ----- | ------------- | ---- |
|
|
16
|
-
{{# for (const field of table.fields) { }} | {{
|
|
18
|
+
{{# for (const field of table.fields) { }} | {{ field.label }} | `{{ field.name }}` | {{ field.pretty_type }} |
|
|
17
19
|
{{# } }}
|
|
18
20
|
|
|
19
21
|
|
|
@@ -32,7 +34,7 @@ The first-order join fields you can use in the constraint formula are:
|
|
|
32
34
|
|
|
33
35
|
{{# for (const field of table.fields.filter(f=>f.is_fkey && f.reftable_name)) { }}
|
|
34
36
|
{{# const reftable = Table.findOne( field.reftable_name); }}
|
|
35
|
-
* Through {{
|
|
37
|
+
* Through {{field.label}} key field: {{ reftable.fields.map(jf=>`\`${field.name}.${jf.name}\``).join(", ") }}
|
|
36
38
|
|
|
37
39
|
|
|
38
40
|
{{# } }}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
The view pattern is a fundamental concept in Saltcorn that determines how
|
|
2
|
+
the view will relate to the database table and the overall behavior of the
|
|
3
|
+
view. A view will always follow one specific view pattern, and will require
|
|
4
|
+
further configuration according to which view pattern is chosen. This
|
|
5
|
+
configuration could be as simple as making a few choices, or it could be as
|
|
6
|
+
complicated as specifying a layout using drag and drop. In addition most view
|
|
7
|
+
patterns require choosing a single table, which it can use as a source or
|
|
8
|
+
destination of data, although in many cases data from other tables can be
|
|
9
|
+
brought in using table relationships.
|
|
10
|
+
|
|
11
|
+
There are fundamental view patterns in Saltcorn that you will use in
|
|
12
|
+
every application:
|
|
13
|
+
|
|
14
|
+
* A List view shows a tabular grid of multiple rows in a table. You can define
|
|
15
|
+
its columns which can be data from this or related tables, action buttons or
|
|
16
|
+
links to other views.
|
|
17
|
+
|
|
18
|
+
* An Edit view is a form that can be used to create a new row or edit an existing
|
|
19
|
+
row in a table.
|
|
20
|
+
|
|
21
|
+
* A Show view shows a single existing row in the table. The row must be specified
|
|
22
|
+
either by linking or embedding from another view or page.
|
|
23
|
+
|
|
24
|
+
* A Filter view does not display any data from the table but can be used to set
|
|
25
|
+
up user interface elements that determine which rows are shown in other views
|
|
26
|
+
on the same page.
|
|
27
|
+
|
|
28
|
+
* A Feed view is configured by another view which shows a single row and will
|
|
29
|
+
repeat this view for all available rows.
|
|
30
|
+
|
|
31
|
+
The additional view patterns available in your system are:
|
|
32
|
+
{{# const vts = Object.values(scState.viewtemplates).filter(vt=>!["List","Show","Edit","Filter","Feed",].includes(vt.name))}}
|
|
33
|
+
{{# for (const vt of vts) { }}
|
|
34
|
+
* {{ vt.name }}{{vt.description ? `: ${vt.description}` : ""}}
|
|
35
|
+
{{# } }}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{{# const srcTable = Table.findOne({ name: query.table_name }) }}
|
|
2
|
+
{{# const field = srcTable.getField(query.field_name) }}
|
|
3
|
+
{{# const refTable = Table.findOne(field.reftable_name) }}
|
|
4
|
+
|
|
5
|
+
The where formula allows you to restrict the options provided. This can be
|
|
6
|
+
done according to a static criterion, all according to a dynamic criterion
|
|
7
|
+
that depends on the value of another form field.
|
|
8
|
+
|
|
9
|
+
The formula should take the form of a JavaScript boolean expression such as
|
|
10
|
+
`a === b` or `x>y && w==1` etc.
|
|
11
|
+
|
|
12
|
+
In this expression you can use variables from the target table from which
|
|
13
|
+
you are selecting values, here {{ refTable.name }}. These can accessed by
|
|
14
|
+
their variable nmame. The fields on the table {{ refTable.name }} are:
|
|
15
|
+
|
|
16
|
+
| Field | Variable name | Type |
|
|
17
|
+
| ----- | ------------- | ---- |
|
|
18
|
+
{{# for (const field of refTable.fields) { }} | {{ field.label }} | `{{ field.name }}` | {{ field.pretty_type }} |
|
|
19
|
+
{{# } }}
|
|
20
|
+
|
|
21
|
+
You can also use variables from the current form. In that case the options will
|
|
22
|
+
respond dynamically to the state of the form fields. These form variables must be proceeded by
|
|
23
|
+
a `$`:
|
|
24
|
+
|
|
25
|
+
| Field | Variable name | Type |
|
|
26
|
+
| ----- | ------------- | ---- |
|
|
27
|
+
{{# for (const field of srcTable.fields) { }} | {{ field.label }} | `${{ field.name }}` | {{ field.pretty_type }} |
|
|
28
|
+
{{# } }}
|
|
29
|
+
|
|
30
|
+
You can mix the two types of variables, as in `a == $b`
|
package/help/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const Table = require("@saltcorn/data/models/table");
|
|
2
|
+
const View = require("@saltcorn/data/models/view");
|
|
2
3
|
const File = require("@saltcorn/data/models/file");
|
|
3
4
|
const _ = require("underscore");
|
|
4
5
|
const fs = require("fs").promises;
|
|
@@ -6,10 +7,12 @@ const MarkdownIt = require("markdown-it"),
|
|
|
6
7
|
md = new MarkdownIt();
|
|
7
8
|
|
|
8
9
|
const { pre } = require("@saltcorn/markup/tags");
|
|
9
|
-
|
|
10
|
+
const path = require("path");
|
|
11
|
+
const { getState } = require("@saltcorn/data/db/state");
|
|
12
|
+
const { oneOf } = require("@saltcorn/types/generators");
|
|
10
13
|
const get_md_file = async (topic) => {
|
|
11
14
|
try {
|
|
12
|
-
const fp =
|
|
15
|
+
const fp = path.join(__dirname, `${File.normalise(topic)}.tmd`);
|
|
13
16
|
const fileBuf = await fs.readFile(fp);
|
|
14
17
|
return fileBuf.toString();
|
|
15
18
|
} catch (e) {
|
|
@@ -17,20 +20,26 @@ const get_md_file = async (topic) => {
|
|
|
17
20
|
}
|
|
18
21
|
};
|
|
19
22
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
interpolate: /\{\{=(.+?)\}\}/g,
|
|
23
|
+
md.renderer.rules.table_open = function (tokens, idx) {
|
|
24
|
+
return '<table class="help-md">';
|
|
23
25
|
};
|
|
24
26
|
|
|
25
27
|
const get_help_markup = async (topic, query, req) => {
|
|
26
28
|
try {
|
|
27
|
-
const context = {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
const context = {
|
|
30
|
+
user: req.user,
|
|
31
|
+
Table,
|
|
32
|
+
View,
|
|
33
|
+
scState: getState(),
|
|
34
|
+
query,
|
|
35
|
+
oneOf,
|
|
36
|
+
};
|
|
31
37
|
const mdTemplate = await get_md_file(topic);
|
|
32
38
|
if (!mdTemplate) return { markup: "Topic not found" };
|
|
33
|
-
const template = _.template(mdTemplate
|
|
39
|
+
const template = _.template(mdTemplate, {
|
|
40
|
+
evaluate: /\{\{#(.+?)\}\}/g,
|
|
41
|
+
interpolate: /\{\{([^#].+?)\}\}/g,
|
|
42
|
+
});
|
|
34
43
|
const mdTopic = template(context);
|
|
35
44
|
const markup = md.render(mdTopic);
|
|
36
45
|
return { 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
|