@saltcorn/builder 0.9.4-beta.8 → 0.9.4

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.
@@ -18,6 +18,8 @@ import FontIconPicker from "@fonticonpicker/react-fonticonpicker";
18
18
  import faIcons from "./faicons";
19
19
  import { Columns, ntimes } from "./Columns";
20
20
  import Tippy from "@tippyjs/react";
21
+ import { RelationType } from "@saltcorn/common-code";
22
+ import Select from "react-select";
21
23
 
22
24
  export const DynamicFontAwesomeIcon = ({ icon, className }) => {
23
25
  if (!icon) return null;
@@ -138,7 +140,7 @@ export /**
138
140
  */
139
141
  const OrFormula = ({ setProp, isFormula, node, nodekey, children }) => {
140
142
  const { mode } = React.useContext(optionsCtx);
141
-
143
+ const allowFormula = mode === "show" || mode === "list";
142
144
  /**
143
145
  * @returns {void}
144
146
  */
@@ -159,14 +161,14 @@ const OrFormula = ({ setProp, isFormula, node, nodekey, children }) => {
159
161
  });
160
162
  };
161
163
  let errorString = false;
162
- if (mode === "show" && isFormula[nodekey]) {
164
+ if (allowFormula && isFormula[nodekey]) {
163
165
  try {
164
166
  Function("return " + node[nodekey]);
165
167
  } catch (error) {
166
168
  errorString = error.message;
167
169
  }
168
170
  }
169
- return mode !== "show" ? (
171
+ return !allowFormula ? (
170
172
  children
171
173
  ) : (
172
174
  <Fragment>
@@ -445,6 +447,10 @@ const fetchPreview = ({ url, body, options, setPreviews, node_id, isView }) => {
445
447
  }
446
448
  const newHtml = $(".preview-scratchpad").html();
447
449
  setPreviews((prevState) => ({ ...prevState, [node_id]: newHtml }));
450
+ })
451
+ .catch((e) => {
452
+ console.log("Unable to fetch the preview:");
453
+ console.log(e);
448
454
  });
449
455
  };
450
456
 
@@ -487,6 +493,7 @@ export const fetchViewPreview =
487
493
  };
488
494
  let viewname,
489
495
  body = configuration ? { ...configuration } : {};
496
+ if (!view) return "";
490
497
  if (view.includes(":")) {
491
498
  const [prefix, rest] = view.split(":");
492
499
  const tokens = rest.split(".");
@@ -897,24 +904,51 @@ const ConfigField = ({
897
904
  spellCheck={false}
898
905
  />
899
906
  ),
900
- select: () => (
901
- <select
902
- className="form-control form-select"
903
- value={value || ""}
904
- onChange={(e) => e.target && myOnChange(e.target.value)}
905
- onBlur={(e) => e.target && myOnChange(e.target.value)}
906
- >
907
- {field.options.map((o, ix) =>
908
- o.name && o.label ? (
909
- <option key={ix} value={o.name}>
910
- {o.label}
911
- </option>
912
- ) : (
913
- <option key={ix}>{o}</option>
914
- )
915
- )}
916
- </select>
917
- ),
907
+ select: () => {
908
+ if (field.class?.includes?.("selectizable")) {
909
+ const seloptions = field.options.map((o, ix) =>
910
+ o.name && o.label
911
+ ? { value: o.name, label: o.label }
912
+ : { value: o, label: o }
913
+ );
914
+ return (
915
+ <Select
916
+ options={seloptions}
917
+ value={seloptions.find((so) => value === so.value)}
918
+ onChange={(e) =>
919
+ (e.name && myOnChange(e.name)) ||
920
+ (e.value && myOnChange(e.value)) ||
921
+ (typeof e === "string" && myOnChange(e))
922
+ }
923
+ onBlur={(e) =>
924
+ (e.name && myOnChange(e.name)) ||
925
+ (e.value && myOnChange(e.value)) ||
926
+ (typeof e === "string" && myOnChange(e))
927
+ }
928
+ menuPortalTarget={document.body}
929
+ styles={{ menuPortal: (base) => ({ ...base, zIndex: 19999 }) }}
930
+ ></Select>
931
+ );
932
+ } else
933
+ return (
934
+ <select
935
+ className="form-control form-select"
936
+ value={value || ""}
937
+ onChange={(e) => e.target && myOnChange(e.target.value)}
938
+ onBlur={(e) => e.target && myOnChange(e.target.value)}
939
+ >
940
+ {field.options.map((o, ix) =>
941
+ o.name && o.label ? (
942
+ <option key={ix} value={o.name}>
943
+ {o.label}
944
+ </option>
945
+ ) : (
946
+ <option key={ix}>{o}</option>
947
+ )
948
+ )}
949
+ </select>
950
+ );
951
+ },
918
952
  btn_select: () => (
919
953
  <div className="btn-group w-100" role="group">
920
954
  {field.options.map((o, ix) => (
@@ -1012,8 +1046,15 @@ export /**
1012
1046
  * @returns {table}
1013
1047
  */
1014
1048
  const SettingsFromFields =
1015
- (fields, opts = {}) =>
1049
+ (fieldsIn, opts = {}) =>
1016
1050
  () => {
1051
+ const fields = [...fieldsIn];
1052
+ if (opts.additionalFieldsOptionKey) {
1053
+ const options = React.useContext(optionsCtx);
1054
+
1055
+ const addFields = options[opts.additionalFieldsOptionKey];
1056
+ fields.push(...(addFields || []));
1057
+ }
1017
1058
  const node = useNode((node) => {
1018
1059
  const ps = {};
1019
1060
  fields.forEach((f) => {
@@ -1253,9 +1294,12 @@ const ButtonOrLinkSettingsRows = ({
1253
1294
  <option value={addBtnClass("btn-outline-secondary")}>
1254
1295
  Secondary outline button
1255
1296
  </option>
1297
+ <option value={addBtnClass("btn-outline-danger")}>
1298
+ Danger outline button
1299
+ </option>
1256
1300
  <option value={addBtnClass("btn-outline-warning")}>
1257
1301
  Warning outline button
1258
- </option>{" "}
1302
+ </option>
1259
1303
  <option value={addBtnClass("btn-outline-info")}>
1260
1304
  Info outline button
1261
1305
  </option>
@@ -1265,7 +1309,7 @@ const ButtonOrLinkSettingsRows = ({
1265
1309
  {!linkFirst ? (
1266
1310
  <option value={addBtnClass("btn-link")}>Link</option>
1267
1311
  ) : null}
1268
- {!linkFirst ? (
1312
+ {!linkFirst && allowRunOnLoad ? (
1269
1313
  <option value="on_page_load">Run on Page Load</option>
1270
1314
  ) : null}
1271
1315
  </select>
@@ -1285,6 +1329,7 @@ const ButtonOrLinkSettingsRows = ({
1285
1329
  <option value="">Standard</option>
1286
1330
  <option value="btn-lg">Large</option>
1287
1331
  <option value="btn-sm">Small</option>
1332
+ <option value="btn-sm btn-xs">Extra Small</option>
1288
1333
  <option value="btn-block">Block</option>
1289
1334
  <option value="btn-block btn-lg">Large block</option>
1290
1335
  </select>
@@ -1447,22 +1492,6 @@ const Tooltip = ({ children }) => {
1447
1492
  );
1448
1493
  };
1449
1494
 
1450
- export const buildTableCaches = (allTables) => {
1451
- const tableIdCache = {};
1452
- const tableNameCache = {};
1453
- const fieldCache = {};
1454
- for (const table of allTables) {
1455
- tableIdCache[table.id] = table;
1456
- tableNameCache[table.name] = table;
1457
- for (const field of table.foreign_keys) {
1458
- if (!fieldCache[field.reftable_name])
1459
- fieldCache[field.reftable_name] = [];
1460
- fieldCache[field.reftable_name].push(field);
1461
- }
1462
- }
1463
- return { tableIdCache, tableNameCache, fieldCache };
1464
- };
1465
-
1466
1495
  export const removeWhitespaces = (str) => {
1467
1496
  return str.replace(/\s/g, "X");
1468
1497
  };
@@ -1513,79 +1542,137 @@ export const arrayChunks = (xs, n) => {
1513
1542
  return arrayOfArrays;
1514
1543
  };
1515
1544
 
1516
- export const prepCacheAndFinder = ({
1517
- tables,
1518
- views,
1519
- max_relations_layer_depth,
1520
- }) => {
1521
- if (tables && views) {
1522
- const caches = buildTableCaches(tables);
1523
- const finder = new relationHelpers.RelationsFinder(
1524
- caches,
1525
- views,
1526
- max_relations_layer_depth || 6
1527
- );
1528
- return { caches, finder };
1529
- } else return { caches: null, finder: null };
1530
- };
1531
-
1532
1545
  /**
1533
- * @param {string[]} paths
1546
+ * @param {string[]} relations
1534
1547
  * @param {string} sourceTbl name of the topview table
1535
1548
  * @returns either a same table relation, a parent relation, a child relation, or the first relation
1536
1549
  */
1537
- export const initialRelation = (paths, sourceTbl) => {
1550
+ export const initialRelation = (relations) => {
1538
1551
  let sameTblRel = null;
1539
1552
  let parentRel = null;
1540
1553
  let childRel = null;
1541
- for (const path of paths) {
1542
- if (!sameTblRel && path === `.${sourceTbl}`) sameTblRel = path;
1543
- else {
1544
- const tokens = path.split(".");
1545
- if (
1546
- !parentRel &&
1547
- tokens.length === 3 &&
1548
- tokens[1] === sourceTbl &&
1549
- tokens[2].indexOf("$") === -1
1550
- )
1551
- parentRel = path;
1552
- else {
1553
- const lastToken = tokens[tokens.length - 1];
1554
- if (
1555
- lastToken.indexOf("$") > 0 &&
1556
- (!childRel || childRel.split(".").length > tokens.length)
1557
- )
1558
- childRel = path;
1559
- }
1554
+ for (const relation of relations) {
1555
+ switch (relation.type) {
1556
+ case RelationType.OWN:
1557
+ sameTblRel = relation;
1558
+ break;
1559
+ case RelationType.PARENT_SHOW:
1560
+ parentRel = relation;
1561
+ break;
1562
+ case RelationType.CHILD_LIST:
1563
+ case RelationType.ONE_TO_ONE_SHOW:
1564
+ childRel = relation;
1565
+ break;
1560
1566
  }
1561
1567
  }
1562
- return sameTblRel || parentRel || childRel || paths[0];
1568
+ return sameTblRel || parentRel || childRel || relations[0];
1563
1569
  };
1564
1570
 
1565
1571
  /**
1566
- * update the builder wide relations cache relations cache
1567
- * if there is no entry for the given tableName and viewname
1568
- * @param {any} relationsCache cache from the context
1569
- * @param {Function} setRelationsCache set cache in context
1570
- * @param {any} options builder options
1571
- * @param {RelationsFinder} finder
1572
- * @param {string} viewname subview name
1572
+ * builder intern path method
1573
+ * @param path
1574
+ * @param tableNameCache
1575
+ * @returns
1573
1576
  */
1574
- export const updateRelationsCache = (
1575
- relationsCache,
1576
- setRelationsCache,
1577
- options,
1578
- finder,
1579
- viewname
1580
- ) => {
1581
- if (!relationsCache[options.tableName])
1582
- relationsCache[options.tableName] = {};
1583
- if (!relationsCache[options.tableName][viewname]) {
1584
- relationsCache[options.tableName][viewname] = finder.findRelations(
1585
- options.tableName,
1586
- viewname,
1587
- options.excluded_subview_templates
1588
- );
1589
- setRelationsCache({ ...relationsCache });
1577
+ export const buildRelationArray = (path, tableNameCache) => {
1578
+ if (path === ".")
1579
+ return [{ type: "Independent", table: "None (no relation)" }];
1580
+ const tokens = path.split(".");
1581
+ if (tokens.length === 2)
1582
+ return [{ type: "Own", table: `${tokens[1]} (same table)` }];
1583
+ else if (tokens.length >= 3) {
1584
+ const result = [];
1585
+ let currentTbl = tokens[1];
1586
+ for (const relation of tokens.slice(2)) {
1587
+ if (relation.indexOf("$") > 0) {
1588
+ const [inboundTbl, inboundKey] = relation.split("$");
1589
+ result.push({ type: "Inbound", table: inboundTbl, key: inboundKey });
1590
+ currentTbl = inboundTbl;
1591
+ } else {
1592
+ const srcTbl = tableNameCache[currentTbl];
1593
+ const fk = srcTbl.foreign_keys.find((fk) => fk.name === relation);
1594
+ if (fk) {
1595
+ const targetTbl = tableNameCache[fk.reftable_name];
1596
+ result.push({
1597
+ type: "Foreign",
1598
+ table: targetTbl.name,
1599
+ key: relation,
1600
+ });
1601
+ currentTbl = targetTbl.name;
1602
+ }
1603
+ }
1604
+ }
1605
+ return result;
1606
+ }
1607
+ };
1608
+
1609
+ export const buildLayers = (relations, tableName, tableNameCache) => {
1610
+ const result = { table: tableName, inboundKeys: [], fkeys: [] };
1611
+ for (const relation of relations) {
1612
+ const relType = relation.type;
1613
+ let currentLevel = result;
1614
+ if (relType === RelationType.INDEPENDENT) {
1615
+ currentLevel.fkeys.push({
1616
+ name: "none (no relation)",
1617
+ table: "",
1618
+ inboundKeys: [],
1619
+ fkeys: [],
1620
+ relPath: relation.relationString,
1621
+ });
1622
+ } else if (relType === RelationType.OWN) {
1623
+ currentLevel.fkeys.push({
1624
+ name: "same table",
1625
+ table: relation.targetTblName,
1626
+ inboundKeys: [],
1627
+ fkeys: [],
1628
+ relPath: relation.relationString,
1629
+ });
1630
+ } else {
1631
+ let currentTbl = relation.sourceTblName;
1632
+ for (const pathElement of relation.path) {
1633
+ if (pathElement.inboundKey) {
1634
+ currentTbl = pathElement.table;
1635
+ const existing = currentLevel.inboundKeys.find(
1636
+ (key) =>
1637
+ key.name === pathElement.inboundKey && key.table === currentTbl
1638
+ );
1639
+ if (existing) {
1640
+ currentLevel = existing;
1641
+ } else {
1642
+ const nextLevel = {
1643
+ name: pathElement.inboundKey,
1644
+ table: currentTbl,
1645
+ inboundKeys: [],
1646
+ fkeys: [],
1647
+ };
1648
+ currentLevel.inboundKeys.push(nextLevel);
1649
+ currentLevel = nextLevel;
1650
+ }
1651
+ } else if (pathElement.fkey) {
1652
+ const tblObj = tableNameCache[currentTbl];
1653
+ const fkey = tblObj.foreign_keys.find(
1654
+ (key) => key.name === pathElement.fkey
1655
+ );
1656
+ currentTbl = fkey.reftable_name;
1657
+ const existing = currentLevel.fkeys.find(
1658
+ (key) => key.name === pathElement.fkey
1659
+ );
1660
+ if (existing) {
1661
+ currentLevel = existing;
1662
+ } else {
1663
+ const nextLevel = {
1664
+ name: pathElement.fkey,
1665
+ table: currentTbl,
1666
+ inboundKeys: [],
1667
+ fkeys: [],
1668
+ };
1669
+ currentLevel.fkeys.push(nextLevel);
1670
+ currentLevel = nextLevel;
1671
+ }
1672
+ }
1673
+ }
1674
+ }
1675
+ currentLevel.relPath = relation.relationString;
1590
1676
  }
1677
+ return result;
1591
1678
  };
@@ -12,6 +12,8 @@ import { Empty } from "./elements/Empty";
12
12
  import { Columns, ntimes, sum } from "./elements/Columns";
13
13
  import { JoinField } from "./elements/JoinField";
14
14
  import { Tabs } from "./elements/Tabs";
15
+ import { ListColumns } from "./elements/ListColumns";
16
+ import { ListColumn } from "./elements/ListColumn";
15
17
  import { Table } from "./elements/Table";
16
18
  import { Aggregation } from "./elements/Aggregation";
17
19
  import { LineBreak } from "./elements/LineBreak";
@@ -76,6 +78,8 @@ const allElements = [
76
78
  DropMenu,
77
79
  Page,
78
80
  Table,
81
+ ListColumn,
82
+ ListColumns,
79
83
  ];
80
84
 
81
85
  export /**
@@ -88,7 +92,7 @@ export /**
88
92
  * @subcategory components
89
93
  * @namespace
90
94
  */
91
- const layoutToNodes = (layout, query, actions, parent = "ROOT") => {
95
+ const layoutToNodes = (layout, query, actions, parent = "ROOT", options) => {
92
96
  //console.log("layoutToNodes", JSON.stringify(layout));
93
97
  /**
94
98
  * @param {object} segment
@@ -129,7 +133,6 @@ const layoutToNodes = (layout, query, actions, parent = "ROOT") => {
129
133
  );
130
134
  else return <MatchElement key={ix} {...props} />;
131
135
  }
132
-
133
136
  if (segment.type === "blank") {
134
137
  return (
135
138
  <Text
@@ -177,6 +180,7 @@ const layoutToNodes = (layout, query, actions, parent = "ROOT") => {
177
180
  step_only_ifs={segment.step_only_ifs || ""}
178
181
  step_action_names={segment.step_action_names || ""}
179
182
  confirm={segment.confirm}
183
+ spinner={segment.spinner}
180
184
  configuration={segment.configuration || {}}
181
185
  block={segment.block || false}
182
186
  minRole={segment.minRole || 10}
@@ -200,6 +204,7 @@ const layoutToNodes = (layout, query, actions, parent = "ROOT") => {
200
204
  minHeight={segment.minHeight}
201
205
  height={segment.height}
202
206
  width={segment.width}
207
+ click_action={segment.click_action}
203
208
  url={segment.url}
204
209
  hoverColor={segment.hoverColor}
205
210
  minHeightUnit={segment.minHeightUnit || "px"}
@@ -258,6 +263,7 @@ const layoutToNodes = (layout, query, actions, parent = "ROOT") => {
258
263
  independent={segment.independent}
259
264
  startClosed={segment.startClosed}
260
265
  deeplink={segment.deeplink}
266
+ acc_init_opens={segment.acc_init_opens}
261
267
  disable_inactive={segment.disable_inactive}
262
268
  serverRendered={segment.serverRendered}
263
269
  tabId={segment.tabId}
@@ -283,6 +289,27 @@ const layoutToNodes = (layout, query, actions, parent = "ROOT") => {
283
289
  )}
284
290
  />
285
291
  );
292
+ } else if (segment.besides && segment.list_columns) {
293
+ const addFields = options.additionalColumnFields;
294
+
295
+ return segment.besides.map((col, jx) => {
296
+ const addProps = {};
297
+ (addFields || []).forEach((f) => {
298
+ addProps[f.name] = col[f.name];
299
+ });
300
+ return (
301
+ <ListColumn
302
+ key={jx}
303
+ alignment={col.alignment}
304
+ header_label={col.header_label}
305
+ col_width={col.col_width}
306
+ showif={col.showif}
307
+ col_width_units={col.col_width_units}
308
+ contents={toTag(col.contents)}
309
+ {...addProps}
310
+ ></ListColumn>
311
+ );
312
+ });
286
313
  } else if (segment.besides) {
287
314
  return (
288
315
  <Columns
@@ -317,7 +344,7 @@ const layoutToNodes = (layout, query, actions, parent = "ROOT") => {
317
344
  segment.above.forEach((child) => {
318
345
  if (child) go(child, parent);
319
346
  });
320
- } else if (segment.besides) {
347
+ } else if (segment.besides && !segment.list_columns) {
321
348
  const node = query
322
349
  .parseReactElement(
323
350
  <Columns
@@ -339,7 +366,13 @@ const layoutToNodes = (layout, query, actions, parent = "ROOT") => {
339
366
  actions.addNodeTree(node, parent);
340
367
  } else {
341
368
  const tag = toTag(segment);
342
- if (tag) {
369
+ if (Array.isArray(tag)) {
370
+ tag.forEach((t) => {
371
+ const node = query.parseReactElement(t).toNodeTree();
372
+ //console.log("other", node);
373
+ actions.addNodeTree(node, parent);
374
+ });
375
+ } else if (tag) {
343
376
  const node = query.parseReactElement(tag).toNodeTree();
344
377
  //console.log("other", node);
345
378
  actions.addNodeTree(node, parent);
@@ -363,7 +396,7 @@ export /**
363
396
  * @subcategory components
364
397
  * @namespace
365
398
  */
366
- const craftToSaltcorn = (nodes, startFrom = "ROOT") => {
399
+ const craftToSaltcorn = (nodes, startFrom = "ROOT", options) => {
367
400
  //console.log(JSON.stringify(nodes, null, 2));
368
401
  var columns = [];
369
402
  /**
@@ -415,10 +448,33 @@ const craftToSaltcorn = (nodes, startFrom = "ROOT") => {
415
448
  related.fields.forEach((f) => {
416
449
  c[f.column_name || f.name || f] = node.props[f.name || f];
417
450
  });
451
+ if (s.isFormula) c.isFormula = s.isFormula;
418
452
  columns.push(c);
419
453
  }
420
454
  return s;
421
455
  }
456
+ if (node.displayName === ListColumns.craft.displayName) {
457
+ return {
458
+ besides: node.nodes.map((nm) => go(nodes[nm])),
459
+ list_columns: true,
460
+ };
461
+ }
462
+ if (node.displayName === ListColumn.craft.displayName) {
463
+ const contents = go(nodes[node.linkedNodes.listcol]);
464
+ const addFields = options.additionalColumnFields;
465
+ const lc = {
466
+ contents,
467
+ col_width: node.props.col_width,
468
+ col_width_units: node.props.col_width_units,
469
+ alignment: node.props.alignment,
470
+ header_label: node.props.header_label,
471
+ showif: node.props.showif,
472
+ };
473
+ (addFields || []).forEach((f) => {
474
+ lc[f.name] = node.props[f.name];
475
+ });
476
+ return lc;
477
+ }
422
478
  if (node.isCanvas) {
423
479
  if (node.displayName === Container.craft.displayName)
424
480
  return {
@@ -458,6 +514,7 @@ const craftToSaltcorn = (nodes, startFrom = "ROOT") => {
458
514
  gradStartColor: node.props.gradStartColor,
459
515
  gradEndColor: node.props.gradEndColor,
460
516
  gradDirection: node.props.gradDirection,
517
+ click_action: node.props.click_action,
461
518
  rotate: node.props.rotate,
462
519
  style: node.props.style,
463
520
  };
@@ -535,6 +592,7 @@ const craftToSaltcorn = (nodes, startFrom = "ROOT") => {
535
592
  field: node.props.field,
536
593
  independent: node.props.independent,
537
594
  startClosed: node.props.startClosed,
595
+ acc_init_opens: node.props.acc_init_opens,
538
596
  deeplink: node.props.deeplink,
539
597
  disable_inactive: node.props.disable_inactive,
540
598
  serverRendered: node.props.serverRendered,
@@ -575,6 +633,7 @@ const craftToSaltcorn = (nodes, startFrom = "ROOT") => {
575
633
  action_textcol: node.props.action_textcol,
576
634
  minRole: node.props.minRole,
577
635
  confirm: node.props.confirm,
636
+ spinner: node.props.spinner,
578
637
  nsteps: node.props.nsteps,
579
638
  step_only_ifs: node.props.step_only_ifs,
580
639
  step_action_names: node.props.step_action_names,
@@ -587,6 +646,7 @@ const craftToSaltcorn = (nodes, startFrom = "ROOT") => {
587
646
  block: node.props.block,
588
647
  configuration: node.props.configuration,
589
648
  confirm: node.props.confirm,
649
+ spinner: node.props.spinner,
590
650
  action_name: node.props.name,
591
651
  ...(node.props.name !== "Clear" && node.props.action_row_variable
592
652
  ? {