@saltcorn/data 1.4.1 → 1.4.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.
@@ -0,0 +1,1562 @@
1
+ "use strict";
2
+ /**
3
+ * @category saltcorn-data
4
+ * @module base-plugin/viewtemplates/viewable_fields
5
+ * @subcategory base-plugin
6
+ */
7
+ const { post_btn } = require("@saltcorn/markup");
8
+ const { text, a, i, div, button, span, script, domReady, input, } = require("@saltcorn/markup/tags");
9
+ const { getState, getReq__ } = require("./db/state");
10
+ const { link_view, displayType, run_action_column, } = require("./plugin-helper");
11
+ const { eval_expression, freeVariables, get_expression_function, } = require("./models/expression");
12
+ const Field = require("./models/field");
13
+ const Form = require("./models/form");
14
+ const { traverseSync } = require("./models/layout");
15
+ const { structuredClone, isWeb, isOfflineMode, getSessionId, interpolate, objectToQueryString, validSqlId, } = require("./utils");
16
+ const db = require("./db");
17
+ const View = require("./models/view");
18
+ const Table = require("./models/table");
19
+ const { isNode, dollarizeObject, getSafeBaseUrl } = require("./utils");
20
+ const { bool, date } = require("./base-plugin/types");
21
+ const _ = require("underscore");
22
+ const renderLayout = require("@saltcorn/markup/layout");
23
+ const Crash = require("./models/crash");
24
+ const { Relation, parseRelationPath, RelationType, ViewDisplayType, } = require("@saltcorn/common-code");
25
+ const { show_icon_and_label } = require("@saltcorn/markup/layout_utils");
26
+ /**
27
+ * formats the column index of a view cfg
28
+ * @param {number|undefined} colIndex
29
+ * @returns json formatted attribute for run_action
30
+ */
31
+ const columnIndex = (colIndex) => colIndex ? `, column_index: ${colIndex}` : "";
32
+ /**
33
+ * @param {string} viewname
34
+ * @param {Table|object} table
35
+ * @param {string} action_name
36
+ * @param {object} r
37
+ * @param {string} colId
38
+ * @param {string} colIdNm
39
+ * @param {string} confirm
40
+ * @param {number|undefined} index
41
+ * @returns {any}
42
+ */
43
+ const action_url = (viewname, table, action_name, r, colId, colIdNm, confirm, colIndex, runAsync) => {
44
+ const pk_name = table.pk_name;
45
+ const __ = getReq__();
46
+ const confirmStr = confirm ? `if(confirm('${__("Are you sure?")}'))` : "";
47
+ if (action_name === "Delete") {
48
+ return {
49
+ javascript: `${confirmStr}${isNode() ? "ajax" : "local"}_post_btn('${!isNode() ? "post" : ""}${table.delete_url(r, `redirect=/view/${viewname}`)}', true)`,
50
+ };
51
+ }
52
+ else if (action_name === "GoBack")
53
+ return {
54
+ javascript: isNode()
55
+ ? "history.back()"
56
+ : "parent.saltcorn.mobileApp.navigation.goBack()",
57
+ };
58
+ else if (action_name.startsWith("Toggle")) {
59
+ const field_name = action_name.replace("Toggle ", "");
60
+ return `/edit/toggle/${table.name}/${r[pk_name]}/${field_name}?redirect=/view/${viewname}`;
61
+ }
62
+ return {
63
+ javascript: `${confirmStr}view_post('${viewname}', 'run_action', {${colIdNm}:'${colId}'${r ? `, ${pk_name}:'${r?.[pk_name]}'` : ""}${columnIndex(colIndex)}}${runAsync ? `,{runAsync:true}` : ""});`,
64
+ };
65
+ };
66
+ /**
67
+ * @param {string} url
68
+ * @param {object} req
69
+ * @param {object} opts
70
+ * @param {string} opts.action_name
71
+ * @param {string} opts.action_label
72
+ * @param {*} opts.confirm
73
+ * @param {*} opts.rndid
74
+ * @param {string} opts.action_style
75
+ * @param {number} opts.action_size
76
+ * @param {*} opts.action_icon
77
+ * @param {string} opts.action_bgcol
78
+ * @param {string} opts.action_bordercol
79
+ * @param {string} opts.action_textcol
80
+ * @param {*} __
81
+ * @returns {object}
82
+ */
83
+ const action_link = (url, req, { action_name, action_label, confirm, rndid, action_style, action_size, action_icon, action_bgcol, action_title, action_class, action_bordercol, action_textcol, spinner, block, }, __ = (s) => s) => {
84
+ const label = action_label === " " ? "" : __(action_label) || action_name;
85
+ let style = action_style === "btn-custom-color"
86
+ ? `background-color: ${action_bgcol || "#000000"};border-color: ${action_bordercol || "#000000"}; color: ${action_textcol || "#000000"}`
87
+ : null;
88
+ if (url.javascript)
89
+ return a({
90
+ href: "javascript:void(0)",
91
+ onclick: `${spinner ? "spin_action_link(this);" : ""}${url.javascript}`,
92
+ class: [
93
+ action_style === "btn-link"
94
+ ? ""
95
+ : `btn ${action_style || "btn-primary"} ${action_size || ""}`,
96
+ action_class,
97
+ ],
98
+ style,
99
+ title: action_title,
100
+ }, action_icon && action_icon !== "empty"
101
+ ? i({ class: action_icon }) + (label ? " " : "")
102
+ : false, label);
103
+ else
104
+ return post_btn(url, label, req.csrfToken(), {
105
+ confirm,
106
+ req,
107
+ icon: action_icon,
108
+ style,
109
+ spinner,
110
+ btnClass: `${action_style || "btn-primary"} ${action_size || ""}`,
111
+ formClass: !block && "d-inline",
112
+ });
113
+ };
114
+ const slug_transform = (row) => (step) => step.transform === "slugify"
115
+ ? `/${db.slugify(row[step.field])}`
116
+ : `/${row[step.field]}`;
117
+ /**
118
+ * @function
119
+ * @param {Field[]} fields
120
+ * @returns {function}
121
+ */
122
+ const get_view_link_query = (fields, view) => {
123
+ if (view && view.slug && view.slug.steps && view.slug.steps.length > 0) {
124
+ return (r) => view.slug.steps.map(slug_transform(r)).join("");
125
+ }
126
+ const pk_name = fields.find((f) => f.primary_key).name;
127
+ return (r) => `?${pk_name}=${r[pk_name]}`;
128
+ };
129
+ /**
130
+ * @function
131
+ * @param {object} opts
132
+ * @param {string} opts.link_text
133
+ * @param {boolean} opts.link_text_formula missing in contract
134
+ * @param {string} [opts.link_url]
135
+ * @param {boolean} opts.link_url_formula
136
+ * @param {boolean} opts.link_target_blank
137
+ * @param {Field[]} fields
138
+ * @returns {object}
139
+ */
140
+ const make_link = ({ link_text, link_text_formula, link_url, link_url_formula, link_target_blank, in_dropdown, in_modal, link_icon, icon, link_style, link_size, }, fields, __ = (s) => s, in_row_click) => {
141
+ return {
142
+ label: "",
143
+ key: (r) => {
144
+ let txt, href;
145
+ const theIcon = link_icon || icon;
146
+ txt = link_text_formula
147
+ ? eval_expression(link_text, r, undefined, "Link text formula")
148
+ : link_text;
149
+ href = link_url_formula
150
+ ? eval_expression(link_url, r, undefined, "Link URL formula")
151
+ : link_url;
152
+ const attrs = { href };
153
+ if (link_target_blank)
154
+ attrs.target = "_blank";
155
+ if (in_dropdown)
156
+ attrs.class = ["dropdown-item"];
157
+ if (link_style)
158
+ attrs.class = [
159
+ ...(attrs.class || []),
160
+ link_style,
161
+ link_style.includes("btn") && "d-inline-block",
162
+ ];
163
+ if (link_size)
164
+ attrs.class = [...(attrs.class || []), link_size];
165
+ if (in_row_click)
166
+ attrs.onclick = "event.stopPropagation()";
167
+ if (in_modal)
168
+ return a({
169
+ ...attrs,
170
+ href: "javascript:void(0)",
171
+ onclick: isNode()
172
+ ? `ajax_modal('${href}');` + (attrs.onclick || "")
173
+ : `mobile_modal('${href}');` + (attrs.onclick || ""),
174
+ }, !!theIcon && theIcon !== "empty" && i({ class: theIcon }), txt);
175
+ return a(attrs, !!theIcon && theIcon !== "empty" && i({ class: theIcon }), txt);
176
+ },
177
+ };
178
+ };
179
+ /**
180
+ * @param {string} view name of the view or a legacy relation (type:telation)
181
+ * @param {string} relation new relation path syntax
182
+ * @returns {object}
183
+ */
184
+ const parse_view_select = (view, relation) => {
185
+ if (relation) {
186
+ const { sourcetable, path } = relation === Relation.fixedUserRelation
187
+ ? { sourcetable: "users", path: [] }
188
+ : parseRelationPath(relation);
189
+ return {
190
+ type: "RelationPath",
191
+ viewname: view,
192
+ sourcetable,
193
+ path,
194
+ };
195
+ }
196
+ else {
197
+ // legacy relation path
198
+ const colonSplit = view.split(":");
199
+ if (colonSplit.length === 1)
200
+ return { type: "Own", viewname: view };
201
+ const [type, vrest] = colonSplit;
202
+ switch (type) {
203
+ case "Own":
204
+ return { type, viewname: vrest };
205
+ case "ChildList":
206
+ case "OneToOneShow":
207
+ const [viewnm, tbl, fld, throughTable, through] = vrest.split(".");
208
+ return {
209
+ type,
210
+ viewname: viewnm,
211
+ table_name: tbl,
212
+ field_name: fld,
213
+ throughTable,
214
+ through,
215
+ };
216
+ case "ParentShow":
217
+ const [pviewnm, ptbl, pfld] = vrest.split(".");
218
+ return { type, viewname: pviewnm, table_name: ptbl, field_name: pfld };
219
+ case "Independent":
220
+ return { type, viewname: vrest };
221
+ }
222
+ }
223
+ };
224
+ const pathToQuery = (relation, srcTable, subTable, row) => {
225
+ const path = relation.path;
226
+ switch (relation.type) {
227
+ case RelationType.CHILD_LIST:
228
+ return path.length === 1
229
+ ? `?${path[0].inboundKey}=${row[srcTable.pk_name]}` // works for OneToOneShow as well
230
+ : `?${path[1].table}.${path[1].inboundKey}.${path[0].table}.${path[0].inboundKey}=${row[srcTable.pk_name]}`;
231
+ case RelationType.PARENT_SHOW:
232
+ const fkey = path[0].fkey;
233
+ const reffield = srcTable.fields.find((f) => f.name === fkey);
234
+ const value = row[fkey];
235
+ return value
236
+ ? `?${reffield.refname}=${typeof value === "object" ? value.id : value}`
237
+ : null;
238
+ case RelationType.OWN:
239
+ const getQuery = get_view_link_query(srcTable.fields, relation.subView || {});
240
+ return getQuery(row);
241
+ case RelationType.INDEPENDENT:
242
+ return "";
243
+ case RelationType.RELATION_PATH:
244
+ const idName = path.length > 0
245
+ ? path[0].fkey
246
+ ? path[0].fkey
247
+ : subTable.pk_name
248
+ : undefined;
249
+ const srcId = row[idName] === null || row[idName]?.id === null
250
+ ? "NULL"
251
+ : row[idName]?.id || row[idName];
252
+ return `?${relation.relationString}=${srcId}`;
253
+ }
254
+ };
255
+ //todo: use above to simplify code
256
+ /**
257
+ * @function
258
+ * @param {object} opts
259
+ * @param {string} opts.view
260
+ * @param {string} opts.relation
261
+ * @param {object} opts.view_label missing in contract
262
+ * @param {object} opts.in_modal
263
+ * @param {object} opts.view_label_formula
264
+ * @param {string} [opts.link_style = ""]
265
+ * @param {string} [opts.link_size = ""]
266
+ * @param {string} [opts.link_icon = ""]
267
+ * @param {string} [opts.textStyle = ""]
268
+ * @param {string} [opts.link_bgcol]
269
+ * @param {string} [opts.link_bordercol]
270
+ * @param {string} [opts.link_textcol]
271
+ * @param {Field[]} fields
272
+ * @returns {object}
273
+ */
274
+ const view_linker = ({ view, relation, view_label, in_modal, view_label_formula, link_style = "", link_size = "", link_icon = "", icon = "", textStyle = "", link_bgcol, link_bordercol, link_textcol, in_dropdown, extra_state_fml, link_target_blank, link_title, link_class, }, fields, __ = (s) => s, isWeb = true, user, targetPrefix = "", state = {}, req, srcViewName, label_attr, //for sorting
275
+ in_row_click) => {
276
+ const safePrefix = (targetPrefix || "").endsWith("/")
277
+ ? targetPrefix.substring(0, targetPrefix.length - 1)
278
+ : targetPrefix || "";
279
+ const get_label = (def, row) => {
280
+ if (!view_label || view_label.length === 0)
281
+ return def;
282
+ if (!view_label_formula)
283
+ return __(view_label);
284
+ return eval_expression(view_label, row, user, "View Link label formula");
285
+ };
286
+ const get_extra_state = (row) => {
287
+ if (!extra_state_fml)
288
+ return "";
289
+ const ctx = {
290
+ ...dollarizeObject(state),
291
+ session_id: getSessionId(req),
292
+ ...row,
293
+ };
294
+ const o = eval_expression(extra_state_fml, ctx, user, "View link extra state formula");
295
+ return Object.entries(o)
296
+ .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
297
+ .join("&");
298
+ };
299
+ if (relation) {
300
+ const topview = View.findOne({ name: srcViewName });
301
+ const srcTable = Table.findOne({ id: topview.table_id });
302
+ const subview = View.findOne({ name: view });
303
+ const subTable = Table.findOne({ id: subview.table_id });
304
+ const relObj = new Relation(relation, subTable ? subTable.name : "", ViewDisplayType.NO_ROW_LIMIT);
305
+ relObj.subView = subview;
306
+ const type = relObj.type;
307
+ return {
308
+ label: view,
309
+ key: (r) => {
310
+ const query = pathToQuery(relObj, srcTable, subTable, r);
311
+ if (query === null)
312
+ return "";
313
+ else {
314
+ let label = "";
315
+ if (type === "ParentShow") {
316
+ const summary_field = r[`summary_field_${subTable.name.toLowerCase()}`];
317
+ label = get_label(typeof summary_field === "undefined" ? view : summary_field, r);
318
+ }
319
+ else
320
+ label = get_label(view, r);
321
+ const target = `${safePrefix}/view/${encodeURIComponent(view)}${query}`;
322
+ return link_view(isWeb || in_modal ? target : `javascript:execLink('${target}')`, label, in_modal && srcViewName && { reload_view: srcViewName }, link_style, link_size, link_icon || icon, textStyle, link_bgcol, link_bordercol, link_textcol, in_dropdown && "dropdown-item", get_extra_state(r), link_target_blank, label_attr, link_title, link_class, req, in_row_click);
323
+ }
324
+ },
325
+ };
326
+ }
327
+ else {
328
+ // legacy relation path
329
+ const [vtype, vrest] = view.split(":");
330
+ switch (vtype) {
331
+ case "Own":
332
+ const vnm = vrest;
333
+ const viewrow = View.findOne({ name: vnm });
334
+ const get_query = get_view_link_query(fields, viewrow || {});
335
+ return {
336
+ label: vnm,
337
+ key: (r) => {
338
+ const target = `${safePrefix}/view/${encodeURIComponent(vnm)}${get_query(r)}`;
339
+ return link_view(isWeb || in_modal ? target : `javascript:execLink('${target}')`, get_label(vnm, r), in_modal && srcViewName && { reload_view: srcViewName }, link_style, link_size, link_icon || icon, textStyle, link_bgcol, link_bordercol, link_textcol, in_dropdown && "dropdown-item", get_extra_state(r), link_target_blank, label_attr, link_title, link_class, req, in_row_click);
340
+ },
341
+ };
342
+ case "Independent":
343
+ const ivnm = vrest;
344
+ return {
345
+ label: ivnm,
346
+ key: (r) => {
347
+ const target = `${safePrefix}/view/${encodeURIComponent(ivnm)}`;
348
+ return link_view(isWeb || in_modal ? target : `javascript:execLink('${target}')`, get_label(ivnm, r), in_modal && srcViewName && { reload_view: srcViewName }, link_style, link_size, link_icon || icon, textStyle, link_bgcol, link_bordercol, link_textcol, in_dropdown && "dropdown-item", get_extra_state(r), link_target_blank, label_attr, link_title, link_class, req, in_row_click);
349
+ },
350
+ };
351
+ case "ChildList":
352
+ case "OneToOneShow":
353
+ const [viewnm, tbl, fld, throughTable, through] = vrest.split(".");
354
+ const varPath = through ? `${throughTable}.${through}.${fld}` : fld;
355
+ return {
356
+ label: viewnm,
357
+ key: (r) => {
358
+ const target = `${safePrefix}/view/${encodeURIComponent(viewnm)}?${varPath}=${r.id}`;
359
+ return link_view(isWeb || in_modal ? target : `javascript:execLink('${target}')`, get_label(viewnm, r), in_modal && srcViewName && { reload_view: srcViewName }, link_style, link_size, link_icon || icon, textStyle, link_bgcol, link_bordercol, link_textcol, in_dropdown && "dropdown-item", get_extra_state(r), link_target_blank, label_attr, link_title, link_class, req, in_row_click);
360
+ },
361
+ };
362
+ case "ParentShow":
363
+ const [pviewnm, ptbl, pfld] = vrest.split(".");
364
+ //console.log([pviewnm, ptbl, pfld])
365
+ return {
366
+ label: pviewnm,
367
+ key: (r) => {
368
+ const reffield = fields.find((f) => f.name === pfld);
369
+ const summary_field = r[`summary_field_${ptbl.toLowerCase()}`];
370
+ if (r[pfld]) {
371
+ const target = `${safePrefix}/view/${encodeURIComponent(pviewnm)}?${reffield.refname}=${typeof r[pfld] === "object" ? r[pfld].id : r[pfld]}`;
372
+ return link_view(isWeb || in_modal ? target : `javascript:execLink('${target}')`, get_label(typeof summary_field === "undefined"
373
+ ? pviewnm
374
+ : summary_field, r), in_modal && srcViewName && { reload_view: srcViewName }, link_style, link_size, link_icon || icon, textStyle, link_bgcol, link_bordercol, link_textcol, in_dropdown && "dropdown-item", get_extra_state(r), link_target_blank, label_attr, link_title, link_class, req, in_row_click);
375
+ }
376
+ else
377
+ return "";
378
+ },
379
+ };
380
+ default:
381
+ throw new Error("Invalid relation: " + view);
382
+ }
383
+ }
384
+ };
385
+ /**
386
+ * @param {string} nm
387
+ * @returns {boolean}
388
+ */
389
+ const action_requires_write = (nm) => {
390
+ if (!nm)
391
+ return false;
392
+ if (nm === "Delete")
393
+ return true;
394
+ if (nm.startsWith("Toggle"))
395
+ return true;
396
+ };
397
+ // flapMap if f returns array
398
+ const flapMapish = (xs, f) => {
399
+ const res = [];
400
+ let index = 0;
401
+ for (const x of xs) {
402
+ const y = f(x, index++);
403
+ if (Array.isArray(y))
404
+ res.push(...y);
405
+ else
406
+ res.push(y);
407
+ }
408
+ return res;
409
+ };
410
+ const get_viewable_fields_from_layout = (viewname, statehash, table, fields, columns, isShow, req, __, state = {}, srcViewName, layoutCols, viewResults, in_row_click) => {
411
+ const typeMap = {
412
+ field: "Field",
413
+ join_field: "JoinField",
414
+ view_link: "ViewLink",
415
+ view: "View",
416
+ link: "Link",
417
+ action: "Action",
418
+ blank: "Text",
419
+ aggregation: "Aggregation",
420
+ dropdown_menu: "DropdownMenu",
421
+ container: "Container",
422
+ };
423
+ const toArray = (x) => !x ? [] : Array.isArray(x) ? x : x.above ? x.above : [x];
424
+ //console.log("layout cols", layoutCols);
425
+ const newCols = layoutCols.map(({ contents, ...rest }) => {
426
+ if (!contents)
427
+ contents = rest;
428
+ if (contents.above) {
429
+ const newContents = { type: "container", contents: contents };
430
+ contents = newContents;
431
+ }
432
+ const col = {
433
+ ...contents,
434
+ ...rest,
435
+ type: typeMap[contents.type] || contents.type,
436
+ };
437
+ switch (contents.type) {
438
+ case "link":
439
+ col.link_text = contents.text;
440
+ col.link_url = contents.url;
441
+ col.link_url_formula = contents.isFormula?.url;
442
+ col.link_text_formula = contents.isFormula?.text;
443
+ col.link_target_blank = contents.target_blank;
444
+ break;
445
+ case "view_link":
446
+ col.view_label_formula = contents.isFormula?.label;
447
+ break;
448
+ case "dropdown_menu":
449
+ col.dropdown_columns = get_viewable_fields_from_layout(viewname, statehash, table, fields, columns, isShow, req, __, state, srcViewName, toArray(contents.contents));
450
+ break;
451
+ case "blank":
452
+ if (contents.isFormula?.text) {
453
+ col.type = "FormulaValue";
454
+ col.formula = col.contents;
455
+ }
456
+ if (contents.isHTML)
457
+ col.interpolator = (row) => interpolate(contents.contents, row, req?.user, "HTML element");
458
+ break;
459
+ case "action":
460
+ col.action_label_formula = contents.isFormula?.action_label;
461
+ break;
462
+ }
463
+ return col;
464
+ });
465
+ //console.log("newCols", newCols);
466
+ return get_viewable_fields(viewname, statehash, table, fields, newCols, isShow, req, __, state, srcViewName, viewResults, in_row_click);
467
+ };
468
+ /**
469
+ * @function
470
+ * @param {string} viewname
471
+ * @param {Table|object} table
472
+ * @param {Field[]} fields
473
+ * @param {object[]} columns
474
+ * @param {boolean} isShow
475
+ * @param {object} req
476
+ * @param {*} __
477
+ * @returns {object[]}
478
+ */
479
+ const get_viewable_fields = (viewname, statehash, table, fields, columns, isShow, req, __, state = {}, srcViewName, viewResults, in_row_click) => {
480
+ const dropdown_actions = [];
481
+ const checkShowIf = (tFieldGenF) => (column, index) => {
482
+ const tfield = tFieldGenF(column, index);
483
+ if (column.showif) {
484
+ const oldKeyF = tfield.key;
485
+ if (typeof oldKeyF !== "function")
486
+ return tfield;
487
+ const newKeyF = (r) => {
488
+ if (eval_expression(column.showif, r, req.user, "Column show if formula"))
489
+ return oldKeyF(r);
490
+ else
491
+ return "";
492
+ };
493
+ tfield.key = newKeyF;
494
+ }
495
+ return tfield;
496
+ };
497
+ const tfields = flapMapish(columns, checkShowIf((column, index) => {
498
+ const role = req.user ? req.user.role_id : 100;
499
+ const user_id = req.user ? req.user.id : null;
500
+ const setWidth = column.col_width
501
+ ? { width: `${column.col_width}${column.col_width_units}` }
502
+ : {};
503
+ setWidth.align =
504
+ !column.alignment || column.alignment === "Default"
505
+ ? undefined
506
+ : column.alignment.toLowerCase();
507
+ if (column.type === "FormulaValue") {
508
+ return {
509
+ ...setWidth,
510
+ label: column.header_label ? __(column.header_label) : "",
511
+ key: (r) => text(eval_expression(column.formula, r, req.user, "Formula value column")),
512
+ };
513
+ }
514
+ else if (column.type === "Text") {
515
+ return {
516
+ ...setWidth,
517
+ label: column.header_label ? __(column.header_label) : "",
518
+ key: (r) => column.interpolator
519
+ ? column.interpolator(r)
520
+ : text(column.contents),
521
+ };
522
+ }
523
+ else if (column.type === "Container") {
524
+ return {
525
+ ...setWidth,
526
+ label: column.header_label ? __(column.header_label) : "",
527
+ key: (r) => {
528
+ const layout = structuredClone({ ...column, type: "container" });
529
+ traverseSync(layout, standardLayoutRowVisitor(viewname, state, table, r, req));
530
+ return renderLayout({
531
+ blockDispatch: {
532
+ ...standardBlockDispatch(viewname, state, table, { req }, r),
533
+ view(column) {
534
+ return viewResults[column.view + column.relation]?.(r);
535
+ },
536
+ },
537
+ layout,
538
+ role,
539
+ is_owner: false,
540
+ req,
541
+ hints: getState().getLayout(req.user).hints || {},
542
+ });
543
+ },
544
+ };
545
+ }
546
+ else if (column.type === "DropdownMenu") {
547
+ //console.log(column);
548
+ const btn_label = column.label == " "
549
+ ? ""
550
+ : column.label || (column.action_icon ? "" : req.__("Action"));
551
+ return {
552
+ ...setWidth,
553
+ label: column.header_label ? __(column.header_label) : "",
554
+ key: (r) => div({ class: "dropdown" }, button({
555
+ class: column.action_style === "btn-link"
556
+ ? "btn btn-link"
557
+ : `btn ${column.action_style || "btn-primary"} ${column.action_size || ""} d-inline-block dropdown-toggle`,
558
+ "data-boundary": "viewport",
559
+ type: "button",
560
+ id: `actiondd${r.id}_${index}`, //TODO need unique
561
+ "data-bs-toggle": "dropdown",
562
+ "aria-haspopup": "true",
563
+ "aria-expanded": "false",
564
+ "aria-label": "Additional actions",
565
+ onclick: in_row_click ? "event.stopPropagation()" : undefined,
566
+ style: column.action_style === "btn-custom-color"
567
+ ? `background-color: ${column.action_bgcol || "#000000"};border-color: ${column.action_bordercol || "#000000"}; color: ${column.action_textcol || "#000000"}`
568
+ : null,
569
+ }, show_icon_and_label(column.action_icon, btn_label)), div({
570
+ class: [
571
+ "dropdown-menu",
572
+ column.menu_direction === "end" && "dropdown-menu-end",
573
+ ],
574
+ "aria-labelledby": `actiondd${r.id}_${index}`,
575
+ }, column.dropdown_columns.map((acol) => div({ class: "dropdown-item" }, acol.key(r))))),
576
+ };
577
+ }
578
+ else if (column.type === "Action") {
579
+ if (column.minRole && column.minRole != 100) {
580
+ const minRole = +column.minRole;
581
+ const userRole = req?.user?.role_id || 100;
582
+ if (minRole < userRole)
583
+ return false;
584
+ }
585
+ const action_col = {
586
+ ...setWidth,
587
+ label: column.header_label ? __(column.header_label) : "",
588
+ key: (r) => {
589
+ if (action_requires_write(column.action_name)) {
590
+ if (table.min_role_write < role && !table.is_owner(req.user, r))
591
+ return "";
592
+ }
593
+ const url = action_url(viewname, table, column.action_name, r, column.rndid || column.action_name, column.rndid ? "rndid" : "action_name", column.confirm, index, column.run_async);
594
+ const label = column.action_label_formula
595
+ ? eval_expression(column.action_label, r, req.user, "Action label formula")
596
+ : __(column.action_label) || __(column.action_name);
597
+ const icon = column.action_icon || column.icon || undefined;
598
+ if (url.javascript)
599
+ return a({
600
+ href: "javascript:void(0)",
601
+ class: [
602
+ column.in_dropdown && "dropdown-item",
603
+ column.action_style !== "btn-link" &&
604
+ `btn ${column.action_style || "btn-primary"} ${column.action_size || ""}`,
605
+ ],
606
+ onclick: url.javascript +
607
+ (column.spinner ? ";spin_action_link(this)" : "") +
608
+ (in_row_click ? ";event.stopPropagation()" : ""),
609
+ ...(!label || label === " "
610
+ ? { "aria-label": column.action_name }
611
+ : {}),
612
+ }, !!icon &&
613
+ icon !== "empty" &&
614
+ i({ class: icon }) + (label === " " ? "" : "&nbsp;"), label);
615
+ else
616
+ return post_btn(url, label, req.csrfToken(), {
617
+ small: true,
618
+ ajax: true,
619
+ icon,
620
+ reload_on_done: true,
621
+ confirm: column.confirm,
622
+ spinner: column.spinner,
623
+ btnClass: column.in_dropdown
624
+ ? "dropdown-item"
625
+ : column.action_style || "btn-primary",
626
+ req,
627
+ });
628
+ },
629
+ };
630
+ if (column.in_dropdown) {
631
+ //legacy
632
+ dropdown_actions.push(action_col);
633
+ return false;
634
+ }
635
+ else
636
+ return action_col;
637
+ }
638
+ else if (column.type === "View") {
639
+ return {
640
+ label: column.header_label ? __(column.header_label) : "",
641
+ key: (r) => viewResults[column.view + column.relation]?.(r),
642
+ };
643
+ }
644
+ else if (column.type === "ViewLink") {
645
+ if (!column.view)
646
+ return;
647
+ const r = view_linker(column, fields, __, isWeb(req), req.user, "", state, req, srcViewName, undefined, in_row_click);
648
+ //console.log(column);
649
+ if (column.view_label_formula) {
650
+ const fml_field = table.getField(column.view_label);
651
+ if (fml_field) {
652
+ if (column.view_label.includes(".")) {
653
+ const path = column.view_label.split(".");
654
+ if (path.length === 2) {
655
+ const [refNm, targetNm] = path;
656
+ r.statekey = `${refNm}.${table.getField(refNm).reftable_name}->${targetNm}`;
657
+ r.header_filter = headerFilterForField(fml_field, state, r.statekey);
658
+ }
659
+ }
660
+ else {
661
+ r.header_filter = headerFilterForField(fml_field, state);
662
+ r.statekey = fml_field.name;
663
+ }
664
+ }
665
+ }
666
+ if (column.header_label)
667
+ r.label = __(column.header_label);
668
+ Object.assign(r, setWidth);
669
+ if (column.in_dropdown) {
670
+ dropdown_actions.push(r);
671
+ return false;
672
+ }
673
+ else
674
+ return r;
675
+ }
676
+ else if (column.type === "Link") {
677
+ const r = make_link(column, fields, __, in_row_click);
678
+ if (column.header_label)
679
+ r.label = __(column.header_label);
680
+ Object.assign(r, setWidth);
681
+ if (column.in_dropdown) {
682
+ dropdown_actions.push(r);
683
+ return false;
684
+ }
685
+ else
686
+ return r;
687
+ }
688
+ else if (column.type === "JoinField") {
689
+ //console.log(column);
690
+ let fvrun;
691
+ const fieldview = column.join_fieldview || column.fieldview;
692
+ let refNm, targetNm, through, key, type;
693
+ const keypath = column.join_field.split(".");
694
+ if (column.join_field.includes("->")) {
695
+ const [relation, target] = column.join_field.split("->");
696
+ const [ontable, ref] = relation.split(".");
697
+ targetNm = target;
698
+ refNm = ref;
699
+ key = validSqlId(column.targetNm ||
700
+ `${ref}_${ontable.replaceAll(" ", "").toLowerCase()}_${target}`);
701
+ }
702
+ else {
703
+ refNm = keypath[0];
704
+ targetNm = keypath[keypath.length - 1];
705
+ key = keypath.join("_");
706
+ }
707
+ const field = table.getField(column.join_field);
708
+ if (column.field_type)
709
+ type = getState().types[column.field_type];
710
+ else {
711
+ if (field && field.type === "File")
712
+ column.field_type = "File";
713
+ else if (field?.type.name && field?.type?.fieldviews[fieldview]) {
714
+ column.field_type = field.type.name;
715
+ type = getState().types[column.field_type];
716
+ }
717
+ }
718
+ if (fieldview && type?.fieldviews?.[fieldview]?.expandColumns) {
719
+ return type.fieldviews[fieldview].expandColumns(field, {
720
+ ...field.attributes,
721
+ ...column,
722
+ }, column);
723
+ }
724
+ let header_filter;
725
+ let statekey;
726
+ if (!column.join_field.includes("->") && keypath.length == 2) {
727
+ statekey = `${refNm}.${table.getField(refNm).reftable_name}->${targetNm}`;
728
+ header_filter = headerFilterForField(field, state, statekey);
729
+ }
730
+ let gofv = fieldview && type && type.fieldviews && type.fieldviews[fieldview]
731
+ ? (row) => type.fieldviews[fieldview].run(row[key], req, {
732
+ row,
733
+ ...(field?.attributes || {}),
734
+ ...column,
735
+ ...(column?.configuration || {}),
736
+ })
737
+ : null;
738
+ if (!gofv && column.field_type === "File") {
739
+ gofv = (row) => row[key]
740
+ ? getState().fileviews[fieldview].run(row[key], "", {
741
+ row,
742
+ ...column,
743
+ ...(column?.configuration || {}),
744
+ })
745
+ : "";
746
+ }
747
+ fvrun = {
748
+ ...setWidth,
749
+ label: headerLabelForName(column.header_label ? __(column.header_label) : targetNm, key, req, __, statehash),
750
+ row_key: key,
751
+ row_label: field?.label,
752
+ statekey,
753
+ header_filter,
754
+ key: gofv ? gofv : (row) => text(row[key]),
755
+ sortlink: sortlinkForName(key, req, viewname, statehash),
756
+ };
757
+ if (column.click_to_edit) {
758
+ const reffield = fields.find((f) => f.name === refNm);
759
+ const oldkey = typeof fvrun.key === "function" ? fvrun.key : (r) => r[fvrun.key];
760
+ const newkey = (row) => div({
761
+ "data-inline-edit-fielddata": encodeURIComponent(JSON.stringify({
762
+ field_name: keypath[0],
763
+ table_name: table.name,
764
+ pk: row[table.pk_name],
765
+ fieldview,
766
+ configuration: column?.configuration,
767
+ join_field: keypath[keypath.length - 1],
768
+ })),
769
+ "data-inline-edit-ajax": "true",
770
+ "data-inline-edit-dest-url": `/api/${table.name}/${row[table.pk_name]}`,
771
+ }, span({ class: "current" }, oldkey(row)), i({ class: "editicon fas fa-edit ms-1" }));
772
+ fvrun.key = newkey;
773
+ }
774
+ return fvrun;
775
+ }
776
+ else if (column.type === "Aggregation") {
777
+ let table, fld, through;
778
+ if (column.agg_relation.includes("->")) {
779
+ let restpath;
780
+ [through, restpath] = column.agg_relation.split("->");
781
+ [table, fld] = restpath.split(".");
782
+ }
783
+ else {
784
+ [table, fld] = column.agg_relation.split(".");
785
+ }
786
+ let targetNm = column.targetNm ||
787
+ db.sqlsanitize((column.stat.replace(" ", "") +
788
+ "_" +
789
+ table +
790
+ "_" +
791
+ fld +
792
+ "_" +
793
+ column.agg_field.split("@")[0] +
794
+ "_" +
795
+ column.aggwhere || "").toLowerCase());
796
+ if (targetNm.length > 58) {
797
+ targetNm = targetNm
798
+ .split("")
799
+ .filter((c, i) => i % 2 == 0)
800
+ .join("");
801
+ }
802
+ let showValue = (value) => {
803
+ if (value === true || value === false)
804
+ return bool.fieldviews.show.run(value);
805
+ if (value instanceof Date)
806
+ return date.fieldviews.show.run(value);
807
+ return value?.toString ? value.toString() : value;
808
+ };
809
+ if (column.agg_fieldview && column.agg_field?.includes("@")) {
810
+ const tname = column.agg_field.split("@")[1];
811
+ const type = getState().types[tname];
812
+ if (type?.fieldviews[column.agg_fieldview])
813
+ showValue = (x) => type.fieldviews[column.agg_fieldview].run(x, req, column);
814
+ }
815
+ else if (column.agg_fieldview) {
816
+ const aggField = Table.findOne(table)?.getField?.(column.agg_field);
817
+ const outcomeType = column.stat === "Percent true" || column.stat === "Percent false"
818
+ ? "Float"
819
+ : column.stat === "Count" || column.stat === "CountUnique"
820
+ ? "Integer"
821
+ : aggField?.type?.name;
822
+ const type = getState().types[outcomeType];
823
+ if (type?.fieldviews[column.agg_fieldview])
824
+ showValue = (x) => type.fieldviews[column.agg_fieldview].run(type.read(x), req, {
825
+ ...column,
826
+ ...(column?.configuration || {}),
827
+ });
828
+ }
829
+ let key = (r) => {
830
+ const value = r[targetNm];
831
+ return showValue(value);
832
+ };
833
+ if (column.stat.toLowerCase() === "array_agg")
834
+ key = (r) => Array.isArray(r[targetNm])
835
+ ? r[targetNm].map((v) => showValue(v)).join(", ")
836
+ : "";
837
+ return {
838
+ ...setWidth,
839
+ label: headerLabelForName(column.header_label
840
+ ? column.header_label
841
+ : column.stat + " " + table, targetNm, req, __, statehash),
842
+ key,
843
+ sortlink: sortlinkForName(targetNm, req, viewname, statehash),
844
+ };
845
+ }
846
+ else if (column.type === "Field") {
847
+ //console.log(column);
848
+ let f = fields.find((fld) => fld.name === column.field_name);
849
+ let f_with_val = f;
850
+ if (f && f.attributes && f.attributes.localized_by) {
851
+ const locale = req?.getLocale?.();
852
+ const localized_fld_nm = f.attributes.localized_by[locale];
853
+ f_with_val = fields.find((fld) => fld.name === localized_fld_nm) || f;
854
+ }
855
+ const isNum = f && f.type && f.type.name === "Integer";
856
+ if (isNum && !setWidth.align)
857
+ setWidth.align = "right";
858
+ let fvrun;
859
+ let header_filter = headerFilterForField(f, state);
860
+ if (column.fieldview &&
861
+ f?.type?.fieldviews?.[column.fieldview]?.expandColumns) {
862
+ fvrun = f.type.fieldviews[column.fieldview].expandColumns(f, {
863
+ ...f.attributes,
864
+ ...column.configuration,
865
+ }, column);
866
+ }
867
+ else
868
+ fvrun = f && {
869
+ ...setWidth,
870
+ label: headerLabelForName(column.header_label ? __(column.header_label) : __(f.label), f.name, req, __, statehash),
871
+ row_key: f_with_val.name,
872
+ row_label: f.label,
873
+ key: column.fieldview && f.type === "File"
874
+ ? (row) => row[f.name] &&
875
+ getState().fileviews[column.fieldview].run(row[f.name], row[`${f.name}__filename`], { row, ...column, ...(column?.configuration || {}) })
876
+ : column.fieldview &&
877
+ f.type.fieldviews &&
878
+ f.type.fieldviews[column.fieldview]
879
+ ? (row) => f.type.fieldviews[column.fieldview].run(row[f_with_val.name], req, { row, ...f.attributes, ...column.configuration })
880
+ : isShow
881
+ ? f.type.showAs
882
+ ? (row) => f.type.showAs(row[f_with_val.name])
883
+ : (row) => text(row[f_with_val.name])
884
+ : f.listKey,
885
+ header_filter,
886
+ sortlink: !f.calculated || f.stored
887
+ ? sortlinkForName(f.name, req, viewname, statehash)
888
+ : undefined,
889
+ };
890
+ if (column.click_to_edit) {
891
+ const updateKey = (fvr, column_key) => {
892
+ const oldkey = typeof fvr.key === "function" ? fvr.key : (r) => r[fvr.key];
893
+ const doSetKey = (column.fieldview === "subfield" ||
894
+ column.fieldview === "keys_expand_columns") &&
895
+ column_key;
896
+ const schema = doSetKey && f.attributes?.hasSchema
897
+ ? (f.attributes.schema || []).find((s) => s.key === column_key)
898
+ : undefined;
899
+ const newkey = (row) => {
900
+ if (role <= table.min_role_write || table.is_owner(req.user, row))
901
+ return div({
902
+ "data-inline-edit-fielddata": encodeURIComponent(JSON.stringify({
903
+ field_name: f.name,
904
+ table_name: table.name,
905
+ pk: row[table.pk_name],
906
+ fieldview: column.fieldview,
907
+ configuration: column?.configuration,
908
+ })),
909
+ "data-inline-edit-ajax": "true",
910
+ "data-inline-edit-dest-url": `/api/${table.name}/${row[table.pk_name]}`,
911
+ }, span({ class: "current" }, oldkey(row)), i({ class: "editicon fas fa-edit ms-1" }));
912
+ else
913
+ return oldkey(row);
914
+ };
915
+ fvr.key = newkey;
916
+ };
917
+ if (Array.isArray(fvrun)) {
918
+ fvrun.forEach((fvr) => {
919
+ updateKey(fvr, fvr.row_key[1]);
920
+ });
921
+ }
922
+ else
923
+ updateKey(fvrun, column.configuration?.key || column.key);
924
+ }
925
+ return fvrun;
926
+ }
927
+ })).filter((v) => !!v);
928
+ if (dropdown_actions.length > 0) {
929
+ //legacy
930
+ tfields.push({
931
+ label: req.__("Action"),
932
+ key: (r) => div({ class: "dropdown" }, button({
933
+ class: "btn btn-sm btn-xs btn-outline-secondary dropdown-toggle",
934
+ "data-boundary": "viewport",
935
+ type: "button",
936
+ id: `actiondd${r.id}`,
937
+ "data-bs-toggle": "dropdown",
938
+ "aria-haspopup": "true",
939
+ "aria-expanded": "false",
940
+ }, req.__("Action")), div({
941
+ class: "dropdown-menu dropdown-menu-end",
942
+ "aria-labelledby": `actiondd${r.id}`,
943
+ }, dropdown_actions.map((acol) => acol.key(r)))),
944
+ });
945
+ }
946
+ return tfields;
947
+ };
948
+ const headerFilterForField = (f, state, path) => (id) => {
949
+ if (f?.type?.name === "Date") {
950
+ const set_initial = state[`_fromdate_${f.name}`] && state[`_todate_${f.name}`]
951
+ ? `defaultDate: ["${state[`_fromdate_${f.name}`]}", "${state[`_todate_${f.name}`]}"],`
952
+ : "";
953
+ return (div({ class: "input-group hdrfilterdate" }, input({
954
+ type: "text",
955
+ class: "form-control",
956
+ id: `daterangefilter${f.name}`,
957
+ //placeholder: ,
958
+ }), button({
959
+ class: "btn btn-outline-secondary btn-border-color-input",
960
+ style: { paddingLeft: "3px", paddingRight: "3px" },
961
+ onclick: `set_state_fields({_fromdate_${f.name}: {unset: true}, _todate_${f.name}: {unset: true} })`,
962
+ }, i({ class: "fas fa-times" }))) +
963
+ script(domReady(`ensure_script_loaded("/static_assets/${db.connectObj.version_tag}/flatpickr.min.js");
964
+ ensure_css_loaded("/static_assets/${db.connectObj.version_tag}/flatpickr.min.css");
965
+ $('#daterangefilter${f.name}').flatpickr({mode:'range',
966
+ dateFormat: "Y-m-d",${set_initial}
967
+ onChange: function(selectedDates, dateStr, instance) {
968
+ set_header_filter($(instance.element));
969
+ if(selectedDates.length==2) {
970
+
971
+ set_state_fields({_fromdate_${f.name}: selectedDates[0].toLocaleDateString('en-CA'), _todate_${f.name}: selectedDates[1].toLocaleDateString('en-CA') }, false, ${id ? `document.getElementById('${id}')` : "this"})
972
+
973
+
974
+ }
975
+ },
976
+ });`)));
977
+ }
978
+ let fieldviewObjs;
979
+ /*if (f.is_fkey) {
980
+ fieldviewObjs = [getState().keyFieldviews.select];
981
+ } else */
982
+ let extraAttrs = {};
983
+ if (f?.type?.name === "Bool") {
984
+ fieldviewObjs = [f.type.fieldviews.tristate];
985
+ extraAttrs.outline_buttons = true;
986
+ }
987
+ else if (f?.type?.name === "String")
988
+ fieldviewObjs = [f.type.fieldviews.edit];
989
+ else if (f?.type?.name === "Integer" || f?.type?.name === "Float")
990
+ fieldviewObjs = [
991
+ f.type.fieldviews.above_input,
992
+ f.type.fieldviews.below_input,
993
+ ];
994
+ if (!fieldviewObjs)
995
+ return "";
996
+ return div({ class: "d-flex" }, fieldviewObjs
997
+ .map((fvObj) => fvObj?.run(f.name, state[path || f.name], {
998
+ preOnChange: `set_header_filter(this);`,
999
+ onChange: `set_header_filter(this);set_state_field('${encodeURIComponent(path || f.name)}', this.value, ${id ? `document.getElementById('${id}')` : "this"})`,
1000
+ isFilter: true,
1001
+ ...f.attributes,
1002
+ ...extraAttrs,
1003
+ }, "", false, f, state) || "")
1004
+ .join(""));
1005
+ };
1006
+ /**
1007
+ * @param {string} fname
1008
+ * @param {object} req
1009
+ * @returns {string}
1010
+ */
1011
+ const sortlinkForName = (fname, req, viewname, statehash) => {
1012
+ const _sortby = req.query ? req.query[`_${statehash}_sortby`] : undefined;
1013
+ const _sortdesc = req.query ? req.query[`_${statehash}_sortdesc`] : undefined;
1014
+ const desc = typeof _sortdesc == "undefined"
1015
+ ? _sortby === fname
1016
+ : _sortdesc
1017
+ ? "false"
1018
+ : "true";
1019
+ return `sortby('${text(fname)}', ${desc}, '${statehash}', this)`;
1020
+ };
1021
+ const standardLayoutRowVisitor = (viewname, state, table, row, req) => {
1022
+ const session_id = getSessionId(req);
1023
+ const locale = req.getLocale();
1024
+ const fields = table.fields;
1025
+ const evalMaybeExpr = (segment, key, fmlkey) => {
1026
+ if (segment.isFormula && segment.isFormula[fmlkey || key]) {
1027
+ segment[key] = eval_expression(segment[key], { session_id, locale, ...row }, req.user, `property ${key} in segment of type ${segment.type}`);
1028
+ }
1029
+ };
1030
+ return {
1031
+ link(segment) {
1032
+ evalMaybeExpr(segment, "url");
1033
+ evalMaybeExpr(segment, "text");
1034
+ if (req?.generate_email &&
1035
+ req.get_base_url &&
1036
+ segment.url.startsWith("/")) {
1037
+ const targetPrefix = req.get_base_url();
1038
+ const safePrefix = (targetPrefix || "").endsWith("/")
1039
+ ? targetPrefix.substring(0, targetPrefix.length - 1)
1040
+ : targetPrefix || "";
1041
+ segment.url = safePrefix + segment.url;
1042
+ }
1043
+ },
1044
+ view_link(segment) {
1045
+ evalMaybeExpr(segment, "view_label", "label");
1046
+ },
1047
+ blank(segment) {
1048
+ evalMaybeExpr(segment, "contents", "text");
1049
+ },
1050
+ tabs(segment) {
1051
+ const to_delete = new Set();
1052
+ (segment.showif || []).forEach((sif, ix) => {
1053
+ if (sif) {
1054
+ const showit = eval_expression(sif, { session_id, ...row }, req.user, `Tabs show if formula`);
1055
+ if (!showit)
1056
+ to_delete.add(ix);
1057
+ }
1058
+ });
1059
+ // TODO mutation here - potential issue with renderRows
1060
+ segment.titles = segment.titles.filter((v, ix) => !to_delete.has(ix));
1061
+ segment.contents = segment.contents.filter((v, ix) => !to_delete.has(ix));
1062
+ (segment.titles || []).forEach((t, ix) => {
1063
+ if (typeof t === "string" && t.includes("{{")) {
1064
+ segment.titles[ix] = interpolate(t, row, req.user, "Tab titles");
1065
+ }
1066
+ });
1067
+ },
1068
+ action(segment) {
1069
+ evalMaybeExpr(segment, "action_label");
1070
+ },
1071
+ card(segment) {
1072
+ evalMaybeExpr(segment, "url");
1073
+ evalMaybeExpr(segment, "title");
1074
+ evalMaybeExpr(segment, "class");
1075
+ },
1076
+ image(segment) {
1077
+ evalMaybeExpr(segment, "url");
1078
+ evalMaybeExpr(segment, "alt");
1079
+ if (segment.srctype === "Field") {
1080
+ const field = fields.find((f) => f.name === segment.field);
1081
+ if (!field)
1082
+ return;
1083
+ if (field.type.name === "String")
1084
+ segment.url = row[segment.field];
1085
+ if (field.type === "File") {
1086
+ segment.url = `/files/serve/${row[segment.field]}`;
1087
+ segment.fileid = row[segment.field];
1088
+ }
1089
+ }
1090
+ },
1091
+ container(segment) {
1092
+ evalMaybeExpr(segment, "bgColor");
1093
+ evalMaybeExpr(segment, "customClass");
1094
+ evalMaybeExpr(segment, "customId");
1095
+ evalMaybeExpr(segment, "url");
1096
+ if (segment.bgType === "Image Field") {
1097
+ segment.bgType = "Image";
1098
+ segment.bgFileId = row[segment.bgField];
1099
+ }
1100
+ if (segment.showIfFormula) {
1101
+ const f = get_expression_function(segment.showIfFormula, fields);
1102
+ if (!f({ ...dollarizeObject(state || {}), ...row }, req.user))
1103
+ segment.hide = true;
1104
+ else
1105
+ segment.hide = false;
1106
+ }
1107
+ if (segment.click_action) {
1108
+ segment.url = `javascript:view_post('${viewname}', 'run_action', {click_action: '${segment.click_action}', ${table.pk_name}: ${JSON.stringify(row[table.pk_name])}})`;
1109
+ }
1110
+ },
1111
+ };
1112
+ };
1113
+ const standardBlockDispatch = (viewname, state, table, extra, row) => {
1114
+ const req = extra.req;
1115
+ const fields = table.fields;
1116
+ const locale = req.getLocale();
1117
+ const role = req.user?.role_id || 100;
1118
+ return {
1119
+ field({ field_name, fieldview, configuration, click_to_edit }) {
1120
+ let field = fields.find((fld) => fld.name === field_name);
1121
+ if (!field)
1122
+ return "";
1123
+ let val = row[field_name];
1124
+ let fvrun;
1125
+ if (field &&
1126
+ field.attributes &&
1127
+ field.attributes.localized_by &&
1128
+ field.attributes.localized_by[locale]) {
1129
+ const localized_fld = field.attributes.localized_by[locale];
1130
+ val = row[localized_fld];
1131
+ }
1132
+ const cfg = {
1133
+ row,
1134
+ ...field.attributes,
1135
+ ...configuration,
1136
+ };
1137
+ if (fieldview && field.type === "File") {
1138
+ if (req.generate_email)
1139
+ cfg.targetPrefix = getSafeBaseUrl();
1140
+ fvrun = val
1141
+ ? getState().fileviews[fieldview].run(val, row[`${field_name}__filename`], cfg)
1142
+ : "";
1143
+ }
1144
+ else if (fieldview &&
1145
+ field.type &&
1146
+ field.type.fieldviews &&
1147
+ field.type.fieldviews[fieldview])
1148
+ fvrun = field.type.fieldviews[fieldview].run(val, req, cfg);
1149
+ else
1150
+ fvrun = text(val);
1151
+ if (click_to_edit &&
1152
+ (role <= table.min_role_write || table.is_owner(req.user, row)))
1153
+ return div({
1154
+ "data-inline-edit-fielddata": encodeURIComponent(JSON.stringify({
1155
+ field_name,
1156
+ table_name: table.name,
1157
+ pk: row[table.pk_name],
1158
+ fieldview,
1159
+ configuration,
1160
+ })),
1161
+ "data-inline-edit-ajax": "true",
1162
+ "data-inline-edit-dest-url": `/api/${table.name}/${row[table.pk_name]}`,
1163
+ class: !isWeb(req) ? "mobile-data-inline-edit" : "",
1164
+ }, fvrun);
1165
+ else
1166
+ return fvrun;
1167
+ },
1168
+ join_field(jf) {
1169
+ const { join_field, field_type, fieldview, configuration, target_field_attributes, click_to_edit, } = jf;
1170
+ const keypath = join_field.split(".");
1171
+ let value;
1172
+ if (join_field.includes("->")) {
1173
+ const [relation, target] = join_field.split("->");
1174
+ const [ontable, ref] = relation.split(".");
1175
+ const key = jf.targetNm ||
1176
+ `${ref}_${ontable.replaceAll(" ", "").toLowerCase()}_${target}`;
1177
+ value = row[validSqlId(key)];
1178
+ }
1179
+ else {
1180
+ value = row[join_field.split(".").join("_")];
1181
+ }
1182
+ if (field_type === "File") {
1183
+ return value
1184
+ ? getState().fileviews[fieldview].run(value, "", configuration || {})
1185
+ : "";
1186
+ }
1187
+ let fvRes;
1188
+ if (field_type && fieldview) {
1189
+ const type = getState().types[field_type];
1190
+ if (type && getState().types[field_type]) {
1191
+ fvRes = type.fieldviews[fieldview].run(value, req, {
1192
+ row,
1193
+ ...(target_field_attributes || {}),
1194
+ ...configuration,
1195
+ });
1196
+ }
1197
+ else
1198
+ fvRes = text(value);
1199
+ }
1200
+ else
1201
+ fvRes = text(value);
1202
+ if (click_to_edit &&
1203
+ (role <= table.min_role_write || table.is_owner(req.user, row)))
1204
+ return div({
1205
+ "data-inline-edit-fielddata": encodeURIComponent(JSON.stringify({
1206
+ field_name: keypath[0],
1207
+ table_name: table.name,
1208
+ pk: row[table.pk_name],
1209
+ fieldview,
1210
+ configuration,
1211
+ join_field: keypath[keypath.length - 1],
1212
+ })),
1213
+ "data-inline-edit-ajax": "true",
1214
+ "data-inline-edit-dest-url": `/api/${table.name}/${row[table.pk_name]}`,
1215
+ class: !isWeb(req) ? "mobile-data-inline-edit" : "",
1216
+ }, fvRes);
1217
+ else
1218
+ return fvRes;
1219
+ },
1220
+ aggregation(column) {
1221
+ const { agg_relation, stat, aggwhere, agg_field } = column;
1222
+ let table, fld, through;
1223
+ if (agg_relation.includes("->")) {
1224
+ let restpath;
1225
+ [through, restpath] = agg_relation.split("->");
1226
+ [table, fld] = restpath.split(".");
1227
+ }
1228
+ else {
1229
+ [table, fld] = agg_relation.split(".");
1230
+ }
1231
+ let targetNm = column.targetNm ||
1232
+ db.sqlsanitize((stat +
1233
+ "_" +
1234
+ table +
1235
+ "_" +
1236
+ fld +
1237
+ "_" +
1238
+ (agg_field || "").split("@")[0] +
1239
+ "_" +
1240
+ aggwhere || "").toLowerCase());
1241
+ if (targetNm.length > 58) {
1242
+ targetNm = targetNm
1243
+ .split("")
1244
+ .filter((c, i) => i % 2 == 0)
1245
+ .join("");
1246
+ }
1247
+ const val = row[targetNm];
1248
+ if (stat.toLowerCase() === "array_agg" && Array.isArray(val))
1249
+ return val.map((v) => text(v?.toString?.())).join(", ");
1250
+ else if (column.agg_fieldview) {
1251
+ const aggField = Table.findOne(table)?.getField?.(column.agg_field);
1252
+ const outcomeType = stat === "Percent true" || stat === "Percent false"
1253
+ ? "Float"
1254
+ : stat === "Count" || stat === "CountUnique"
1255
+ ? "Integer"
1256
+ : aggField.type?.name;
1257
+ const type = getState().types[outcomeType];
1258
+ if (type?.fieldviews[column.agg_fieldview]) {
1259
+ const readval = type.read(val);
1260
+ return type.fieldviews[column.agg_fieldview].run(readval, req, column?.configuration || {});
1261
+ }
1262
+ }
1263
+ return text(val);
1264
+ },
1265
+ action(segment) {
1266
+ if (segment.action_style === "on_page_load") {
1267
+ if (extra?.isPreview)
1268
+ return "";
1269
+ run_action_column({
1270
+ col: { ...segment },
1271
+ referrer: req?.get?.("Referrer"),
1272
+ req: req,
1273
+ }).catch((e) => Crash.create(e, req));
1274
+ return "";
1275
+ }
1276
+ let url = action_url(viewname, table, segment.action_name, row, segment.rndid, "rndid", segment.confirm, undefined, !!segment.run_async);
1277
+ if (segment.action_name === "Delete" &&
1278
+ segment.configuration?.after_delete_action == "Reload page") {
1279
+ url = {
1280
+ javascript: `ajax_post('${table.delete_url(row)}', {success:()=>{close_saltcorn_modal();location.reload();}})`,
1281
+ };
1282
+ return action_link(url, req, segment);
1283
+ }
1284
+ else if (segment.action_name === "Delete")
1285
+ url = `${table.delete_url(row, `redirect=${encodeURIComponent(interpolate(segment.configuration?.after_delete_url || "/", row, req?.user, "delete action: after delete URL"))}`)}`;
1286
+ return action_link(url, req, segment);
1287
+ },
1288
+ view_link(view) {
1289
+ const prefix = req.generate_email && req.get_base_url ? req.get_base_url() : "";
1290
+ const { key } = view_linker(view, fields, (s) => s, isWeb(req), req.user, prefix, state, req, viewname);
1291
+ return key(row);
1292
+ },
1293
+ tabs(segment, go) {
1294
+ if (segment.tabsStyle !== "Value switch")
1295
+ return false;
1296
+ const rval = row[segment.field];
1297
+ const value = rval?.id || rval; // TODO pkname of join table
1298
+ const ix = segment.titles.findIndex((t) => typeof t.value === "undefined"
1299
+ ? `${t}` === `${value}`
1300
+ : value === t.value);
1301
+ if (ix === -1)
1302
+ return "";
1303
+ return go(segment.contents[ix]);
1304
+ },
1305
+ blank(segment) {
1306
+ if (segment.isHTML) {
1307
+ return interpolate(segment.contents, { locale, ...row }, req?.user, "HTML element");
1308
+ }
1309
+ else
1310
+ return segment.contents;
1311
+ },
1312
+ };
1313
+ };
1314
+ /**
1315
+ * @param {object} column
1316
+ * @param {object} f
1317
+ * @param {object} req
1318
+ * @param {*} __
1319
+ * @returns {string}
1320
+ */
1321
+ const headerLabelForName = (label, fname, req, __, statehash) => {
1322
+ //const { _sortby, _sortdesc } = req.query || {};
1323
+ const _sortby = req?.query ? req.query[`_${statehash}_sortby`] : undefined;
1324
+ const _sortdesc = req?.query
1325
+ ? req.query[`_${statehash}_sortdesc`]
1326
+ : undefined;
1327
+ let arrow = _sortby !== fname
1328
+ ? ""
1329
+ : _sortdesc
1330
+ ? i({ class: "fas fa-caret-down sortdir" })
1331
+ : i({ class: "fas fa-caret-up sortdir" });
1332
+ return arrow ? span({ class: "text-nowrap" }, label + arrow) : label;
1333
+ };
1334
+ /**
1335
+ * @function
1336
+ * @param {Field[]} fields
1337
+ * @param {object} state
1338
+ * @param {boolean} [fuzzyStrings]
1339
+ * @returns {object}
1340
+ */
1341
+ const splitUniques = (fields, state, fuzzyStrings) => {
1342
+ let uniques = {};
1343
+ let nonUniques = {};
1344
+ Object.entries(state).forEach(([k, v]) => {
1345
+ const field = fields.find((f) => f.name === k);
1346
+ if (field &&
1347
+ (field.is_unique || field.primary_key) &&
1348
+ fuzzyStrings &&
1349
+ field.type &&
1350
+ field.type.name === "String")
1351
+ uniques[k] = { ilike: v };
1352
+ else if (field && (field.is_unique || field.primary_key))
1353
+ uniques[k] = field.type.read ? field.type.read(v, field.attributes) : v;
1354
+ else
1355
+ nonUniques[k] = v;
1356
+ });
1357
+ return { uniques, nonUniques };
1358
+ };
1359
+ /**
1360
+ * @param {object} table
1361
+ * @param {string} viewname
1362
+ * @param {object[]} [columns]
1363
+ * @param {object} layout0
1364
+ * @param {boolean|null} id
1365
+ * @param {object} req
1366
+ * @param {boolean} isRemote
1367
+ * @returns {Promise<Form>}
1368
+ */
1369
+ const getForm = async (table, viewname, columns, layout0, id, req, isRemote) => {
1370
+ const fields = table.getFields();
1371
+ const state = getState();
1372
+ const tfields = (columns || [])
1373
+ .map((column) => {
1374
+ if (column.type === "Field") {
1375
+ const f0 = fields.find((fld) => fld.name === column.field_name);
1376
+ if (f0) {
1377
+ const f = new Field(f0);
1378
+ f.fieldview = column.fieldview;
1379
+ if (f.type === "Key") {
1380
+ if (state.keyFieldviews[column.fieldview])
1381
+ f.fieldviewObj = state.keyFieldviews[column.fieldview];
1382
+ f.input_type =
1383
+ !f.fieldview ||
1384
+ !f.fieldviewObj ||
1385
+ (f.fieldview === "select" && !f.fieldviewObj)
1386
+ ? "select"
1387
+ : "fromtype";
1388
+ }
1389
+ if (f.type === "File") {
1390
+ const fvNm = column.fieldview || "upload";
1391
+ if (state.fileviews[fvNm])
1392
+ f.fieldviewObj = state.fileviews[fvNm];
1393
+ f.input_type =
1394
+ !f.fieldview || !f.fieldviewObj ? "file" : "fromtype";
1395
+ }
1396
+ if (f.calculated) {
1397
+ const qs = objToQueryString(column.configuration);
1398
+ f.sourceURL = `/field/show-calculated/${table.name}/${f.name}/${f.fieldview}?${qs}`;
1399
+ }
1400
+ f.attributes = { ...column.configuration, ...f.attributes };
1401
+ if (typeof column.block !== "undefined" &&
1402
+ typeof f.attributes.block === "undefined")
1403
+ f.attributes.block = column.block;
1404
+ return f;
1405
+ }
1406
+ else if (table.name === "users" && column.field_name === "password") {
1407
+ return new Field({
1408
+ name: "password",
1409
+ fieldview: column.fieldview,
1410
+ type: "String",
1411
+ });
1412
+ }
1413
+ else if (table.name === "users" &&
1414
+ column.field_name === "passwordRepeat") {
1415
+ return new Field({
1416
+ name: "passwordRepeat",
1417
+ fieldview: column.fieldview,
1418
+ type: "String",
1419
+ });
1420
+ }
1421
+ else if (table.name === "users" && column.field_name === "remember") {
1422
+ return new Field({
1423
+ name: "remember",
1424
+ fieldview: column.fieldview,
1425
+ type: "Bool",
1426
+ });
1427
+ }
1428
+ }
1429
+ })
1430
+ .filter((tf) => !!tf);
1431
+ const path = isWeb(req) ? req.baseUrl + req.path : "";
1432
+ const qs = objectToQueryString(req.query);
1433
+ let action = `/view/${viewname}${qs ? "?" + qs : ""}`;
1434
+ if (path && path.startsWith("/auth/"))
1435
+ action = path;
1436
+ const layout = structuredClone(layout0);
1437
+ traverseSync(layout, {
1438
+ container(segment) {
1439
+ if (segment.showIfFormula) {
1440
+ segment.showIfFormulaInputs = segment.showIfFormula;
1441
+ const fvs = [...freeVariables(segment.showIfFormula)];
1442
+ const jfFvs = fvs.filter((fv) => fv.includes(".") && !fv.startsWith("user."));
1443
+ if (jfFvs.length)
1444
+ segment.showIfFormulaJoinFields = jfFvs
1445
+ .map((jf) => {
1446
+ const [ref, target] = jf.split(".");
1447
+ const refField = table.getField(ref);
1448
+ if (!refField || !refField?.reftable_name)
1449
+ return null;
1450
+ return {
1451
+ ref: ref.replace("?", ""),
1452
+ target,
1453
+ refTable: refField.reftable_name,
1454
+ };
1455
+ })
1456
+ .filter(Boolean);
1457
+ }
1458
+ },
1459
+ });
1460
+ if (!req.layout_hints)
1461
+ req.layout_hints = state.getLayout(req.user).hints || {};
1462
+ let isMobileLogin = false;
1463
+ if (isRemote) {
1464
+ const loginForm = getState().getConfig("login_form", "");
1465
+ if (loginForm && viewname === loginForm)
1466
+ isMobileLogin = true;
1467
+ }
1468
+ let submitActionJS = undefined;
1469
+ const submitActionCol = columns.find((c) => c.is_submit_action);
1470
+ if (submitActionCol) {
1471
+ submitActionJS = `event.preventDefault();view_post(this, 'run_action', {rndid:'${submitActionCol.rndid}', ...get_form_record(this) })`;
1472
+ if (layout.above)
1473
+ layout.above.push(`<input type="submit" hidden />`);
1474
+ //TODO what if there is no above, e.g. all in card or container
1475
+ }
1476
+ const form = new Form({
1477
+ action: action,
1478
+ onSubmit: isRemote || isOfflineMode()
1479
+ ? `javascript:${!isMobileLogin
1480
+ ? `formSubmit(this, '/view/', '${viewname}')`
1481
+ : "loginFormSubmit(this)"}`
1482
+ : submitActionJS,
1483
+ viewname: viewname,
1484
+ fields: tfields,
1485
+ layout,
1486
+ req,
1487
+ pk_name: table.pk_name,
1488
+ });
1489
+ if (id)
1490
+ form.hidden(form.pk_name);
1491
+ return form;
1492
+ };
1493
+ /**
1494
+ * @param {object} table
1495
+ * @param {object} req
1496
+ * @param {object} fixed
1497
+ * @returns {Promise<object>}
1498
+ */
1499
+ const fill_presets = async (table, req, fixed) => {
1500
+ if (!table)
1501
+ return fixed;
1502
+ const fields = table.getFields();
1503
+ Object.keys(fixed || {}).forEach((k) => {
1504
+ if (k.startsWith("preset_")) {
1505
+ if (fixed[k]) {
1506
+ const fldnm = k.replace("preset_", "");
1507
+ const fld = fields.find((f) => f.name === fldnm);
1508
+ if (fld) {
1509
+ if (table.name === "users" && fld.primary_key)
1510
+ fixed[fldnm] = req.user ? req.user.id : null;
1511
+ else
1512
+ fixed[fldnm] = fld.presets[fixed[k]]({
1513
+ user: req.user,
1514
+ req,
1515
+ field: fld,
1516
+ });
1517
+ }
1518
+ }
1519
+ delete fixed[k];
1520
+ }
1521
+ else {
1522
+ const fld = fields.find((f) => f.name === k);
1523
+ if (!fld)
1524
+ delete fixed[k];
1525
+ if (fixed[k] === null || fixed[k] === "")
1526
+ delete fixed[k];
1527
+ }
1528
+ });
1529
+ return fixed;
1530
+ };
1531
+ const objToQueryString = (o) => Object.entries(o || {})
1532
+ .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
1533
+ .join("&");
1534
+ // edit template build in actions
1535
+ const edit_build_in_actions = [
1536
+ "Save",
1537
+ "SaveAndContinue",
1538
+ "UpdateMatchingRows",
1539
+ "SubmitWithAjax",
1540
+ "Reset",
1541
+ "GoBack",
1542
+ "Delete",
1543
+ "Cancel",
1544
+ ];
1545
+ module.exports = {
1546
+ get_viewable_fields,
1547
+ get_viewable_fields_from_layout,
1548
+ action_url,
1549
+ objToQueryString,
1550
+ action_link,
1551
+ view_linker,
1552
+ parse_view_select,
1553
+ splitUniques,
1554
+ getForm,
1555
+ fill_presets,
1556
+ get_view_link_query,
1557
+ make_link,
1558
+ edit_build_in_actions,
1559
+ standardBlockDispatch,
1560
+ standardLayoutRowVisitor,
1561
+ };
1562
+ //# sourceMappingURL=viewable_fields.js.map