@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 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 { a, div, span, code, h5, i, p } = require("@saltcorn/markup/tags");
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 flash_restart_if_required = (cfgForm, req) => {
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
- if (restart) flash_restart(req);
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 name of field_names) {
400
- if (typeof name === "object" && name.section_header) {
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: name.section_header,
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
- flash_restart_if_required,
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.4",
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.4",
10
- "@saltcorn/builder": "0.8.5-beta.4",
11
- "@saltcorn/data": "0.8.5-beta.4",
12
- "@saltcorn/admin-models": "0.8.5-beta.4",
13
- "@saltcorn/filemanager": "0.8.5-beta.4",
14
- "@saltcorn/markup": "0.8.5-beta.4",
15
- "@saltcorn/sbadmin2": "0.8.5-beta.4",
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
+ }
@@ -398,3 +398,7 @@ table.table-inner-grid td {
398
398
  margin: 8px;
399
399
  }
400
400
  }
401
+
402
+ div.unread-notify {
403
+ border-left: 4px solid green;
404
+ }
@@ -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
- * Router get /
187
- * @name get
188
- * @function
189
- * @memberof module:routes/admin~routes/adminRouter
190
- */
191
- router.get(
192
- "/",
193
- isAdmin,
194
- error_catcher(async (req, res) => {
195
- const form = await site_id_form(req);
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
- * @name post
212
- * @function
213
- * @memberof module:routes/admin~routes/adminRouter
214
- */
215
- router.post(
216
- "/",
217
- isAdmin,
218
- error_catcher(async (req, res) => {
219
- const form = await site_id_form(req);
220
- form.validate(req.body);
221
- if (form.hasErrors) {
222
- send_admin_page({
223
- res,
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
- * Developer settings form
1972
- * @param {object} req request
1973
- * @returns {Promise<Form>} form
1974
- */
1975
- const dev_form = async (req) => {
1976
- const tenants_set_npm_modules = getRootState().getConfig(
1977
- "tenants_set_npm_modules",
1978
- false
1979
- );
1980
- const isRoot = db.getTenantSchema() === db.connectObj.default_schema;
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
- return await config_fields_form({
1983
- req,
1984
- field_names: [
1985
- "development_mode",
1986
- "log_sql",
1987
- "log_client_errors",
1988
- "log_level",
1989
- ...(isRoot || tenants_set_npm_modules ? ["npm_available_js_code"] : []),
1990
- ],
1991
- action: "/admin/dev",
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
- * Development mode
2031
- * @name post/email
2032
- * @function
2033
- * @memberof module:routes/admin~routes/adminRouter
2034
- */
2035
- router.post(
2036
- "/dev",
2037
- isAdmin,
2038
- error_catcher(async (req, res) => {
2039
- const form = await dev_form(req);
2040
- form.validate(req.body);
2041
- if (form.hasErrors) {
2042
- send_admin_page({
2043
- res,
2044
- req,
2045
- active_sub: "Development",
2046
- contents: {
2047
- type: "card",
2048
- title: req.__("Development settings"),
2049
- contents: [renderForm(form, req.csrfToken())],
2050
- },
2051
- });
2052
- } else {
2053
- await save_config_from_form(form);
2054
- if (!req.xhr) {
2055
- req.flash("success", req.__("Development mode settings updated"));
2056
- res.redirect("/admin/dev");
2057
- } else res.json({ success: "ok" });
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
@@ -348,6 +348,7 @@ router.post(
348
348
  const resp = await action.run({
349
349
  configuration: trigger.configuration,
350
350
  body: req.body,
351
+ row: req.body,
351
352
  req,
352
353
  });
353
354
  res.json({ success: true, data: resp });
@@ -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 = await Table.findOne({ id });
567
+ if (id) table = Table.findOne({ id });
552
568
  else {
553
- table = await Table.findOne({ name: idorname });
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.external ? { exttable_name: table.name } : { table_id: table.id }
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
- { text: span({ class: "fw-bold text-body" }, table.name) },
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
  };
@@ -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();
@@ -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,