@saltcorn/server 0.9.3-beta.4 → 0.9.3-beta.6

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.
@@ -0,0 +1,678 @@
1
+ const Router = require("express-promise-router");
2
+
3
+ const PageGroup = require("@saltcorn/data/models/page_group");
4
+ const PageGroupMember = require("@saltcorn/data/models/page_group_member");
5
+ const Page = require("@saltcorn/data/models/page");
6
+ const Form = require("@saltcorn/data/models/form");
7
+ const User = require("@saltcorn/data/models/user");
8
+ const { div, a, code, span, br } = require("@saltcorn/markup/tags");
9
+ const {
10
+ renderForm,
11
+ mkTable,
12
+ post_btn,
13
+ post_delete_btn,
14
+ link,
15
+ } = require("@saltcorn/markup");
16
+ const { add_to_menu } = require("@saltcorn/admin-models/models/pack");
17
+ const { error_catcher, isAdmin, setRole } = require("./utils.js");
18
+ const { getState } = require("@saltcorn/data/db/state");
19
+
20
+ const router = new Router();
21
+ module.exports = router;
22
+
23
+ const groupPropsForm = async (req, isNew) => {
24
+ const roles = await User.get_roles();
25
+ const pages = await Page.find();
26
+ const groups = await PageGroup.find();
27
+ return new Form({
28
+ action: "/page_groupedit/edit-properties",
29
+ ...(isNew
30
+ ? {}
31
+ : {
32
+ onChange: `saveAndContinue(this, (res) => {
33
+ history.replaceState(null, '', res.responseJSON.row.name);
34
+ });`,
35
+ }),
36
+ noSubmitButton: !isNew,
37
+ fields: [
38
+ {
39
+ name: "name",
40
+ label: req.__("Name"),
41
+ type: "String",
42
+ required: true,
43
+ validator(s, whole) {
44
+ if (s.length < 1) return req.__("Missing name");
45
+ if (pages.find((p) => p.name === s))
46
+ return req.__("A page with this name already exists");
47
+ if (
48
+ groups.find((g) =>
49
+ !isNew ? g.name === s && g.id !== +whole.id : g.name === s
50
+ )
51
+ ) {
52
+ return req.__("A page group with this name already exists");
53
+ }
54
+ },
55
+ sublabel: req.__("A short name that will be in your URL"),
56
+ attributes: { autofocus: true },
57
+ },
58
+ {
59
+ name: "description",
60
+ label: req.__("Description"),
61
+ type: "String",
62
+ sublabel: req.__("A longer description"),
63
+ },
64
+ {
65
+ name: "min_role",
66
+ label: req.__("Minimum role"),
67
+ sublabel: req.__("Role required to access page"),
68
+ input_type: "select",
69
+ options: roles.map((r) => ({ value: r.id, label: r.role })),
70
+ },
71
+ ],
72
+ });
73
+ };
74
+
75
+ const memberForm = async (action, req, groupName, pageValidator) => {
76
+ const pageOptions = (await Page.find()).map((p) => p.name);
77
+ return new Form({
78
+ action,
79
+ fields: [
80
+ {
81
+ name: "page_name",
82
+ label: req.__("Page"),
83
+ sublabel: req.__("Page to be served"),
84
+ type: "String",
85
+ required: true,
86
+ validator: pageValidator,
87
+ attributes: {
88
+ options: pageOptions,
89
+ },
90
+ },
91
+ {
92
+ name: "description",
93
+ label: req.__("Description"),
94
+ type: "String",
95
+ sublabel: req.__("A description of the group member"),
96
+ },
97
+ {
98
+ name: "eligible_formula",
99
+ label: req.__("Eligible Formula"),
100
+ sublabel:
101
+ req.__("Formula to determine if this page should be served.") +
102
+ br() +
103
+ span(
104
+ "Variables in scope: ",
105
+ [
106
+ "width",
107
+ "height",
108
+ "innerWidth",
109
+ "innerHeight",
110
+ "user",
111
+ "locale",
112
+ "device",
113
+ ]
114
+ .map((f) => code(f))
115
+ .join(", ")
116
+ ),
117
+ help: {
118
+ topic: "Eligible Formula",
119
+ },
120
+ type: "String",
121
+ required: true,
122
+ class: "validate-expression",
123
+ },
124
+ ],
125
+ additionalButtons: [
126
+ {
127
+ label: req.__("Cancel"),
128
+ class: "btn btn-primary",
129
+ onclick: `cancelMemberEdit('${groupName}');`,
130
+ },
131
+ ],
132
+ });
133
+ };
134
+
135
+ const editMemberForm = async (member, req) => {
136
+ const group = PageGroup.findOne({ id: member.page_group_id });
137
+ const validator = (s, whole) => {
138
+ const page = Page.findOne({ name: s });
139
+ if (group.members.find((m) => m.page_id === page.id && +whole.id !== m.id))
140
+ return req.__("A member with this page already exists");
141
+ };
142
+ return await memberForm(
143
+ `/page_groupedit/edit-member/${member.id}`,
144
+ req,
145
+ group.name,
146
+ validator
147
+ );
148
+ };
149
+
150
+ const addMemberForm = async (group, req) => {
151
+ const groupPages = await group.loadPages();
152
+ const validator = (s) => {
153
+ if (groupPages.find((page) => page.name === s))
154
+ return req.__("A member with this page already exists");
155
+ };
156
+ return await memberForm(
157
+ `/page_groupedit/add-member/${group.name}`,
158
+ req,
159
+ group.name,
160
+ validator
161
+ );
162
+ };
163
+
164
+ const wrapGroup = (contents, req) => {
165
+ return {
166
+ above: [
167
+ {
168
+ type: "breadcrumbs",
169
+ crumbs: [
170
+ { text: req.__("Page Groups"), href: "/pageedit" },
171
+ { text: req.__("Create") },
172
+ ],
173
+ },
174
+ {
175
+ type: "card",
176
+ title: req.__("New"),
177
+ contents,
178
+ },
179
+ ],
180
+ };
181
+ };
182
+
183
+ const wrapMember = (contents, req, pageGroup, pageMember) => {
184
+ const memberCrumb = (pageId) => {
185
+ const page = Page.findOne({ id: pageId });
186
+ return page ? { text: page.name, href: `/page/${page.name}` } : {};
187
+ };
188
+ return {
189
+ above: [
190
+ {
191
+ type: "breadcrumbs",
192
+ crumbs: [
193
+ { text: req.__("Pages"), href: "/pageedit" },
194
+ { text: pageGroup.name, href: `/page_groupedit/${pageGroup.name}` },
195
+ pageMember
196
+ ? memberCrumb(pageMember.page_id)
197
+ : { text: req.__("add-member") },
198
+ ],
199
+ },
200
+ {
201
+ type: "card",
202
+ title: pageMember
203
+ ? req.__("edit member of %s", pageGroup.name)
204
+ : req.__("add member to %s", pageGroup.name),
205
+ contents,
206
+ },
207
+ ],
208
+ };
209
+ };
210
+
211
+ const pageGroupMembers = async (pageGroup, req) => {
212
+ const db = require("@saltcorn/data/db");
213
+ const pages = !db.isSQLite
214
+ ? await Page.find({
215
+ id: { in: pageGroup.members.map((r) => r.page_id) },
216
+ })
217
+ : await Page.find();
218
+ const pageIdToName = pages.reduce((acc, page) => {
219
+ acc[page.id] = page.name;
220
+ return acc;
221
+ }, {});
222
+ let members = pageGroup.sortedMembers();
223
+ const upDownBtns = (r, req) => {
224
+ if (members.length <= 1) return "";
225
+ else
226
+ return div(
227
+ { class: "container" },
228
+ div(
229
+ { class: "row" },
230
+ div(
231
+ { class: "col-1" },
232
+ r.sequence !== members[0].sequence
233
+ ? post_btn(
234
+ `/page_groupedit/move-member/${r.id}/Up`,
235
+ `<i class="fa fa-arrow-up"></i>`,
236
+ req.csrfToken(),
237
+ {
238
+ small: true,
239
+ ajax: true,
240
+ reload_on_done: true,
241
+ btnClass: "btn btn-secondary btn-sm me-1",
242
+ req,
243
+ formClass: "d-inline",
244
+ }
245
+ )
246
+ : ""
247
+ ),
248
+ div(
249
+ { class: "col-1" },
250
+ r.sequence !== members[members.length - 1].sequence
251
+ ? post_btn(
252
+ `/page_groupedit/move-member/${r.id}/Down`,
253
+ `<i class="fa fa-arrow-down"></i>`,
254
+ req.csrfToken(),
255
+ {
256
+ small: true,
257
+ ajax: true,
258
+ reload_on_done: true,
259
+ btnClass: "btn btn-secondary btn-sm me-1",
260
+ req,
261
+ formClass: "d-inline",
262
+ }
263
+ )
264
+ : ""
265
+ )
266
+ )
267
+ );
268
+ };
269
+
270
+ return mkTable(
271
+ [
272
+ {
273
+ label: req.__("Page"),
274
+ key: (r) =>
275
+ link(`/page/${pageIdToName[r.page_id]}`, pageIdToName[r.page_id]),
276
+ },
277
+ {
278
+ label: "",
279
+ key: (r) => upDownBtns(r, req),
280
+ },
281
+ {
282
+ label: req.__("Edit"),
283
+ key: (member) =>
284
+ link(`/page_groupedit/edit-member/${member.id}`, req.__("Edit")),
285
+ },
286
+ {
287
+ label: req.__("Delete"),
288
+ key: (member) =>
289
+ post_delete_btn(
290
+ `/page_groupedit/remove-member/${member.id}`,
291
+ req,
292
+ req.__("Member %s", member.sequence)
293
+ ),
294
+ },
295
+ ],
296
+ members,
297
+ {
298
+ hover: true,
299
+ }
300
+ );
301
+ };
302
+
303
+ /**
304
+ * load a form to create a new page group
305
+ */
306
+ router.get(
307
+ "/new",
308
+ isAdmin,
309
+ error_catcher(async (req, res) => {
310
+ const form = await groupPropsForm(req, true);
311
+ res.sendWrap(
312
+ req.__(`New page group`),
313
+ wrapGroup(renderForm(form, req.csrfToken()), req)
314
+ );
315
+ })
316
+ );
317
+
318
+ /**
319
+ * load the page group editor
320
+ */
321
+ router.get(
322
+ "/:page_groupname",
323
+ isAdmin,
324
+ error_catcher(async (req, res) => {
325
+ const { page_groupname } = req.params;
326
+ const pageGroup = PageGroup.findOne({ name: page_groupname });
327
+ const propertiesForm = await groupPropsForm(req);
328
+ propertiesForm.hidden("id");
329
+ propertiesForm.values = pageGroup;
330
+ res.sendWrap(req.__("%s edit", page_groupname), {
331
+ above: [
332
+ {
333
+ type: "breadcrumbs",
334
+ crumbs: [
335
+ { text: req.__("Pages"), href: "/pageedit" },
336
+ {
337
+ text: pageGroup.name,
338
+ href: `/page/${page_groupname}`,
339
+ pageGroupLink: true,
340
+ },
341
+ ],
342
+ },
343
+ {
344
+ type: "card",
345
+ title: req.__("Members"),
346
+ contents: div(
347
+ await pageGroupMembers(pageGroup, req),
348
+ a(
349
+ {
350
+ href: `/page_groupedit/add-member/${pageGroup.name}`,
351
+ class: "btn btn-primary",
352
+ },
353
+ req.__("Add member")
354
+ )
355
+ ),
356
+ },
357
+ {
358
+ type: "card",
359
+ title: req.__("Edit group properties"),
360
+ titleAjaxIndicator: true,
361
+ contents: div(renderForm(propertiesForm, req.csrfToken())),
362
+ },
363
+ ],
364
+ });
365
+ })
366
+ );
367
+ /**
368
+ * edit the properties of a page group
369
+ */
370
+ router.post(
371
+ "/edit-properties",
372
+ isAdmin,
373
+ error_catcher(async (req, res) => {
374
+ const form = await groupPropsForm(req, !req.body.id);
375
+ form.hidden("id");
376
+ form.validate(req.body);
377
+ if (form.hasErrors) {
378
+ if (!req.xhr) {
379
+ // from new
380
+ res.sendWrap(
381
+ req.__(`Pagegroup attributes`),
382
+ wrapGroup(renderForm(form, req.csrfToken()), req)
383
+ );
384
+ } else {
385
+ // from edit
386
+ const error = form.errorSummary;
387
+ getState().log(2, `POST /page_groupedit/edit-properties: '${error}'`);
388
+ res.status(400).json({ notify: { type: "danger", text: error } });
389
+ }
390
+ } else {
391
+ const { id, ...row } = form.values;
392
+ if (+id) {
393
+ await PageGroup.update(id, row);
394
+ res.json({ success: "ok", row });
395
+ } else {
396
+ const pageGroup = await PageGroup.create(row);
397
+ res.redirect(`/page_groupedit/${pageGroup.name}`);
398
+ }
399
+ }
400
+ })
401
+ );
402
+
403
+ /**
404
+ * load a form to add a member to a group
405
+ */
406
+ router.get(
407
+ "/add-member/:page_groupname",
408
+ isAdmin,
409
+ error_catcher(async (req, res) => {
410
+ const { page_groupname } = req.params;
411
+ const group = PageGroup.findOne({ name: page_groupname });
412
+ if (!group) {
413
+ req.flash("error", req.__("Page group %s not found", page_groupname));
414
+ res.redirect(`/page_groupedit/${page_groupname}`);
415
+ } else {
416
+ const form = await addMemberForm(group, req);
417
+ res.sendWrap(
418
+ req.__(`%s add-member`, group.name),
419
+ wrapMember(renderForm(form, req.csrfToken()), req, group)
420
+ );
421
+ }
422
+ })
423
+ );
424
+
425
+ /**
426
+ * add a member to a group
427
+ */
428
+ router.post(
429
+ "/add-member/:page_groupname",
430
+ isAdmin,
431
+ error_catcher(async (req, res) => {
432
+ const { page_groupname } = req.params;
433
+ const group = PageGroup.findOne({ name: page_groupname });
434
+ if (!group) {
435
+ req.flash("error", req.__("Page group %s not found", page_groupname));
436
+ res.redirect(`/page_groupedit/${page_groupname}`);
437
+ }
438
+ const form = await addMemberForm(group, req);
439
+ form.validate(req.body);
440
+ if (form.hasErrors) {
441
+ res.sendWrap(
442
+ req.__(`%s add-member`, group.name),
443
+ wrapMember(renderForm(form, req.csrfToken()), req, group)
444
+ );
445
+ } else {
446
+ const { page_name, eligible_formula, description } = form.values;
447
+ const page = Page.findOne({ name: page_name });
448
+ if (!page) {
449
+ req.flash("error", req.__("Page %s not found", page_name));
450
+ res.sendWrap(
451
+ req.__(`%s add-member`, group.name),
452
+ wrapMember(renderForm(form, req.csrfToken()), req, group)
453
+ );
454
+ } else {
455
+ await group.addMember({
456
+ page_id: page.id,
457
+ eligible_formula,
458
+ description: description || "",
459
+ });
460
+ req.flash("success", req.__("Added member"));
461
+ res.redirect(`/page_groupedit/${page_groupname}`);
462
+ }
463
+ }
464
+ })
465
+ );
466
+
467
+ /**
468
+ * move a group-member up/dowm
469
+ */
470
+ router.post(
471
+ "/move-member/:member_id/:mode",
472
+ isAdmin,
473
+ error_catcher(async (req, res) => {
474
+ const { member_id, mode } = req.params;
475
+ try {
476
+ const member = PageGroupMember.findOne({ id: member_id });
477
+ const pageGroup = PageGroup.findOne({ id: member.page_group_id });
478
+ await pageGroup.moveMember(member, mode);
479
+ res.json({ success: "ok" });
480
+ } catch (error) {
481
+ getState().log(2, `POST /page_groupedit/move-member: '${error.message}'`);
482
+ res.status(400).json({ error: error.message || error });
483
+ }
484
+ })
485
+ );
486
+
487
+ /**
488
+ * load a form to edit a group-member
489
+ */
490
+ router.get(
491
+ "/edit-member/:member_id",
492
+ isAdmin,
493
+ error_catcher(async (req, res) => {
494
+ const { member_id } = req.params;
495
+ const member = PageGroupMember.findOne({ id: member_id });
496
+ if (!member) {
497
+ req.flash("error", req.__("member %s does not exist", member_id));
498
+ return res.redirect("/pageedit");
499
+ }
500
+ const group = PageGroup.findOne({ id: member.page_group_id });
501
+ if (!group) {
502
+ req.flash(
503
+ "error",
504
+ req.__("Page group %s not found", member.page_group_id)
505
+ );
506
+ return res.redirect("/pageedit");
507
+ }
508
+ const page = Page.findOne({ id: member.page_id });
509
+ if (!page) {
510
+ req.flash("error", req.__("Page %s not found", member.page_id));
511
+ return res.redirect(`/page_groupedit/${group.name}`);
512
+ }
513
+ const form = await editMemberForm(member, req);
514
+ form.hidden("id");
515
+ form.values = { ...member, page_name: page.name };
516
+
517
+ res.sendWrap(
518
+ req.__(`%s edit-member`, member.name || member.id),
519
+ wrapMember(renderForm(form, req.csrfToken()), req, group, member)
520
+ );
521
+ })
522
+ );
523
+
524
+ /**
525
+ * edit a member of a group
526
+ */
527
+ router.post(
528
+ "/edit-member/:member_id",
529
+ isAdmin,
530
+ error_catcher(async (req, res) => {
531
+ const { member_id } = req.params;
532
+ const member = PageGroupMember.findOne({ id: member_id });
533
+ const group = PageGroup.findOne({ id: member.page_group_id });
534
+ const form = await editMemberForm(member, req);
535
+ form.hidden("id");
536
+ form.validate(req.body);
537
+ if (form.hasErrors) {
538
+ res.sendWrap(
539
+ req.__(`%s edit-member`, member.name || member.id),
540
+ wrapMember(renderForm(form, req.csrfToken()), req, group, member)
541
+ );
542
+ } else {
543
+ const { id, page_name, eligible_formula, description } = form.values;
544
+ const page = Page.findOne({ name: page_name });
545
+ if (!page) {
546
+ req.flash("error", req.__("Page %s not found", page_name));
547
+ res.redirect(`/page_groupedit/${group.name}`);
548
+ } else
549
+ await PageGroupMember.update(id, {
550
+ page_group_id: group.id,
551
+ page_id: page.id,
552
+ eligible_formula,
553
+ description: description || "",
554
+ });
555
+ req.flash("success", req.__("Updated member"));
556
+ res.redirect(`/page_groupedit/${group.name}`);
557
+ }
558
+ })
559
+ );
560
+
561
+ /**
562
+ * delete a group
563
+ */
564
+ router.post(
565
+ "/delete/:group_id",
566
+ isAdmin,
567
+ error_catcher(async (req, res) => {
568
+ const { group_id } = req.params;
569
+ const group = PageGroup.findOne({ id: group_id });
570
+ if (!group) {
571
+ req.flash("error", req.__("Page group %s not found", group_id));
572
+ res.redirect("/pageedit");
573
+ } else {
574
+ await group.delete();
575
+ req.flash("success", req.__("Deleted page group %s", group_id));
576
+ res.redirect("/pageedit");
577
+ }
578
+ })
579
+ );
580
+
581
+ /**
582
+ * delete a group-member
583
+ */
584
+ router.post(
585
+ "/remove-member/:member_id",
586
+ isAdmin,
587
+ error_catcher(async (req, res) => {
588
+ const { member_id } = req.params;
589
+ const member = PageGroupMember.findOne({ id: member_id });
590
+ if (!member) {
591
+ req.flash("error", req.__("Page group member %s not found", member_id));
592
+ res.redirect("/pageedit");
593
+ } else {
594
+ const group = PageGroup.findOne({ id: member.page_group_id });
595
+ if (!group) {
596
+ req.flash(
597
+ "error",
598
+ req.__("Page group %s not found", member.page_group_id)
599
+ );
600
+ res.redirect("/pageedit");
601
+ } else {
602
+ try {
603
+ await group.removeMember(member_id);
604
+ req.flash(
605
+ "success",
606
+ req.__("Removed member %s", member.name || member_id)
607
+ );
608
+ res.redirect(`/page_groupedit/${group.name}`);
609
+ } catch (e) {
610
+ req.flash("error", e.message);
611
+ res.redirect(`/page_groupedit/${group.name}`);
612
+ }
613
+ }
614
+ }
615
+ })
616
+ );
617
+
618
+ /**
619
+ * clone a group
620
+ */
621
+ router.post(
622
+ "/clone/:id",
623
+ isAdmin,
624
+ error_catcher(async (req, res) => {
625
+ const { id } = req.params;
626
+ const group = PageGroup.findOne({ id });
627
+ if (!group) {
628
+ req.flash("error", req.__("Page group %s not found", id));
629
+ res.redirect("/pageedit");
630
+ } else {
631
+ const copy = await group.clone();
632
+ req.flash("success", req.__("Cloned page group %s", group.name));
633
+ res.redirect(`/page_groupedit/${copy.name}`);
634
+ }
635
+ })
636
+ );
637
+
638
+ /**
639
+ * add a pagegroup-link to the menu
640
+ */
641
+ router.post(
642
+ "/add-to-menu/:id",
643
+ isAdmin,
644
+ error_catcher(async (req, res) => {
645
+ const { id } = req.params;
646
+ const group = PageGroup.findOne({ id });
647
+ if (!group) {
648
+ req.flash("error", req.__("Page group %s not found", id));
649
+ res.redirect("/pageedit");
650
+ } else {
651
+ await add_to_menu({
652
+ label: group.name,
653
+ type: "Page", // TODO PageGroup
654
+ min_role: group.min_role,
655
+ pagename: group.name,
656
+ });
657
+ req.flash(
658
+ "success",
659
+ req.__(
660
+ "Page %s added to menu. Adjust access permissions in Settings &raquo; Menu",
661
+ group.name
662
+ )
663
+ );
664
+ res.redirect(`/pageedit`);
665
+ }
666
+ })
667
+ );
668
+
669
+ /**
670
+ * set the role of a group
671
+ */
672
+ router.post(
673
+ "/setrole/:id",
674
+ isAdmin,
675
+ error_catcher(async (req, res) => {
676
+ await setRole(req, res, PageGroup);
677
+ })
678
+ );