@saltcorn/server 0.8.5-beta.3 → 0.8.5-beta.5
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 +23 -2
- package/auth/testhelp.js +15 -0
- package/locales/da.json +3 -1
- package/locales/en.json +26 -1
- package/locales/pl.json +17 -1
- package/markup/admin.js +15 -6
- package/package.json +8 -8
- package/public/saltcorn-common.js +30 -1
- package/public/saltcorn.css +27 -0
- package/public/saltcorn.js +3 -0
- package/public/serviceworker.js +1 -0
- package/public/vis-network.min.js +49 -0
- package/routes/admin.js +110 -219
- package/routes/api.js +66 -10
- package/routes/common_lists.js +1 -0
- package/routes/fields.js +11 -1
- package/routes/index.js +2 -0
- package/routes/list.js +8 -5
- package/routes/notifications.js +136 -0
- package/routes/plugins.js +47 -16
- package/routes/tables.js +160 -23
- package/routes/tenant.js +4 -0
- package/routes/utils.js +64 -1
- package/tests/api.test.js +2 -2
- package/tests/crud.test.js +53 -0
- package/tests/plugins.test.js +1 -1
- package/tests/table.test.js +17 -0
- package/wrapper.js +40 -3
package/routes/list.js
CHANGED
|
@@ -241,13 +241,16 @@ router.get(
|
|
|
241
241
|
for (const f of fields) {
|
|
242
242
|
if (f.type === "File") f.attributes = { select_file_where: {} };
|
|
243
243
|
await f.fill_fkey_options();
|
|
244
|
+
|
|
245
|
+
if (f.type === "File") {
|
|
246
|
+
//add existing values in folders
|
|
247
|
+
const dvs = await f.distinct_values();
|
|
248
|
+
dvs.forEach((dv) => {
|
|
249
|
+
if (dv?.value?.includes("/")) f.options.push(dv);
|
|
250
|
+
});
|
|
251
|
+
}
|
|
244
252
|
}
|
|
245
253
|
|
|
246
|
-
//console.log(fields);
|
|
247
|
-
// todo remove keyfields - unused
|
|
248
|
-
const keyfields = fields
|
|
249
|
-
.filter((f) => f.type === "Key" || f.type === "File")
|
|
250
|
-
.map((f) => ({ name: f.name, type: f.reftype }));
|
|
251
254
|
const jsfields = arrangeIdFirst(fields).map((f) =>
|
|
252
255
|
typeToGridType(f.type, f)
|
|
253
256
|
);
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @category server
|
|
3
|
+
* @module routes/notifications
|
|
4
|
+
* @subcategory routes
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const Router = require("express-promise-router");
|
|
8
|
+
const { isAdmin, setTenant, error_catcher, loggedIn } = require("./utils.js");
|
|
9
|
+
const Notification = require("@saltcorn/data/models/notification");
|
|
10
|
+
const { div, a, i, text, h5, p, span } = require("@saltcorn/markup/tags");
|
|
11
|
+
const moment = require("moment");
|
|
12
|
+
const { getState } = require("@saltcorn/data/db/state");
|
|
13
|
+
const Form = require("@saltcorn/data/models/form");
|
|
14
|
+
const User = require("@saltcorn/data/models/user");
|
|
15
|
+
const { renderForm } = require("@saltcorn/markup");
|
|
16
|
+
|
|
17
|
+
const router = new Router();
|
|
18
|
+
module.exports = router;
|
|
19
|
+
|
|
20
|
+
const notificationSettingsForm = () =>
|
|
21
|
+
new Form({
|
|
22
|
+
action: `/notifications/settings`,
|
|
23
|
+
noSubmitButton: true,
|
|
24
|
+
onChange: `saveAndContinue(this)`,
|
|
25
|
+
labelCols: 4,
|
|
26
|
+
fields: [{ name: "notify_email", label: "Email", type: "Bool" }],
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
router.get(
|
|
30
|
+
"/",
|
|
31
|
+
loggedIn,
|
|
32
|
+
error_catcher(async (req, res) => {
|
|
33
|
+
const nots = await Notification.find(
|
|
34
|
+
{ user_id: req.user.id },
|
|
35
|
+
{ orderBy: "id", orderDesc: true, limit: 20 }
|
|
36
|
+
);
|
|
37
|
+
await Notification.mark_as_read({
|
|
38
|
+
id: { in: nots.filter((n) => !n.read).map((n) => n.id) },
|
|
39
|
+
});
|
|
40
|
+
const notifyCards = nots.length
|
|
41
|
+
? nots.map((not) => ({
|
|
42
|
+
type: "card",
|
|
43
|
+
class: [!not.read && "unread-notify"],
|
|
44
|
+
contents: [
|
|
45
|
+
div(
|
|
46
|
+
{ class: "d-flex" },
|
|
47
|
+
span({ class: "fw-bold" }, not.title),
|
|
48
|
+
span({ class: "ms-2 text-muted" }, moment(not.created).fromNow())
|
|
49
|
+
),
|
|
50
|
+
not.body && p(not.body),
|
|
51
|
+
not.link && a({ href: not.link }, "Link"),
|
|
52
|
+
],
|
|
53
|
+
}))
|
|
54
|
+
: [
|
|
55
|
+
{
|
|
56
|
+
type: "card",
|
|
57
|
+
contents: [h5(req.__("No notifications"))],
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
res.sendWrap(req.__("Notifications"), {
|
|
61
|
+
above: [
|
|
62
|
+
{
|
|
63
|
+
type: "breadcrumbs",
|
|
64
|
+
crumbs: [{ text: req.__("Notifications") }],
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
widths: [4, 8],
|
|
68
|
+
breakpoint: "md",
|
|
69
|
+
besides: [
|
|
70
|
+
{
|
|
71
|
+
type: "card",
|
|
72
|
+
contents: [
|
|
73
|
+
"Receive notifications by:",
|
|
74
|
+
renderForm(notificationSettingsForm(), req.csrfToken()),
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
{ above: notifyCards },
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
});
|
|
82
|
+
})
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
router.get(
|
|
86
|
+
"/count-unread",
|
|
87
|
+
loggedIn,
|
|
88
|
+
error_catcher(async (req, res) => {
|
|
89
|
+
const num_unread = await Notification.count({
|
|
90
|
+
user_id: req.user.id,
|
|
91
|
+
read: false,
|
|
92
|
+
});
|
|
93
|
+
res.set("Cache-Control", "public, max-age=60"); // 1 minute
|
|
94
|
+
res.json({ success: num_unread });
|
|
95
|
+
})
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
router.post(
|
|
99
|
+
"/settings",
|
|
100
|
+
loggedIn,
|
|
101
|
+
error_catcher(async (req, res) => {
|
|
102
|
+
const user = await User.findOne({ id: req.user.id });
|
|
103
|
+
const form = notificationSettingsForm();
|
|
104
|
+
form.validate(req.body);
|
|
105
|
+
const _attributes = { ...user._attributes, ...form.values };
|
|
106
|
+
await user.update({ _attributes });
|
|
107
|
+
res.json({ success: "ok" });
|
|
108
|
+
})
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
router.get(
|
|
112
|
+
"/manifest.json",
|
|
113
|
+
error_catcher(async (req, res) => {
|
|
114
|
+
const state = getState();
|
|
115
|
+
const manifest = {
|
|
116
|
+
name: state.getConfig("site_name"),
|
|
117
|
+
start_url: state.getConfig("base_url") || "/",
|
|
118
|
+
display: state.getConfig("pwa_display", "browser"),
|
|
119
|
+
};
|
|
120
|
+
const site_logo = state.getConfig("site_logo_id");
|
|
121
|
+
if (site_logo)
|
|
122
|
+
manifest.icons = [
|
|
123
|
+
{
|
|
124
|
+
src: `/files/serve/${site_logo}`,
|
|
125
|
+
},
|
|
126
|
+
];
|
|
127
|
+
if (state.getConfig("pwa_set_colors", false)) {
|
|
128
|
+
manifest.theme_color = state.getConfig("pwa_theme_color", "black");
|
|
129
|
+
manifest.background_color = state.getConfig(
|
|
130
|
+
"pwa_background_color",
|
|
131
|
+
"red"
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
res.json(manifest);
|
|
135
|
+
})
|
|
136
|
+
);
|
package/routes/plugins.js
CHANGED
|
@@ -14,11 +14,18 @@ const {
|
|
|
14
14
|
post_btn,
|
|
15
15
|
post_delete_btn,
|
|
16
16
|
} = require("@saltcorn/markup");
|
|
17
|
-
const {
|
|
17
|
+
const {
|
|
18
|
+
getState,
|
|
19
|
+
restart_tenant,
|
|
20
|
+
getRootState,
|
|
21
|
+
} = require("@saltcorn/data/db/state");
|
|
18
22
|
const Form = require("@saltcorn/data/models/form");
|
|
19
23
|
const Field = require("@saltcorn/data/models/field");
|
|
20
24
|
const Plugin = require("@saltcorn/data/models/plugin");
|
|
21
25
|
const { fetch_available_packs } = require("@saltcorn/admin-models/models/pack");
|
|
26
|
+
const {
|
|
27
|
+
upgrade_all_tenants_plugins,
|
|
28
|
+
} = require("@saltcorn/admin-models/models/tenant");
|
|
22
29
|
const { getConfig, setConfig } = require("@saltcorn/data/models/config");
|
|
23
30
|
const db = require("@saltcorn/data/db");
|
|
24
31
|
const {
|
|
@@ -150,7 +157,10 @@ const local_has_theme = (name) => {
|
|
|
150
157
|
const get_store_items = async () => {
|
|
151
158
|
const installed_plugins = await Plugin.find({});
|
|
152
159
|
const isRoot = db.getTenantSchema() === db.connectObj.default_schema;
|
|
153
|
-
|
|
160
|
+
const tenants_unsafe_plugins = getRootState().getConfig(
|
|
161
|
+
"tenants_unsafe_plugins",
|
|
162
|
+
false
|
|
163
|
+
);
|
|
154
164
|
const instore = await Plugin.store_plugins_available();
|
|
155
165
|
const packs_available = await fetch_available_packs();
|
|
156
166
|
const packs_installed = getState().getConfig("installed_packs", []);
|
|
@@ -168,7 +178,7 @@ const get_store_items = async () => {
|
|
|
168
178
|
has_auth: plugin.has_auth,
|
|
169
179
|
unsafe: plugin.unsafe,
|
|
170
180
|
}))
|
|
171
|
-
.filter((p) => !p.unsafe || isRoot);
|
|
181
|
+
.filter((p) => !p.unsafe || isRoot || tenants_unsafe_plugins);
|
|
172
182
|
const local_logins = installed_plugins
|
|
173
183
|
.filter((p) => !store_plugin_names.includes(p.name) && p.name !== "base")
|
|
174
184
|
.map((plugin) => ({
|
|
@@ -424,8 +434,12 @@ const filter_items_set = (items, query) => {
|
|
|
424
434
|
* @param {object} req
|
|
425
435
|
* @returns {div}
|
|
426
436
|
*/
|
|
427
|
-
const store_actions_dropdown = (req) =>
|
|
428
|
-
|
|
437
|
+
const store_actions_dropdown = (req) => {
|
|
438
|
+
const tenants_install_git = getRootState().getConfig(
|
|
439
|
+
"tenants_install_git",
|
|
440
|
+
false
|
|
441
|
+
);
|
|
442
|
+
return div(
|
|
429
443
|
{ class: "dropdown" },
|
|
430
444
|
button(
|
|
431
445
|
{
|
|
@@ -460,7 +474,8 @@ const store_actions_dropdown = (req) =>
|
|
|
460
474
|
'<i class="far fa-arrow-alt-circle-up"></i> ' +
|
|
461
475
|
req.__("Upgrade installed modules")
|
|
462
476
|
),
|
|
463
|
-
db.getTenantSchema() === db.connectObj.default_schema
|
|
477
|
+
(db.getTenantSchema() === db.connectObj.default_schema ||
|
|
478
|
+
tenants_install_git) &&
|
|
464
479
|
a(
|
|
465
480
|
{
|
|
466
481
|
class: "dropdown-item",
|
|
@@ -488,6 +503,7 @@ const store_actions_dropdown = (req) =>
|
|
|
488
503
|
//create pack
|
|
489
504
|
)
|
|
490
505
|
);
|
|
506
|
+
};
|
|
491
507
|
|
|
492
508
|
/**
|
|
493
509
|
* @param {object[]} items
|
|
@@ -926,14 +942,22 @@ router.get(
|
|
|
926
942
|
"/upgrade",
|
|
927
943
|
isAdmin,
|
|
928
944
|
error_catcher(async (req, res) => {
|
|
929
|
-
const
|
|
930
|
-
|
|
931
|
-
await
|
|
945
|
+
const schema = db.getTenantSchema();
|
|
946
|
+
if (schema === db.connectObj.default_schema) {
|
|
947
|
+
await upgrade_all_tenants_plugins((p, f) =>
|
|
948
|
+
load_plugins.loadPlugin(p, f)
|
|
949
|
+
);
|
|
950
|
+
req.flash("success", req.__(`Modules up-to-date. Please restart server`));
|
|
951
|
+
} else {
|
|
952
|
+
const installed_plugins = await Plugin.find({});
|
|
953
|
+
for (const plugin of installed_plugins) {
|
|
954
|
+
await plugin.upgrade_version((p, f) => load_plugins.loadPlugin(p, f));
|
|
955
|
+
}
|
|
956
|
+
req.flash("success", req.__(`Modules up-to-date`));
|
|
957
|
+
await restart_tenant(loadAllPlugins);
|
|
958
|
+
process.send &&
|
|
959
|
+
process.send({ restart_tenant: true, tenant: db.getTenantSchema() });
|
|
932
960
|
}
|
|
933
|
-
req.flash("success", req.__(`Modules up-to-date`));
|
|
934
|
-
await restart_tenant(loadAllPlugins);
|
|
935
|
-
process.send &&
|
|
936
|
-
process.send({ restart_tenant: true, tenant: db.getTenantSchema() });
|
|
937
961
|
res.redirect(`/plugins`);
|
|
938
962
|
})
|
|
939
963
|
);
|
|
@@ -970,7 +994,11 @@ router.post(
|
|
|
970
994
|
error_catcher(async (req, res) => {
|
|
971
995
|
const plugin = new Plugin(req.body);
|
|
972
996
|
const schema = db.getTenantSchema();
|
|
973
|
-
|
|
997
|
+
const tenants_install_git = getRootState().getConfig(
|
|
998
|
+
"tenants_install_git",
|
|
999
|
+
false
|
|
1000
|
+
);
|
|
1001
|
+
if (schema !== db.connectObj.default_schema && !tenants_install_git) {
|
|
974
1002
|
req.flash(
|
|
975
1003
|
"error",
|
|
976
1004
|
req.__(`Only store modules can be installed on tenant instances`)
|
|
@@ -1039,7 +1067,10 @@ router.post(
|
|
|
1039
1067
|
isAdmin,
|
|
1040
1068
|
error_catcher(async (req, res) => {
|
|
1041
1069
|
const { name } = req.params;
|
|
1042
|
-
|
|
1070
|
+
const tenants_unsafe_plugins = getRootState().getConfig(
|
|
1071
|
+
"tenants_unsafe_plugins",
|
|
1072
|
+
false
|
|
1073
|
+
);
|
|
1043
1074
|
const plugin = await Plugin.store_by_name(decodeURIComponent(name));
|
|
1044
1075
|
if (!plugin) {
|
|
1045
1076
|
req.flash(
|
|
@@ -1050,7 +1081,7 @@ router.post(
|
|
|
1050
1081
|
return;
|
|
1051
1082
|
}
|
|
1052
1083
|
const isRoot = db.getTenantSchema() === db.connectObj.default_schema;
|
|
1053
|
-
if (!isRoot && plugin.unsafe) {
|
|
1084
|
+
if (!isRoot && plugin.unsafe && !tenants_unsafe_plugins) {
|
|
1054
1085
|
req.flash(
|
|
1055
1086
|
"error",
|
|
1056
1087
|
req.__("Cannot install unsafe modules on subdomain tenants")
|
package/routes/tables.js
CHANGED
|
@@ -51,6 +51,7 @@ const {
|
|
|
51
51
|
const { getState } = require("@saltcorn/data/db/state");
|
|
52
52
|
const { cardHeaderTabs } = require("@saltcorn/markup/layout_utils");
|
|
53
53
|
const { tablesList } = require("./common_lists");
|
|
54
|
+
const { InvalidConfiguration } = require("@saltcorn/data/utils");
|
|
54
55
|
|
|
55
56
|
/**
|
|
56
57
|
* @type {object}
|
|
@@ -180,6 +181,7 @@ router.get(
|
|
|
180
181
|
"/new/",
|
|
181
182
|
isAdmin,
|
|
182
183
|
error_catcher(async (req, res) => {
|
|
184
|
+
const table_provider_names = Object.keys(getState().table_providers);
|
|
183
185
|
res.sendWrap(req.__(`New table`), {
|
|
184
186
|
above: [
|
|
185
187
|
{
|
|
@@ -203,6 +205,20 @@ router.get(
|
|
|
203
205
|
input_type: "text",
|
|
204
206
|
required: true,
|
|
205
207
|
},
|
|
208
|
+
...(table_provider_names.length
|
|
209
|
+
? [
|
|
210
|
+
{
|
|
211
|
+
label: req.__("Table provider"),
|
|
212
|
+
name: "provider_name",
|
|
213
|
+
input_type: "select",
|
|
214
|
+
options: [
|
|
215
|
+
req.__("Database table"),
|
|
216
|
+
...table_provider_names,
|
|
217
|
+
],
|
|
218
|
+
required: true,
|
|
219
|
+
},
|
|
220
|
+
]
|
|
221
|
+
: []),
|
|
206
222
|
],
|
|
207
223
|
}),
|
|
208
224
|
req.csrfToken()
|
|
@@ -439,8 +455,7 @@ router.get(
|
|
|
439
455
|
title: req.__("Tables"),
|
|
440
456
|
headers: [
|
|
441
457
|
{
|
|
442
|
-
script:
|
|
443
|
-
"https://unpkg.com/vis-network@9.1.2/standalone/umd/vis-network.min.js",
|
|
458
|
+
script: `/static_assets/${db.connectObj.version_tag}/vis-network.min.js`,
|
|
444
459
|
},
|
|
445
460
|
],
|
|
446
461
|
},
|
|
@@ -549,9 +564,9 @@ router.get(
|
|
|
549
564
|
const { idorname } = req.params;
|
|
550
565
|
let id = parseInt(idorname);
|
|
551
566
|
let table;
|
|
552
|
-
if (id) table =
|
|
567
|
+
if (id) table = Table.findOne({ id });
|
|
553
568
|
else {
|
|
554
|
-
table =
|
|
569
|
+
table = Table.findOne({ name: idorname });
|
|
555
570
|
}
|
|
556
571
|
|
|
557
572
|
if (!table) {
|
|
@@ -646,7 +661,7 @@ router.get(
|
|
|
646
661
|
var viewCard;
|
|
647
662
|
if (fields.length > 0) {
|
|
648
663
|
const views = await View.find(
|
|
649
|
-
table.
|
|
664
|
+
table.id ? { table_id: table.id } : { exttable_name: table.name }
|
|
650
665
|
);
|
|
651
666
|
var viewCardContents;
|
|
652
667
|
if (views.length > 0) {
|
|
@@ -727,6 +742,16 @@ router.get(
|
|
|
727
742
|
: req.__("Edit")
|
|
728
743
|
)
|
|
729
744
|
),
|
|
745
|
+
table.provider_name &&
|
|
746
|
+
div(
|
|
747
|
+
{ class: "mx-auto" },
|
|
748
|
+
a(
|
|
749
|
+
{ href: `/table/provider-cfg/${table.id}` },
|
|
750
|
+
i({ class: "fas fa-2x fa-tools" }),
|
|
751
|
+
"<br/>",
|
|
752
|
+
req.__("Configure provider")
|
|
753
|
+
)
|
|
754
|
+
),
|
|
730
755
|
div(
|
|
731
756
|
{ class: "mx-auto" },
|
|
732
757
|
a(
|
|
@@ -821,7 +846,13 @@ router.get(
|
|
|
821
846
|
type: "breadcrumbs",
|
|
822
847
|
crumbs: [
|
|
823
848
|
{ text: req.__("Tables"), href: "/table" },
|
|
824
|
-
{
|
|
849
|
+
{
|
|
850
|
+
text: span(
|
|
851
|
+
{ class: "fw-bold text-body" },
|
|
852
|
+
table.name,
|
|
853
|
+
table.provider_name && ` (${table.provider_name} provider)`
|
|
854
|
+
),
|
|
855
|
+
},
|
|
825
856
|
],
|
|
826
857
|
},
|
|
827
858
|
{
|
|
@@ -876,7 +907,14 @@ router.post(
|
|
|
876
907
|
} else if (db.sqlsanitize(name) === "") {
|
|
877
908
|
req.flash("error", req.__(`Invalid table name %s`, name));
|
|
878
909
|
res.redirect(`/table/new`);
|
|
910
|
+
} else if (
|
|
911
|
+
rest.provider_name &&
|
|
912
|
+
rest.provider_name !== "Database table"
|
|
913
|
+
) {
|
|
914
|
+
const table = await Table.create(name, rest);
|
|
915
|
+
res.redirect(`/table/provider-cfg/${table.id}`);
|
|
879
916
|
} else {
|
|
917
|
+
delete rest.provider_name;
|
|
880
918
|
const table = await Table.create(name, rest);
|
|
881
919
|
req.flash("success", req.__(`Table %s created`, name));
|
|
882
920
|
res.redirect(`/table/${table.id}`);
|
|
@@ -1005,23 +1043,6 @@ router.post(
|
|
|
1005
1043
|
}
|
|
1006
1044
|
})
|
|
1007
1045
|
);
|
|
1008
|
-
/**
|
|
1009
|
-
* Table badges to show in System Table list views
|
|
1010
|
-
* Currently supports:
|
|
1011
|
-
* - Owned - if ownership_field_id? What is it?
|
|
1012
|
-
* - History - if table has versioning
|
|
1013
|
-
* - External - if this is external table
|
|
1014
|
-
* @param {object} t table object
|
|
1015
|
-
* @param {object} req http request
|
|
1016
|
-
* @returns {string} html string with list of badges
|
|
1017
|
-
*/
|
|
1018
|
-
const tableBadges = (t, req) => {
|
|
1019
|
-
let s = "";
|
|
1020
|
-
if (t.ownership_field_id) s += badge("primary", req.__("Owned"));
|
|
1021
|
-
if (t.versioned) s += badge("success", req.__("History"));
|
|
1022
|
-
if (t.external) s += badge("info", req.__("External"));
|
|
1023
|
-
return s;
|
|
1024
|
-
};
|
|
1025
1046
|
|
|
1026
1047
|
/**
|
|
1027
1048
|
* List Views of Tables (GET Handler)
|
|
@@ -1529,3 +1550,119 @@ router.post(
|
|
|
1529
1550
|
res.redirect(`/table/${table.id}`);
|
|
1530
1551
|
})
|
|
1531
1552
|
);
|
|
1553
|
+
|
|
1554
|
+
const respondWorkflow = (table, wf, wfres, req, res) => {
|
|
1555
|
+
const wrap = (contents, noCard, previewURL) => ({
|
|
1556
|
+
above: [
|
|
1557
|
+
{
|
|
1558
|
+
type: "breadcrumbs",
|
|
1559
|
+
crumbs: [
|
|
1560
|
+
{ text: req.__("Tables"), href: "/table" },
|
|
1561
|
+
{ href: `/table/${table.id || table.name}`, text: table.name },
|
|
1562
|
+
{ text: req.__("Configuration") },
|
|
1563
|
+
],
|
|
1564
|
+
},
|
|
1565
|
+
{
|
|
1566
|
+
type: noCard ? "container" : "card",
|
|
1567
|
+
class: !noCard && "mt-0",
|
|
1568
|
+
title: wfres.title,
|
|
1569
|
+
titleAjaxIndicator: true,
|
|
1570
|
+
contents,
|
|
1571
|
+
},
|
|
1572
|
+
],
|
|
1573
|
+
});
|
|
1574
|
+
if (wfres.flash) req.flash(wfres.flash[0], wfres.flash[1]);
|
|
1575
|
+
if (wfres.renderForm)
|
|
1576
|
+
res.sendWrap(
|
|
1577
|
+
{
|
|
1578
|
+
title: req.__(`%s configuration`, table.name),
|
|
1579
|
+
headers: [
|
|
1580
|
+
{
|
|
1581
|
+
script: `/static_assets/${db.connectObj.version_tag}/jquery-menu-editor.min.js`,
|
|
1582
|
+
},
|
|
1583
|
+
{
|
|
1584
|
+
script: `/static_assets/${db.connectObj.version_tag}/iconset-fontawesome5-3-1.min.js`,
|
|
1585
|
+
},
|
|
1586
|
+
{
|
|
1587
|
+
script: `/static_assets/${db.connectObj.version_tag}/bootstrap-iconpicker.js`,
|
|
1588
|
+
},
|
|
1589
|
+
{
|
|
1590
|
+
css: `/static_assets/${db.connectObj.version_tag}/bootstrap-iconpicker.min.css`,
|
|
1591
|
+
},
|
|
1592
|
+
],
|
|
1593
|
+
},
|
|
1594
|
+
wrap(
|
|
1595
|
+
renderForm(wfres.renderForm, req.csrfToken()),
|
|
1596
|
+
false,
|
|
1597
|
+
wfres.previewURL
|
|
1598
|
+
)
|
|
1599
|
+
);
|
|
1600
|
+
else res.redirect(wfres.redirect);
|
|
1601
|
+
};
|
|
1602
|
+
|
|
1603
|
+
const get_provider_workflow = (table, req) => {
|
|
1604
|
+
const provider = getState().table_providers[table.provider_name];
|
|
1605
|
+
if (!provider) {
|
|
1606
|
+
throw new InvalidConfiguration(
|
|
1607
|
+
`Provider not found for rable ${table.name}: table.provider_name`
|
|
1608
|
+
);
|
|
1609
|
+
}
|
|
1610
|
+
const workflow = provider.configuration_workflow(req);
|
|
1611
|
+
workflow.action = `/table/provider-cfg/${table.id}`;
|
|
1612
|
+
const oldOnDone = workflow.onDone || ((c) => c);
|
|
1613
|
+
workflow.onDone = async (ctx) => {
|
|
1614
|
+
const { table_id, ...configuration } = await oldOnDone(ctx);
|
|
1615
|
+
await table.update({ provider_cfg: configuration });
|
|
1616
|
+
|
|
1617
|
+
return {
|
|
1618
|
+
redirect: `/table/${table.id}`,
|
|
1619
|
+
flash: ["success", `Table ${this.name || ""} saved`],
|
|
1620
|
+
};
|
|
1621
|
+
};
|
|
1622
|
+
return workflow;
|
|
1623
|
+
};
|
|
1624
|
+
|
|
1625
|
+
router.get(
|
|
1626
|
+
"/provider-cfg/:id",
|
|
1627
|
+
isAdmin,
|
|
1628
|
+
error_catcher(async (req, res) => {
|
|
1629
|
+
const { id } = req.params;
|
|
1630
|
+
const { step } = req.query;
|
|
1631
|
+
|
|
1632
|
+
const table = await Table.findOne({ id });
|
|
1633
|
+
if (!table) {
|
|
1634
|
+
req.flash("error", `Table not found`);
|
|
1635
|
+
res.redirect(`/table`);
|
|
1636
|
+
return;
|
|
1637
|
+
}
|
|
1638
|
+
const workflow = get_provider_workflow(table, req);
|
|
1639
|
+
const wfres = await workflow.run(
|
|
1640
|
+
{
|
|
1641
|
+
...(table.provider_cfg || {}),
|
|
1642
|
+
table_id: table.id,
|
|
1643
|
+
...(step ? { stepName: step } : {}),
|
|
1644
|
+
},
|
|
1645
|
+
req
|
|
1646
|
+
);
|
|
1647
|
+
respondWorkflow(table, workflow, wfres, req, res);
|
|
1648
|
+
})
|
|
1649
|
+
);
|
|
1650
|
+
|
|
1651
|
+
router.post(
|
|
1652
|
+
"/provider-cfg/:id",
|
|
1653
|
+
isAdmin,
|
|
1654
|
+
error_catcher(async (req, res) => {
|
|
1655
|
+
const { id } = req.params;
|
|
1656
|
+
const { step } = req.query;
|
|
1657
|
+
|
|
1658
|
+
const table = await Table.findOne({ id });
|
|
1659
|
+
if (!table) {
|
|
1660
|
+
req.flash("error", `Table not found`);
|
|
1661
|
+
res.redirect(`/table`);
|
|
1662
|
+
return;
|
|
1663
|
+
}
|
|
1664
|
+
const workflow = get_provider_workflow(table, req);
|
|
1665
|
+
const wfres = await workflow.run(req.body, req);
|
|
1666
|
+
respondWorkflow(table, workflow, wfres, req, res);
|
|
1667
|
+
})
|
|
1668
|
+
);
|
package/routes/tenant.js
CHANGED
|
@@ -426,6 +426,10 @@ const tenant_settings_form = (req) =>
|
|
|
426
426
|
"create_tenant_warning",
|
|
427
427
|
"create_tenant_warning_text",
|
|
428
428
|
"tenant_template",
|
|
429
|
+
{ section_header: "Tenant application capabilities" },
|
|
430
|
+
"tenants_install_git",
|
|
431
|
+
"tenants_set_npm_modules",
|
|
432
|
+
"tenants_unsafe_plugins",
|
|
429
433
|
],
|
|
430
434
|
action: "/tenant/settings",
|
|
431
435
|
submitLabel: req.__("Save"),
|
package/routes/utils.js
CHANGED
|
@@ -19,7 +19,12 @@ const is = require("contractis/is");
|
|
|
19
19
|
const { validateHeaderName, validateHeaderValue } = require("http");
|
|
20
20
|
const Crash = require("@saltcorn/data/models/crash");
|
|
21
21
|
const si = require("systeminformation");
|
|
22
|
-
|
|
22
|
+
const {
|
|
23
|
+
config_fields_form,
|
|
24
|
+
save_config_from_form,
|
|
25
|
+
check_if_restart_required,
|
|
26
|
+
flash_restart,
|
|
27
|
+
} = require("../markup/admin.js");
|
|
23
28
|
const get_sys_info = async () => {
|
|
24
29
|
const disks = await si.fsSize();
|
|
25
30
|
let size = 0;
|
|
@@ -318,6 +323,63 @@ const is_relative_url = (url) => {
|
|
|
318
323
|
return typeof url === "string" && !url.includes(":/") && !url.includes("//");
|
|
319
324
|
};
|
|
320
325
|
|
|
326
|
+
const admin_config_route = ({
|
|
327
|
+
router,
|
|
328
|
+
path,
|
|
329
|
+
super_path = "",
|
|
330
|
+
get_form,
|
|
331
|
+
field_names,
|
|
332
|
+
response,
|
|
333
|
+
flash,
|
|
334
|
+
}) => {
|
|
335
|
+
const getTheForm = async (req) =>
|
|
336
|
+
!get_form && field_names
|
|
337
|
+
? await config_fields_form({
|
|
338
|
+
req,
|
|
339
|
+
field_names,
|
|
340
|
+
action: super_path + path,
|
|
341
|
+
})
|
|
342
|
+
: typeof get_form === "function"
|
|
343
|
+
? await get_form(req)
|
|
344
|
+
: get_form;
|
|
345
|
+
|
|
346
|
+
router.get(
|
|
347
|
+
path,
|
|
348
|
+
isAdmin,
|
|
349
|
+
error_catcher(async (req, res) => {
|
|
350
|
+
response(await getTheForm(req), req, res);
|
|
351
|
+
})
|
|
352
|
+
);
|
|
353
|
+
router.post(
|
|
354
|
+
path,
|
|
355
|
+
isAdmin,
|
|
356
|
+
error_catcher(async (req, res) => {
|
|
357
|
+
const form = await getTheForm(req);
|
|
358
|
+
form.validate(req.body);
|
|
359
|
+
if (form.hasErrors) {
|
|
360
|
+
response(form, req, res);
|
|
361
|
+
} else {
|
|
362
|
+
const restart_required = check_if_restart_required(form, req);
|
|
363
|
+
|
|
364
|
+
await save_config_from_form(form);
|
|
365
|
+
if (!req.xhr) {
|
|
366
|
+
if (restart_required) {
|
|
367
|
+
flash_restart(req);
|
|
368
|
+
} else req.flash("success", req.__(flash));
|
|
369
|
+
res.redirect(super_path + path);
|
|
370
|
+
} else {
|
|
371
|
+
if (restart_required)
|
|
372
|
+
res.json({
|
|
373
|
+
success: "ok",
|
|
374
|
+
notify: req.__("Restart required for changes to take effect."),
|
|
375
|
+
});
|
|
376
|
+
else res.json({ success: "ok" });
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
})
|
|
380
|
+
);
|
|
381
|
+
};
|
|
382
|
+
|
|
321
383
|
module.exports = {
|
|
322
384
|
sqlsanitize,
|
|
323
385
|
csrfField,
|
|
@@ -333,4 +395,5 @@ module.exports = {
|
|
|
333
395
|
addOnDoneRedirect,
|
|
334
396
|
is_relative_url,
|
|
335
397
|
get_sys_info,
|
|
398
|
+
admin_config_route,
|
|
336
399
|
};
|
package/tests/api.test.js
CHANGED
|
@@ -19,7 +19,7 @@ beforeAll(async () => {
|
|
|
19
19
|
afterAll(db.close);
|
|
20
20
|
|
|
21
21
|
describe("API read", () => {
|
|
22
|
-
it("should get books for public", async () => {
|
|
22
|
+
it("should get books for public simple", async () => {
|
|
23
23
|
const app = await getApp({ disableCsrf: true });
|
|
24
24
|
await request(app)
|
|
25
25
|
.get("/api/books/")
|
|
@@ -32,7 +32,7 @@ describe("API read", () => {
|
|
|
32
32
|
)
|
|
33
33
|
);
|
|
34
34
|
});
|
|
35
|
-
it("should get books for public", async () => {
|
|
35
|
+
it("should get books for public fts", async () => {
|
|
36
36
|
const app = await getApp({ disableCsrf: true });
|
|
37
37
|
await request(app)
|
|
38
38
|
.get("/api/books/?_fts=Herman")
|