@saltcorn/server 0.9.2-rc.1 → 0.9.3-beta.0

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/locales/en.json CHANGED
@@ -1285,5 +1285,6 @@
1285
1285
  "Available themes": "Available themes",
1286
1286
  "Install more themes »": "Install more themes »",
1287
1287
  "Configure action": "Configure action",
1288
- "No changes detected, snapshot skipped": "No changes detected, snapshot skipped"
1288
+ "No changes detected, snapshot skipped": "No changes detected, snapshot skipped",
1289
+ "Cannot remove module: views %s depend on it": "Cannot remove module: views %s depend on it"
1289
1290
  }
package/markup/admin.js CHANGED
@@ -52,7 +52,7 @@ const restore_backup = (csrf, inner, action = `/admin/restore`) =>
52
52
  class: "d-none",
53
53
  name: "file",
54
54
  type: "file",
55
- accept: "application/zip,.zip",
55
+ accept: "application/zip,.zip,.sczip",
56
56
  onchange: "notifyAlert('Restoring backup...', true);this.form.submit();",
57
57
  })
58
58
  );
package/package.json CHANGED
@@ -1,19 +1,19 @@
1
1
  {
2
2
  "name": "@saltcorn/server",
3
- "version": "0.9.2-rc.1",
3
+ "version": "0.9.3-beta.0",
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
9
  "@aws-sdk/client-s3": "^3.451.0",
10
- "@saltcorn/base-plugin": "0.9.2-rc.1",
11
- "@saltcorn/builder": "0.9.2-rc.1",
12
- "@saltcorn/data": "0.9.2-rc.1",
13
- "@saltcorn/admin-models": "0.9.2-rc.1",
14
- "@saltcorn/filemanager": "0.9.2-rc.1",
15
- "@saltcorn/markup": "0.9.2-rc.1",
16
- "@saltcorn/sbadmin2": "0.9.2-rc.1",
10
+ "@saltcorn/base-plugin": "0.9.3-beta.0",
11
+ "@saltcorn/builder": "0.9.3-beta.0",
12
+ "@saltcorn/data": "0.9.3-beta.0",
13
+ "@saltcorn/admin-models": "0.9.3-beta.0",
14
+ "@saltcorn/filemanager": "0.9.3-beta.0",
15
+ "@saltcorn/markup": "0.9.3-beta.0",
16
+ "@saltcorn/sbadmin2": "0.9.3-beta.0",
17
17
  "@socket.io/cluster-adapter": "^0.2.1",
18
18
  "@socket.io/sticky": "^1.0.1",
19
19
  "adm-zip": "0.5.10",
@@ -0,0 +1,348 @@
1
+ var relationHelpers = (() => {
2
+ // internal helper to build an object, structured for the picker
3
+ const buildLayers = (path, pathArr, result) => {
4
+ let currentLevel = result;
5
+ for (const relation of pathArr) {
6
+ if (relation.type === "Inbound") {
7
+ const existing = currentLevel.inboundKeys.find(
8
+ (key) => key.name === relation.key && key.table === relation.table
9
+ );
10
+ if (existing) {
11
+ currentLevel = existing;
12
+ } else {
13
+ const nextLevel = {
14
+ name: relation.key,
15
+ table: relation.table,
16
+ inboundKeys: [],
17
+ fkeys: [],
18
+ };
19
+ currentLevel.inboundKeys.push(nextLevel);
20
+ currentLevel = nextLevel;
21
+ }
22
+ } else if (relation.type === "Foreign") {
23
+ const existing = currentLevel.fkeys.find(
24
+ (key) => key.name === relation.key
25
+ );
26
+ if (existing) {
27
+ currentLevel = existing;
28
+ } else {
29
+ const nextLevel = {
30
+ name: relation.key,
31
+ table: relation.table,
32
+ inboundKeys: [],
33
+ fkeys: [],
34
+ };
35
+ currentLevel.fkeys.push(nextLevel);
36
+ currentLevel = nextLevel;
37
+ }
38
+ } else if (relation.type === "Independent") {
39
+ currentLevel.fkeys.push({
40
+ name: "None (no relation)",
41
+ table: relation.table,
42
+ inboundKeys: [],
43
+ fkeys: [],
44
+ relPath: path,
45
+ });
46
+ } else if (relation.type === "Own") {
47
+ currentLevel.fkeys.push({
48
+ name: "Same table",
49
+ table: "",
50
+ inboundKeys: [],
51
+ fkeys: [],
52
+ relPath: path,
53
+ });
54
+ }
55
+ }
56
+ currentLevel.relPath = path;
57
+ };
58
+
59
+ /**
60
+ * build an array of relation objects from a path string
61
+ * '.' stands for no relation
62
+ * '.table' stands for same table
63
+ * @param {*} path relation path string separated by '.', the first token is the source table
64
+ * @param {*} tableNameCache an object with table name as key and table object as value
65
+ * @returns
66
+ */
67
+ const parseRelationPath = (path, tableNameCache) => {
68
+ if (path === ".")
69
+ return [{ type: "Independent", table: "None (no relation)" }];
70
+ const tokens = path.split(".");
71
+ if (tokens.length === 2)
72
+ return [{ type: "Own", table: `${tokens[1]} (same table)` }];
73
+ else if (tokens.length >= 3) {
74
+ const result = [];
75
+ let currentTbl = tokens[1];
76
+ for (const relation of tokens.slice(2)) {
77
+ if (relation.indexOf("$") > 0) {
78
+ const [inboundTbl, inboundKey] = relation.split("$");
79
+ result.push({ type: "Inbound", table: inboundTbl, key: inboundKey });
80
+ currentTbl = inboundTbl;
81
+ } else {
82
+ const srcTbl = tableNameCache[currentTbl];
83
+ const fk = srcTbl.foreign_keys.find((fk) => fk.name === relation);
84
+ if (fk) {
85
+ const targetTbl = tableNameCache[fk.reftable_name];
86
+ result.push({
87
+ type: "Foreign",
88
+ table: targetTbl.name,
89
+ key: relation,
90
+ });
91
+ currentTbl = targetTbl.name;
92
+ }
93
+ }
94
+ }
95
+ return result;
96
+ }
97
+ };
98
+
99
+ /**
100
+ * build an array of relation objects from a legacy relation
101
+ * @param {string} type relation type (ChildList, Independent, Own, OneToOneShow, ParentShow)
102
+ * @param {string} rest rest of the legaccy relation
103
+ * @param {string} parentTbl source table
104
+ * @returns
105
+ */
106
+ const parseLegacyRelation = (type, rest, parentTbl) => {
107
+ switch (type) {
108
+ case "ChildList": {
109
+ const path = rest ? rest.split(".") : [];
110
+ if (path.length === 3) {
111
+ const [viewName, table, key] = path;
112
+ return [
113
+ {
114
+ type: "Inbound",
115
+ table,
116
+ key,
117
+ },
118
+ ];
119
+ } else if (path.length === 5) {
120
+ const [viewName, thrTbl, thrTblFkey, fromTbl, fromTblFkey] = path;
121
+ return [
122
+ {
123
+ type: "Inbound",
124
+ table: thrTbl,
125
+ key: thrTblFkey,
126
+ },
127
+ {
128
+ type: "Inbound",
129
+ table: fromTbl,
130
+ key: fromTblFkey,
131
+ },
132
+ ];
133
+ }
134
+ break;
135
+ }
136
+ case "Independent": {
137
+ return [{ type: "Independent", table: "None (no relation)" }];
138
+ }
139
+ case "Own": {
140
+ return [{ type: "Own", table: `${parentTbl} (same table)` }];
141
+ }
142
+ case "OneToOneShow": {
143
+ const tokens = rest ? rest.split(".") : [];
144
+ if (tokens.length !== 3) break;
145
+ const [viewname, relatedTbl, fkey] = tokens;
146
+ return [{ type: "Inbound", table: relatedTbl, key: fkey }];
147
+ }
148
+ case "ParentShow": {
149
+ const tokens = rest ? rest.split(".") : [];
150
+ if (tokens.length !== 3) break;
151
+ const [viewname, parentTbl, fkey] = tokens;
152
+ return [{ type: "Foreign", table: parentTbl, key: fkey }];
153
+ }
154
+ }
155
+ return [];
156
+ };
157
+
158
+ const ViewDisplayType = {
159
+ ROW_REQUIRED: "ROW_REQUIRED",
160
+ NO_ROW_LIMIT: "NO_ROW_LIMIT",
161
+ INVALID: "INVALID",
162
+ };
163
+
164
+ /**
165
+ * prepare the relations finder
166
+ * @param {object} tablesCache
167
+ * @param {object} allViews
168
+ * @param {number} maxDepth
169
+ */
170
+ const RelationsFinder = function (tablesCache, allViews, maxDepth) {
171
+ this.maxDepth = +maxDepth;
172
+ this.allViews = allViews;
173
+ const { tableIdCache, tableNameCache, fieldCache } = tablesCache;
174
+ this.tableIdCache = tableIdCache;
175
+ this.tableNameCache = tableNameCache;
176
+ this.fieldCache = fieldCache;
177
+ };
178
+
179
+ /**
180
+ * find relations between a source table and a subview
181
+ * @param {string} sourceTblName
182
+ * @param {string} subView
183
+ * @param {string[]} excluded
184
+ * @returns {object} {paths: string[], layers: object}
185
+ */
186
+ RelationsFinder.prototype.findRelations = function (
187
+ sourceTblName,
188
+ subView,
189
+ excluded
190
+ ) {
191
+ let paths = [];
192
+ const layers = { table: sourceTblName, inboundKeys: [], fkeys: [] };
193
+ try {
194
+ const view = this.allViews.find((v) => v.name === subView);
195
+ if (!view) throw new Error(`The view ${subView} does not exist`);
196
+ if (excluded?.find((e) => e === view.viewtemplate)) {
197
+ console.log(`view ${subView} is excluded`);
198
+ return { paths, layers };
199
+ }
200
+ switch (view.display_type) {
201
+ case ViewDisplayType.ROW_REQUIRED:
202
+ paths = this.singleRelationPaths(sourceTblName, subView, excluded);
203
+ break;
204
+ case ViewDisplayType.NO_ROW_LIMIT:
205
+ paths = this.multiRelationPaths(sourceTblName, subView, excluded);
206
+ break;
207
+ default:
208
+ throw new Error(
209
+ `view ${subView}: The displayType (${view.display_type}) is not valid`
210
+ );
211
+ }
212
+ for (const path of paths)
213
+ buildLayers(path, parseRelationPath(path, this.tableNameCache), layers);
214
+ } catch (error) {
215
+ console.log(error);
216
+ } finally {
217
+ return { paths, layers };
218
+ }
219
+ };
220
+
221
+ /**
222
+ * find relations between a source table and a subview with single row display (e.g. show)
223
+ * @param {string} sourceTblName
224
+ * @param {string} subView
225
+ * @param {string[]} excluded
226
+ * @returns
227
+ */
228
+ RelationsFinder.prototype.singleRelationPaths = function (
229
+ sourceTblName,
230
+ subView,
231
+ excluded
232
+ ) {
233
+ const result = [];
234
+ const subViewObj = this.allViews.find((v) => v.name === subView);
235
+ if (!subViewObj) throw new Error(`The view ${subView} does not exist`);
236
+ if (excluded?.find((e) => e === subViewObj.viewtemplate)) {
237
+ console.log(`view ${subView} is excluded`);
238
+ return result;
239
+ }
240
+ const sourceTbl = this.tableNameCache[sourceTblName];
241
+ if (!sourceTbl)
242
+ throw new Error(`The table ${sourceTblName} does not exist`);
243
+ // 1. parent relations
244
+ const parentRelations = sourceTbl.foreign_keys;
245
+ if (sourceTbl.id === subViewObj.table_id) result.push(`.${sourceTblName}`);
246
+ for (const relation of parentRelations) {
247
+ const targetTbl = this.tableNameCache[relation.reftable_name];
248
+ if (!targetTbl)
249
+ throw new Error(`The table ${relation.reftable_name} does not exist`);
250
+ if (targetTbl.id === subViewObj.table_id)
251
+ result.push(`.${sourceTblName}.${relation.name}`);
252
+ }
253
+ // 2. OneToOneShow
254
+ const uniqueFksToSrc = (this.fieldCache[sourceTblName] || []).filter(
255
+ (f) => f.is_unique
256
+ );
257
+ for (const relation of uniqueFksToSrc) {
258
+ const targetTbl = this.tableIdCache[relation.table_id];
259
+ if (!targetTbl)
260
+ throw new Error(`The table ${relation.table_id} does not exist`);
261
+ if (targetTbl.id === subViewObj.table_id)
262
+ result.push(`.${sourceTblName}.${targetTbl.name}$${relation.name}`);
263
+ }
264
+ // 3. inbound_self_relations
265
+ const srcFks = sourceTbl.foreign_keys;
266
+ for (const fkToSrc of uniqueFksToSrc) {
267
+ const refTable = this.tableIdCache[fkToSrc.table_id];
268
+ if (!refTable)
269
+ throw new Error(`The table ${fkToSrc.table_id} does not exist`);
270
+ const fromSrcToRef = srcFks.filter(
271
+ (field) => field.reftable_name === refTable.name
272
+ );
273
+ for (const toRef of fromSrcToRef) {
274
+ if (fkToSrc.reftable_name === sourceTblName)
275
+ result.push(`.${sourceTblName}.${toRef.name}.${fkToSrc.name}`);
276
+ }
277
+ }
278
+ return result;
279
+ };
280
+
281
+ /**
282
+ * find relations between a source table and a subview with multiple rows display (e.g. list)
283
+ * @param {string} sourceTblName
284
+ * @param {string} subView
285
+ * @param {string[]} excluded
286
+ * @returns
287
+ */
288
+ RelationsFinder.prototype.multiRelationPaths = function (
289
+ sourceTblName,
290
+ subView,
291
+ excluded
292
+ ) {
293
+ const result = ["."]; // none no relation
294
+ const subViewObj = this.allViews.find((v) => v.name === subView);
295
+ if (!subViewObj) throw new Error(`The view ${subView} does not exist`);
296
+ if (excluded?.find((e) => e === subViewObj.viewtemplate)) {
297
+ console.log(`view ${subView} is excluded`);
298
+ return result;
299
+ }
300
+ const sourceTbl = this.tableNameCache[sourceTblName];
301
+ if (!sourceTbl)
302
+ throw new Error(`The table ${sourceTblName} does not exist`);
303
+ const searcher = (current, path, level, visited) => {
304
+ if (level > this.maxDepth) return;
305
+ const visitedFkCopy = new Set(visited);
306
+ const fks = current.foreign_keys.filter((f) => !visitedFkCopy.has(f.id));
307
+ for (const fk of fks) {
308
+ visitedFkCopy.add(fk.id);
309
+ const target = this.tableNameCache[fk.reftable_name];
310
+ if (!target)
311
+ throw new Error(`The table ${fk.reftable_name} does not exist`);
312
+ const newPath = `${path}.${fk.name}`;
313
+ if (target.id === subViewObj.table_id) result.push(newPath);
314
+ searcher(target, newPath, level + 1, visitedFkCopy);
315
+ }
316
+
317
+ const visitedInboundCopy = new Set(visited);
318
+ const inbounds = (this.fieldCache[current.name] || []).filter(
319
+ (f) => !visitedInboundCopy.has(f.id)
320
+ );
321
+ for (const inbound of inbounds) {
322
+ visitedInboundCopy.add(inbound.id);
323
+ const target = this.tableIdCache[inbound.table_id];
324
+ if (!target)
325
+ throw new Error(`The table ${inbound.table_id} does not exist`);
326
+ const newPath = `${path}.${target.name}$${inbound.name}`;
327
+ if (target.id === subViewObj.table_id) result.push(newPath);
328
+ searcher(target, newPath, level + 1, visitedInboundCopy);
329
+ }
330
+ };
331
+ const path = `.${sourceTblName}`;
332
+ const visited = new Set();
333
+ searcher(sourceTbl, path, 0, visited);
334
+ return result;
335
+ };
336
+
337
+ return {
338
+ RelationsFinder: RelationsFinder,
339
+ ViewDisplayType: ViewDisplayType,
340
+ parseRelationPath: parseRelationPath,
341
+ parseLegacyRelation: parseLegacyRelation,
342
+ };
343
+ })();
344
+
345
+ // make the module available for jest with react
346
+ if (typeof process !== "undefined" && process.env?.NODE_ENV === "test") {
347
+ module.exports = relationHelpers;
348
+ }
@@ -1101,6 +1101,10 @@ async function common_done(res, viewname, isWeb = true) {
1101
1101
  const f = new Function(`viewname, row, {${res.field_names}}`, s);
1102
1102
  const evalres = await f(viewname, res.row, res.row);
1103
1103
  if (evalres) await common_done(evalres, viewname, isWeb);
1104
+ } else if (res.row) {
1105
+ const f = new Function(`viewname, row`, s);
1106
+ const evalres = await f(viewname, res.row);
1107
+ if (evalres) await common_done(evalres, viewname, isWeb);
1104
1108
  } else {
1105
1109
  const f = new Function(`viewname`, s);
1106
1110
  const evalres = await f(viewname);
@@ -1138,16 +1142,25 @@ async function common_done(res, viewname, isWeb = true) {
1138
1142
  });
1139
1143
  }
1140
1144
  if (res.set_fields && viewname) {
1141
- Object.keys(res.set_fields).forEach((k) => {
1142
- const form = $(`form[data-viewname=${viewname}]`);
1143
- const input = form.find(
1144
- `input[name=${k}], textarea[name=${k}], select[name=${k}]`
1145
+ const form = $(`form[data-viewname=${viewname}]`);
1146
+ if (form.length === 0 && set_state_fields) {
1147
+ // assume this is a filter
1148
+ set_state_fields(
1149
+ res.set_fields,
1150
+ false
1151
+ // $(`[data-sc-embed-viewname="${viewname}"]`)
1145
1152
  );
1146
- if (input.attr("type") === "checkbox")
1147
- input.prop("checked", res.set_fields[k]);
1148
- else input.val(res.set_fields[k]);
1149
- input.trigger("set_form_field");
1150
- });
1153
+ } else {
1154
+ Object.keys(res.set_fields).forEach((k) => {
1155
+ const input = form.find(
1156
+ `input[name=${k}], textarea[name=${k}], select[name=${k}]`
1157
+ );
1158
+ if (input.attr("type") === "checkbox")
1159
+ input.prop("checked", res.set_fields[k]);
1160
+ else input.val(res.set_fields[k]);
1161
+ input.trigger("set_form_field");
1162
+ });
1163
+ }
1151
1164
  }
1152
1165
  if (res.goto && !isWeb)
1153
1166
  // TODO ch
@@ -37,12 +37,23 @@ function updateQueryStringParameter(uri1, key, value) {
37
37
  var re = new RegExp("([?&])" + key + "=.*?(&|$)", "i");
38
38
  var separator = uri.indexOf("?") !== -1 ? "&" : "?";
39
39
  if (uri.match(re)) {
40
- return (
41
- uri.replace(re, "$1" + key + "=" + encodeURIComponent(value) + "$2") +
42
- hash
43
- );
40
+ if (Array.isArray(value)) {
41
+ var rmuri = removeQueryStringParameter(uri, key);
42
+ return updateQueryStringParameter(rmuri, key, value);
43
+ } else
44
+ return (
45
+ uri.replace(re, "$1" + key + "=" + encodeURIComponent(value) + "$2") +
46
+ hash
47
+ );
44
48
  } else {
45
- return uri + separator + key + "=" + encodeURIComponent(value) + hash;
49
+ if (Array.isArray(value))
50
+ return (
51
+ uri +
52
+ separator +
53
+ value.map((val) => key + "=" + encodeURIComponent(val)).join("&") +
54
+ hash
55
+ );
56
+ else return uri + separator + key + "=" + encodeURIComponent(value) + hash;
46
57
  }
47
58
  }
48
59
 
@@ -87,7 +87,12 @@ const viewTable = (views, req) =>
87
87
  {
88
88
  label: req.__("Edit"),
89
89
  key: (r) =>
90
- link(`/viewedit/edit/${encodeURIComponent(r.name)}`, req.__("Edit")),
90
+ r.singleton
91
+ ? ""
92
+ : link(
93
+ `/viewedit/edit/${encodeURIComponent(r.name)}`,
94
+ req.__("Edit")
95
+ ),
91
96
  },
92
97
  ],
93
98
  views
package/routes/plugins.js CHANGED
@@ -1243,6 +1243,7 @@ router.post(
1243
1243
  }
1244
1244
  await load_plugins.loadAndSaveNewPlugin(plugin, forceReInstall);
1245
1245
  const plugin_module = getState().plugins[name];
1246
+ await sleep(1000); // Allow other workers to load this plugin
1246
1247
  await getState().refresh_views();
1247
1248
 
1248
1249
  if (plugin_module && plugin_module.configuration_workflow) {
@@ -1254,7 +1255,6 @@ router.post(
1254
1255
  plugin_db.name
1255
1256
  )
1256
1257
  );
1257
- await sleep(1000); // Allow other workers to load this plugin
1258
1258
  res.redirect(`/plugins/configure/${plugin_db.name}`);
1259
1259
  } else {
1260
1260
  req.flash("success", req.__(`Module %s installed`, plugin.name));
@@ -47,6 +47,29 @@ test("updateQueryStringParameter", () => {
47
47
  "AK"
48
48
  )
49
49
  ).toBe("/foo?publisher.publisher->name=AK");
50
+ expect(updateQueryStringParameter("/foo", "_or_field", ["baz", "bar"])).toBe(
51
+ "/foo?_or_field=baz&_or_field=bar"
52
+ );
53
+ expect(
54
+ updateQueryStringParameter("/foo?_or_field=zoo", "_or_field", [
55
+ "baz",
56
+ "bar",
57
+ ])
58
+ ).toBe("/foo?_or_field=baz&_or_field=bar");
59
+ expect(
60
+ updateQueryStringParameter(
61
+ "/foo?_or_field=baz&_or_field=bar",
62
+ "_or_field",
63
+ ["baz"]
64
+ )
65
+ ).toBe("/foo?&_or_field=baz"); //or no ampersand
66
+ expect(
67
+ updateQueryStringParameter(
68
+ "/foo?_or_field=baz&_or_field=bar",
69
+ "_or_field",
70
+ []
71
+ )
72
+ ).toBe("/foo?&"); //or no ampersand / question mark
50
73
  });
51
74
  //publisher.publisher->name
52
75
  test("updateQueryStringParameter hash", () => {
@@ -193,7 +193,7 @@ describe("load remote insert/updates", () => {
193
193
  .post("/table")
194
194
  .set("Cookie", loginCookie)
195
195
  .send(`name=${encodeURIComponent("Table with capitals")}`)
196
- .expect(toRedirect("/table/16"));
196
+ .expect(toRedirect("/table/26"));
197
197
  // add a field
198
198
  await request(app)
199
199
  .post("/field/")
@@ -202,17 +202,17 @@ describe("load remote insert/updates", () => {
202
202
  .send("label=StringField")
203
203
  .send("type=String")
204
204
  .send(
205
- `contextEnc=${encodeURIComponent(JSON.stringify({ table_id: 16 }))}`
205
+ `contextEnc=${encodeURIComponent(JSON.stringify({ table_id: 26 }))}`
206
206
  )
207
207
  .set("Cookie", loginCookie)
208
208
  .expect(toInclude("options"));
209
209
  // init sync_info table
210
210
  await request(app)
211
211
  .post("/table")
212
- .send("id=16")
212
+ .send("id=26")
213
213
  .send("has_sync_info=on")
214
214
  .set("Cookie", loginCookie)
215
- .expect(toRedirect("/table/16"));
215
+ .expect(toRedirect("/table/26"));
216
216
  const dbTime = await db.time();
217
217
 
218
218
  // call load changes
@@ -39,7 +39,7 @@ describe("Table Endpoints", () => {
39
39
  .post("/table/")
40
40
  .send("name=mypostedtable")
41
41
  .set("Cookie", loginCookie)
42
- .expect(toRedirect("/table/16"));
42
+ .expect(toRedirect("/table/26"));
43
43
  await request(app)
44
44
  .get("/table/10")
45
45
  .set("Cookie", loginCookie)
@@ -155,7 +155,7 @@ Pencil, 0.5,2, t`;
155
155
  .set("Cookie", loginCookie)
156
156
  .field("name", "expenses")
157
157
  .attach("file", Buffer.from(csv, "utf-8"))
158
- .expect(toRedirect("/table/17"));
158
+ .expect(toRedirect("/table/27"));
159
159
  });
160
160
  it("should upload csv to existing table", async () => {
161
161
  const csv = `author,Pages
@@ -17,9 +17,6 @@ const View = require("@saltcorn/data/models/view");
17
17
  const Table = require("@saltcorn/data/models/table");
18
18
 
19
19
  const { plugin_with_routes } = require("@saltcorn/data/tests/mocks");
20
- const {
21
- prepareArtistsAlbumRelation,
22
- } = require("@saltcorn/data/tests/common_helpers");
23
20
 
24
21
  afterAll(db.close);
25
22
  beforeAll(async () => {
@@ -523,7 +520,7 @@ describe("inbound relations", () => {
523
520
 
524
521
  await request(app)
525
522
  .get(
526
- `/view/blog_posts_feed?_inbound_relation_path_=${encodeURIComponent(
523
+ `/view/blog_posts_feed?_relation_path_=${encodeURIComponent(
527
524
  JSON.stringify(queryObj)
528
525
  )}`
529
526
  )
@@ -535,7 +532,7 @@ describe("inbound relations", () => {
535
532
  queryObj.srcId = 2;
536
533
  await request(app)
537
534
  .get(
538
- `/view/blog_posts_feed?_inbound_relation_path_=${encodeURIComponent(
535
+ `/view/blog_posts_feed?_relation_path_=${encodeURIComponent(
539
536
  JSON.stringify(queryObj)
540
537
  )}`
541
538
  )
@@ -547,7 +544,7 @@ describe("inbound relations", () => {
547
544
  queryObj.srcId = 3;
548
545
  await request(app)
549
546
  .get(
550
- `/view/blog_posts_feed?_inbound_relation_path_=${encodeURIComponent(
547
+ `/view/blog_posts_feed?_relation_path_=${encodeURIComponent(
551
548
  JSON.stringify(queryObj)
552
549
  )}`
553
550
  )
@@ -567,7 +564,7 @@ describe("inbound relations", () => {
567
564
  const loginCookie = await getAdminLoginCookie();
568
565
  await request(app)
569
566
  .get(
570
- `/view/blog_posts_feed?_inbound_relation_path_=${encodeURIComponent(
567
+ `/view/blog_posts_feed?_relation_path_=${encodeURIComponent(
571
568
  JSON.stringify(queryObj)
572
569
  )}`
573
570
  )
@@ -579,7 +576,7 @@ describe("inbound relations", () => {
579
576
  queryObj.srcId = 2;
580
577
  await request(app)
581
578
  .get(
582
- `/view/blog_posts_feed?_inbound_relation_path_=${encodeURIComponent(
579
+ `/view/blog_posts_feed?_relation_path_=${encodeURIComponent(
583
580
  JSON.stringify(queryObj)
584
581
  )}`
585
582
  )
@@ -591,10 +588,6 @@ describe("inbound relations", () => {
591
588
  });
592
589
 
593
590
  describe("many to many relations", () => {
594
- beforeAll(async () => {
595
- await prepareArtistsAlbumRelation();
596
- });
597
-
598
591
  it("artist_plays_on_album", async () => {
599
592
  const app = await getApp({ disableCsrf: true });
600
593
  const loginCookie = await getAdminLoginCookie();
@@ -621,7 +614,7 @@ describe("many to many relations", () => {
621
614
  };
622
615
  await request(app)
623
616
  .get(
624
- `/view/albums_feed?_inbound_relation_path_=${encodeURIComponent(
617
+ `/view/albums_feed?_relation_path_=${encodeURIComponent(
625
618
  JSON.stringify(queryObj_1)
626
619
  )}`
627
620
  )
@@ -635,7 +628,7 @@ describe("many to many relations", () => {
635
628
  };
636
629
  await request(app)
637
630
  .get(
638
- `/view/albums_feed?_inbound_relation_path_=${encodeURIComponent(
631
+ `/view/albums_feed?_relation_path_=${encodeURIComponent(
639
632
  JSON.stringify(queryObj_2)
640
633
  )}`
641
634
  )
@@ -655,7 +648,7 @@ describe("many to many relations", () => {
655
648
  };
656
649
  await request(app)
657
650
  .get(
658
- `/view/fan_club_feed?_inbound_relation_path_=${encodeURIComponent(
651
+ `/view/fan_club_feed?_relation_path_=${encodeURIComponent(
659
652
  JSON.stringify(queryObj_1)
660
653
  )}`
661
654
  )
@@ -672,7 +665,7 @@ describe("many to many relations", () => {
672
665
  };
673
666
  await request(app)
674
667
  .get(
675
- `/view/fan_club_feed?_inbound_relation_path_=${encodeURIComponent(
668
+ `/view/fan_club_feed?_relation_path_=${encodeURIComponent(
676
669
  JSON.stringify(queryObj_2)
677
670
  )}`
678
671
  )
@@ -683,3 +676,52 @@ describe("many to many relations", () => {
683
676
  .expect(toInclude("fan club official"));
684
677
  });
685
678
  });
679
+
680
+ describe("legacy relations with relation path", () => {
681
+ it("Independent feed", async () => {
682
+ const app = await getApp({ disableCsrf: true });
683
+ const loginCookie = await getAdminLoginCookie();
684
+
685
+ const queryObj = {
686
+ relation: ".",
687
+ srcId: 1,
688
+ };
689
+ await request(app)
690
+ .get(
691
+ `/view/fan_club_feed?_relation_path_=${encodeURIComponent(
692
+ JSON.stringify(queryObj)
693
+ )}`
694
+ )
695
+ .set("Cookie", loginCookie)
696
+ .expect(toInclude("crazy fan club"))
697
+ .expect(toInclude("another club"))
698
+ .expect(toInclude("fan club"))
699
+ .expect(toInclude("fan club official"));
700
+ });
701
+
702
+ it("Independent feed as subview", async () => {
703
+ const app = await getApp({ disableCsrf: true });
704
+ const loginCookie = await getAdminLoginCookie();
705
+ await request(app)
706
+ .get("/view/show_pressing_job_with_new_indenpendent_relation_path?id=1")
707
+ .set("Cookie", loginCookie)
708
+ .expect(toInclude("crazy fan club"))
709
+ .expect(toInclude("another club"))
710
+ .expect(toInclude("fan club"))
711
+ .expect(toInclude("fan club official"));
712
+ });
713
+
714
+ it("Own same table subview", async () => {
715
+ const app = await getApp({ disableCsrf: true });
716
+ const loginCookie = await getAdminLoginCookie();
717
+
718
+ await request(app)
719
+ .get("/view/show_album_with_subview_new_relation_path?id=1")
720
+ .set("Cookie", loginCookie)
721
+ .expect(toInclude("album A"));
722
+ await request(app)
723
+ .get("/view/show_album_with_subview_new_relation_path?id=2")
724
+ .set("Cookie", loginCookie)
725
+ .expect(toInclude("album B"));
726
+ });
727
+ });
package/wrapper.js CHANGED
@@ -195,6 +195,7 @@ 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` },
198
199
  ];
199
200
  let from_cfg = [];
200
201
  if (state.getConfig("page_custom_css", ""))