@saltcorn/server 0.8.5-beta.4 → 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/locales/da.json +3 -1
- package/locales/en.json +19 -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 +4 -0
- package/public/saltcorn.js +3 -0
- package/public/serviceworker.js +1 -0
- package/routes/admin.js +100 -217
- package/routes/api.js +1 -0
- package/routes/common_lists.js +1 -0
- package/routes/index.js +2 -0
- package/routes/notifications.js +136 -0
- package/routes/tables.js +159 -21
- package/routes/utils.js +64 -1
- package/tests/crud.test.js +53 -0
- package/tests/plugins.test.js +1 -1
- package/wrapper.js +26 -1
package/auth/admin.js
CHANGED
|
@@ -23,7 +23,16 @@ const {
|
|
|
23
23
|
const { isAdmin, error_catcher } = require("../routes/utils");
|
|
24
24
|
const { send_reset_email } = require("./resetpw");
|
|
25
25
|
const { getState } = require("@saltcorn/data/db/state");
|
|
26
|
-
const {
|
|
26
|
+
const {
|
|
27
|
+
a,
|
|
28
|
+
div,
|
|
29
|
+
span,
|
|
30
|
+
code,
|
|
31
|
+
h5,
|
|
32
|
+
i,
|
|
33
|
+
p,
|
|
34
|
+
input,
|
|
35
|
+
} = require("@saltcorn/markup/tags");
|
|
27
36
|
const Table = require("@saltcorn/data/models/table");
|
|
28
37
|
const {
|
|
29
38
|
send_users_page,
|
|
@@ -243,6 +252,18 @@ router.get(
|
|
|
243
252
|
type: "card",
|
|
244
253
|
title: req.__("Users"),
|
|
245
254
|
contents: [
|
|
255
|
+
div(
|
|
256
|
+
{ class: "row mb-3" },
|
|
257
|
+
div(
|
|
258
|
+
{ class: "col-sm-6 offset-sm-3" },
|
|
259
|
+
input({
|
|
260
|
+
class: "form-control",
|
|
261
|
+
type: "search",
|
|
262
|
+
"data-filter-table": "table.user-admin",
|
|
263
|
+
placeholder: "🔍 Search",
|
|
264
|
+
})
|
|
265
|
+
)
|
|
266
|
+
),
|
|
246
267
|
mkTable(
|
|
247
268
|
[
|
|
248
269
|
{ label: req.__("ID"), key: "id" },
|
|
@@ -273,7 +294,7 @@ router.get(
|
|
|
273
294
|
},
|
|
274
295
|
],
|
|
275
296
|
users,
|
|
276
|
-
{ hover: true }
|
|
297
|
+
{ hover: true, class: "user-admin" }
|
|
277
298
|
),
|
|
278
299
|
link(`/useradmin/new`, req.__("Create user")),
|
|
279
300
|
],
|
package/locales/da.json
CHANGED
|
@@ -704,5 +704,7 @@
|
|
|
704
704
|
"Tables organise data by fields and rows.": "Tables organise data by fields and rows.",
|
|
705
705
|
"Views display data from tables. A view is a view pattern applied to a table, with configuration.": "Views display data from tables. A view is a view pattern applied to a table, with configuration.",
|
|
706
706
|
"No views": "No views",
|
|
707
|
-
"No pages": "No pages"
|
|
707
|
+
"No pages": "No pages",
|
|
708
|
+
"Welcome to Saltcorn!": "Welcome to Saltcorn!",
|
|
709
|
+
"Notifications": "Notifications"
|
|
708
710
|
}
|
package/locales/en.json
CHANGED
|
@@ -1126,5 +1126,23 @@
|
|
|
1126
1126
|
"Install git plugins": "Install git plugins",
|
|
1127
1127
|
"Set available npm modules": "Set available npm modules",
|
|
1128
1128
|
"Only store modules can be installed on tenant instances": "Only store modules can be installed on tenant instances",
|
|
1129
|
-
"Unsafe modules": "Unsafe modules"
|
|
1129
|
+
"Unsafe modules": "Unsafe modules",
|
|
1130
|
+
"Notifications": "Notifications",
|
|
1131
|
+
"No notifications": "No notifications",
|
|
1132
|
+
"In user menu": "In user menu",
|
|
1133
|
+
"Show notifications in the user menu": "Show notifications in the user menu",
|
|
1134
|
+
"Notification settings": "Notification settings",
|
|
1135
|
+
"PWA": "PWA",
|
|
1136
|
+
"Progressive Web Application": "Progressive Web Application",
|
|
1137
|
+
"Display": "Display",
|
|
1138
|
+
"Progressive Web Application enabled": "Progressive Web Application enabled",
|
|
1139
|
+
"Saltcorn test email": "Saltcorn test email",
|
|
1140
|
+
"Hello from Saltcorn": "Hello from Saltcorn",
|
|
1141
|
+
"Set colors": "Set colors",
|
|
1142
|
+
"Theme color": "Theme color",
|
|
1143
|
+
"Background color": "Background color",
|
|
1144
|
+
"Table provider": "Table provider",
|
|
1145
|
+
"Database table": "Database table",
|
|
1146
|
+
"Configure provider": "Configure provider",
|
|
1147
|
+
"In scope:": "In scope:"
|
|
1130
1148
|
}
|
package/locales/pl.json
CHANGED
|
@@ -1106,5 +1106,21 @@
|
|
|
1106
1106
|
"Clear": "Wyczyść",
|
|
1107
1107
|
"Restore a snapshot": "Przywróć snapshot",
|
|
1108
1108
|
"Snapshot restored": "Snapshot przywrócony",
|
|
1109
|
-
"Configuration check report": "Raport sprawdzenia konfiguracji"
|
|
1109
|
+
"Configuration check report": "Raport sprawdzenia konfiguracji",
|
|
1110
|
+
"Re-run": "Uruchom ponownie",
|
|
1111
|
+
"Add constraint:": "Dodaj ograniczenie:",
|
|
1112
|
+
"Index": "Index",
|
|
1113
|
+
"Add constraint: ": "Dodaj ograniczenie: ",
|
|
1114
|
+
"Choose the field to be indexed": "Wybierz pole do indeksacji",
|
|
1115
|
+
"Constraint formula": "Formuła ograniczenia",
|
|
1116
|
+
"Formula must evaluate to true for valid rows. In scope: ": "Formuła musi być prawdziwa dla uwierzytelnienia wierszy. W zakresie: ",
|
|
1117
|
+
"Add %s constraint to %s": "Dodaj %s ograniczenie do %s",
|
|
1118
|
+
"What": "Co",
|
|
1119
|
+
"Choose the field to be indexed. This make searching the table faster.": "Wybierz pola, jakie mają być indeksowane. To przyspieszy wyszukiwanie w tabeli.",
|
|
1120
|
+
"Disk usage": "Zużycie dysku",
|
|
1121
|
+
"CPU usage": "Zużycie procesora",
|
|
1122
|
+
"Mem usage": "Zużycie pamięci",
|
|
1123
|
+
"The field that will be shown to the user when choosing a value": "Pole jakie będzie pokazane użytkownikowi przy wyborze wartości",
|
|
1124
|
+
"String value must match regular expression": "Wartość ciągu musi odpowiadać wyrażeniu regularnemu",
|
|
1125
|
+
"Modules up-to-date. Please restart server": "Moduły zaktualizowane. Proszę zresetować serwer"
|
|
1110
1126
|
}
|
package/markup/admin.js
CHANGED
|
@@ -315,6 +315,7 @@ const send_admin_page = (args) => {
|
|
|
315
315
|
{ text: "System", href: "/admin/system" },
|
|
316
316
|
{ text: "Mobile app", href: "/admin/build-mobile-app" },
|
|
317
317
|
{ text: "Development", href: "/admin/dev" },
|
|
318
|
+
{ text: "Notifications", href: "/admin/notifications" },
|
|
318
319
|
],
|
|
319
320
|
...args,
|
|
320
321
|
});
|
|
@@ -343,7 +344,7 @@ const viewAttributes = async (key) => {
|
|
|
343
344
|
* @param {*} req
|
|
344
345
|
* @returns {void}
|
|
345
346
|
*/
|
|
346
|
-
const
|
|
347
|
+
const check_if_restart_required = (cfgForm, req) => {
|
|
347
348
|
let restart = false;
|
|
348
349
|
cfgForm.fields.forEach((f) => {
|
|
349
350
|
if (configTypes[f.name]?.restart_required) {
|
|
@@ -351,7 +352,7 @@ const flash_restart_if_required = (cfgForm, req) => {
|
|
|
351
352
|
if (current !== cfgForm.values[f.name]) restart = true;
|
|
352
353
|
}
|
|
353
354
|
});
|
|
354
|
-
|
|
355
|
+
return restart;
|
|
355
356
|
};
|
|
356
357
|
|
|
357
358
|
/**
|
|
@@ -396,14 +397,21 @@ const config_fields_form = async ({
|
|
|
396
397
|
const tens = await db.select("_sc_tenants");
|
|
397
398
|
return { options: tens.map((t) => t.subdomain) };
|
|
398
399
|
};
|
|
399
|
-
for (const
|
|
400
|
-
if (typeof
|
|
400
|
+
for (const name0 of field_names) {
|
|
401
|
+
if (typeof name0 === "object" && name0.section_header) {
|
|
401
402
|
fields.push({
|
|
402
403
|
input_type: "section_header",
|
|
403
|
-
label:
|
|
404
|
+
label: name0.section_header,
|
|
404
405
|
});
|
|
405
406
|
continue;
|
|
406
407
|
}
|
|
408
|
+
let name, showIf;
|
|
409
|
+
if (typeof name0 === "object" && name0.name) {
|
|
410
|
+
name = name0.name;
|
|
411
|
+
showIf = name0.showIf;
|
|
412
|
+
} else {
|
|
413
|
+
name = name0;
|
|
414
|
+
}
|
|
407
415
|
values[name] = state.getConfig(name);
|
|
408
416
|
// console.log(`config field name: %s`,name);
|
|
409
417
|
if (configTypes[name].root_only && tenant !== db.connectObj.default_schema)
|
|
@@ -427,6 +435,7 @@ const config_fields_form = async ({
|
|
|
427
435
|
? undefined
|
|
428
436
|
: configTypes[name].type,
|
|
429
437
|
input_type: configTypes[name].input_type,
|
|
438
|
+
showIf,
|
|
430
439
|
attributes: isView
|
|
431
440
|
? await viewAttributes(name)
|
|
432
441
|
: isRole
|
|
@@ -553,7 +562,7 @@ module.exports = {
|
|
|
553
562
|
send_admin_page,
|
|
554
563
|
send_files_page,
|
|
555
564
|
save_config_from_form,
|
|
556
|
-
|
|
565
|
+
check_if_restart_required,
|
|
557
566
|
flash_restart,
|
|
558
567
|
send_tags_page,
|
|
559
568
|
};
|
package/package.json
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saltcorn/server",
|
|
3
|
-
"version": "0.8.5-beta.
|
|
3
|
+
"version": "0.8.5-beta.5",
|
|
4
4
|
"description": "Server app for Saltcorn, open-source no-code platform",
|
|
5
5
|
"homepage": "https://saltcorn.com",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"dependencies": {
|
|
9
|
-
"@saltcorn/base-plugin": "0.8.5-beta.
|
|
10
|
-
"@saltcorn/builder": "0.8.5-beta.
|
|
11
|
-
"@saltcorn/data": "0.8.5-beta.
|
|
12
|
-
"@saltcorn/admin-models": "0.8.5-beta.
|
|
13
|
-
"@saltcorn/filemanager": "0.8.5-beta.
|
|
14
|
-
"@saltcorn/markup": "0.8.5-beta.
|
|
15
|
-
"@saltcorn/sbadmin2": "0.8.5-beta.
|
|
9
|
+
"@saltcorn/base-plugin": "0.8.5-beta.5",
|
|
10
|
+
"@saltcorn/builder": "0.8.5-beta.5",
|
|
11
|
+
"@saltcorn/data": "0.8.5-beta.5",
|
|
12
|
+
"@saltcorn/admin-models": "0.8.5-beta.5",
|
|
13
|
+
"@saltcorn/filemanager": "0.8.5-beta.5",
|
|
14
|
+
"@saltcorn/markup": "0.8.5-beta.5",
|
|
15
|
+
"@saltcorn/sbadmin2": "0.8.5-beta.5",
|
|
16
16
|
"@socket.io/cluster-adapter": "^0.2.1",
|
|
17
17
|
"@socket.io/sticky": "^1.0.1",
|
|
18
18
|
"adm-zip": "0.5.10",
|
|
@@ -167,7 +167,16 @@ function apply_showif() {
|
|
|
167
167
|
}
|
|
168
168
|
});
|
|
169
169
|
});
|
|
170
|
-
|
|
170
|
+
$("[data-filter-table]").each(function (ix, element) {
|
|
171
|
+
const e = $(element);
|
|
172
|
+
const target = $(e.attr("data-filter-table"));
|
|
173
|
+
$(e).on("keyup", function () {
|
|
174
|
+
const value = $(this).val().toLowerCase();
|
|
175
|
+
target.find("tr").filter(function () {
|
|
176
|
+
$(this).toggle($(this).text().toLowerCase().indexOf(value) > -1);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
});
|
|
171
180
|
$("[data-source-url]").each(function (ix, element) {
|
|
172
181
|
const e = $(element);
|
|
173
182
|
const rec0 = get_form_record(e);
|
|
@@ -1045,3 +1054,23 @@ function split_paste_handler(e) {
|
|
|
1045
1054
|
function is_paging_param(key) {
|
|
1046
1055
|
return key.endsWith("_page") || key.endsWith("_pagesize");
|
|
1047
1056
|
}
|
|
1057
|
+
function check_saltcorn_notifications() {
|
|
1058
|
+
$.ajax(`/notifications/count-unread`).then((resp) => {
|
|
1059
|
+
if (resp.success) {
|
|
1060
|
+
const n = resp.success;
|
|
1061
|
+
const menu_item = $(`a.notify-menu-item`);
|
|
1062
|
+
|
|
1063
|
+
menu_item.html(
|
|
1064
|
+
`<i class="fa-fw mr-05 fas fa-bell"></i>Notifications (${n})`
|
|
1065
|
+
);
|
|
1066
|
+
$(".user-nav-section").html(
|
|
1067
|
+
`<i class="fa-fw mr-05 fas fa-user"></i>User (${n})`
|
|
1068
|
+
);
|
|
1069
|
+
$(".user-nav-section-with-span").html(
|
|
1070
|
+
`<i class="fa-fw mr-05 fas fa-user"></i><span>User (${n})</span>`
|
|
1071
|
+
);
|
|
1072
|
+
window.update_theme_notification_count &&
|
|
1073
|
+
window.update_theme_notification_count(n);
|
|
1074
|
+
}
|
|
1075
|
+
});
|
|
1076
|
+
}
|
package/public/saltcorn.css
CHANGED
package/public/saltcorn.js
CHANGED
|
@@ -326,6 +326,9 @@ function saveAndContinue(e, k) {
|
|
|
326
326
|
`<input type="hidden" class="form-control " name="id" value="${res.id}">`
|
|
327
327
|
);
|
|
328
328
|
}
|
|
329
|
+
if (res.notify) {
|
|
330
|
+
notifyAlert(res.notify);
|
|
331
|
+
}
|
|
329
332
|
},
|
|
330
333
|
error: function (request) {
|
|
331
334
|
var ct = request.getResponseHeader("content-type") || "";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// do nothing so far
|
package/routes/admin.js
CHANGED
|
@@ -10,6 +10,7 @@ const {
|
|
|
10
10
|
error_catcher,
|
|
11
11
|
getGitRevision,
|
|
12
12
|
setTenant,
|
|
13
|
+
admin_config_route,
|
|
13
14
|
get_sys_info,
|
|
14
15
|
} = require("./utils.js");
|
|
15
16
|
const Table = require("@saltcorn/data/models/table");
|
|
@@ -84,7 +85,6 @@ const {
|
|
|
84
85
|
//send_files_page,
|
|
85
86
|
config_fields_form,
|
|
86
87
|
save_config_from_form,
|
|
87
|
-
flash_restart_if_required,
|
|
88
88
|
} = require("../markup/admin.js");
|
|
89
89
|
const packagejson = require("../package.json");
|
|
90
90
|
const Form = require("@saltcorn/data/models/form");
|
|
@@ -107,52 +107,6 @@ const Crash = require("@saltcorn/data/models/crash");
|
|
|
107
107
|
const router = new Router();
|
|
108
108
|
module.exports = router;
|
|
109
109
|
|
|
110
|
-
/**
|
|
111
|
-
* Site identity form
|
|
112
|
-
* @param {object} req -http request
|
|
113
|
-
* @returns {Promise<Form>} form
|
|
114
|
-
*/
|
|
115
|
-
const site_id_form = (req) =>
|
|
116
|
-
config_fields_form({
|
|
117
|
-
req,
|
|
118
|
-
field_names: [
|
|
119
|
-
"site_name",
|
|
120
|
-
"timezone",
|
|
121
|
-
"base_url",
|
|
122
|
-
...(getConfigFile() ? ["multitenancy_enabled"] : []),
|
|
123
|
-
{ section_header: "Logo image" },
|
|
124
|
-
"site_logo_id",
|
|
125
|
-
"favicon_id",
|
|
126
|
-
{ section_header: "Custom code" },
|
|
127
|
-
"page_custom_css",
|
|
128
|
-
"page_custom_html",
|
|
129
|
-
{ section_header: "Extension store" },
|
|
130
|
-
"plugins_store_endpoint",
|
|
131
|
-
"packs_store_endpoint",
|
|
132
|
-
],
|
|
133
|
-
action: "/admin",
|
|
134
|
-
submitLabel: req.__("Save"),
|
|
135
|
-
});
|
|
136
|
-
/**
|
|
137
|
-
* Email settings form
|
|
138
|
-
* @param {object} req request
|
|
139
|
-
* @returns {Promise<Form>} form
|
|
140
|
-
*/
|
|
141
|
-
const email_form = async (req) => {
|
|
142
|
-
return await config_fields_form({
|
|
143
|
-
req,
|
|
144
|
-
field_names: [
|
|
145
|
-
"smtp_host",
|
|
146
|
-
"smtp_username",
|
|
147
|
-
"smtp_password",
|
|
148
|
-
"smtp_port",
|
|
149
|
-
"smtp_secure",
|
|
150
|
-
"email_from",
|
|
151
|
-
],
|
|
152
|
-
action: "/admin/email",
|
|
153
|
-
});
|
|
154
|
-
};
|
|
155
|
-
|
|
156
110
|
const app_files_table = (files, buildDirName, req) =>
|
|
157
111
|
mkTable(
|
|
158
112
|
[
|
|
@@ -182,17 +136,27 @@ const app_files_table = (files, buildDirName, req) =>
|
|
|
182
136
|
files
|
|
183
137
|
);
|
|
184
138
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
139
|
+
admin_config_route({
|
|
140
|
+
router,
|
|
141
|
+
path: "/",
|
|
142
|
+
super_path: "/admin",
|
|
143
|
+
flash: "Site identity settings updated",
|
|
144
|
+
field_names: [
|
|
145
|
+
"site_name",
|
|
146
|
+
"timezone",
|
|
147
|
+
"base_url",
|
|
148
|
+
...(getConfigFile() ? ["multitenancy_enabled"] : []),
|
|
149
|
+
{ section_header: "Logo image" },
|
|
150
|
+
"site_logo_id",
|
|
151
|
+
"favicon_id",
|
|
152
|
+
{ section_header: "Custom code" },
|
|
153
|
+
"page_custom_css",
|
|
154
|
+
"page_custom_html",
|
|
155
|
+
{ section_header: "Extension store" },
|
|
156
|
+
"plugins_store_endpoint",
|
|
157
|
+
"packs_store_endpoint",
|
|
158
|
+
],
|
|
159
|
+
response(form, req, res) {
|
|
196
160
|
send_admin_page({
|
|
197
161
|
res,
|
|
198
162
|
req,
|
|
@@ -204,53 +168,23 @@ router.get(
|
|
|
204
168
|
contents: [renderForm(form, req.csrfToken())],
|
|
205
169
|
},
|
|
206
170
|
});
|
|
207
|
-
}
|
|
208
|
-
);
|
|
171
|
+
},
|
|
172
|
+
});
|
|
209
173
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
req,
|
|
225
|
-
active_sub: "Site identity",
|
|
226
|
-
contents: {
|
|
227
|
-
type: "card",
|
|
228
|
-
title: req.__("Site identity settings"),
|
|
229
|
-
contents: [renderForm(form, req.csrfToken())],
|
|
230
|
-
},
|
|
231
|
-
});
|
|
232
|
-
} else {
|
|
233
|
-
flash_restart_if_required(form, req);
|
|
234
|
-
await save_config_from_form(form);
|
|
235
|
-
|
|
236
|
-
if (!req.xhr) {
|
|
237
|
-
req.flash("success", req.__("Site identity settings updated"));
|
|
238
|
-
res.redirect("/admin");
|
|
239
|
-
} else res.json({ success: "ok" });
|
|
240
|
-
}
|
|
241
|
-
})
|
|
242
|
-
);
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* @name get/email
|
|
246
|
-
* @function
|
|
247
|
-
* @memberof module:routes/admin~routes/adminRouter
|
|
248
|
-
*/
|
|
249
|
-
router.get(
|
|
250
|
-
"/email",
|
|
251
|
-
isAdmin,
|
|
252
|
-
error_catcher(async (req, res) => {
|
|
253
|
-
const form = await email_form(req);
|
|
174
|
+
admin_config_route({
|
|
175
|
+
router,
|
|
176
|
+
path: "/email",
|
|
177
|
+
super_path: "/admin",
|
|
178
|
+
flash: "Email settings updated",
|
|
179
|
+
field_names: [
|
|
180
|
+
"smtp_host",
|
|
181
|
+
"smtp_username",
|
|
182
|
+
"smtp_password",
|
|
183
|
+
"smtp_port",
|
|
184
|
+
"smtp_secure",
|
|
185
|
+
"email_from",
|
|
186
|
+
],
|
|
187
|
+
response(form, req, res) {
|
|
254
188
|
send_admin_page({
|
|
255
189
|
res,
|
|
256
190
|
req,
|
|
@@ -272,8 +206,8 @@ router.get(
|
|
|
272
206
|
],
|
|
273
207
|
},
|
|
274
208
|
});
|
|
275
|
-
}
|
|
276
|
-
);
|
|
209
|
+
},
|
|
210
|
+
});
|
|
277
211
|
|
|
278
212
|
/**
|
|
279
213
|
* @name get/send-test-email
|
|
@@ -305,38 +239,6 @@ router.get(
|
|
|
305
239
|
})
|
|
306
240
|
);
|
|
307
241
|
|
|
308
|
-
/**
|
|
309
|
-
* @name post/email
|
|
310
|
-
* @function
|
|
311
|
-
* @memberof module:routes/admin~routes/adminRouter
|
|
312
|
-
*/
|
|
313
|
-
router.post(
|
|
314
|
-
"/email",
|
|
315
|
-
isAdmin,
|
|
316
|
-
error_catcher(async (req, res) => {
|
|
317
|
-
const form = await email_form(req);
|
|
318
|
-
form.validate(req.body);
|
|
319
|
-
if (form.hasErrors) {
|
|
320
|
-
send_admin_page({
|
|
321
|
-
res,
|
|
322
|
-
req,
|
|
323
|
-
active_sub: "Email",
|
|
324
|
-
contents: {
|
|
325
|
-
type: "card",
|
|
326
|
-
title: req.__("Email settings"),
|
|
327
|
-
contents: [renderForm(form, req.csrfToken())],
|
|
328
|
-
},
|
|
329
|
-
});
|
|
330
|
-
} else {
|
|
331
|
-
await save_config_from_form(form);
|
|
332
|
-
if (!req.xhr) {
|
|
333
|
-
req.flash("success", req.__("Email settings updated"));
|
|
334
|
-
res.redirect("/admin/email");
|
|
335
|
-
} else res.json({ success: "ok" });
|
|
336
|
-
}
|
|
337
|
-
})
|
|
338
|
-
);
|
|
339
|
-
|
|
340
242
|
/**
|
|
341
243
|
* @name get/backup
|
|
342
244
|
* @function
|
|
@@ -1967,41 +1869,31 @@ router.post(
|
|
|
1967
1869
|
})
|
|
1968
1870
|
);
|
|
1969
1871
|
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1872
|
+
admin_config_route({
|
|
1873
|
+
router,
|
|
1874
|
+
path: "/dev",
|
|
1875
|
+
super_path: "/admin",
|
|
1876
|
+
flash: "Development mode settings updated",
|
|
1877
|
+
async get_form(req) {
|
|
1878
|
+
const tenants_set_npm_modules = getRootState().getConfig(
|
|
1879
|
+
"tenants_set_npm_modules",
|
|
1880
|
+
false
|
|
1881
|
+
);
|
|
1882
|
+
const isRoot = db.getTenantSchema() === db.connectObj.default_schema;
|
|
1981
1883
|
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
}
|
|
1994
|
-
|
|
1995
|
-
* Developer Mode page
|
|
1996
|
-
* @name get/dev
|
|
1997
|
-
* @function
|
|
1998
|
-
* @memberof module:routes/admin~routes/adminRouter
|
|
1999
|
-
*/
|
|
2000
|
-
router.get(
|
|
2001
|
-
"/dev",
|
|
2002
|
-
isAdmin,
|
|
2003
|
-
error_catcher(async (req, res) => {
|
|
2004
|
-
const form = await dev_form(req);
|
|
1884
|
+
return await config_fields_form({
|
|
1885
|
+
req,
|
|
1886
|
+
field_names: [
|
|
1887
|
+
"development_mode",
|
|
1888
|
+
"log_sql",
|
|
1889
|
+
"log_client_errors",
|
|
1890
|
+
"log_level",
|
|
1891
|
+
...(isRoot || tenants_set_npm_modules ? ["npm_available_js_code"] : []),
|
|
1892
|
+
],
|
|
1893
|
+
action: "/admin/dev",
|
|
1894
|
+
});
|
|
1895
|
+
},
|
|
1896
|
+
response(form, req, res) {
|
|
2005
1897
|
send_admin_page({
|
|
2006
1898
|
res,
|
|
2007
1899
|
req,
|
|
@@ -2010,51 +1902,42 @@ router.get(
|
|
|
2010
1902
|
type: "card",
|
|
2011
1903
|
title: req.__("Development settings"),
|
|
2012
1904
|
titleAjaxIndicator: true,
|
|
2013
|
-
contents: [
|
|
2014
|
-
renderForm(form, req.csrfToken()) /*,
|
|
2015
|
-
a(
|
|
2016
|
-
{
|
|
2017
|
-
id: "testemail",
|
|
2018
|
-
href: "/admin/send-test-email",
|
|
2019
|
-
class: "btn btn-primary",
|
|
2020
|
-
},
|
|
2021
|
-
req.__("Send test email")
|
|
2022
|
-
),*/,
|
|
2023
|
-
],
|
|
1905
|
+
contents: [renderForm(form, req.csrfToken())],
|
|
2024
1906
|
},
|
|
2025
1907
|
});
|
|
2026
|
-
}
|
|
2027
|
-
);
|
|
1908
|
+
},
|
|
1909
|
+
});
|
|
2028
1910
|
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
1911
|
+
admin_config_route({
|
|
1912
|
+
router,
|
|
1913
|
+
path: "/notifications",
|
|
1914
|
+
super_path: "/admin",
|
|
1915
|
+
field_names: [
|
|
1916
|
+
"notification_in_menu",
|
|
1917
|
+
{ section_header: "Progressive Web Application" },
|
|
1918
|
+
"pwa_enabled",
|
|
1919
|
+
{ name: "pwa_display", showIf: { pwa_enabled: true } },
|
|
1920
|
+
{ name: "pwa_set_colors", showIf: { pwa_enabled: true } },
|
|
1921
|
+
{
|
|
1922
|
+
name: "pwa_theme_color",
|
|
1923
|
+
showIf: { pwa_enabled: true, pwa_set_colors: true },
|
|
1924
|
+
},
|
|
1925
|
+
{
|
|
1926
|
+
name: "pwa_background_color",
|
|
1927
|
+
showIf: { pwa_enabled: true, pwa_set_colors: true },
|
|
1928
|
+
},
|
|
1929
|
+
],
|
|
1930
|
+
response(form, req, res) {
|
|
1931
|
+
send_admin_page({
|
|
1932
|
+
res,
|
|
1933
|
+
req,
|
|
1934
|
+
active_sub: "Notifications",
|
|
1935
|
+
contents: {
|
|
1936
|
+
type: "card",
|
|
1937
|
+
title: req.__("Notification settings"),
|
|
1938
|
+
titleAjaxIndicator: true,
|
|
1939
|
+
contents: [renderForm(form, req.csrfToken())],
|
|
1940
|
+
},
|
|
1941
|
+
});
|
|
1942
|
+
},
|
|
1943
|
+
});
|
package/routes/api.js
CHANGED
package/routes/common_lists.js
CHANGED
|
@@ -33,6 +33,7 @@ const tableBadges = (t, req) => {
|
|
|
33
33
|
if (t.ownership_field_id) s += badge("primary", req.__("Owned"));
|
|
34
34
|
if (t.versioned) s += badge("success", req.__("History"));
|
|
35
35
|
if (t.external) s += badge("info", req.__("External"));
|
|
36
|
+
if (t.provider_name) s += badge("success", t.provider_name);
|
|
36
37
|
return s;
|
|
37
38
|
};
|
|
38
39
|
|
package/routes/index.js
CHANGED
|
@@ -64,6 +64,7 @@ const edit = require("./edit");
|
|
|
64
64
|
const config = require("./config");
|
|
65
65
|
const viewedit = require("./viewedit");
|
|
66
66
|
const crashlog = require("./crashlog");
|
|
67
|
+
const notifications = require("./notifications");
|
|
67
68
|
const del = require("./delete");
|
|
68
69
|
const auth = require("../auth/routes");
|
|
69
70
|
const useradmin = require("../auth/admin");
|
|
@@ -96,6 +97,7 @@ module.exports =
|
|
|
96
97
|
app.use("/actions", actions);
|
|
97
98
|
app.use("/eventlog", eventlog);
|
|
98
99
|
app.use("/library", library);
|
|
100
|
+
app.use("/notifications", notifications);
|
|
99
101
|
app.use("/site-structure", infoarch);
|
|
100
102
|
app.use("/search", search);
|
|
101
103
|
app.use("/admin", admin);
|
|
@@ -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/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()
|
|
@@ -548,9 +564,9 @@ router.get(
|
|
|
548
564
|
const { idorname } = req.params;
|
|
549
565
|
let id = parseInt(idorname);
|
|
550
566
|
let table;
|
|
551
|
-
if (id) table =
|
|
567
|
+
if (id) table = Table.findOne({ id });
|
|
552
568
|
else {
|
|
553
|
-
table =
|
|
569
|
+
table = Table.findOne({ name: idorname });
|
|
554
570
|
}
|
|
555
571
|
|
|
556
572
|
if (!table) {
|
|
@@ -645,7 +661,7 @@ router.get(
|
|
|
645
661
|
var viewCard;
|
|
646
662
|
if (fields.length > 0) {
|
|
647
663
|
const views = await View.find(
|
|
648
|
-
table.
|
|
664
|
+
table.id ? { table_id: table.id } : { exttable_name: table.name }
|
|
649
665
|
);
|
|
650
666
|
var viewCardContents;
|
|
651
667
|
if (views.length > 0) {
|
|
@@ -726,6 +742,16 @@ router.get(
|
|
|
726
742
|
: req.__("Edit")
|
|
727
743
|
)
|
|
728
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
|
+
),
|
|
729
755
|
div(
|
|
730
756
|
{ class: "mx-auto" },
|
|
731
757
|
a(
|
|
@@ -820,7 +846,13 @@ router.get(
|
|
|
820
846
|
type: "breadcrumbs",
|
|
821
847
|
crumbs: [
|
|
822
848
|
{ text: req.__("Tables"), href: "/table" },
|
|
823
|
-
{
|
|
849
|
+
{
|
|
850
|
+
text: span(
|
|
851
|
+
{ class: "fw-bold text-body" },
|
|
852
|
+
table.name,
|
|
853
|
+
table.provider_name && ` (${table.provider_name} provider)`
|
|
854
|
+
),
|
|
855
|
+
},
|
|
824
856
|
],
|
|
825
857
|
},
|
|
826
858
|
{
|
|
@@ -875,7 +907,14 @@ router.post(
|
|
|
875
907
|
} else if (db.sqlsanitize(name) === "") {
|
|
876
908
|
req.flash("error", req.__(`Invalid table name %s`, name));
|
|
877
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}`);
|
|
878
916
|
} else {
|
|
917
|
+
delete rest.provider_name;
|
|
879
918
|
const table = await Table.create(name, rest);
|
|
880
919
|
req.flash("success", req.__(`Table %s created`, name));
|
|
881
920
|
res.redirect(`/table/${table.id}`);
|
|
@@ -1004,23 +1043,6 @@ router.post(
|
|
|
1004
1043
|
}
|
|
1005
1044
|
})
|
|
1006
1045
|
);
|
|
1007
|
-
/**
|
|
1008
|
-
* Table badges to show in System Table list views
|
|
1009
|
-
* Currently supports:
|
|
1010
|
-
* - Owned - if ownership_field_id? What is it?
|
|
1011
|
-
* - History - if table has versioning
|
|
1012
|
-
* - External - if this is external table
|
|
1013
|
-
* @param {object} t table object
|
|
1014
|
-
* @param {object} req http request
|
|
1015
|
-
* @returns {string} html string with list of badges
|
|
1016
|
-
*/
|
|
1017
|
-
const tableBadges = (t, req) => {
|
|
1018
|
-
let s = "";
|
|
1019
|
-
if (t.ownership_field_id) s += badge("primary", req.__("Owned"));
|
|
1020
|
-
if (t.versioned) s += badge("success", req.__("History"));
|
|
1021
|
-
if (t.external) s += badge("info", req.__("External"));
|
|
1022
|
-
return s;
|
|
1023
|
-
};
|
|
1024
1046
|
|
|
1025
1047
|
/**
|
|
1026
1048
|
* List Views of Tables (GET Handler)
|
|
@@ -1528,3 +1550,119 @@ router.post(
|
|
|
1528
1550
|
res.redirect(`/table/${table.id}`);
|
|
1529
1551
|
})
|
|
1530
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/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/crud.test.js
CHANGED
|
@@ -13,6 +13,7 @@ const {
|
|
|
13
13
|
const db = require("@saltcorn/data/db");
|
|
14
14
|
const Table = require("@saltcorn/data/models/table");
|
|
15
15
|
const View = require("@saltcorn/data/models/view");
|
|
16
|
+
const Notification = require("@saltcorn/data/models/notification");
|
|
16
17
|
const User = require("@saltcorn/data/models/user");
|
|
17
18
|
const reset = require("@saltcorn/data/db/reset_schema");
|
|
18
19
|
|
|
@@ -42,6 +43,58 @@ describe("standard edit form", () => {
|
|
|
42
43
|
});
|
|
43
44
|
});
|
|
44
45
|
|
|
46
|
+
describe("notifications", () => {
|
|
47
|
+
it("show empty notifications", async () => {
|
|
48
|
+
const loginCookie = await getStaffLoginCookie();
|
|
49
|
+
const app = await getApp({ disableCsrf: true });
|
|
50
|
+
await request(app)
|
|
51
|
+
.get("/notifications")
|
|
52
|
+
.set("Cookie", loginCookie)
|
|
53
|
+
.expect(toInclude("No notifications"));
|
|
54
|
+
});
|
|
55
|
+
it("no unread notifications", async () => {
|
|
56
|
+
const loginCookie = await getStaffLoginCookie();
|
|
57
|
+
const app = await getApp({ disableCsrf: true });
|
|
58
|
+
await request(app)
|
|
59
|
+
.get("/notifications/count-unread")
|
|
60
|
+
.set("Cookie", loginCookie)
|
|
61
|
+
.expect(succeedJsonWith((n) => n === 0));
|
|
62
|
+
});
|
|
63
|
+
it("add notification", async () => {
|
|
64
|
+
const user = await User.findOne({ role_id: 4 });
|
|
65
|
+
await Notification.create({
|
|
66
|
+
user_id: user.id,
|
|
67
|
+
title: "This is a staff announcement",
|
|
68
|
+
body: "Will a member of staff please proceed to the checkout area",
|
|
69
|
+
link: "https://www.sainsburys.co.uk/",
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
it("one unread notifications", async () => {
|
|
73
|
+
const loginCookie = await getStaffLoginCookie();
|
|
74
|
+
const app = await getApp({ disableCsrf: true });
|
|
75
|
+
await request(app)
|
|
76
|
+
.get("/notifications/count-unread")
|
|
77
|
+
.set("Cookie", loginCookie)
|
|
78
|
+
.expect(succeedJsonWith((n) => n === 1));
|
|
79
|
+
});
|
|
80
|
+
it("show new notifications", async () => {
|
|
81
|
+
const loginCookie = await getStaffLoginCookie();
|
|
82
|
+
const app = await getApp({ disableCsrf: true });
|
|
83
|
+
await request(app)
|
|
84
|
+
.get("/notifications")
|
|
85
|
+
.set("Cookie", loginCookie)
|
|
86
|
+
.expect(toInclude("This is a staff announcement"))
|
|
87
|
+
.expect(toInclude("unread-notify"));
|
|
88
|
+
});
|
|
89
|
+
it("no unread notifications", async () => {
|
|
90
|
+
const loginCookie = await getStaffLoginCookie();
|
|
91
|
+
const app = await getApp({ disableCsrf: true });
|
|
92
|
+
await request(app)
|
|
93
|
+
.get("/notifications/count-unread")
|
|
94
|
+
.set("Cookie", loginCookie)
|
|
95
|
+
.expect(succeedJsonWith((n) => n === 0));
|
|
96
|
+
});
|
|
97
|
+
});
|
|
45
98
|
describe("homepage", () => {
|
|
46
99
|
it("shows to admin", async () => {
|
|
47
100
|
const loginCookie = await getAdminLoginCookie();
|
package/tests/plugins.test.js
CHANGED
|
@@ -318,7 +318,7 @@ describe("config endpoints", () => {
|
|
|
318
318
|
.send("site_name=FooSiteName")
|
|
319
319
|
.send("multitenancy_enabled=on")
|
|
320
320
|
.set("Cookie", loginCookie)
|
|
321
|
-
.expect(toRedirect("/admin"));
|
|
321
|
+
.expect(toRedirect("/admin/"));
|
|
322
322
|
await request(app)
|
|
323
323
|
.get("/admin")
|
|
324
324
|
.set("Cookie", loginCookie)
|
package/wrapper.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
const { getState } = require("@saltcorn/data/db/state");
|
|
6
6
|
const { get_extra_menu } = require("@saltcorn/data/web-mobile-commons");
|
|
7
7
|
//const db = require("@saltcorn/data/db");
|
|
8
|
-
const { h3, div, small } = require("@saltcorn/markup/tags");
|
|
8
|
+
const { h3, div, small, domReady } = require("@saltcorn/markup/tags");
|
|
9
9
|
const { renderForm, link } = require("@saltcorn/markup");
|
|
10
10
|
const renderLayout = require("@saltcorn/markup/layout");
|
|
11
11
|
/**
|
|
@@ -30,6 +30,7 @@ const get_menu = (req) => {
|
|
|
30
30
|
const role = (req.user || {}).role_id || 10;
|
|
31
31
|
|
|
32
32
|
const allow_signup = state.getConfig("allow_signup");
|
|
33
|
+
const notification_in_menu = state.getConfig("notification_in_menu");
|
|
33
34
|
const login_menu = state.getConfig("login_menu");
|
|
34
35
|
const locale = req.getLocale();
|
|
35
36
|
const __ = (s) => state.i18n.__({ phrase: s, locale }) || s;
|
|
@@ -42,6 +43,16 @@ const get_menu = (req) => {
|
|
|
42
43
|
isUser: true,
|
|
43
44
|
subitems: [
|
|
44
45
|
{ label: small((req.user.email || "").split("@")[0]) },
|
|
46
|
+
...(notification_in_menu
|
|
47
|
+
? [
|
|
48
|
+
{
|
|
49
|
+
label: req.__("Notifications"),
|
|
50
|
+
icon: "far fa-bell",
|
|
51
|
+
class: "notify-menu-item",
|
|
52
|
+
link: "/notifications",
|
|
53
|
+
},
|
|
54
|
+
]
|
|
55
|
+
: []),
|
|
45
56
|
{
|
|
46
57
|
label: req.__("User Settings"),
|
|
47
58
|
icon: "fas fa-user-cog",
|
|
@@ -153,6 +164,8 @@ const get_menu = (req) => {
|
|
|
153
164
|
const get_headers = (req, version_tag, description, extras = []) => {
|
|
154
165
|
const state = getState();
|
|
155
166
|
const favicon = state.getConfig("favicon_id", null);
|
|
167
|
+
const notification_in_menu = state.getConfig("notification_in_menu");
|
|
168
|
+
const pwa_enabled = state.getConfig("pwa_enabled");
|
|
156
169
|
|
|
157
170
|
const iconHeader = favicon
|
|
158
171
|
? [
|
|
@@ -187,6 +200,18 @@ const get_headers = (req, version_tag, description, extras = []) => {
|
|
|
187
200
|
for (const hs of Object.values(state.headers)) {
|
|
188
201
|
state_headers.push(...hs);
|
|
189
202
|
}
|
|
203
|
+
if (notification_in_menu)
|
|
204
|
+
from_cfg.push({ scriptBody: domReady(`check_saltcorn_notifications()`) });
|
|
205
|
+
if (pwa_enabled) {
|
|
206
|
+
from_cfg.push({
|
|
207
|
+
headerTag: `<link rel="manifest" href="/notifications/manifest.json">`,
|
|
208
|
+
});
|
|
209
|
+
from_cfg.push({
|
|
210
|
+
scriptBody: `if('serviceWorker' in navigator) {
|
|
211
|
+
navigator.serviceWorker.register('/serviceworker.js', { scope: '/' });
|
|
212
|
+
}`,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
190
215
|
return [
|
|
191
216
|
...stdHeaders,
|
|
192
217
|
...iconHeader,
|