@saltcorn/server 0.7.0-beta.2 → 0.7.0-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/routes.js +56 -10
- package/locales/en.json +9 -1
- package/locales/it.json +3 -1
- package/markup/admin.js +9 -2
- package/package.json +7 -7
- package/public/gridedit.js +4 -0
- package/public/saltcorn.css +5 -1
- package/public/saltcorn.js +10 -4
- package/routes/admin.js +20 -2
- package/routes/api.js +45 -0
- package/routes/fields.js +26 -2
- package/routes/list.js +2 -2
- package/routes/plugins.js +1 -0
- package/tests/fields.test.js +2 -2
- package/tests/table.test.js +3 -3
package/auth/routes.js
CHANGED
|
@@ -41,6 +41,8 @@ const {
|
|
|
41
41
|
code,
|
|
42
42
|
pre,
|
|
43
43
|
p,
|
|
44
|
+
script,
|
|
45
|
+
domReady,
|
|
44
46
|
} = require("@saltcorn/markup/tags");
|
|
45
47
|
const {
|
|
46
48
|
available_languages,
|
|
@@ -416,6 +418,7 @@ router.get(
|
|
|
416
418
|
const form = loginForm(req, true);
|
|
417
419
|
form.action = "/auth/create_first_user";
|
|
418
420
|
form.submitLabel = req.__("Create user");
|
|
421
|
+
form.class = "create-first-user";
|
|
419
422
|
form.blurb = req.__(
|
|
420
423
|
"Please create your first user account, which will have administrative privileges. You can add other users and give them administrative privileges later."
|
|
421
424
|
);
|
|
@@ -424,7 +427,17 @@ router.get(
|
|
|
424
427
|
[i({ class: "fas fa-upload me-2 mt-2" }), req.__("Restore a backup")],
|
|
425
428
|
`/auth/create_from_restore`
|
|
426
429
|
);
|
|
427
|
-
res.sendAuthWrap(
|
|
430
|
+
res.sendAuthWrap(
|
|
431
|
+
req.__(`Create first user`),
|
|
432
|
+
form,
|
|
433
|
+
{},
|
|
434
|
+
restore +
|
|
435
|
+
script(
|
|
436
|
+
domReady(
|
|
437
|
+
`$('form.create-first-user button[type=submit]').click(function(){press_store_button(this)})`
|
|
438
|
+
)
|
|
439
|
+
)
|
|
440
|
+
);
|
|
428
441
|
} else {
|
|
429
442
|
req.flash("danger", req.__("Users already present"));
|
|
430
443
|
res.redirect("/auth/login");
|
|
@@ -1194,14 +1207,12 @@ const userSettings = async ({ req, res, pwform, user }) => {
|
|
|
1194
1207
|
),
|
|
1195
1208
|
div(
|
|
1196
1209
|
user._attributes.totp_enabled
|
|
1197
|
-
?
|
|
1198
|
-
"/auth/twofa/disable/totp",
|
|
1199
|
-
"Disable",
|
|
1200
|
-
req.csrfToken(),
|
|
1210
|
+
? a(
|
|
1201
1211
|
{
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
}
|
|
1212
|
+
href: "/auth/twofa/disable/totp",
|
|
1213
|
+
class: "btn btn-danger mt-2",
|
|
1214
|
+
},
|
|
1215
|
+
"Disable"
|
|
1205
1216
|
)
|
|
1206
1217
|
: a(
|
|
1207
1218
|
{
|
|
@@ -1552,6 +1563,7 @@ router.post(
|
|
|
1552
1563
|
if (!user._attributes.totp_key) {
|
|
1553
1564
|
//key not set
|
|
1554
1565
|
req.flash("danger", req.__("2FA TOTP Key not set"));
|
|
1566
|
+
console.log("2FA TOTP Key not set");
|
|
1555
1567
|
res.redirect("/auth/twofa/setup/totp");
|
|
1556
1568
|
return;
|
|
1557
1569
|
}
|
|
@@ -1560,6 +1572,8 @@ router.post(
|
|
|
1560
1572
|
form.validate(req.body);
|
|
1561
1573
|
if (form.hasErrors) {
|
|
1562
1574
|
req.flash("danger", req.__("Error processing form"));
|
|
1575
|
+
console.log("Error processing form");
|
|
1576
|
+
|
|
1563
1577
|
res.redirect("/auth/twofa/setup/totp");
|
|
1564
1578
|
return;
|
|
1565
1579
|
}
|
|
@@ -1569,6 +1583,7 @@ router.post(
|
|
|
1569
1583
|
});
|
|
1570
1584
|
if (!rv) {
|
|
1571
1585
|
req.flash("danger", req.__("Could not verify code"));
|
|
1586
|
+
console.log("Could not verify code");
|
|
1572
1587
|
res.redirect("/auth/twofa/setup/totp");
|
|
1573
1588
|
return;
|
|
1574
1589
|
}
|
|
@@ -1585,11 +1600,42 @@ router.post(
|
|
|
1585
1600
|
})
|
|
1586
1601
|
);
|
|
1587
1602
|
|
|
1603
|
+
router.get(
|
|
1604
|
+
"/twofa/disable/totp",
|
|
1605
|
+
loggedIn,
|
|
1606
|
+
error_catcher(async (req, res) => {
|
|
1607
|
+
res.sendWrap(req.__("Disable two-factor authentication"), {
|
|
1608
|
+
type: "card",
|
|
1609
|
+
title: req.__("Disable two-factor authentication"),
|
|
1610
|
+
contents: [
|
|
1611
|
+
h4(req.__("Enter your two-factor code in order to disable it")),
|
|
1612
|
+
renderForm(totpForm(req, "/auth/twofa/disable/totp"), req.csrfToken()),
|
|
1613
|
+
],
|
|
1614
|
+
});
|
|
1615
|
+
})
|
|
1616
|
+
);
|
|
1617
|
+
|
|
1588
1618
|
router.post(
|
|
1589
1619
|
"/twofa/disable/totp",
|
|
1590
1620
|
loggedIn,
|
|
1591
1621
|
error_catcher(async (req, res) => {
|
|
1592
1622
|
const user = await User.findOne({ id: req.user.id });
|
|
1623
|
+
const form = totpForm(req, "/auth/twofa/disable/totp");
|
|
1624
|
+
form.validate(req.body);
|
|
1625
|
+
if (form.hasErrors) {
|
|
1626
|
+
req.flash("danger", req.__("Error processing form"));
|
|
1627
|
+
res.redirect("/auth/twofa/disable/totp");
|
|
1628
|
+
return;
|
|
1629
|
+
}
|
|
1630
|
+
const code = `${form.values.totpCode}`;
|
|
1631
|
+
const rv = totp.verify(code, user._attributes.totp_key, {
|
|
1632
|
+
time: 30,
|
|
1633
|
+
});
|
|
1634
|
+
if (!rv) {
|
|
1635
|
+
req.flash("danger", req.__("Could not verify code"));
|
|
1636
|
+
res.redirect("/auth/twofa/disable/totp");
|
|
1637
|
+
return;
|
|
1638
|
+
}
|
|
1593
1639
|
user._attributes.totp_enabled = false;
|
|
1594
1640
|
delete user._attributes.totp_key;
|
|
1595
1641
|
await user.update({ _attributes: user._attributes });
|
|
@@ -1602,9 +1648,9 @@ router.post(
|
|
|
1602
1648
|
res.redirect("/auth/settings");
|
|
1603
1649
|
})
|
|
1604
1650
|
);
|
|
1605
|
-
const totpForm = (req) =>
|
|
1651
|
+
const totpForm = (req, action) =>
|
|
1606
1652
|
new Form({
|
|
1607
|
-
action: "/auth/twofa/setup/totp",
|
|
1653
|
+
action: action || "/auth/twofa/setup/totp",
|
|
1608
1654
|
fields: [
|
|
1609
1655
|
{
|
|
1610
1656
|
name: "totpCode",
|
package/locales/en.json
CHANGED
|
@@ -870,5 +870,13 @@
|
|
|
870
870
|
"Steps to go back": "Steps to go back",
|
|
871
871
|
"Place in dropdown": "Place in dropdown",
|
|
872
872
|
"Hide null columns": "Hide null columns",
|
|
873
|
-
"Do not display a column if it contains entirely missing values": "Do not display a column if it contains entirely missing values"
|
|
873
|
+
"Do not display a column if it contains entirely missing values": "Do not display a column if it contains entirely missing values",
|
|
874
|
+
"Show a warning to users creating a tenant disclaiming warrenty of availability or security": "Show a warning to users creating a tenant disclaiming warrenty of availability or security",
|
|
875
|
+
"Set to 0 for expration at the end of browser session": "Set to 0 for expration at the end of browser session",
|
|
876
|
+
"Could not verify code": "Could not verify code",
|
|
877
|
+
"Disable two-factor authentication": "Disable two-factor authentication",
|
|
878
|
+
"Enter your two-factor code in order to disable it": "Enter your two-factor code in order to disable it",
|
|
879
|
+
"Allow the user to enter a new key that is not in the schema": "Allow the user to enter a new key that is not in the schema",
|
|
880
|
+
"Check for updates": "Check for updates",
|
|
881
|
+
"Versions refreshed": "Versions refreshed"
|
|
874
882
|
}
|
package/locales/it.json
CHANGED
|
@@ -475,5 +475,7 @@
|
|
|
475
475
|
"Events": "Events",
|
|
476
476
|
"Verified": "Verified",
|
|
477
477
|
"SSL": "SSL",
|
|
478
|
-
"Generate": "Generate"
|
|
478
|
+
"Generate": "Generate",
|
|
479
|
+
"Two-factor authentication": "Two-factor authentication",
|
|
480
|
+
"Two-factor authentication is disabled": "Two-factor authentication is disabled"
|
|
479
481
|
}
|
package/markup/admin.js
CHANGED
|
@@ -46,14 +46,21 @@ const restore_backup = (csrf, inner, action = `/admin/restore`) =>
|
|
|
46
46
|
encType: "multipart/form-data",
|
|
47
47
|
},
|
|
48
48
|
input({ type: "hidden", name: "_csrf", value: csrf }),
|
|
49
|
-
label(
|
|
49
|
+
label(
|
|
50
|
+
{
|
|
51
|
+
class: "btn-link",
|
|
52
|
+
for: "upload_to_restore",
|
|
53
|
+
style: { cursor: "pointer" },
|
|
54
|
+
},
|
|
55
|
+
inner
|
|
56
|
+
),
|
|
50
57
|
input({
|
|
51
58
|
id: "upload_to_restore",
|
|
52
59
|
class: "d-none",
|
|
53
60
|
name: "file",
|
|
54
61
|
type: "file",
|
|
55
62
|
accept: "application/zip,.zip",
|
|
56
|
-
onchange: "this.form.submit();",
|
|
63
|
+
onchange: "notifyAlert('Restoring backup...', true);this.form.submit();",
|
|
57
64
|
})
|
|
58
65
|
);
|
|
59
66
|
|
package/package.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saltcorn/server",
|
|
3
|
-
"version": "0.7.0-beta.
|
|
3
|
+
"version": "0.7.0-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.7.0-beta.
|
|
10
|
-
"@saltcorn/builder": "0.7.0-beta.
|
|
11
|
-
"@saltcorn/data": "0.7.0-beta.
|
|
12
|
-
"@saltcorn/admin-models": "0.7.0-beta.
|
|
13
|
-
"@saltcorn/markup": "0.7.0-beta.
|
|
14
|
-
"@saltcorn/sbadmin2": "0.7.0-beta.
|
|
9
|
+
"@saltcorn/base-plugin": "0.7.0-beta.5",
|
|
10
|
+
"@saltcorn/builder": "0.7.0-beta.5",
|
|
11
|
+
"@saltcorn/data": "0.7.0-beta.5",
|
|
12
|
+
"@saltcorn/admin-models": "0.7.0-beta.5",
|
|
13
|
+
"@saltcorn/markup": "0.7.0-beta.5",
|
|
14
|
+
"@saltcorn/sbadmin2": "0.7.0-beta.5",
|
|
15
15
|
"@socket.io/cluster-adapter": "^0.1.0",
|
|
16
16
|
"@socket.io/sticky": "^1.0.1",
|
|
17
17
|
"aws-sdk": "^2.1037.0",
|
package/public/gridedit.js
CHANGED
|
@@ -8,6 +8,10 @@ function lookupIntToString(cell, formatterParams, onRendered) {
|
|
|
8
8
|
const res = formatterParams.values[val];
|
|
9
9
|
return res;
|
|
10
10
|
}
|
|
11
|
+
function deleteIcon() {
|
|
12
|
+
//plain text value
|
|
13
|
+
return '<i class="far fa-trash-alt"></i>';
|
|
14
|
+
}
|
|
11
15
|
|
|
12
16
|
function flatpickerEditor(cell, onRendered, success, cancel, editorParams) {
|
|
13
17
|
var input = $("<input type='text'/>");
|
package/public/saltcorn.css
CHANGED
package/public/saltcorn.js
CHANGED
|
@@ -423,7 +423,7 @@ function tristateClick(nm) {
|
|
|
423
423
|
}
|
|
424
424
|
}
|
|
425
425
|
|
|
426
|
-
function notifyAlert(note) {
|
|
426
|
+
function notifyAlert(note, spin) {
|
|
427
427
|
if (Array.isArray(note)) {
|
|
428
428
|
note.forEach(notifyAlert);
|
|
429
429
|
return;
|
|
@@ -438,10 +438,16 @@ function notifyAlert(note) {
|
|
|
438
438
|
}
|
|
439
439
|
|
|
440
440
|
$("#alerts-area")
|
|
441
|
-
.append(`<div class="alert alert-${type} alert-dismissible fade show
|
|
441
|
+
.append(`<div class="alert alert-${type} alert-dismissible fade show ${
|
|
442
|
+
spin ? "d-flex align-items-center" : ""
|
|
443
|
+
}" role="alert">
|
|
442
444
|
${txt}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
+
${
|
|
446
|
+
spin
|
|
447
|
+
? `<div class="spinner-border ms-auto" role="status" aria-hidden="true"></div>`
|
|
448
|
+
: `<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close">
|
|
449
|
+
</button>`
|
|
450
|
+
}
|
|
445
451
|
</div>`);
|
|
446
452
|
}
|
|
447
453
|
|
package/routes/admin.js
CHANGED
|
@@ -71,6 +71,7 @@ const {
|
|
|
71
71
|
} = require("../markup/admin");
|
|
72
72
|
const moment = require("moment");
|
|
73
73
|
const View = require("@saltcorn/data/models/view");
|
|
74
|
+
const { getConfigFile } = require("@saltcorn/data/db/connect");
|
|
74
75
|
|
|
75
76
|
/**
|
|
76
77
|
* @type {object}
|
|
@@ -99,7 +100,7 @@ const site_id_form = (req) =>
|
|
|
99
100
|
"page_custom_html",
|
|
100
101
|
"development_mode",
|
|
101
102
|
"log_sql",
|
|
102
|
-
"multitenancy_enabled",
|
|
103
|
+
...(getConfigFile() ? ["multitenancy_enabled"] : []),
|
|
103
104
|
],
|
|
104
105
|
action: "/admin",
|
|
105
106
|
submitLabel: req.__("Save"),
|
|
@@ -398,6 +399,15 @@ router.get(
|
|
|
398
399
|
? span(
|
|
399
400
|
{ class: "badge bg-primary ms-2" },
|
|
400
401
|
req.__("Latest")
|
|
402
|
+
) +
|
|
403
|
+
post_btn(
|
|
404
|
+
"/admin/check-for-upgrade",
|
|
405
|
+
req.__("Check for updates"),
|
|
406
|
+
req.csrfToken(),
|
|
407
|
+
{
|
|
408
|
+
btnClass: "btn-primary btn-sm px-1 py-0",
|
|
409
|
+
formClass: "d-inline",
|
|
410
|
+
}
|
|
401
411
|
)
|
|
402
412
|
: "")
|
|
403
413
|
)
|
|
@@ -498,7 +508,15 @@ router.post(
|
|
|
498
508
|
}
|
|
499
509
|
})
|
|
500
510
|
);
|
|
501
|
-
|
|
511
|
+
router.post(
|
|
512
|
+
"/check-for-upgrade",
|
|
513
|
+
isAdmin,
|
|
514
|
+
error_catcher(async (req, res) => {
|
|
515
|
+
await getState().deleteConfig("latest_npm_version");
|
|
516
|
+
req.flash("success", req.__(`Versions refreshed`));
|
|
517
|
+
res.redirect(`/admin/system`);
|
|
518
|
+
})
|
|
519
|
+
);
|
|
502
520
|
/**
|
|
503
521
|
* @name post/backup
|
|
504
522
|
* @function
|
package/routes/api.js
CHANGED
|
@@ -111,6 +111,51 @@ function accessAllowed(req, user, trigger) {
|
|
|
111
111
|
return role <= trigger.min_role;
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
router.get(
|
|
115
|
+
"/:tableName/distinct/:fieldName",
|
|
116
|
+
//passport.authenticate("api-bearer", { session: false }),
|
|
117
|
+
error_catcher(async (req, res, next) => {
|
|
118
|
+
let { tableName, fieldName } = req.params;
|
|
119
|
+
const table = await Table.findOne(
|
|
120
|
+
strictParseInt(tableName)
|
|
121
|
+
? { id: strictParseInt(tableName) }
|
|
122
|
+
: { name: tableName }
|
|
123
|
+
);
|
|
124
|
+
if (!table) {
|
|
125
|
+
res.status(404).json({ error: req.__("Not found") });
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
await passport.authenticate(
|
|
130
|
+
"api-bearer",
|
|
131
|
+
{ session: false },
|
|
132
|
+
async function (err, user, info) {
|
|
133
|
+
if (accessAllowedRead(req, user, table)) {
|
|
134
|
+
const field = (await table.getFields()).find(
|
|
135
|
+
(f) => f.name === fieldName
|
|
136
|
+
);
|
|
137
|
+
if (!field) {
|
|
138
|
+
res.status(404).json({ error: req.__("Not found") });
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
let dvs;
|
|
142
|
+
if (
|
|
143
|
+
field.is_fkey ||
|
|
144
|
+
(field.type.name === "String" && field.attributes?.options)
|
|
145
|
+
) {
|
|
146
|
+
dvs = await field.distinct_values();
|
|
147
|
+
} else {
|
|
148
|
+
dvs = await table.distinctValues(fieldName);
|
|
149
|
+
}
|
|
150
|
+
res.json({ success: dvs });
|
|
151
|
+
} else {
|
|
152
|
+
res.status(401).json({ error: req.__("Not authorized") });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
)(req, res, next);
|
|
156
|
+
})
|
|
157
|
+
);
|
|
158
|
+
|
|
114
159
|
/**
|
|
115
160
|
* Select Table rows using GET
|
|
116
161
|
* @name get/:tableName/
|
package/routes/fields.js
CHANGED
|
@@ -631,14 +631,19 @@ router.post(
|
|
|
631
631
|
if (fieldName.includes(".")) {
|
|
632
632
|
//join field
|
|
633
633
|
const kpath = fieldName.split(".");
|
|
634
|
-
|
|
635
634
|
if (kpath.length === 2 && row[kpath[0]]) {
|
|
636
635
|
const field = fields.find((f) => f.name === kpath[0]);
|
|
637
636
|
const reftable = await Table.findOne({ name: field.reftable_name });
|
|
638
637
|
const targetField = (await reftable.getFields()).find(
|
|
639
638
|
(f) => f.name === kpath[1]
|
|
640
639
|
);
|
|
641
|
-
|
|
640
|
+
//console.log({ kpath, fieldview, targetField });
|
|
641
|
+
let fv = targetField.type.fieldviews[fieldview];
|
|
642
|
+
if (!fv) {
|
|
643
|
+
fv =
|
|
644
|
+
targetField.type.fieldviews.show ||
|
|
645
|
+
targetField.type.fieldviews.as_text;
|
|
646
|
+
}
|
|
642
647
|
const q = { [reftable.pk_name]: row[kpath[0]] };
|
|
643
648
|
const refRow = await reftable.getRow(q);
|
|
644
649
|
const configuration = req.query;
|
|
@@ -648,6 +653,25 @@ router.post(
|
|
|
648
653
|
readState(configuration, configFields);
|
|
649
654
|
res.send(fv.run(refRow[kpath[1]], req, configuration));
|
|
650
655
|
return;
|
|
656
|
+
} else if (row[kpath[0]]) {
|
|
657
|
+
let oldTable = table;
|
|
658
|
+
let oldRow = row;
|
|
659
|
+
for (const ref of kpath) {
|
|
660
|
+
const ofields = await oldTable.getFields();
|
|
661
|
+
const field = ofields.find((f) => f.name === ref);
|
|
662
|
+
if (field.is_fkey) {
|
|
663
|
+
const reftable = await Table.findOne({ name: field.reftable_name });
|
|
664
|
+
if (!oldRow[ref]) break;
|
|
665
|
+
const q = { [reftable.pk_name]: oldRow[ref] };
|
|
666
|
+
oldRow = await reftable.getRow(q);
|
|
667
|
+
oldTable = reftable;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
if (oldRow) {
|
|
671
|
+
const value = oldRow[kpath[kpath.length - 1]];
|
|
672
|
+
res.send(value);
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
651
675
|
}
|
|
652
676
|
res.send("");
|
|
653
677
|
return;
|
package/routes/list.js
CHANGED
package/routes/plugins.js
CHANGED
|
@@ -450,6 +450,7 @@ const store_actions_dropdown = (req) =>
|
|
|
450
450
|
{
|
|
451
451
|
class: "dropdown-item",
|
|
452
452
|
href: `/plugins/upgrade`,
|
|
453
|
+
onClick: `notifyAlert('Upgrading plugins...', true)`,
|
|
453
454
|
},
|
|
454
455
|
'<i class="far fa-arrow-alt-circle-up"></i> ' +
|
|
455
456
|
req.__("Upgrade installed plugins")
|
package/tests/fields.test.js
CHANGED
|
@@ -100,8 +100,8 @@ describe("Field Endpoints", () => {
|
|
|
100
100
|
await request(app)
|
|
101
101
|
.post("/field/")
|
|
102
102
|
.send("stepName=Basic properties")
|
|
103
|
-
.send("name=
|
|
104
|
-
.send("label=
|
|
103
|
+
.send("name=Editor")
|
|
104
|
+
.send("label=Editor")
|
|
105
105
|
.send("type=String")
|
|
106
106
|
.send("contextEnc=" + ctx)
|
|
107
107
|
.set("Cookie", loginCookie)
|
package/tests/table.test.js
CHANGED
|
@@ -33,9 +33,9 @@ describe("Table Endpoints", () => {
|
|
|
33
33
|
.post("/table/")
|
|
34
34
|
.send("name=mypostedtable")
|
|
35
35
|
.set("Cookie", loginCookie)
|
|
36
|
-
.expect(toRedirect("/table/
|
|
36
|
+
.expect(toRedirect("/table/7"));
|
|
37
37
|
await request(app)
|
|
38
|
-
.get("/table/
|
|
38
|
+
.get("/table/7")
|
|
39
39
|
.set("Cookie", loginCookie)
|
|
40
40
|
.expect(toInclude("mypostedtable"));
|
|
41
41
|
await request(app)
|
|
@@ -149,7 +149,7 @@ Pencil, 0.5,2, t`;
|
|
|
149
149
|
.set("Cookie", loginCookie)
|
|
150
150
|
.field("name", "expenses")
|
|
151
151
|
.attach("file", Buffer.from(csv, "utf-8"))
|
|
152
|
-
.expect(toRedirect("/table/
|
|
152
|
+
.expect(toRedirect("/table/8"));
|
|
153
153
|
});
|
|
154
154
|
it("should upload csv to existing table", async () => {
|
|
155
155
|
const csv = `author,Pages
|