@saltcorn/server 0.8.7-beta.6 → 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/routes/diagram.js CHANGED
@@ -102,228 +102,243 @@ router.get(
102
102
  title: req.__(`Application diagram`),
103
103
  contents: [
104
104
  div(
105
- { class: "btn-group" },
106
- // New dropdown
107
- button(
108
- {
109
- type: "button",
110
- class: "btn btn-primary m-2 rounded",
111
- "data-bs-toggle": "dropdown",
112
- "aria-expanded": false,
113
- },
114
- "New",
115
- i({ class: "fas fa-plus-square ms-2" })
116
- ),
117
-
105
+ { class: "d-flex justify-content-between" },
118
106
  div(
119
- {
120
- class: "dropdown-menu",
121
- },
122
- // New View
123
- div(
124
- { class: "m-3" },
125
-
126
- a(
127
- {
128
- href: "/viewedit/new?on_done_redirect=diagram",
129
- },
130
- req.__("View")
131
- )
107
+ { class: "btn-group" },
108
+ // New dropdown
109
+ button(
110
+ {
111
+ type: "button",
112
+ class: "btn btn-primary m-2 rounded",
113
+ "data-bs-toggle": "dropdown",
114
+ "aria-expanded": false,
115
+ },
116
+ "New",
117
+ i({ class: "fas fa-plus-square ms-2" })
132
118
  ),
133
- // New Page
119
+
134
120
  div(
135
- { class: "m-3" },
136
- a(
137
- {
138
- href: "/pageedit/new?on_done_redirect=diagram",
139
- },
140
- req.__("Page")
121
+ {
122
+ class: "dropdown-menu",
123
+ },
124
+ // New View
125
+ div(
126
+ { class: "m-3" },
127
+
128
+ a(
129
+ {
130
+ href: "/viewedit/new?on_done_redirect=diagram",
131
+ },
132
+ req.__("View")
133
+ )
134
+ ),
135
+ // New Page
136
+ div(
137
+ { class: "m-3" },
138
+ a(
139
+ {
140
+ href: "/pageedit/new?on_done_redirect=diagram",
141
+ },
142
+ req.__("Page")
143
+ )
144
+ ),
145
+ // New Table
146
+ div(
147
+ { class: "m-3" },
148
+ a(
149
+ {
150
+ href: "/table/new",
151
+ },
152
+ req.__("Table")
153
+ )
154
+ ),
155
+ // New Trigger
156
+ div(
157
+ { class: "m-3" },
158
+ a(
159
+ {
160
+ href: "/actions/new?on_done_redirect=diagram",
161
+ },
162
+ req.__("Trigger")
163
+ )
141
164
  )
142
165
  ),
143
- // New Table
144
- div(
145
- { class: "m-3" },
146
- a(
147
- {
148
- href: "/table/new",
149
- },
150
- req.__("Table")
151
- )
166
+ // Entity type filter dropdown
167
+ button(
168
+ {
169
+ type: "button",
170
+ class: "btn btn-primary m-2 rounded",
171
+ "data-bs-toggle": "dropdown",
172
+ "aria-expanded": false,
173
+ },
174
+ req.__("All entities")
152
175
  ),
153
- // New Trigger
154
176
  div(
155
- { class: "m-3" },
156
- a(
157
- {
158
- href: "/actions/new?on_done_redirect=diagram",
159
- },
160
- req.__("Trigger")
161
- )
162
- )
163
- ),
164
- // Entity type filter dropdown
165
- button(
166
- {
167
- type: "button",
168
- class: "btn btn-primary m-2 rounded",
169
- "data-bs-toggle": "dropdown",
170
- "aria-expanded": false,
171
- },
172
- req.__("All entities")
173
- ),
174
- div(
175
- {
176
- class: "dropdown-menu",
177
- },
178
- // Views checkbox
179
- div(
180
- { class: "m-3 form-check" },
181
- label(
182
- { class: "form-check-label", for: "showViewsId" },
183
- req.__("Views")
177
+ {
178
+ class: "dropdown-menu",
179
+ },
180
+ // Views checkbox
181
+ div(
182
+ { class: "m-3 form-check" },
183
+ label(
184
+ { class: "form-check-label", for: "showViewsId" },
185
+ req.__("Views")
186
+ ),
187
+ input({
188
+ type: "checkbox",
189
+ class: "form-check-input",
190
+ id: "showViewsId",
191
+ checked: true,
192
+ name: "show_views",
193
+ value: "true",
194
+ onclick: "toggleEntityFilter('views'); reloadCy();",
195
+ autocomplete: "off",
196
+ })
184
197
  ),
185
- input({
186
- type: "checkbox",
187
- class: "form-check-input",
188
- id: "showViewsId",
189
- checked: true,
190
- name: "show_views",
191
- value: "true",
192
- onclick: "toggleEntityFilter('views'); reloadCy();",
193
- autocomplete: "off",
194
- })
195
- ),
196
- // Pages checkbox
197
- div(
198
- { class: "m-3 form-check" },
199
- label(
200
- { class: "form-check-label", for: "showPagesId" },
201
- req.__("Pages")
198
+ // Pages checkbox
199
+ div(
200
+ { class: "m-3 form-check" },
201
+ label(
202
+ { class: "form-check-label", for: "showPagesId" },
203
+ req.__("Pages")
204
+ ),
205
+ input({
206
+ type: "checkbox",
207
+ class: "form-check-input",
208
+ id: "showPagesId",
209
+ name: "show_pages",
210
+ value: "true",
211
+ checked: true,
212
+ onclick: "toggleEntityFilter('pages'); reloadCy();",
213
+ autocomplete: "off",
214
+ })
202
215
  ),
203
- input({
204
- type: "checkbox",
205
- class: "form-check-input",
206
- id: "showPagesId",
207
- name: "show_pages",
208
- value: "true",
209
- checked: true,
210
- onclick: "toggleEntityFilter('pages'); reloadCy();",
211
- autocomplete: "off",
212
- })
213
- ),
214
- // Tables checkbox
215
- div(
216
- { class: "m-3 form-check" },
217
- label(
218
- { class: "form-check-label", for: "showTablesId" },
219
- req.__("Tables")
216
+ // Tables checkbox
217
+ div(
218
+ { class: "m-3 form-check" },
219
+ label(
220
+ { class: "form-check-label", for: "showTablesId" },
221
+ req.__("Tables")
222
+ ),
223
+ input({
224
+ type: "checkbox",
225
+ class: "form-check-input",
226
+ id: "showTablesId",
227
+ name: "show_tables",
228
+ value: "true",
229
+ checked: true,
230
+ onclick: "toggleEntityFilter('tables'); reloadCy();",
231
+ autocomplete: "off",
232
+ })
220
233
  ),
221
- input({
222
- type: "checkbox",
223
- class: "form-check-input",
224
- id: "showTablesId",
225
- name: "show_tables",
226
- value: "true",
227
- checked: true,
228
- onclick: "toggleEntityFilter('tables'); reloadCy();",
229
- autocomplete: "off",
230
- })
234
+ // Trigger checkbox
235
+ div(
236
+ { class: "m-3 form-check" },
237
+ label(
238
+ { class: "form-check-label", for: "showTriggerId" },
239
+ req.__("Triggers")
240
+ ),
241
+ input({
242
+ type: "checkbox",
243
+ class: "form-check-input",
244
+ id: "showTriggerId",
245
+ name: "show_trigger",
246
+ value: "true",
247
+ checked: true,
248
+ onclick: "toggleEntityFilter('trigger'); reloadCy();",
249
+ autocomplete: "off",
250
+ })
251
+ )
231
252
  ),
232
- // Trigger checkbox
233
- div(
234
- { class: "m-3 form-check" },
235
- label(
236
- { class: "form-check-label", for: "showTriggerId" },
237
- req.__("Triggers")
238
- ),
239
- input({
240
- type: "checkbox",
241
- class: "form-check-input",
242
- id: "showTriggerId",
243
- name: "show_trigger",
244
- value: "true",
245
- checked: true,
246
- onclick: "toggleEntityFilter('trigger'); reloadCy();",
247
- autocomplete: "off",
248
- })
249
- )
250
- ),
251
- // Tags filter dropdown
252
- button(
253
- {
254
- type: "button",
255
- class: "btn btn-primary m-2 rounded",
256
- "data-bs-toggle": "dropdown",
257
- "aria-expanded": false,
258
- },
259
- req.__("Tags")
260
- ),
261
- div(
262
- {
263
- class: "dropdown-menu",
264
- },
265
- // no tags checkbox
266
- div(
267
- { class: "m-3 form-check" },
268
- label(
269
- { class: "form-check-label", for: "noTagsId" },
270
- req.__("no tags")
271
- ),
272
- input({
273
- type: "checkbox",
274
- class: "form-check-input",
275
- id: "noTagsId",
276
- name: "no_tags",
277
- value: "true",
278
- checked: true,
279
- onclick: "toggleTagFilterMode(); reloadCy();",
280
- autocomplete: "off",
281
- })
253
+ // Tags filter dropdown
254
+ button(
255
+ {
256
+ type: "button",
257
+ class: "btn btn-primary m-2 rounded",
258
+ "data-bs-toggle": "dropdown",
259
+ "aria-expanded": false,
260
+ },
261
+ req.__("Tags")
282
262
  ),
283
- tags.map((tag) => {
284
- const inputId = `tagFilter_box_${tag.name}_id`;
285
- return div(
263
+ div(
264
+ {
265
+ class: "dropdown-menu",
266
+ },
267
+ // no tags checkbox
268
+ div(
286
269
  { class: "m-3 form-check" },
287
270
  label(
288
- {
289
- class: "form-check-label",
290
- id: `tagFilter_label_${tag.name}`,
291
- style: "opacity: 0.5;",
292
- for: inputId,
293
- },
294
- tag.name
271
+ { class: "form-check-label", for: "noTagsId" },
272
+ req.__("no tags")
295
273
  ),
296
274
  input({
297
275
  type: "checkbox",
298
276
  class: "form-check-input",
299
- id: inputId,
300
- name: "choice",
301
- value: tag.id,
302
- checked: false,
303
- onclick: `toggleTagFilter(${tag.id}); reloadCy();`,
277
+ id: "noTagsId",
278
+ name: "no_tags",
279
+ value: "true",
280
+ checked: true,
281
+ onclick: "toggleTagFilterMode(); reloadCy();",
304
282
  autocomplete: "off",
305
283
  })
306
- );
307
- }),
308
- div(
309
- { class: "m-3" },
310
- a(
311
- {
312
- href: "/tag/new",
313
- },
314
- req.__("Add tag"),
315
- i({ class: "fas fa-plus ms-2" })
284
+ ),
285
+ tags.map((tag) => {
286
+ const inputId = `tagFilter_box_${tag.name}_id`;
287
+ return div(
288
+ { class: "m-3 form-check" },
289
+ label(
290
+ {
291
+ class: "form-check-label",
292
+ id: `tagFilter_label_${tag.name}`,
293
+ style: "opacity: 0.5;",
294
+ for: inputId,
295
+ },
296
+ tag.name
297
+ ),
298
+ input({
299
+ type: "checkbox",
300
+ class: "form-check-input",
301
+ id: inputId,
302
+ name: "choice",
303
+ value: tag.id,
304
+ checked: false,
305
+ onclick: `toggleTagFilter(${tag.id}); reloadCy();`,
306
+ autocomplete: "off",
307
+ })
308
+ );
309
+ }),
310
+ div(
311
+ { class: "m-3" },
312
+ a(
313
+ {
314
+ href: "/tag/new",
315
+ },
316
+ req.__("Add tag"),
317
+ i({ class: "fas fa-plus ms-2" })
318
+ )
316
319
  )
320
+ ),
321
+ // refresh button
322
+ button(
323
+ {
324
+ type: "button",
325
+ class: "btn btn-primary m-2 rounded",
326
+ onclick: "reloadCy(true);",
327
+ },
328
+ i({ class: "fas fa-sync-alt" })
317
329
  )
318
330
  ),
319
- // refresh button
320
- button(
321
- {
322
- type: "button",
323
- class: "btn btn-primary m-2 rounded",
324
- onclick: "reloadCy(true);",
325
- },
326
- i({ class: "fas fa-sync-alt" })
331
+ // screenshot button
332
+ div(
333
+ { class: "ad-screenshot-panel" },
334
+ button(
335
+ {
336
+ type: "button",
337
+ class: "btn btn-primary m-2 rounded",
338
+ onclick: "takePicture()",
339
+ },
340
+ i({ class: "fas fa-camera" })
341
+ )
327
342
  )
328
343
  ),
329
344
  div({ id: "cy" }),
package/routes/fields.js CHANGED
@@ -65,6 +65,10 @@ const fieldForm = async (req, fkey_opts, existing_names, id, hasData) => {
65
65
  isPrimary = !!field.primary_key;
66
66
  }
67
67
  }
68
+ const roleOptions = (await User.get_roles()).map((r) => ({
69
+ value: r.id,
70
+ label: r.role,
71
+ }));
68
72
  return new Form({
69
73
  action: "/field",
70
74
  validator: (vs) => {
@@ -140,6 +144,13 @@ const fieldForm = async (req, fkey_opts, existing_names, id, hasData) => {
140
144
  showIf: { calculated: false },
141
145
  type: "Bool",
142
146
  }),
147
+ new Field({
148
+ label: req.__("Error message"),
149
+ name: "unique_error_msg",
150
+ sublabel: req.__("Error shown to user if uniqueness is violated"),
151
+ showIf: { calculated: false, is_unique: true },
152
+ type: "String",
153
+ }),
143
154
 
144
155
  new Field({
145
156
  label: req.__("Stored"),
@@ -149,6 +160,22 @@ const fieldForm = async (req, fkey_opts, existing_names, id, hasData) => {
149
160
  disabled: !!id,
150
161
  showIf: { calculated: true },
151
162
  }),
163
+ new Field({
164
+ label: req.__("Protected"),
165
+ name: "protected",
166
+ sublabel: req.__("Set role to access"),
167
+ type: "Bool",
168
+ }),
169
+ {
170
+ label: req.__("Minimum role to write"),
171
+ name: "min_role_write",
172
+ input_type: "select",
173
+ sublabel: req.__(
174
+ "User must have this role or higher to update or create field values"
175
+ ),
176
+ options: roleOptions,
177
+ showIf: { protected: true },
178
+ },
152
179
  ],
153
180
  });
154
181
  };
@@ -199,6 +226,9 @@ const fieldFlow = (req) =>
199
226
  attributes.include_fts = context.include_fts;
200
227
  attributes.on_delete_cascade = context.on_delete_cascade;
201
228
  attributes.on_delete = context.on_delete;
229
+ attributes.unique_error_msg = context.unique_error_msg;
230
+ if (context.protected) attributes.min_role_write = context.min_role_write;
231
+ else attributes.min_role_write = undefined;
202
232
  const {
203
233
  table_id,
204
234
  name,
@@ -206,10 +236,18 @@ const fieldFlow = (req) =>
206
236
  required,
207
237
  is_unique,
208
238
  calculated,
209
- expression,
210
239
  stored,
211
240
  description,
212
241
  } = context;
242
+ let expression = context.expression;
243
+ if (context.expression_type === "Model prediction") {
244
+ const { model, model_instance, model_output } = context;
245
+ expression = `${model}(${
246
+ model_instance && model_instance !== "Default"
247
+ ? `"${model_instance}",`
248
+ : ""
249
+ }row).${model_output}`;
250
+ }
213
251
  const { reftable_name, type } = calcFieldType(context.type);
214
252
  const fldRow = {
215
253
  table_id,
@@ -280,6 +318,7 @@ const fieldFlow = (req) =>
280
318
  if (context.type === "Key" && context.reftable_name) {
281
319
  form.values.type = `Key to ${context.reftable_name}`;
282
320
  }
321
+ if (context.min_role_write) context.protected = true;
283
322
  return form;
284
323
  },
285
324
  },
@@ -360,9 +399,72 @@ const fieldFlow = (req) =>
360
399
  form: async (context) => {
361
400
  const table = Table.findOne({ id: context.table_id });
362
401
  const fields = table.getFields();
402
+ const models = await table.get_models();
403
+ const instance_options = {};
404
+ const output_options = {};
405
+ for (const model of models) {
406
+ instance_options[model.name] = ["Default"];
407
+ const instances = await model.get_instances();
408
+ instance_options[model.name].push(...instances.map((i) => i.name));
409
+
410
+ const outputs = await applyAsync(
411
+ model.templateObj.prediction_outputs || [],
412
+ { table, configuration: model.configuration }
413
+ );
414
+ output_options[model.name] = outputs.map((o) => o.name);
415
+ }
363
416
  return new Form({
364
- blurb: expressionBlurb(context.type, context.stored, fields, req),
365
417
  fields: [
418
+ {
419
+ name: "expression_type",
420
+ label: "Formula type",
421
+ input_type: "select",
422
+ options: [
423
+ "JavaScript expression",
424
+ ...(models.length ? ["Model prediction"] : []),
425
+ ],
426
+ },
427
+ {
428
+ name: "model",
429
+ label: req.__("Model"),
430
+ input_type: "select",
431
+ options: models.map((m) => m.name),
432
+ showIf: { expression_type: "Model prediction" },
433
+ },
434
+ {
435
+ name: "model_instance",
436
+ label: req.__("Model instance"),
437
+ type: "String",
438
+ required: true,
439
+ attributes: {
440
+ calcOptions: ["model", instance_options],
441
+ },
442
+ showIf: { expression_type: "Model prediction" },
443
+ },
444
+ {
445
+ name: "model_output",
446
+ label: req.__("Prediction output"),
447
+ type: "String",
448
+ required: true,
449
+ attributes: {
450
+ calcOptions: ["model", output_options],
451
+ },
452
+ showIf: { expression_type: "Model prediction" },
453
+ },
454
+ {
455
+ input_type: "custom_html",
456
+ name: "expr_blurb",
457
+ label: " ",
458
+ showIf: { expression_type: "JavaScript expression" },
459
+ attributes: {
460
+ html: expressionBlurb(
461
+ context.type,
462
+ context.stored,
463
+ fields,
464
+ req
465
+ ),
466
+ },
467
+ },
366
468
  new Field({
367
469
  name: "expression",
368
470
  label: req.__("Formula"),
@@ -370,10 +472,12 @@ const fieldFlow = (req) =>
370
472
  type: "String",
371
473
  class: "validate-expression",
372
474
  validator: expressionValidator,
475
+ showIf: { expression_type: "JavaScript expression" },
373
476
  }),
374
477
  new Field({
375
478
  name: "test_btn",
376
479
  label: req.__("Test"),
480
+ showIf: { expression_type: "JavaScript expression" },
377
481
  // todo sublabel
378
482
  input_type: "custom_html",
379
483
  attributes: {
@@ -455,10 +559,12 @@ const fieldFlow = (req) =>
455
559
  attributes: {
456
560
  explainers: {
457
561
  Fail: req.__("Prevent any deletion of parent rows"),
458
- Cascade:
459
- req.__("If the parent row is deleted, automatically delete the child rows."),
460
- "Set null":
461
- req.__("If the parent row is deleted, set key fields on child rows to null"),
562
+ Cascade: req.__(
563
+ "If the parent row is deleted, automatically delete the child rows."
564
+ ),
565
+ "Set null": req.__(
566
+ "If the parent row is deleted, set key fields on child rows to null"
567
+ ),
462
568
  },
463
569
  },
464
570
  sublabel: req.__(
@@ -720,6 +826,7 @@ router.post(
720
826
  } is: <pre>${JSON.stringify(result)}</pre>`
721
827
  );
722
828
  } catch (e) {
829
+ console.error(e);
723
830
  return res.send(
724
831
  `Error on running on row with id=${rows[0].id}: ${e.message}`
725
832
  );
package/routes/index.js CHANGED
@@ -17,6 +17,7 @@ const eventlog = require("./eventlog");
17
17
  const infoarch = require("./infoarch");
18
18
  const events = require("./events");
19
19
  const tenant = require("./tenant");
20
+ const models = require("./models");
20
21
  const library = require("./library");
21
22
  const settings = require("./settings");
22
23
  const plugins = require("./plugins");
@@ -54,6 +55,7 @@ module.exports =
54
55
  app.use("/crashlog", crashlog);
55
56
  app.use("/events", events);
56
57
  app.use("/page", page);
58
+ app.use("/models", models);
57
59
  app.use("/settings", settings);
58
60
  app.use("/pageedit", pageedit);
59
61
  app.use("/actions", actions);