@saltcorn/server 0.7.3 → 0.7.4-beta.2

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/routes/tables.js CHANGED
@@ -56,6 +56,7 @@ const {
56
56
  } = require("@saltcorn/data/models/discovery");
57
57
  const { getState } = require("@saltcorn/data/db/state");
58
58
  const { cardHeaderTabs } = require("@saltcorn/markup/layout_utils");
59
+ const { tablesList } = require("./common_lists");
59
60
 
60
61
  /**
61
62
  * @type {object}
@@ -88,33 +89,33 @@ const tableForm = async (table, req) => {
88
89
  fields: [
89
90
  ...(!table.external
90
91
  ? [
91
- {
92
- label: req.__("Ownership field"),
93
- name: "ownership_field_id",
94
- sublabel: req.__(
95
- "The user referred to in this field will be the owner of the row"
96
- ),
97
- input_type: "select",
98
- options: [
99
- { value: "", label: req.__("None") },
100
- ...userFields,
101
- { value: "_formula", label: req.__("Formula") },
102
- ],
103
- },
104
- {
105
- name: "ownership_formula",
106
- label: req.__("Ownership formula"),
107
- validator: expressionValidator,
108
- type: "String",
109
- class: "validate-expression",
110
- sublabel:
111
- req.__("User is treated as owner if true. In scope: ") +
112
- ["user", ...fields.map((f) => f.name)]
113
- .map((fn) => code(fn))
114
- .join(", "),
115
- showIf: { ownership_field_id: "_formula" },
116
- },
117
- ]
92
+ {
93
+ label: req.__("Ownership field"),
94
+ name: "ownership_field_id",
95
+ sublabel: req.__(
96
+ "The user referred to in this field will be the owner of the row"
97
+ ),
98
+ input_type: "select",
99
+ options: [
100
+ { value: "", label: req.__("None") },
101
+ ...userFields,
102
+ { value: "_formula", label: req.__("Formula") },
103
+ ],
104
+ },
105
+ {
106
+ name: "ownership_formula",
107
+ label: req.__("Ownership formula"),
108
+ validator: expressionValidator,
109
+ type: "String",
110
+ class: "validate-expression",
111
+ sublabel:
112
+ req.__("User is treated as owner if true. In scope: ") +
113
+ ["user", ...fields.map((f) => f.name)]
114
+ .map((fn) => code(fn))
115
+ .join(", "),
116
+ showIf: { ownership_field_id: "_formula" },
117
+ },
118
+ ]
118
119
  : []),
119
120
  // description of table
120
121
  {
@@ -129,7 +130,7 @@ const tableForm = async (table, req) => {
129
130
  {
130
131
  label: req.__("Minimum role to read"),
131
132
  sublabel: req.__(
132
- "User must have this role or higher to read rows from the table"
133
+ "User must have this role or higher to read rows from the table, unless they are the owner"
133
134
  ),
134
135
  name: "min_role_read",
135
136
  input_type: "select",
@@ -138,24 +139,24 @@ const tableForm = async (table, req) => {
138
139
  ...(table.external
139
140
  ? []
140
141
  : [
141
- {
142
- label: req.__("Minimum role to write"),
143
- name: "min_role_write",
144
- input_type: "select",
145
- sublabel: req.__(
146
- "User must have this role or higher to edit or create new rows in the table"
147
- ),
148
- options: roleOptions,
149
- },
150
- {
151
- label: req.__("Version history"),
152
- sublabel: req.__(
153
- "Version history allows to track table data changes"
154
- ),
155
- name: "versioned",
156
- type: "Bool",
157
- },
158
- ]),
142
+ {
143
+ label: req.__("Minimum role to write"),
144
+ name: "min_role_write",
145
+ input_type: "select",
146
+ sublabel: req.__(
147
+ "User must have this role or higher to edit or create new rows in the table, unless they are the owner"
148
+ ),
149
+ options: roleOptions,
150
+ },
151
+ {
152
+ label: req.__("Version history"),
153
+ sublabel: req.__(
154
+ "Version history allows to track table data changes"
155
+ ),
156
+ name: "versioned",
157
+ type: "Bool",
158
+ },
159
+ ]),
159
160
  ],
160
161
  });
161
162
  if (table) {
@@ -222,11 +223,11 @@ const discoverForm = (tables, req) => {
222
223
  blurb:
223
224
  tables.length > 0
224
225
  ? req.__(
225
- "The following tables in your database can be imported into Saltcorn:"
226
- )
226
+ "The following tables in your database can be imported into Saltcorn:"
227
+ )
227
228
  : req.__(
228
- "There are no tables in the database that can be imported into Saltcorn."
229
- ),
229
+ "There are no tables in the database that can be imported into Saltcorn."
230
+ ),
230
231
  submitLabel: req.__("Import"),
231
232
  fields: tables.map((t) => ({
232
233
  name: t.table_name,
@@ -579,11 +580,11 @@ router.get(
579
580
  key: (r) =>
580
581
  r.type === "Key"
581
582
  ? `Key to ` +
582
- a({ href: `/table/${r.reftable_name}` }, r.reftable_name)
583
+ a({ href: `/table/${r.reftable_name}` }, r.reftable_name)
583
584
  : (r.type && r.type.name) ||
584
- r.type ||
585
- r.typename +
586
- span({ class: "badge bg-danger ms-1" }, "Unknown type"),
585
+ r.type ||
586
+ r.typename +
587
+ span({ class: "badge bg-danger ms-1" }, "Unknown type"),
587
588
  },
588
589
  {
589
590
  label: "",
@@ -597,23 +598,23 @@ router.get(
597
598
  ...(table.external
598
599
  ? []
599
600
  : [
600
- {
601
- label: req.__("Edit"),
602
- key: (r) => link(`/field/${r.id}`, req.__("Edit")),
603
- },
604
- ]),
601
+ {
602
+ label: req.__("Edit"),
603
+ key: (r) => link(`/field/${r.id}`, req.__("Edit")),
604
+ },
605
+ ]),
605
606
  ...(table.external || db.isSQLite
606
607
  ? []
607
608
  : [
608
- {
609
- label: req.__("Delete"),
610
- key: (r) =>
611
- (table.name === "users" && r.name === "email") ||
609
+ {
610
+ label: req.__("Delete"),
611
+ key: (r) =>
612
+ (table.name === "users" && r.name === "email") ||
612
613
  r.primary_key
613
- ? ""
614
- : post_delete_btn(`/field/delete/${r.id}`, req, r.name),
615
- },
616
- ]),
614
+ ? ""
615
+ : post_delete_btn(`/field/delete/${r.id}`, req, r.name),
616
+ },
617
+ ]),
617
618
  ],
618
619
  fields,
619
620
  { hover: true }
@@ -622,17 +623,17 @@ router.get(
622
623
  tableHtml,
623
624
  inbound_refs.length > 0
624
625
  ? req.__("Inbound keys: ") +
625
- inbound_refs.map((tnm) => link(`/table/${tnm}`, tnm)).join(", ") +
626
- "<br>"
626
+ inbound_refs.map((tnm) => link(`/table/${tnm}`, tnm)).join(", ") +
627
+ "<br>"
627
628
  : "",
628
629
  !table.external &&
629
- a(
630
- {
631
- href: `/field/new/${table.id}`,
632
- class: "btn btn-primary add-field mt-2",
633
- },
634
- req.__("Add field")
635
- ),
630
+ a(
631
+ {
632
+ href: `/field/new/${table.id}`,
633
+ class: "btn btn-primary add-field mt-2",
634
+ },
635
+ req.__("Add field")
636
+ ),
636
637
  ];
637
638
  }
638
639
  var viewCard;
@@ -645,7 +646,7 @@ router.get(
645
646
  viewCardContents = mkTable(
646
647
  [
647
648
  { label: req.__("Name"), key: "name" },
648
- { label: req.__("Template"), key: "viewtemplate" },
649
+ { label: req.__("Pattern"), key: "viewtemplate" },
649
650
  {
650
651
  label: req.__("Run"),
651
652
  key: (r) =>
@@ -704,8 +705,8 @@ router.get(
704
705
  table.name === "users"
705
706
  ? `/useradmin/`
706
707
  : fields.length === 1
707
- ? `javascript:;` // Fix problem with edition of table with only one column ID / Primary Key
708
- : `/list/${table.name}`,
708
+ ? `javascript:;` // Fix problem with edition of table with only one column ID / Primary Key
709
+ : `/list/${table.name}`,
709
710
  },
710
711
  i({ class: "fas fa-2x fa-edit" }),
711
712
  "<br/>",
@@ -725,74 +726,74 @@ router.get(
725
726
  )
726
727
  ),
727
728
  !table.external &&
728
- div(
729
- { class: "mx-auto" },
730
- form(
731
- {
732
- method: "post",
733
- action: `/table/upload_to_table/${table.name}`,
734
- encType: "multipart/form-data",
735
- },
736
- input({ type: "hidden", name: "_csrf", value: req.csrfToken() }),
737
- label(
738
- { class: "btn-link", for: "upload_to_table" },
739
- i({ class: "fas fa-2x fa-upload" }),
740
- "<br/>",
741
- req.__("Upload CSV")
742
- ),
743
- input({
744
- id: "upload_to_table",
745
- name: "file",
746
- type: "file",
747
- accept: "text/csv,.csv",
748
- onchange: "this.form.submit();",
749
- })
750
- )
751
- ),
729
+ div(
730
+ { class: "mx-auto" },
731
+ form(
732
+ {
733
+ method: "post",
734
+ action: `/table/upload_to_table/${table.name}`,
735
+ encType: "multipart/form-data",
736
+ },
737
+ input({ type: "hidden", name: "_csrf", value: req.csrfToken() }),
738
+ label(
739
+ { class: "btn-link", for: "upload_to_table" },
740
+ i({ class: "fas fa-2x fa-upload" }),
741
+ "<br/>",
742
+ req.__("Upload CSV")
743
+ ),
744
+ input({
745
+ id: "upload_to_table",
746
+ name: "file",
747
+ type: "file",
748
+ accept: "text/csv,.csv",
749
+ onchange: "this.form.submit();",
750
+ })
751
+ )
752
+ ),
752
753
  // only if table is not external
753
754
  !table.external &&
754
- div(
755
- { class: "mx-auto" },
756
- settingsDropdown(`dataMenuButton`, [
757
- a(
758
- {
759
- class: "dropdown-item",
760
- href: `/table/constraints/${table.id}`,
761
- },
762
- '<i class="fas fa-ban"></i>&nbsp;' + req.__("Constraints")
763
- ),
764
- // rename table doesnt supported for sqlite
765
- !db.isSQLite &&
766
- table.name !== "users" &&
767
- a(
768
- {
769
- class: "dropdown-item",
770
- href: `/table/rename/${table.id}`,
771
- },
772
- '<i class="fas fa-edit"></i>&nbsp;' + req.__("Rename table")
773
- ),
774
- post_dropdown_item(
775
- `/table/recalc-stored/${table.name}`,
776
- '<i class="fas fa-sync"></i>&nbsp;' +
777
- req.__("Recalculate stored fields"),
778
- req
779
- ),
780
- post_dropdown_item(
781
- `/table/delete-all-rows/${table.name}`,
782
- '<i class="far fa-trash-alt"></i>&nbsp;' +
783
- req.__("Delete all rows"),
784
- req,
785
- true
786
- ),
787
- table.name !== "users" &&
788
- post_dropdown_item(
789
- `/table/forget-table/${table.id}`,
790
- '<i class="fas fa-recycle"></i>&nbsp;' + req.__("Forget table"),
791
- req,
792
- true
793
- ),
794
- ])
795
- )
755
+ div(
756
+ { class: "mx-auto" },
757
+ settingsDropdown(`dataMenuButton`, [
758
+ a(
759
+ {
760
+ class: "dropdown-item",
761
+ href: `/table/constraints/${table.id}`,
762
+ },
763
+ '<i class="fas fa-ban"></i>&nbsp;' + req.__("Constraints")
764
+ ),
765
+ // rename table doesnt supported for sqlite
766
+ !db.isSQLite &&
767
+ table.name !== "users" &&
768
+ a(
769
+ {
770
+ class: "dropdown-item",
771
+ href: `/table/rename/${table.id}`,
772
+ },
773
+ '<i class="fas fa-edit"></i>&nbsp;' + req.__("Rename table")
774
+ ),
775
+ post_dropdown_item(
776
+ `/table/recalc-stored/${table.name}`,
777
+ '<i class="fas fa-sync"></i>&nbsp;' +
778
+ req.__("Recalculate stored fields"),
779
+ req
780
+ ),
781
+ post_dropdown_item(
782
+ `/table/delete-all-rows/${table.name}`,
783
+ '<i class="far fa-trash-alt"></i>&nbsp;' +
784
+ req.__("Delete all rows"),
785
+ req,
786
+ true
787
+ ),
788
+ table.name !== "users" &&
789
+ post_dropdown_item(
790
+ `/table/forget-table/${table.id}`,
791
+ '<i class="fas fa-recycle"></i>&nbsp;' + req.__("Forget table"),
792
+ req,
793
+ true
794
+ ),
795
+ ])
796
+ )
796
797
  );
797
798
  // add table form
798
799
  if (table.ownership_formula && !table.ownership_field_id)
@@ -815,12 +816,12 @@ router.get(
815
816
  },
816
817
  ...(fields.length > 0
817
818
  ? [
818
- {
819
- type: "card",
820
- title: req.__("Table data"),
821
- contents: dataCard,
822
- },
823
- ]
819
+ {
820
+ type: "card",
821
+ title: req.__("Table data"),
822
+ contents: dataCard,
823
+ },
824
+ ]
824
825
  : []),
825
826
  ...(viewCard ? [viewCard] : []),
826
827
  {
@@ -888,7 +889,6 @@ router.post(
888
889
  if (rest.ownership_field_id === "_formula") {
889
890
  rest.ownership_field_id = null;
890
891
  const fmlValidRes = expressionValidator(rest.ownership_formula);
891
- console.log({ fmlValidRes });
892
892
  if (typeof fmlValidRes === "string") {
893
893
  req.flash(
894
894
  "error",
@@ -1011,42 +1011,7 @@ router.get(
1011
1011
  const rows = await Table.find_with_external({}, { orderBy: "name" });
1012
1012
  const roles = await User.get_roles();
1013
1013
  const getRole = (rid) => roles.find((r) => r.id === rid).role;
1014
- const mainCard =
1015
- rows.length > 0
1016
- ? mkTable(
1017
- [
1018
- {
1019
- label: req.__("Name"),
1020
- key: (r) => link(`/table/${r.id || r.name}`, text(r.name)),
1021
- },
1022
- {
1023
- label: "",
1024
- key: (r) => tableBadges(r, req),
1025
- },
1026
- {
1027
- label: req.__("Access Read/Write"),
1028
- key: (t) =>
1029
- t.external
1030
- ? `${getRole(t.min_role_read)} (read only)`
1031
- : `${getRole(t.min_role_read)}/${getRole(
1032
- t.min_role_write
1033
- )}`,
1034
- },
1035
- {
1036
- label: req.__("Delete"),
1037
- key: (r) =>
1038
- r.name === "users" || r.external
1039
- ? ""
1040
- : post_delete_btn(`/table/delete/${r.id}`, req, r.name),
1041
- },
1042
- ],
1043
- rows,
1044
- { hover: true }
1045
- )
1046
- : div(
1047
- h4(req.__("No tables defined")),
1048
- p(req.__("Tables hold collections of similar data"))
1049
- );
1014
+ const mainCard = await tablesList(rows, req);
1050
1015
  const createCard = div(
1051
1016
  h5(req.__("Create table")),
1052
1017
  a(
@@ -1063,11 +1028,11 @@ router.get(
1063
1028
  req.__("Create from CSV upload")
1064
1029
  ),
1065
1030
  !db.isSQLite &&
1066
- a(
1067
- { href: `/table/discover`, class: "btn btn-secondary mt-1" },
1068
- i({ class: "fas fa-map-signs me-1" }),
1069
- req.__("Discover tables")
1070
- )
1031
+ a(
1032
+ { href: `/table/discover`, class: "btn btn-secondary mt-1" },
1033
+ i({ class: "fas fa-map-signs me-1" }),
1034
+ req.__("Discover tables")
1035
+ )
1071
1036
  );
1072
1037
  res.sendWrap(req.__("Tables"), {
1073
1038
  above: [
@@ -0,0 +1,173 @@
1
+ const {
2
+ a,
3
+ div,
4
+ text,
5
+ button,
6
+ i,
7
+ form,
8
+ select,
9
+ option,
10
+ label,
11
+ } = require("@saltcorn/markup/tags");
12
+
13
+ const Tag = require("@saltcorn/data/models/tag");
14
+ const TagEntry = require("@saltcorn/data/models/tag_entry");
15
+ const Router = require("express-promise-router");
16
+
17
+ const { isAdmin, error_catcher, csrfField } = require("./utils");
18
+
19
+ const Table = require("@saltcorn/data/models/table");
20
+ const View = require("@saltcorn/data/models/view");
21
+ const Page = require("@saltcorn/data/models/page");
22
+ const Trigger = require("@saltcorn/data/models/trigger");
23
+
24
+ const router = new Router();
25
+ module.exports = router;
26
+
27
+ const buildFields = (entryType, formOptions, req) => {
28
+ return Object.entries(formOptions).map(([type, list]) => {
29
+ return div(
30
+ { class: "form-group row" },
31
+ div({ class: "col-sm-2" }, label("type")),
32
+ div(
33
+ { class: "col-sm-10" },
34
+ select(
35
+ { name: "ids", class: "form-control form-select", multiple: true },
36
+ list.map((entry) => {
37
+ return option({ value: entry.id, label: entry.name });
38
+ })
39
+ )
40
+ ),
41
+ div(
42
+ { class: "col-sm-12" },
43
+ button({ type: "submit", class: "btn btn-primary" }, req.__("Save"))
44
+ )
45
+ );
46
+ });
47
+ };
48
+
49
+ const buildForm = (entryType, tag_id, formOptions, req) => {
50
+ return form(
51
+ { action: `/tag-entries/add/${entryType}/${tag_id}`, method: "post" },
52
+ csrfField(req),
53
+ buildFields(entryType, formOptions, req)
54
+ );
55
+ };
56
+
57
+ const formOptions = async (type, tag_id) => {
58
+ const tag = await Tag.findOne({ id: tag_id });
59
+ switch (type) {
60
+ case "tables": {
61
+ const ids = await tag.getTableIds();
62
+ return {
63
+ tables: (await Table.find()).filter(
64
+ (value) => ids.indexOf(value.id) === -1
65
+ ),
66
+ };
67
+ }
68
+ case "views": {
69
+ const ids = await tag.getViewIds();
70
+ return {
71
+ views: (await View.find()).filter(
72
+ (value) => ids.indexOf(value.id) === -1
73
+ ),
74
+ };
75
+ }
76
+ case "pages": {
77
+ const ids = await tag.getPageIds();
78
+ return {
79
+ pages: (await Page.find()).filter(
80
+ (value) => ids.indexOf(value.id) === -1
81
+ ),
82
+ };
83
+ }
84
+ case "trigger": {
85
+ const ids = await tag.getTriggerIds();
86
+ return {
87
+ trigger: (await Trigger.find()).filter(
88
+ (value) => ids.indexOf(value.id) === -1
89
+ ),
90
+ };
91
+ }
92
+ }
93
+ };
94
+
95
+ router.get(
96
+ "/add/:entry_type/:tag_id",
97
+ isAdmin,
98
+ error_catcher(async (req, res) => {
99
+ const { entry_type, tag_id } = req.params;
100
+ res.sendWrap(req.__("Add %s to tag"), {
101
+ above: [
102
+ {
103
+ type: "breadcrumbs",
104
+ crumbs: [{ text: `Tag entry` }],
105
+ },
106
+ {
107
+ type: "card",
108
+ title: `Add entries to tag`,
109
+ contents: buildForm(
110
+ entry_type,
111
+ tag_id,
112
+ await formOptions(entry_type, tag_id),
113
+ req
114
+ ),
115
+ },
116
+ ],
117
+ });
118
+ })
119
+ );
120
+
121
+ const idField = (entryType) => {
122
+ switch (entryType) {
123
+ case "tables": {
124
+ return "table_id";
125
+ }
126
+ case "views": {
127
+ return "view_id";
128
+ }
129
+ case "pages": {
130
+ return "page_id";
131
+ }
132
+ case "trigger": {
133
+ return "trigger_id";
134
+ }
135
+ }
136
+ return null;
137
+ };
138
+
139
+ router.post(
140
+ "/add/:entry_type/:tag_id",
141
+ isAdmin,
142
+ error_catcher(async (req, res) => {
143
+ const { entry_type, tag_id } = req.params;
144
+ const { ids } = req.body;
145
+ if (!ids) {
146
+ req.flash("error", req.__("Please select at least on item"));
147
+ return res.redirect(`/tag-entries/add/${entry_type}/${tag_id}`);
148
+ }
149
+ const fieldName = idField(entry_type);
150
+ const tag = await Tag.findOne({ id: tag_id });
151
+ for (const id of ids) {
152
+ await tag.addEntry({ [fieldName]: id });
153
+ }
154
+ res.redirect(`/tag/${tag_id}?show_list=${entry_type}`);
155
+ })
156
+ );
157
+
158
+ router.post(
159
+ "/remove/:entry_type/:entry_id/:tag_id",
160
+ isAdmin,
161
+ error_catcher(async (req, res) => {
162
+ const { tag_id, entry_type, entry_id } = req.params;
163
+ const fieldName = idField(entry_type);
164
+ const entry = await TagEntry.findOne({ tag_id, [fieldName]: entry_id });
165
+ entry[fieldName] = undefined;
166
+ if (entry.isEmpty()) {
167
+ await entry.delete();
168
+ } else {
169
+ await TagEntry.update(entry.id, { [fieldName]: null });
170
+ }
171
+ res.redirect(`/tag/${tag_id}?show_list=${entry_type}`);
172
+ })
173
+ );