@saltcorn/server 1.1.1-rc.1 → 1.1.1-rc.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/CHANGELOG.md +10 -0
- package/locales/en.json +7 -1
- package/package.json +9 -9
- package/routes/actions.js +17 -1
- package/routes/search.js +91 -11
- package/routes/tables.js +18 -4
- package/tests/files.test.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## 1.1.1 - In beta
|
|
4
4
|
|
|
5
|
+
* Full-text search improvements:
|
|
6
|
+
- An index for full-text search can now be created. When creating an index in
|
|
7
|
+
the constraints setting for a table, you can select "Full-text search" in
|
|
8
|
+
the field selector. This will dramatically speed up search on large tables.
|
|
9
|
+
- Use websearch_to_tsquery if available. This is a more natural and modern syntax.
|
|
10
|
+
- Link to syntax examples in /search
|
|
11
|
+
- Use default locale's language for search localisation.
|
|
12
|
+
- Option to show results in tabs in search configuration.
|
|
13
|
+
|
|
5
14
|
* select_by_view fieldview for Key fields: the user selects the value of a
|
|
6
15
|
Key field based on an clicking in a row of rendered views (typically a Show view) of the joined table. Works for both Edit and Filter views.
|
|
7
16
|
|
|
@@ -38,6 +47,7 @@
|
|
|
38
47
|
- ForLoop step type for loops over arrays.
|
|
39
48
|
- Varius UX improvements for editing workflows
|
|
40
49
|
- Integrate copilot, if installed, in workflow editing
|
|
50
|
+
- Call non-workflow trigger actions.
|
|
41
51
|
|
|
42
52
|
* sbadmin2 theme - Color update: dark side bar, darker primary blue
|
|
43
53
|
|
package/locales/en.json
CHANGED
|
@@ -1545,5 +1545,11 @@
|
|
|
1545
1545
|
"The home page is the page that is served when the user visits the home location (/). This can be set for each user role.": "The home page is the page that is served when the user visits the home location (/). This can be set for each user role.",
|
|
1546
1546
|
"Trigger %s deleted": "Trigger %s deleted",
|
|
1547
1547
|
"Edit menu": "Edit menu",
|
|
1548
|
-
"Minimum role to edit menu": "Minimum role to edit menu"
|
|
1548
|
+
"Minimum role to edit menu": "Minimum role to edit menu",
|
|
1549
|
+
"Full-text search index is not available as the table contains Key fields (%s) with the \"Include in full-text search\" option enabled. Disable this before creating a Full-text search index": "Full-text search index is not available as the table contains Key fields (%s) with the \"Include in full-text search\" option enabled. Disable this before creating a Full-text search index",
|
|
1550
|
+
"Share Extension Provisioning Profile": "Share Extension Provisioning Profile",
|
|
1551
|
+
"Show results in": "Show results in",
|
|
1552
|
+
"Show results from each table in this type of element": "Show results from each table in this type of element",
|
|
1553
|
+
"Search syntax help": "Search syntax help",
|
|
1554
|
+
"Search syntax": "Search syntax"
|
|
1549
1555
|
}
|
package/package.json
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saltcorn/server",
|
|
3
|
-
"version": "1.1.1-rc.
|
|
3
|
+
"version": "1.1.1-rc.2",
|
|
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": "1.1.1-rc.
|
|
11
|
-
"@saltcorn/builder": "1.1.1-rc.
|
|
12
|
-
"@saltcorn/data": "1.1.1-rc.
|
|
13
|
-
"@saltcorn/admin-models": "1.1.1-rc.
|
|
14
|
-
"@saltcorn/filemanager": "1.1.1-rc.
|
|
15
|
-
"@saltcorn/markup": "1.1.1-rc.
|
|
16
|
-
"@saltcorn/plugins-loader": "1.1.1-rc.
|
|
17
|
-
"@saltcorn/sbadmin2": "1.1.1-rc.
|
|
10
|
+
"@saltcorn/base-plugin": "1.1.1-rc.2",
|
|
11
|
+
"@saltcorn/builder": "1.1.1-rc.2",
|
|
12
|
+
"@saltcorn/data": "1.1.1-rc.2",
|
|
13
|
+
"@saltcorn/admin-models": "1.1.1-rc.2",
|
|
14
|
+
"@saltcorn/filemanager": "1.1.1-rc.2",
|
|
15
|
+
"@saltcorn/markup": "1.1.1-rc.2",
|
|
16
|
+
"@saltcorn/plugins-loader": "1.1.1-rc.2",
|
|
17
|
+
"@saltcorn/sbadmin2": "1.1.1-rc.2",
|
|
18
18
|
"@socket.io/cluster-adapter": "^0.2.1",
|
|
19
19
|
"@socket.io/sticky": "^1.0.1",
|
|
20
20
|
"adm-zip": "0.5.10",
|
package/routes/actions.js
CHANGED
|
@@ -731,7 +731,7 @@ const getWorkflowStepForm = async (
|
|
|
731
731
|
},
|
|
732
732
|
};
|
|
733
733
|
if (cfgFld.input_type === "code") cfgFld.input_type = "textarea";
|
|
734
|
-
actionConfigFields.push(cfgFld)
|
|
734
|
+
actionConfigFields.push(cfgFld);
|
|
735
735
|
}
|
|
736
736
|
} catch {}
|
|
737
737
|
}
|
|
@@ -745,6 +745,21 @@ const getWorkflowStepForm = async (
|
|
|
745
745
|
wf_action_name: Trigger.find({ action: "Workflow" }).map((wf) => wf.name),
|
|
746
746
|
},
|
|
747
747
|
});
|
|
748
|
+
const nonWfTriggerNames = Trigger.find({})
|
|
749
|
+
.filter((tr) => tr.action !== "Workflow")
|
|
750
|
+
.map((wf) => wf.name);
|
|
751
|
+
|
|
752
|
+
actionConfigFields.push({
|
|
753
|
+
label: "Row expression",
|
|
754
|
+
name: "row_expr",
|
|
755
|
+
type: "String",
|
|
756
|
+
class: "validate-expression",
|
|
757
|
+
sublabel:
|
|
758
|
+
"Expression for the object to set the <code>row</code> value to inside the action. If blank, set to whole context",
|
|
759
|
+
showIf: {
|
|
760
|
+
wf_action_name: nonWfTriggerNames,
|
|
761
|
+
},
|
|
762
|
+
});
|
|
748
763
|
|
|
749
764
|
const builtInActionExplainers = WorkflowStep.builtInActionExplainers({
|
|
750
765
|
api_call: trigger.when_trigger == "API call",
|
|
@@ -752,6 +767,7 @@ const getWorkflowStepForm = async (
|
|
|
752
767
|
const actionsNotRequiringRow = Trigger.action_options({
|
|
753
768
|
notRequireRow: true,
|
|
754
769
|
noMultiStep: true,
|
|
770
|
+
apiNeverTriggers: true,
|
|
755
771
|
builtInLabel: "Workflow Actions",
|
|
756
772
|
builtIns: Object.keys(builtInActionExplainers),
|
|
757
773
|
forWorkflow: true,
|
package/routes/search.js
CHANGED
|
@@ -8,6 +8,8 @@ const Router = require("express-promise-router");
|
|
|
8
8
|
const { span, h5, h4, nbsp, p, a, div } = require("@saltcorn/markup/tags");
|
|
9
9
|
|
|
10
10
|
const { getState } = require("@saltcorn/data/db/state");
|
|
11
|
+
const db = require("@saltcorn/data/db");
|
|
12
|
+
|
|
11
13
|
const { isAdmin, error_catcher } = require("./utils.js");
|
|
12
14
|
const Form = require("@saltcorn/data/models/form");
|
|
13
15
|
const Table = require("@saltcorn/data/models/table");
|
|
@@ -58,6 +60,13 @@ const searchConfigForm = (tables, views, req) => {
|
|
|
58
60
|
sublabel: req.__("Use table description instead of name as header"),
|
|
59
61
|
type: "Bool",
|
|
60
62
|
});
|
|
63
|
+
fields.push({
|
|
64
|
+
name: "search_results_decoration",
|
|
65
|
+
label: req.__("Show results in"),
|
|
66
|
+
sublabel: req.__("Show results from each table in this type of element"),
|
|
67
|
+
input_type: "select",
|
|
68
|
+
options: ["Cards", "Tabs"],
|
|
69
|
+
});
|
|
61
70
|
const blurb1 = req.__(
|
|
62
71
|
`Choose views for <a href="/search">search results</a> for each table.<br/>Set to blank to omit table from global search.`
|
|
63
72
|
);
|
|
@@ -95,6 +104,10 @@ router.get(
|
|
|
95
104
|
"search_table_description",
|
|
96
105
|
false
|
|
97
106
|
);
|
|
107
|
+
form.values.search_results_decoration = getState().getConfig(
|
|
108
|
+
"search_results_decoration",
|
|
109
|
+
"Cards"
|
|
110
|
+
);
|
|
98
111
|
send_infoarch_page({
|
|
99
112
|
res,
|
|
100
113
|
req,
|
|
@@ -126,13 +139,20 @@ router.post(
|
|
|
126
139
|
const result = form.validate(req.body);
|
|
127
140
|
|
|
128
141
|
if (result.success) {
|
|
142
|
+
const dbversion = await db.getVersion(true);
|
|
129
143
|
const search_table_description =
|
|
130
144
|
!!result.success.search_table_description;
|
|
131
145
|
await getState().setConfig(
|
|
132
146
|
"search_table_description",
|
|
133
147
|
search_table_description
|
|
134
148
|
);
|
|
149
|
+
await getState().setConfig(
|
|
150
|
+
"search_results_decoration",
|
|
151
|
+
result.success.search_results_decoration || "Cards"
|
|
152
|
+
);
|
|
153
|
+
await getState().setConfig("search_use_websearch", +dbversion >= 11.0);
|
|
135
154
|
delete result.success.search_table_description;
|
|
155
|
+
delete result.success.search_results_decoration;
|
|
136
156
|
await getState().setConfig("globalSearch", result.success);
|
|
137
157
|
if (!req.xhr) res.redirect("/search/config");
|
|
138
158
|
else res.json({ success: "ok" });
|
|
@@ -196,9 +216,12 @@ const runSearch = async ({ q, _page, table }, req, res) => {
|
|
|
196
216
|
"search_table_description",
|
|
197
217
|
false
|
|
198
218
|
);
|
|
219
|
+
const search_results_decoration = getState().getConfig(
|
|
220
|
+
"search_results_decoration",
|
|
221
|
+
"Cards"
|
|
222
|
+
);
|
|
199
223
|
const current_page = parseInt(_page) || 1;
|
|
200
224
|
const offset = (current_page - 1) * page_size;
|
|
201
|
-
let resp = [];
|
|
202
225
|
let tablesWithResults = [];
|
|
203
226
|
let tablesConfigured = 0;
|
|
204
227
|
for (const [tableName, viewName] of Object.entries(cfg)) {
|
|
@@ -206,7 +229,9 @@ const runSearch = async ({ q, _page, table }, req, res) => {
|
|
|
206
229
|
!viewName ||
|
|
207
230
|
viewName === "" ||
|
|
208
231
|
viewName === "search_table_description" ||
|
|
209
|
-
tableName === "search_table_description"
|
|
232
|
+
tableName === "search_table_description" ||
|
|
233
|
+
viewName === "search_results_decoration" ||
|
|
234
|
+
tableName === "search_results_decoration"
|
|
210
235
|
)
|
|
211
236
|
continue;
|
|
212
237
|
tablesConfigured += 1;
|
|
@@ -238,10 +263,9 @@ const runSearch = async ({ q, _page, table }, req, res) => {
|
|
|
238
263
|
}
|
|
239
264
|
|
|
240
265
|
if (vresps.length > 0) {
|
|
241
|
-
tablesWithResults.push({
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
title: span({ id: tableName }, sectionHeader),
|
|
266
|
+
tablesWithResults.push({
|
|
267
|
+
tableName,
|
|
268
|
+
label: sectionHeader,
|
|
245
269
|
contents: vresps.map((vr) => vr.html).join("<hr>") + paginate,
|
|
246
270
|
});
|
|
247
271
|
}
|
|
@@ -251,17 +275,36 @@ const runSearch = async ({ q, _page, table }, req, res) => {
|
|
|
251
275
|
const form = searchForm();
|
|
252
276
|
form.validate({ q });
|
|
253
277
|
|
|
278
|
+
const mkResultDisplay = () => {
|
|
279
|
+
switch (search_results_decoration) {
|
|
280
|
+
case "Tabs":
|
|
281
|
+
const tabContents = {};
|
|
282
|
+
tablesWithResults.forEach((tblRes) => {
|
|
283
|
+
tabContents[tblRes.label] = tblRes.contents;
|
|
284
|
+
});
|
|
285
|
+
return [{ type: "card", tabContents }];
|
|
286
|
+
|
|
287
|
+
default:
|
|
288
|
+
return tablesWithResults.map((tblRes) => ({
|
|
289
|
+
type: "card",
|
|
290
|
+
title: span({ id: tblRes.tableName }, tblRes.label),
|
|
291
|
+
contents: tblRes.contents,
|
|
292
|
+
}));
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
|
|
254
296
|
// Prepare search result visualization
|
|
255
297
|
const searchResult =
|
|
256
|
-
|
|
298
|
+
tablesWithResults.length === 0
|
|
257
299
|
? [{ type: "card", contents: req.__("Not found") }]
|
|
258
|
-
:
|
|
300
|
+
: mkResultDisplay();
|
|
259
301
|
res.sendWrap(req.__("Search all tables"), {
|
|
260
302
|
above: [
|
|
261
303
|
{
|
|
262
304
|
type: "card",
|
|
263
305
|
contents: div(
|
|
264
306
|
renderForm(form, false),
|
|
307
|
+
syntax_help_link(req),
|
|
265
308
|
typeof table !== "undefined" &&
|
|
266
309
|
tablesConfigured > 1 &&
|
|
267
310
|
div(
|
|
@@ -296,6 +339,19 @@ const runSearch = async ({ q, _page, table }, req, res) => {
|
|
|
296
339
|
});
|
|
297
340
|
};
|
|
298
341
|
|
|
342
|
+
const syntax_help_link = (req) => {
|
|
343
|
+
const use_websearch = getState().getConfig("search_use_websearch", false);
|
|
344
|
+
if (use_websearch)
|
|
345
|
+
return a(
|
|
346
|
+
{
|
|
347
|
+
href: "javascript:void(0);",
|
|
348
|
+
onclick: "ajax_modal('/search/syntax-help')",
|
|
349
|
+
},
|
|
350
|
+
req.__("Search syntax")
|
|
351
|
+
);
|
|
352
|
+
else return "";
|
|
353
|
+
};
|
|
354
|
+
|
|
299
355
|
/**
|
|
300
356
|
* Execute search or only show search form
|
|
301
357
|
* @name get
|
|
@@ -327,9 +383,33 @@ router.get(
|
|
|
327
383
|
}
|
|
328
384
|
|
|
329
385
|
const form = searchForm();
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
386
|
+
res.sendWrap(req.__("Search all tables"), {
|
|
387
|
+
type: "card",
|
|
388
|
+
contents: renderForm(form, false) + syntax_help_link(req),
|
|
389
|
+
});
|
|
333
390
|
}
|
|
334
391
|
})
|
|
335
392
|
);
|
|
393
|
+
|
|
394
|
+
router.get(
|
|
395
|
+
"/syntax-help",
|
|
396
|
+
error_catcher(async (req, res) => {
|
|
397
|
+
res.sendWrap(
|
|
398
|
+
req.__("Search syntax help"),
|
|
399
|
+
div(
|
|
400
|
+
p(
|
|
401
|
+
`Individual words matched independently. Example <code>large cat</code>`
|
|
402
|
+
),
|
|
403
|
+
p(
|
|
404
|
+
`Double quotes to match phrase as a single unit. Example <code>"large cat"</code> matches "the large cat sat..." but not "the large brown cat".`
|
|
405
|
+
),
|
|
406
|
+
p(
|
|
407
|
+
`"or" to match either of two phrases. Example <code>cat or mouse</code>`
|
|
408
|
+
),
|
|
409
|
+
p(
|
|
410
|
+
`"-" to exclude a word or phrase. Example <code>cat -mouse</code> does not match "cat and mouse"`
|
|
411
|
+
)
|
|
412
|
+
)
|
|
413
|
+
);
|
|
414
|
+
})
|
|
415
|
+
);
|
package/routes/tables.js
CHANGED
|
@@ -1533,6 +1533,8 @@ router.get(
|
|
|
1533
1533
|
key: (r) =>
|
|
1534
1534
|
r.type === "Unique"
|
|
1535
1535
|
? r.configuration.fields.join(", ")
|
|
1536
|
+
: r.type === "Index" && r.configuration?.field === "_fts"
|
|
1537
|
+
? "Full text search"
|
|
1536
1538
|
: r.type === "Index"
|
|
1537
1539
|
? r.configuration.field
|
|
1538
1540
|
: r.type === "Formula"
|
|
@@ -1629,11 +1631,23 @@ const constraintForm = (req, table, fields, type) => {
|
|
|
1629
1631
|
],
|
|
1630
1632
|
});
|
|
1631
1633
|
case "Index":
|
|
1634
|
+
const fieldopts = fields.map((f) => ({ label: f.label, name: f.name }));
|
|
1635
|
+
const hasIncludeFts = fields.filter((f) => f.attributes?.include_fts);
|
|
1636
|
+
if (!db.isSQLite && !hasIncludeFts.length)
|
|
1637
|
+
fieldopts.push({ label: "Full-text search", name: "_fts" });
|
|
1632
1638
|
return new Form({
|
|
1633
1639
|
action: `/table/add-constraint/${table.id}/${type}`,
|
|
1634
|
-
blurb:
|
|
1635
|
-
|
|
1636
|
-
|
|
1640
|
+
blurb:
|
|
1641
|
+
req.__(
|
|
1642
|
+
"Choose the field to be indexed. This make searching the table faster."
|
|
1643
|
+
) +
|
|
1644
|
+
" " +
|
|
1645
|
+
(hasIncludeFts.length
|
|
1646
|
+
? req.__(
|
|
1647
|
+
`Full-text search index is not available as the table contains Key fields (%s) with the "Include in full-text search" option enabled. Disable this before creating a Full-text search index`,
|
|
1648
|
+
hasIncludeFts.map((f) => f.name).join(",")
|
|
1649
|
+
)
|
|
1650
|
+
: ""),
|
|
1637
1651
|
fields: [
|
|
1638
1652
|
{
|
|
1639
1653
|
type: "String",
|
|
@@ -1641,7 +1655,7 @@ const constraintForm = (req, table, fields, type) => {
|
|
|
1641
1655
|
label: "Field",
|
|
1642
1656
|
required: true,
|
|
1643
1657
|
attributes: {
|
|
1644
|
-
options:
|
|
1658
|
+
options: fieldopts,
|
|
1645
1659
|
},
|
|
1646
1660
|
},
|
|
1647
1661
|
],
|
package/tests/files.test.js
CHANGED
|
@@ -407,7 +407,7 @@ describe("visible_entries test", () => {
|
|
|
407
407
|
.set("Cookie", staffCookie);
|
|
408
408
|
expect(resp.statusCode).toBe(200);
|
|
409
409
|
expect(
|
|
410
|
-
resp.body.directories.find((file) => file.filename === "subsubfolder")
|
|
410
|
+
resp.body.directories.find((file) => file.filename === "_sc_test_subfolder_one/subsubfolder")
|
|
411
411
|
).toBeDefined();
|
|
412
412
|
});
|
|
413
413
|
|