@saltcorn/server 0.8.7 → 0.8.8-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/auth/admin.js +20 -16
- package/auth/routes.js +12 -8
- package/locales/en.json +29 -1
- package/locales/si.json +1197 -0
- package/package.json +8 -8
- package/public/diagram_utils.js +21 -1
- package/public/saltcorn-common.js +40 -19
- package/public/saltcorn.js +11 -5
- package/routes/admin.js +2 -0
- package/routes/common_lists.js +34 -19
- package/routes/diagram.js +214 -199
- package/routes/fields.js +113 -6
- package/routes/index.js +2 -0
- package/routes/models.js +492 -0
- package/routes/page.js +10 -6
- package/routes/tables.js +67 -41
- package/routes/view.js +10 -6
- package/routes/viewedit.js +27 -5
- package/tests/view.test.js +217 -0
package/routes/models.js
ADDED
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @category server
|
|
3
|
+
* @module routes/models
|
|
4
|
+
* @subcategory routes
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const Router = require("express-promise-router");
|
|
8
|
+
|
|
9
|
+
const { error_catcher, is_relative_url, isAdmin } = require("./utils.js");
|
|
10
|
+
const Table = require("@saltcorn/data/models/table");
|
|
11
|
+
const Form = require("@saltcorn/data/models/form");
|
|
12
|
+
const Model = require("@saltcorn/data/models/model");
|
|
13
|
+
const ModelInstance = require("@saltcorn/data/models/model_instance");
|
|
14
|
+
const { getState } = require("@saltcorn/data/db/state");
|
|
15
|
+
const db = require("@saltcorn/data/db");
|
|
16
|
+
const moment = require("moment");
|
|
17
|
+
|
|
18
|
+
const { mkTable, renderForm, post_delete_btn } = require("@saltcorn/markup");
|
|
19
|
+
const {
|
|
20
|
+
span,
|
|
21
|
+
h4,
|
|
22
|
+
p,
|
|
23
|
+
a,
|
|
24
|
+
div,
|
|
25
|
+
i,
|
|
26
|
+
form,
|
|
27
|
+
label,
|
|
28
|
+
input,
|
|
29
|
+
text,
|
|
30
|
+
script,
|
|
31
|
+
domReady,
|
|
32
|
+
code,
|
|
33
|
+
iframe,
|
|
34
|
+
style,
|
|
35
|
+
pre,
|
|
36
|
+
} = require("@saltcorn/markup/tags");
|
|
37
|
+
|
|
38
|
+
const router = new Router();
|
|
39
|
+
module.exports = router;
|
|
40
|
+
|
|
41
|
+
const newModelForm = (table, req) => {
|
|
42
|
+
return new Form({
|
|
43
|
+
action: "/models/new/" + table.id,
|
|
44
|
+
fields: [
|
|
45
|
+
{ name: "name", label: "Name", type: "String", required: true },
|
|
46
|
+
{
|
|
47
|
+
name: "modelpattern",
|
|
48
|
+
label: "Model pattern",
|
|
49
|
+
type: "String",
|
|
50
|
+
required: true,
|
|
51
|
+
attributes: { options: Object.keys(getState().modelpatterns) },
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
router.get(
|
|
58
|
+
"/new/:table_id",
|
|
59
|
+
isAdmin,
|
|
60
|
+
error_catcher(async (req, res) => {
|
|
61
|
+
const { table_id } = req.params;
|
|
62
|
+
const table = await Table.findOne({ id: table_id });
|
|
63
|
+
res.sendWrap(req.__(`New model`), {
|
|
64
|
+
above: [
|
|
65
|
+
{
|
|
66
|
+
type: "breadcrumbs",
|
|
67
|
+
crumbs: [
|
|
68
|
+
{ text: req.__("Tables"), href: "/table" },
|
|
69
|
+
{ href: `/table/${table.id}`, text: table.name },
|
|
70
|
+
{ text: req.__(`New model`) },
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
type: "card",
|
|
75
|
+
class: "mt-0",
|
|
76
|
+
title: req.__(`New model`),
|
|
77
|
+
contents: renderForm(newModelForm(table, req), req.csrfToken()),
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
});
|
|
81
|
+
})
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
router.post(
|
|
85
|
+
"/new/:table_id",
|
|
86
|
+
isAdmin,
|
|
87
|
+
error_catcher(async (req, res) => {
|
|
88
|
+
const { table_id } = req.params;
|
|
89
|
+
const table = await Table.findOne({ id: table_id });
|
|
90
|
+
const form = newModelForm(table, req);
|
|
91
|
+
form.validate(req.body);
|
|
92
|
+
if (form.hasErrors) {
|
|
93
|
+
res.sendWrap(req.__(`New model`), renderForm(form, req.csrfToken()));
|
|
94
|
+
} else {
|
|
95
|
+
const model = await Model.create({ ...form.values, table_id: table.id });
|
|
96
|
+
if (model.templateObj.configuration_workflow)
|
|
97
|
+
res.redirect(`/models/config/${model.id}`);
|
|
98
|
+
else res.redirect(`/models/show/${model.id}`);
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const respondWorkflow = (model, table, wf, wfres, req, res) => {
|
|
104
|
+
const wrap = (contents, noCard, previewURL) => ({
|
|
105
|
+
above: [
|
|
106
|
+
{
|
|
107
|
+
type: "breadcrumbs",
|
|
108
|
+
crumbs: [
|
|
109
|
+
{ text: req.__("Tables"), href: "/table" },
|
|
110
|
+
{ href: `/table/${table.id || table.name}`, text: table.name },
|
|
111
|
+
{ href: `/models/show/${model.id}`, text: model.name },
|
|
112
|
+
{ text: req.__("Configuration") },
|
|
113
|
+
],
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
type: noCard ? "container" : "card",
|
|
117
|
+
class: !noCard && "mt-0",
|
|
118
|
+
title: wfres.title,
|
|
119
|
+
titleAjaxIndicator: true,
|
|
120
|
+
contents,
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
});
|
|
124
|
+
if (wfres.flash) req.flash(wfres.flash[0], wfres.flash[1]);
|
|
125
|
+
if (wfres.renderForm)
|
|
126
|
+
res.sendWrap(
|
|
127
|
+
{
|
|
128
|
+
title: req.__(`%s configuration`, model.name),
|
|
129
|
+
headers: [
|
|
130
|
+
{
|
|
131
|
+
script: `/static_assets/${db.connectObj.version_tag}/jquery-menu-editor.min.js`,
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
script: `/static_assets/${db.connectObj.version_tag}/iconset-fontawesome5-3-1.min.js`,
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
script: `/static_assets/${db.connectObj.version_tag}/bootstrap-iconpicker.js`,
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
css: `/static_assets/${db.connectObj.version_tag}/bootstrap-iconpicker.min.css`,
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
wrap(
|
|
145
|
+
renderForm(wfres.renderForm, req.csrfToken()),
|
|
146
|
+
false,
|
|
147
|
+
wfres.previewURL
|
|
148
|
+
)
|
|
149
|
+
);
|
|
150
|
+
else res.redirect(wfres.redirect);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const get_model_workflow = (model, req) => {
|
|
154
|
+
const workflow = model.templateObj.configuration_workflow(req);
|
|
155
|
+
workflow.action = `/models/config/${model.id}`;
|
|
156
|
+
const oldOnDone = workflow.onDone || ((c) => c);
|
|
157
|
+
workflow.onDone = async (ctx) => {
|
|
158
|
+
const { id, ...configuration } = await oldOnDone(ctx);
|
|
159
|
+
await model.update({ configuration });
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
redirect: `/models/show/${model.id}`,
|
|
163
|
+
flash: ["success", `Model ${this.name || ""} saved`],
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
return workflow;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
router.get(
|
|
170
|
+
"/config/:id",
|
|
171
|
+
isAdmin,
|
|
172
|
+
error_catcher(async (req, res) => {
|
|
173
|
+
const { id } = req.params;
|
|
174
|
+
const { step } = req.query;
|
|
175
|
+
|
|
176
|
+
const model = await Model.findOne({ id });
|
|
177
|
+
const table = await Table.findOne({ id: model.table_id });
|
|
178
|
+
const configFlow = get_model_workflow(model, req);
|
|
179
|
+
const wfres = await configFlow.run(
|
|
180
|
+
{
|
|
181
|
+
...model.configuration,
|
|
182
|
+
id: model.id,
|
|
183
|
+
table_id: model.table_id,
|
|
184
|
+
...(step ? { stepName: step } : {}),
|
|
185
|
+
},
|
|
186
|
+
req
|
|
187
|
+
);
|
|
188
|
+
respondWorkflow(model, table, configFlow, wfres, req, res);
|
|
189
|
+
})
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
router.post(
|
|
193
|
+
"/config/:id",
|
|
194
|
+
isAdmin,
|
|
195
|
+
error_catcher(async (req, res) => {
|
|
196
|
+
const { id } = req.params;
|
|
197
|
+
const { step } = req.query;
|
|
198
|
+
|
|
199
|
+
const model = await Model.findOne({ id });
|
|
200
|
+
const table = await Table.findOne({ id: model.table_id });
|
|
201
|
+
if (!table) {
|
|
202
|
+
req.flash("error", `Table not found`);
|
|
203
|
+
res.redirect(`/table`);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const workflow = get_model_workflow(model, req);
|
|
207
|
+
const wfres = await workflow.run(req.body, req);
|
|
208
|
+
respondWorkflow(model, table, workflow, wfres, req, res);
|
|
209
|
+
})
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
router.get(
|
|
213
|
+
"/show/:id",
|
|
214
|
+
isAdmin,
|
|
215
|
+
error_catcher(async (req, res) => {
|
|
216
|
+
const { id } = req.params;
|
|
217
|
+
const model = await Model.findOne({ id });
|
|
218
|
+
const table = await Table.findOne({ id: model.table_id });
|
|
219
|
+
const instances = await ModelInstance.find({ model_id: model.id });
|
|
220
|
+
const metrics = model.templateObj.metrics || {};
|
|
221
|
+
const metricCols = Object.entries(metrics).map(([k, v]) => ({
|
|
222
|
+
label: k,
|
|
223
|
+
key: (inst) => inst.metric_values?.[k]?.toPrecision(6),
|
|
224
|
+
}));
|
|
225
|
+
const anyReport = instances.some((i) => !!i.report);
|
|
226
|
+
res.sendWrap(req.__(`Show model`), {
|
|
227
|
+
above: [
|
|
228
|
+
{
|
|
229
|
+
type: "breadcrumbs",
|
|
230
|
+
crumbs: [
|
|
231
|
+
{ text: req.__("Tables"), href: "/table" },
|
|
232
|
+
{ href: `/table/${table.id}`, text: table.name },
|
|
233
|
+
{ text: model.name },
|
|
234
|
+
],
|
|
235
|
+
after: a(
|
|
236
|
+
{ href: `/models/config/${model.id}` },
|
|
237
|
+
req.__("Edit"),
|
|
238
|
+
i({ class: "ms-1 fas fa-edit" })
|
|
239
|
+
),
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
type: "card",
|
|
243
|
+
class: "mt-0",
|
|
244
|
+
title: req.__("Model instances"),
|
|
245
|
+
contents: div(
|
|
246
|
+
mkTable(
|
|
247
|
+
[
|
|
248
|
+
{ label: req.__("Name"), key: "name" },
|
|
249
|
+
{
|
|
250
|
+
label: req.__("Trained"),
|
|
251
|
+
key: (inst) => moment(inst.trained_on).fromNow(),
|
|
252
|
+
},
|
|
253
|
+
...(anyReport
|
|
254
|
+
? [
|
|
255
|
+
{
|
|
256
|
+
label: req.__("Report"),
|
|
257
|
+
key: (inst) =>
|
|
258
|
+
inst.report
|
|
259
|
+
? a(
|
|
260
|
+
{ href: `/models/show-report/${inst.id}` },
|
|
261
|
+
i({ class: "fas fa-file-alt" })
|
|
262
|
+
)
|
|
263
|
+
: "",
|
|
264
|
+
},
|
|
265
|
+
]
|
|
266
|
+
: []),
|
|
267
|
+
...metricCols,
|
|
268
|
+
{
|
|
269
|
+
label: req.__("Default"),
|
|
270
|
+
key: (inst) =>
|
|
271
|
+
form(
|
|
272
|
+
{
|
|
273
|
+
action: `/models/make-default-instance/${inst.id}`,
|
|
274
|
+
method: "POST",
|
|
275
|
+
},
|
|
276
|
+
span(
|
|
277
|
+
{ class: "form-switch" },
|
|
278
|
+
input({
|
|
279
|
+
class: ["form-check-input"],
|
|
280
|
+
type: "checkbox",
|
|
281
|
+
onChange: "this.form.submit()",
|
|
282
|
+
role: "switch",
|
|
283
|
+
name: "enabled",
|
|
284
|
+
...(inst.is_default && { checked: true }),
|
|
285
|
+
})
|
|
286
|
+
),
|
|
287
|
+
input({
|
|
288
|
+
type: "hidden",
|
|
289
|
+
name: "_csrf",
|
|
290
|
+
value: req.csrfToken(),
|
|
291
|
+
})
|
|
292
|
+
),
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
label: req.__("Delete"),
|
|
296
|
+
key: (r) =>
|
|
297
|
+
post_delete_btn(
|
|
298
|
+
`/models/delete-instance/${encodeURIComponent(r.id)}`,
|
|
299
|
+
req
|
|
300
|
+
),
|
|
301
|
+
},
|
|
302
|
+
],
|
|
303
|
+
instances
|
|
304
|
+
),
|
|
305
|
+
a(
|
|
306
|
+
{ href: `/models/train/${model.id}`, class: "btn btn-primary" },
|
|
307
|
+
i({ class: "fas fa-graduation-cap me-1" }),
|
|
308
|
+
req.__("Train new instance")
|
|
309
|
+
)
|
|
310
|
+
),
|
|
311
|
+
},
|
|
312
|
+
],
|
|
313
|
+
});
|
|
314
|
+
})
|
|
315
|
+
);
|
|
316
|
+
const model_train_form = (model, table, req) => {
|
|
317
|
+
const hyperparameter_fields =
|
|
318
|
+
model.templateObj.hyperparameter_fields?.({
|
|
319
|
+
table,
|
|
320
|
+
...model,
|
|
321
|
+
}) || [];
|
|
322
|
+
return new Form({
|
|
323
|
+
action: `/models/train/${model.id}`,
|
|
324
|
+
onSubmit: "press_store_button(this)",
|
|
325
|
+
fields: [
|
|
326
|
+
{
|
|
327
|
+
name: "name",
|
|
328
|
+
label: req.__("Name"),
|
|
329
|
+
type: "String",
|
|
330
|
+
required: true,
|
|
331
|
+
},
|
|
332
|
+
...hyperparameter_fields,
|
|
333
|
+
],
|
|
334
|
+
});
|
|
335
|
+
};
|
|
336
|
+
router.post(
|
|
337
|
+
"/delete/:id",
|
|
338
|
+
isAdmin,
|
|
339
|
+
error_catcher(async (req, res) => {
|
|
340
|
+
const { id } = req.params;
|
|
341
|
+
const model = await Model.findOne({ id });
|
|
342
|
+
await model.delete();
|
|
343
|
+
req.flash("success", req.__("Model %s deleted", model.name));
|
|
344
|
+
res.redirect(`/table/${model.table_id}`);
|
|
345
|
+
})
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
router.post(
|
|
349
|
+
"/delete-instance/:id",
|
|
350
|
+
isAdmin,
|
|
351
|
+
error_catcher(async (req, res) => {
|
|
352
|
+
const { id } = req.params;
|
|
353
|
+
const model_inst = await ModelInstance.findOne({ id });
|
|
354
|
+
await model_inst.delete();
|
|
355
|
+
req.flash("success", req.__("Model instance %s deleted", model_inst.name));
|
|
356
|
+
res.redirect(`/models/show/${model_inst.model_id}`);
|
|
357
|
+
})
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
router.get(
|
|
361
|
+
"/train/:id",
|
|
362
|
+
isAdmin,
|
|
363
|
+
error_catcher(async (req, res) => {
|
|
364
|
+
const { id } = req.params;
|
|
365
|
+
const model = await Model.findOne({ id });
|
|
366
|
+
const table = await Table.findOne({ id: model.table_id });
|
|
367
|
+
const form = model_train_form(model, table, req);
|
|
368
|
+
res.sendWrap(req.__(`Train model`), {
|
|
369
|
+
above: [
|
|
370
|
+
{
|
|
371
|
+
type: "breadcrumbs",
|
|
372
|
+
crumbs: [
|
|
373
|
+
{ text: req.__("Tables"), href: "/table" },
|
|
374
|
+
{ href: `/table/${table.id}`, text: table.name },
|
|
375
|
+
{ href: `/models/show/${model.id}`, text: model.name },
|
|
376
|
+
{ text: req.__(`Train`) },
|
|
377
|
+
],
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
type: "card",
|
|
381
|
+
class: "mt-0",
|
|
382
|
+
title: req.__(`New model`),
|
|
383
|
+
contents: renderForm(form, req.csrfToken()),
|
|
384
|
+
},
|
|
385
|
+
],
|
|
386
|
+
});
|
|
387
|
+
})
|
|
388
|
+
);
|
|
389
|
+
router.post(
|
|
390
|
+
"/train/:id",
|
|
391
|
+
isAdmin,
|
|
392
|
+
error_catcher(async (req, res) => {
|
|
393
|
+
const { id } = req.params;
|
|
394
|
+
const model = await Model.findOne({ id });
|
|
395
|
+
const table = Table.findOne({ id: model.table_id });
|
|
396
|
+
const form = model_train_form(model, table, req);
|
|
397
|
+
form.validate(req.body);
|
|
398
|
+
if (form.hasErrors) {
|
|
399
|
+
res.sendWrap(req.__(`Train model`), renderForm(form, req.csrfToken()));
|
|
400
|
+
} else {
|
|
401
|
+
const { name, ...hyperparameters } = form.values;
|
|
402
|
+
|
|
403
|
+
const train_res = await model.train_instance(name, hyperparameters, {});
|
|
404
|
+
if (typeof train_res === "string") {
|
|
405
|
+
res.sendWrap(req.__(`Model training error`), {
|
|
406
|
+
above: [
|
|
407
|
+
{
|
|
408
|
+
type: "breadcrumbs",
|
|
409
|
+
crumbs: [
|
|
410
|
+
{ text: req.__("Tables"), href: "/table" },
|
|
411
|
+
{ href: `/table/${table.id}`, text: table.name },
|
|
412
|
+
{ href: `/models/show/${model.id}`, text: model.name },
|
|
413
|
+
{ text: req.__(`Training error`) },
|
|
414
|
+
],
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
type: "card",
|
|
418
|
+
class: "mt-0",
|
|
419
|
+
title: req.__(`Training error`),
|
|
420
|
+
contents: pre(train_res),
|
|
421
|
+
},
|
|
422
|
+
],
|
|
423
|
+
});
|
|
424
|
+
} else {
|
|
425
|
+
req.flash("success", "Model trained");
|
|
426
|
+
res.redirect(`/models/show/${model.id}`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
})
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
router.post(
|
|
433
|
+
"/make-default-instance/:id",
|
|
434
|
+
isAdmin,
|
|
435
|
+
error_catcher(async (req, res) => {
|
|
436
|
+
const { id } = req.params;
|
|
437
|
+
const model_instance = await ModelInstance.findOne({ id });
|
|
438
|
+
await model_instance.make_default(!req.body.enabled);
|
|
439
|
+
res.redirect(`/models/show/${model_instance.model_id}`);
|
|
440
|
+
})
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
const encode = (s) =>
|
|
444
|
+
s.replace(
|
|
445
|
+
//https://stackoverflow.com/a/57448862/19839414
|
|
446
|
+
/[&<>'"]/g,
|
|
447
|
+
(tag) =>
|
|
448
|
+
({
|
|
449
|
+
"&": "&",
|
|
450
|
+
"<": "<",
|
|
451
|
+
">": ">",
|
|
452
|
+
"'": "'",
|
|
453
|
+
'"': """,
|
|
454
|
+
}[tag])
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
router.get(
|
|
458
|
+
"/show-report/:id",
|
|
459
|
+
isAdmin,
|
|
460
|
+
error_catcher(async (req, res) => {
|
|
461
|
+
const { id } = req.params;
|
|
462
|
+
const model_instance = await ModelInstance.findOne({ id });
|
|
463
|
+
const model = await Model.findOne({ id: model_instance.model_id });
|
|
464
|
+
const table = Table.findOne({ id: model.table_id });
|
|
465
|
+
res.sendWrap(req.__(`Train model`), {
|
|
466
|
+
above: [
|
|
467
|
+
{
|
|
468
|
+
type: "breadcrumbs",
|
|
469
|
+
crumbs: [
|
|
470
|
+
{ text: req.__("Tables"), href: "/table" },
|
|
471
|
+
{ href: `/table/${table.id}`, text: table.name },
|
|
472
|
+
{ href: `/models/show/${model.id}`, text: model.name },
|
|
473
|
+
{ text: model_instance.name },
|
|
474
|
+
{ text: req.__(`Report`) },
|
|
475
|
+
],
|
|
476
|
+
},
|
|
477
|
+
{
|
|
478
|
+
type: "card",
|
|
479
|
+
class: "mt-0",
|
|
480
|
+
title: req.__(`Model training report`),
|
|
481
|
+
contents:
|
|
482
|
+
iframe({
|
|
483
|
+
id: "trainreport",
|
|
484
|
+
width: "100%",
|
|
485
|
+
height: "100vh",
|
|
486
|
+
srcdoc: encode(model_instance.report),
|
|
487
|
+
}) + style(`iframe#trainreport { height: 100vh}`),
|
|
488
|
+
},
|
|
489
|
+
],
|
|
490
|
+
});
|
|
491
|
+
})
|
|
492
|
+
);
|
package/routes/page.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
const Router = require("express-promise-router");
|
|
8
8
|
|
|
9
9
|
const Page = require("@saltcorn/data/models/page");
|
|
10
|
+
const Trigger = require("@saltcorn/data/models/trigger");
|
|
10
11
|
const { getState } = require("@saltcorn/data/db/state");
|
|
11
12
|
const {
|
|
12
13
|
error_catcher,
|
|
@@ -40,18 +41,21 @@ router.get(
|
|
|
40
41
|
const { pagename } = req.params;
|
|
41
42
|
const state = getState();
|
|
42
43
|
state.log(3, `Route /page/${pagename} user=${req.user?.id}`);
|
|
43
|
-
const tic =
|
|
44
|
+
const tic = new Date();
|
|
44
45
|
|
|
45
46
|
const role = req.user && req.user.id ? req.user.role_id : 100;
|
|
46
47
|
const db_page = await Page.findOne({ name: pagename });
|
|
47
48
|
if (db_page && role <= db_page.min_role) {
|
|
48
49
|
const contents = await db_page.run(req.query, { res, req });
|
|
49
50
|
const title = scan_for_page_title(contents, db_page.title);
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
const tock = new Date();
|
|
52
|
+
const ms = tock.getTime() - tic.getTime();
|
|
53
|
+
Trigger.emitEvent("PageLoad", null, req.user, {
|
|
54
|
+
text: req.__("Page '%s' was loaded", pagename),
|
|
55
|
+
type: "page",
|
|
56
|
+
name: pagename,
|
|
57
|
+
render_time: ms,
|
|
58
|
+
});
|
|
55
59
|
res.sendWrap(
|
|
56
60
|
{
|
|
57
61
|
title,
|
package/routes/tables.js
CHANGED
|
@@ -11,6 +11,7 @@ const Table = require("@saltcorn/data/models/table");
|
|
|
11
11
|
const File = require("@saltcorn/data/models/file");
|
|
12
12
|
const View = require("@saltcorn/data/models/view");
|
|
13
13
|
const User = require("@saltcorn/data/models/user");
|
|
14
|
+
const Model = require("@saltcorn/data/models/model");
|
|
14
15
|
const Trigger = require("@saltcorn/data/models/trigger");
|
|
15
16
|
const {
|
|
16
17
|
mkTable,
|
|
@@ -54,7 +55,7 @@ const {
|
|
|
54
55
|
} = require("@saltcorn/data/models/discovery");
|
|
55
56
|
const { getState } = require("@saltcorn/data/db/state");
|
|
56
57
|
const { cardHeaderTabs } = require("@saltcorn/markup/layout_utils");
|
|
57
|
-
const { tablesList } = require("./common_lists");
|
|
58
|
+
const { tablesList, viewsList } = require("./common_lists");
|
|
58
59
|
const {
|
|
59
60
|
InvalidConfiguration,
|
|
60
61
|
removeAllWhiteSpace,
|
|
@@ -654,7 +655,14 @@ const attribBadges = (f) => {
|
|
|
654
655
|
let s = "";
|
|
655
656
|
if (f.attributes) {
|
|
656
657
|
Object.entries(f.attributes).forEach(([k, v]) => {
|
|
657
|
-
if (
|
|
658
|
+
if (
|
|
659
|
+
[
|
|
660
|
+
"summary_field",
|
|
661
|
+
"on_delete_cascade",
|
|
662
|
+
"on_delete",
|
|
663
|
+
"unique_error_msg",
|
|
664
|
+
].includes(k)
|
|
665
|
+
)
|
|
658
666
|
return;
|
|
659
667
|
if (v || v === 0) s += badge("secondary", k);
|
|
660
668
|
});
|
|
@@ -785,37 +793,10 @@ router.get(
|
|
|
785
793
|
);
|
|
786
794
|
var viewCardContents;
|
|
787
795
|
if (views.length > 0) {
|
|
788
|
-
viewCardContents =
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
key: (r) => link(`/view/${encodeURIComponent(r.name)}`, r.name),
|
|
793
|
-
},
|
|
794
|
-
{ label: req.__("Pattern"), key: "viewtemplate" },
|
|
795
|
-
{
|
|
796
|
-
label: req.__("Configure"),
|
|
797
|
-
key: (r) =>
|
|
798
|
-
link(
|
|
799
|
-
`/viewedit/config/${encodeURIComponent(
|
|
800
|
-
r.name
|
|
801
|
-
)}?on_done_redirect=${encodeURIComponent(
|
|
802
|
-
`table/${table.name}`
|
|
803
|
-
)}`,
|
|
804
|
-
req.__("Configure")
|
|
805
|
-
),
|
|
806
|
-
},
|
|
807
|
-
{
|
|
808
|
-
label: req.__("Delete"),
|
|
809
|
-
key: (r) =>
|
|
810
|
-
post_delete_btn(
|
|
811
|
-
`/viewedit/delete/${encodeURIComponent(r.id)}`,
|
|
812
|
-
req
|
|
813
|
-
),
|
|
814
|
-
},
|
|
815
|
-
],
|
|
816
|
-
views,
|
|
817
|
-
{ hover: true }
|
|
818
|
-
);
|
|
796
|
+
viewCardContents = await viewsList(views, req, {
|
|
797
|
+
on_done_redirect: encodeURIComponent(`table/${table.name}`),
|
|
798
|
+
notable: true,
|
|
799
|
+
});
|
|
819
800
|
} else {
|
|
820
801
|
viewCardContents = div(
|
|
821
802
|
h4(req.__("No views defined")),
|
|
@@ -838,6 +819,33 @@ router.get(
|
|
|
838
819
|
),
|
|
839
820
|
};
|
|
840
821
|
}
|
|
822
|
+
const models = await Model.find({ table_id: table.id });
|
|
823
|
+
const modelCard = div(
|
|
824
|
+
mkTable(
|
|
825
|
+
[
|
|
826
|
+
{
|
|
827
|
+
label: req.__("Name"),
|
|
828
|
+
key: (r) => link(`/models/show/${r.id}`, r.name),
|
|
829
|
+
},
|
|
830
|
+
{ label: req.__("Pattern"), key: "modelpattern" },
|
|
831
|
+
{
|
|
832
|
+
label: req.__("Delete"),
|
|
833
|
+
key: (r) =>
|
|
834
|
+
post_delete_btn(
|
|
835
|
+
`/models/delete/${encodeURIComponent(r.id)}`,
|
|
836
|
+
req
|
|
837
|
+
),
|
|
838
|
+
},
|
|
839
|
+
],
|
|
840
|
+
models
|
|
841
|
+
),
|
|
842
|
+
a(
|
|
843
|
+
{ href: `/models/new/${table.id}`, class: "btn btn-primary" },
|
|
844
|
+
i({ class: "fas fa-plus-square me-1" }),
|
|
845
|
+
req.__("Create model")
|
|
846
|
+
)
|
|
847
|
+
);
|
|
848
|
+
|
|
841
849
|
// Table Data card
|
|
842
850
|
const dataCard = div(
|
|
843
851
|
{ class: "d-flex text-center" },
|
|
@@ -997,6 +1005,15 @@ router.get(
|
|
|
997
1005
|
titleAjaxIndicator: true,
|
|
998
1006
|
contents: renderForm(tblForm, req.csrfToken()),
|
|
999
1007
|
},
|
|
1008
|
+
...(Model.has_templates
|
|
1009
|
+
? [
|
|
1010
|
+
{
|
|
1011
|
+
type: "card",
|
|
1012
|
+
title: req.__("Models"),
|
|
1013
|
+
contents: modelCard,
|
|
1014
|
+
},
|
|
1015
|
+
]
|
|
1016
|
+
: []),
|
|
1000
1017
|
],
|
|
1001
1018
|
});
|
|
1002
1019
|
})
|
|
@@ -1389,11 +1406,19 @@ const constraintForm = (req, table_id, fields, type) => {
|
|
|
1389
1406
|
blurb: req.__(
|
|
1390
1407
|
"Tick the boxes for the fields that should be jointly unique"
|
|
1391
1408
|
),
|
|
1392
|
-
fields:
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1409
|
+
fields: [
|
|
1410
|
+
...fields.map((f) => ({
|
|
1411
|
+
name: f.name,
|
|
1412
|
+
label: f.label,
|
|
1413
|
+
type: "Bool",
|
|
1414
|
+
})),
|
|
1415
|
+
{
|
|
1416
|
+
name: "errormsg",
|
|
1417
|
+
label: "Error message",
|
|
1418
|
+
sublabel: "Shown the user if joint uniqueness is violated",
|
|
1419
|
+
type: "String",
|
|
1420
|
+
},
|
|
1421
|
+
],
|
|
1397
1422
|
});
|
|
1398
1423
|
case "Index":
|
|
1399
1424
|
return new Form({
|
|
@@ -1485,11 +1510,12 @@ router.post(
|
|
|
1485
1510
|
if (form.hasErrors) req.flash("error", req.__("An error occurred"));
|
|
1486
1511
|
else {
|
|
1487
1512
|
let configuration = {};
|
|
1488
|
-
if (type === "Unique")
|
|
1513
|
+
if (type === "Unique") {
|
|
1489
1514
|
configuration.fields = fields
|
|
1490
1515
|
.map((f) => f.name)
|
|
1491
1516
|
.filter((f) => form.values[f]);
|
|
1492
|
-
|
|
1517
|
+
configuration.errormsg = form.values.errormsg;
|
|
1518
|
+
} else configuration = form.values;
|
|
1493
1519
|
await TableConstraint.create({
|
|
1494
1520
|
table_id: table.id,
|
|
1495
1521
|
type,
|
|
@@ -1873,7 +1899,7 @@ const get_provider_workflow = (table, req) => {
|
|
|
1873
1899
|
|
|
1874
1900
|
return {
|
|
1875
1901
|
redirect: `/table/${table.id}`,
|
|
1876
|
-
flash: ["success", `Table ${
|
|
1902
|
+
flash: ["success", `Table ${table.name || ""} saved`],
|
|
1877
1903
|
};
|
|
1878
1904
|
};
|
|
1879
1905
|
return workflow;
|