@saltcorn/server 0.9.4-beta.8 → 0.9.4-beta.9

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
@@ -13,6 +13,7 @@ const View = require("@saltcorn/data/models/view");
13
13
  const User = require("@saltcorn/data/models/user");
14
14
  const Model = require("@saltcorn/data/models/model");
15
15
  const Trigger = require("@saltcorn/data/models/trigger");
16
+ const TagEntry = require("@saltcorn/data/models/tag_entry");
16
17
  const {
17
18
  mkTable,
18
19
  renderForm,
@@ -64,6 +65,7 @@ const {
64
65
  const { EOL } = require("os");
65
66
 
66
67
  const path = require("path");
68
+ const Tag = require("@saltcorn/data/models/tag");
67
69
  /**
68
70
  * @type {object}
69
71
  * @const
@@ -1222,13 +1224,23 @@ router.get(
1222
1224
  "/",
1223
1225
  isAdmin,
1224
1226
  error_catcher(async (req, res) => {
1225
- const rows = await Table.find_with_external(
1226
- {},
1227
- { orderBy: "name", nocase: true }
1228
- );
1227
+ const tblq = {};
1228
+ let filterOnTag;
1229
+ if (req.query._tag) {
1230
+ const tagEntries = await TagEntry.find({
1231
+ tag_id: +req.query._tag,
1232
+ not: { table_id: null },
1233
+ });
1234
+ tblq.id = { in: tagEntries.map((te) => te.table_id).filter(Boolean) };
1235
+ filterOnTag = await Tag.findOne({ id: +req.query._tag });
1236
+ }
1237
+ const rows = await Table.find_with_external(tblq, {
1238
+ orderBy: "name",
1239
+ nocase: true,
1240
+ });
1229
1241
  const roles = await User.get_roles();
1230
1242
  const getRole = (rid) => roles.find((r) => r.id === rid).role;
1231
- const mainCard = await tablesList(rows, req);
1243
+ const mainCard = await tablesList(rows, req, { filterOnTag });
1232
1244
  const createCard = div(
1233
1245
  a(
1234
1246
  { href: `/table/new`, class: "btn btn-primary mt-1 me-3" },
@@ -5,6 +5,7 @@ const {
5
5
  select,
6
6
  option,
7
7
  label,
8
+ text,
8
9
  } = require("@saltcorn/markup/tags");
9
10
 
10
11
  const Tag = require("@saltcorn/data/models/tag");
@@ -94,15 +95,21 @@ router.get(
94
95
  isAdmin,
95
96
  error_catcher(async (req, res) => {
96
97
  const { entry_type, tag_id } = req.params;
97
- res.sendWrap(req.__("Add %s to tag"), {
98
+ const tag = await Tag.findOne({ id: tag_id });
99
+
100
+ res.sendWrap(req.__("Add %s to tag %s", entry_type, tag.name), {
98
101
  above: [
99
102
  {
100
103
  type: "breadcrumbs",
101
- crumbs: [{ text: req.__(`Tag entry`) }],
104
+ crumbs: [
105
+ { text: req.__(`Tags`), href: "/tag" },
106
+ { text: tag.name, href: `/tag/${tag.id}` },
107
+ { text: req.__(`Add %s`, text(entry_type)) },
108
+ ],
102
109
  },
103
110
  {
104
111
  type: "card",
105
- title: req.__(`Add entries to tag`),
112
+ title: req.__(`Add entries to tag %s`, tag.name),
106
113
  contents: buildForm(
107
114
  entry_type,
108
115
  tag_id,
@@ -148,9 +155,10 @@ router.post(
148
155
  req.flash("error", req.__("Please select at least one item"));
149
156
  return res.redirect(`/tag-entries/add/${entry_type}/${tag_id}`);
150
157
  }
158
+ const ids_array = Array.isArray(ids) ? ids : [ids];
151
159
  const fieldName = idField(entry_type);
152
160
  const tag = await Tag.findOne({ id: tag_id });
153
- for (const id of ids) {
161
+ for (const id of ids_array) {
154
162
  await tag.addEntry({ [fieldName]: id });
155
163
  }
156
164
  res.redirect(`/tag/${tag_id}?show_list=${entry_type}`);
package/routes/tags.js CHANGED
@@ -1,9 +1,10 @@
1
- const { a, text } = require("@saltcorn/markup/tags");
1
+ const { a, text, i } = require("@saltcorn/markup/tags");
2
2
 
3
3
  const Tag = require("@saltcorn/data/models/tag");
4
4
  const Router = require("express-promise-router");
5
5
  const Form = require("@saltcorn/data/models/form");
6
6
  const User = require("@saltcorn/data/models/user");
7
+ const stream = require("stream");
7
8
 
8
9
  const { isAdmin, error_catcher, csrfField } = require("./utils");
9
10
  const { send_infoarch_page } = require("../markup/admin");
@@ -23,6 +24,10 @@ const {
23
24
  getTriggerList,
24
25
  } = require("./common_lists");
25
26
 
27
+ const db = require("@saltcorn/data/db");
28
+ const { getState } = require("@saltcorn/data/db/state");
29
+ const { create_pack_from_tag } = require("@saltcorn/admin-models/models/pack");
30
+
26
31
  const router = new Router();
27
32
  module.exports = router;
28
33
 
@@ -42,7 +47,7 @@ router.get(
42
47
  mkTable(
43
48
  [
44
49
  {
45
- label: req.__("Tagname"),
50
+ label: req.__("Tag name"),
46
51
  key: (r) =>
47
52
  link(`/tag/${r.id || r.name}?show_list=tables`, text(r.name)),
48
53
  },
@@ -57,7 +62,7 @@ router.get(
57
62
  a(
58
63
  {
59
64
  href: `/tag/new`,
60
- class: "btn btn-primary",
65
+ class: "btn btn-primary mt-3",
61
66
  },
62
67
  req.__("Create tag")
63
68
  ),
@@ -73,6 +78,13 @@ router.get(
73
78
  error_catcher(async (req, res) => {
74
79
  res.sendWrap(req.__(`New tag`), {
75
80
  above: [
81
+ {
82
+ type: "breadcrumbs",
83
+ crumbs: [
84
+ { text: req.__(`Tags`), href: "/tag" },
85
+ { text: req.__(`New`) },
86
+ ],
87
+ },
76
88
  {
77
89
  type: "card",
78
90
  title: req.__(`New tag`),
@@ -97,7 +109,26 @@ router.get(
97
109
  })
98
110
  );
99
111
 
100
- const headerWithCollapser = (title, cardId, showList) =>
112
+ router.get(
113
+ "/download-pack/:idorname",
114
+ isAdmin,
115
+ error_catcher(async (req, res) => {
116
+ const { idorname } = req.params;
117
+ const id = parseInt(idorname);
118
+ const tag = await Tag.findOne(id ? { id } : { name: idorname });
119
+ if (!tag) {
120
+ req.flash("error", req.__("Tag not found"));
121
+ return res.redirect(`/tag`);
122
+ }
123
+ const pack = await create_pack_from_tag(tag);
124
+ const readStream = new stream.PassThrough();
125
+ readStream.end(JSON.stringify(pack));
126
+ res.type("application/json");
127
+ res.attachment(`${tag.name}-pack.json`);
128
+ readStream.pipe(res);
129
+ })
130
+ );
131
+ const headerWithCollapser = (title, cardId, showList, count) =>
101
132
  a(
102
133
  {
103
134
  class: `card-header-left-collapse ${!showList ? "collapsed" : ""} ps-3`,
@@ -107,7 +138,8 @@ const headerWithCollapser = (title, cardId, showList) =>
107
138
  "aria-controls": cardId,
108
139
  role: "button",
109
140
  },
110
- title
141
+ title,
142
+ ` (${count})`
111
143
  );
112
144
 
113
145
  const isShowList = (showList, listType) => showList === listType;
@@ -139,14 +171,15 @@ router.get(
139
171
  above: [
140
172
  {
141
173
  type: "breadcrumbs",
142
- crumbs: [{ text: req.__(`Tag: %s`, tag.name) }],
174
+ crumbs: [{ text: req.__(`Tags`), href: "/tag" }, { text: tag.name }],
143
175
  },
144
176
  {
145
177
  type: "card",
146
178
  title: headerWithCollapser(
147
179
  req.__("Tables"),
148
180
  tablesDomId,
149
- isShowList(show_list, "tables")
181
+ isShowList(show_list, "tables"),
182
+ tables.length
150
183
  ),
151
184
  contents: [
152
185
  await tablesList(tables, req, {
@@ -168,7 +201,8 @@ router.get(
168
201
  title: headerWithCollapser(
169
202
  req.__("Views"),
170
203
  viewsDomId,
171
- isShowList(show_list, "views")
204
+ isShowList(show_list, "views"),
205
+ views.length
172
206
  ),
173
207
  contents: [
174
208
  await viewsList(views, req, {
@@ -190,10 +224,11 @@ router.get(
190
224
  title: headerWithCollapser(
191
225
  req.__("Pages"),
192
226
  pagesDomId,
193
- isShowList(show_list, "pages")
227
+ isShowList(show_list, "pages"),
228
+ pages.length
194
229
  ),
195
230
  contents: [
196
- getPageList(pages, roles, req, {
231
+ await getPageList(pages, roles, req, {
197
232
  tagId: tag.id,
198
233
  domId: pagesDomId,
199
234
  showList: isShowList(show_list, "pages"),
@@ -213,10 +248,11 @@ router.get(
213
248
  title: headerWithCollapser(
214
249
  req.__("Triggers"),
215
250
  triggersDomId,
216
- isShowList(show_list, "triggers")
251
+ isShowList(show_list, "triggers"),
252
+ triggers.length
217
253
  ),
218
254
  contents: [
219
- getTriggerList(triggers, req, {
255
+ await getTriggerList(triggers, req, {
220
256
  tagId: tag.id,
221
257
  domId: triggersDomId,
222
258
  showList: isShowList(show_list, "triggers"),
@@ -230,6 +266,19 @@ router.get(
230
266
  ),
231
267
  ],
232
268
  },
269
+ {
270
+ type: "card",
271
+ contents: [
272
+ a(
273
+ {
274
+ class: "btn btn-outline-primary",
275
+ href: `/tag/download-pack/${tag.id}`,
276
+ },
277
+ i({ class: "fas fa-download me-2" }),
278
+ "Download pack"
279
+ ),
280
+ ],
281
+ },
233
282
  ],
234
283
  });
235
284
  })
package/routes/view.js CHANGED
@@ -86,6 +86,13 @@ router.get(
86
86
  view.attributes?.popup_width_units || "px"
87
87
  }`
88
88
  );
89
+ if (isModal && view.attributes?.popup_minwidth)
90
+ res.set(
91
+ "SaltcornModalMinWidth",
92
+ `${view.attributes?.popup_minwidth}${
93
+ view.attributes?.popup_minwidth_units || "px"
94
+ }`
95
+ );
89
96
  if (isModal && view.attributes?.popup_save_indicator)
90
97
  res.set("SaltcornModalSaveIndicator", `true`);
91
98
  if (isModal && view.attributes?.popup_link_out)
@@ -29,6 +29,9 @@ const Workflow = require("@saltcorn/data/models/workflow");
29
29
  const User = require("@saltcorn/data/models/user");
30
30
  const Page = require("@saltcorn/data/models/page");
31
31
  const File = require("@saltcorn/data/models/file");
32
+ const Tag = require("@saltcorn/data/models/tag");
33
+ const TagEntry = require("@saltcorn/data/models/tag_entry");
34
+
32
35
  const db = require("@saltcorn/data/db");
33
36
  const { sleep } = require("@saltcorn/data/utils");
34
37
 
@@ -56,7 +59,18 @@ router.get(
56
59
  error_catcher(async (req, res) => {
57
60
  let orderBy = "name";
58
61
  if (req.query._sortby === "viewtemplate") orderBy = "viewtemplate";
59
- const views = await View.find({}, { orderBy, nocase: true });
62
+ const viewq = {};
63
+ let filterOnTag;
64
+ if (req.query._tag) {
65
+ const tagEntries = await TagEntry.find({
66
+ tag_id: +req.query._tag,
67
+ not: { view_id: null },
68
+ });
69
+ viewq.id = { in: tagEntries.map((te) => te.view_id).filter(Boolean) };
70
+ filterOnTag = await Tag.findOne({ id: +req.query._tag });
71
+ }
72
+
73
+ const views = await View.find(viewq, { orderBy, nocase: true });
60
74
  await setTableRefs(views);
61
75
 
62
76
  if (req.query._sortby === "table")
@@ -64,7 +78,7 @@ router.get(
64
78
  a.table.toLowerCase() > b.table.toLowerCase() ? 1 : -1
65
79
  );
66
80
 
67
- const viewMarkup = await viewsList(views, req);
81
+ const viewMarkup = await viewsList(views, req, { filterOnTag });
68
82
  const tables = await Table.find();
69
83
 
70
84
  res.sendWrap(req.__(`Views`), {
@@ -258,6 +272,26 @@ const viewForm = async (req, tableOptions, roles, pages, values) => {
258
272
  options: ["px", "%", "vw", "em", "rem"],
259
273
  },
260
274
  },
275
+ {
276
+ name: "popup_minwidth",
277
+ label: req.__("Popup min width"),
278
+ type: "Integer",
279
+ tab: "Popup settings",
280
+ parent_field: "attributes",
281
+ attributes: { asideNext: true },
282
+ },
283
+ {
284
+ name: "popup_minwidth_units",
285
+ label: req.__("Units"),
286
+ type: "String",
287
+ tab: "Popup settings",
288
+ fieldview: "radio_group",
289
+ parent_field: "attributes",
290
+ attributes: {
291
+ inline: true,
292
+ options: ["px", "%", "vw", "em", "rem"],
293
+ },
294
+ },
261
295
  {
262
296
  name: "popup_save_indicator",
263
297
  label: req.__("Save indicator"),
@@ -782,7 +816,7 @@ router.post(
782
816
  await View.update({ configuration: newcfg }, +id);
783
817
  res.json({ success: "ok" });
784
818
  } else {
785
- res.json({ error: "no view" });
819
+ res.json({ error: req.__("Unable to save: No view") });
786
820
  }
787
821
  })
788
822
  );
@@ -419,6 +419,10 @@ describe("update matching rows", () => {
419
419
  await field.update({ is_unique: false });
420
420
  });
421
421
 
422
+ afterAll(async () => {
423
+ await resetToFixtures();
424
+ });
425
+
422
426
  it("update matching books normal", async () => {
423
427
  const table = Table.findOne({ name: "books" });
424
428
  await updateMatchingRows({
@@ -755,7 +759,7 @@ describe("relation path to query and state", () => {
755
759
  .expect(toNotInclude("album B"));
756
760
  });
757
761
 
758
- it("OneToOneSHow", async () => {
762
+ it("OneToOneShow", async () => {
759
763
  const app = await getApp({ disableCsrf: true });
760
764
  const loginCookie = await getAdminLoginCookie();
761
765
  await request(app)
@@ -805,7 +809,7 @@ describe("relation path to query and state", () => {
805
809
  .expect(toInclude(`value="artist B"`));
806
810
  });
807
811
 
808
- it("Parent", async () => {
812
+ it("Parent one layer", async () => {
809
813
  const app = await getApp({ disableCsrf: true });
810
814
  const loginCookie = await getAdminLoginCookie();
811
815
  await request(app)
@@ -833,6 +837,28 @@ describe("relation path to query and state", () => {
833
837
  .expect(toInclude("No row selected"));
834
838
  });
835
839
 
840
+ it("Parent two layers", async () => {
841
+ const app = await getApp({ disableCsrf: true });
842
+ const loginCookie = await getAdminLoginCookie();
843
+ await request(app)
844
+ .get(`/view/show_patient_with_publisher?id=2`)
845
+ .set("Cookie", loginCookie)
846
+ // view link
847
+ .expect(toInclude("/view/show_publisher?.patients.favbook.publisher=2"))
848
+ // embedded show
849
+ .expect(toInclude("Michael Douglas"))
850
+ .expect(toInclude("AK Press"));
851
+
852
+ await request(app)
853
+ .get(`/view/show_patient_with_publisher?id=1`)
854
+ .set("Cookie", loginCookie)
855
+ // view link
856
+ .expect(toInclude("/view/show_publisher?.patients.favbook.publisher=1"))
857
+ // embedded show
858
+ .expect(toInclude("Kirk Douglas"))
859
+ .expect(toInclude("No row selected"));
860
+ });
861
+
836
862
  it("RelationPath", async () => {
837
863
  const app = await getApp({ disableCsrf: true });
838
864
  const loginCookie = await getAdminLoginCookie();
@@ -873,11 +899,23 @@ describe("edit-in-edit with relation path and legacy", () => {
873
899
  const app = await getApp({ disableCsrf: true });
874
900
  const loginCookie = await getAdminLoginCookie();
875
901
  await request(app)
876
- .get("/view/edit_department_with_edit_in_edit_legacy?id=1")
902
+ .get("/view/edit_department_with_edit_in_edit_relation_path?id=1")
877
903
  .set("Cookie", loginCookie)
878
904
  .expect(toInclude("add_repeater"));
879
-
880
- // TODO post
905
+ await request(app)
906
+ .post("/view/edit_department_with_edit_in_edit_relation_path?id=1")
907
+ .set("Cookie", loginCookie)
908
+ .send({
909
+ department_0: "1",
910
+ department_1: "1",
911
+ id: "1",
912
+ id_0: "1",
913
+ id_1: "2",
914
+ name: "my_department",
915
+ name_0: "manager",
916
+ name_1: "my_employee",
917
+ })
918
+ .expect(toRedirect("/"));
881
919
  });
882
920
 
883
921
  it("edit-in-edit with relation path two layer", async () => {
@@ -887,10 +925,21 @@ describe("edit-in-edit with relation path and legacy", () => {
887
925
  .get("/view/edit_cover_with_edit_artist_on_album_rel_path?id=1")
888
926
  .set("Cookie", loginCookie)
889
927
  .expect(toInclude("add_repeater"));
890
-
891
- // TODO post
928
+ await request(app)
929
+ .post("/view/edit_cover_with_edit_artist_on_album_rel_path?id=1")
930
+ .set("Cookie", loginCookie)
931
+ .send({
932
+ album_0: "1",
933
+ album_1: "1",
934
+ artist_0: "1",
935
+ artist_1: "2",
936
+ id: "1",
937
+ id_0: "1",
938
+ id_1: "3",
939
+ name: "green cover",
940
+ })
941
+ .expect(toRedirect("/"));
892
942
  });
893
-
894
943
  it("edit-in-edit legacy one layer", async () => {
895
944
  const app = await getApp({ disableCsrf: true });
896
945
  const loginCookie = await getAdminLoginCookie();
@@ -898,8 +947,20 @@ describe("edit-in-edit with relation path and legacy", () => {
898
947
  .get("/view/edit_department_with_edit_in_edit_legacy?id=1")
899
948
  .set("Cookie", loginCookie)
900
949
  .expect(toInclude("add_repeater"));
901
-
902
- // TODO post
950
+ await request(app)
951
+ .post("/view/edit_department_with_edit_in_edit_legacy?id=1")
952
+ .set("Cookie", loginCookie)
953
+ .send({
954
+ department_0: "1",
955
+ department_1: "1",
956
+ id: "1",
957
+ id_0: "1",
958
+ id_1: "2",
959
+ name: "my_department",
960
+ name_0: "manager",
961
+ name_1: "my_employee",
962
+ })
963
+ .expect(toRedirect("/"));
903
964
  });
904
965
 
905
966
  it("edit-in-edit with relation path two layer", async () => {
@@ -909,8 +970,20 @@ describe("edit-in-edit with relation path and legacy", () => {
909
970
  .get("/view/edit_cover_with_edit_artist_on_album_rel_path?id=1")
910
971
  .set("Cookie", loginCookie)
911
972
  .expect(toInclude("add_repeater"));
912
-
913
- // TODO post
973
+ await request(app)
974
+ .post("/view/edit_cover_with_edit_artist_on_album_rel_path?id=1")
975
+ .set("Cookie", loginCookie)
976
+ .send({
977
+ album_0: "1",
978
+ album_1: "1",
979
+ artist_0: "1",
980
+ artist_1: "2",
981
+ id: "1",
982
+ id_0: "1",
983
+ id_1: "3",
984
+ name: "green cover",
985
+ })
986
+ .expect(toRedirect("/"));
914
987
  });
915
988
 
916
989
  it("edit-in-edit legacy two layer", async () => {
@@ -920,8 +993,20 @@ describe("edit-in-edit with relation path and legacy", () => {
920
993
  .get("/view/edit_cover_with_edit_artist_on_album_legacy?id=1")
921
994
  .set("Cookie", loginCookie)
922
995
  .expect(toInclude("add_repeater"));
923
-
924
- // TODO post
996
+ await request(app)
997
+ .post("/view/edit_cover_with_edit_artist_on_album_legacy?id=1")
998
+ .set("Cookie", loginCookie)
999
+ .send({
1000
+ album_0: "1",
1001
+ album_1: "1",
1002
+ artist_0: "1",
1003
+ artist_1: "2",
1004
+ id: "1",
1005
+ id_0: "1",
1006
+ id_1: "3",
1007
+ name: "green cover",
1008
+ })
1009
+ .expect(toRedirect("/"));
925
1010
  });
926
1011
  });
927
1012
 
@@ -982,6 +1067,21 @@ describe("legacy relations with relation path", () => {
982
1067
  await request(app)
983
1068
  .get("/view/authoredit_with_show?id=1")
984
1069
  .set("Cookie", loginCookie)
985
- .expect(toInclude(["Herman Melville", "agi"]));
1070
+ .expect(toInclude("Herman Melville"));
1071
+ });
1072
+
1073
+ it("edit-view with independent list", async () => {
1074
+ const app = await getApp({ disableCsrf: true });
1075
+ const loginCookie = await getAdminLoginCookie();
1076
+ await request(app)
1077
+ .get("/view/authoredit_with_independent_list")
1078
+ .set("Cookie", loginCookie)
1079
+ .expect(toInclude("Herman Melville"))
1080
+ .expect(toInclude("Delete"));
1081
+ await request(app)
1082
+ .get("/view/authoredit_with_independent_list?id=1")
1083
+ .set("Cookie", loginCookie)
1084
+ .expect(toInclude("Herman Melville"))
1085
+ .expect(toInclude("Delete"));
986
1086
  });
987
1087
  });
package/wrapper.js CHANGED
@@ -195,7 +195,6 @@ const get_headers = (req, version_tag, description, extras = []) => {
195
195
  { script: `/static_assets/${version_tag}/saltcorn-common.js` },
196
196
  { script: `/static_assets/${version_tag}/saltcorn.js` },
197
197
  { script: `/static_assets/${version_tag}/dayjs.min.js` },
198
- { script: `/static_assets/${version_tag}/relation_helpers.js` },
199
198
  ];
200
199
  let from_cfg = [];
201
200
  if (state.getConfig("page_custom_css", ""))