@saltcorn/builder 0.9.4-beta.2 → 0.9.4-beta.21

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) => {
@@ -1245,19 +1286,27 @@ const ButtonOrLinkSettingsRows = ({
1245
1286
  <option value={addBtnClass("btn-secondary")}>Secondary button</option>
1246
1287
  <option value={addBtnClass("btn-success")}>Success button</option>
1247
1288
  <option value={addBtnClass("btn-danger")}>Danger button</option>
1289
+ <option value={addBtnClass("btn-warning")}>Warning button</option>
1290
+ <option value={addBtnClass("btn-info")}>Info button</option>
1248
1291
  <option value={addBtnClass("btn-outline-primary")}>
1249
1292
  Primary outline button
1250
1293
  </option>
1251
1294
  <option value={addBtnClass("btn-outline-secondary")}>
1252
1295
  Secondary outline button
1253
1296
  </option>
1297
+ <option value={addBtnClass("btn-outline-warning")}>
1298
+ Warning outline button
1299
+ </option>{" "}
1300
+ <option value={addBtnClass("btn-outline-info")}>
1301
+ Info outline button
1302
+ </option>
1254
1303
  <option value={addBtnClass("btn-custom-color")}>
1255
1304
  Button custom color
1256
1305
  </option>
1257
1306
  {!linkFirst ? (
1258
1307
  <option value={addBtnClass("btn-link")}>Link</option>
1259
1308
  ) : null}
1260
- {!linkFirst ? (
1309
+ {!linkFirst && allowRunOnLoad ? (
1261
1310
  <option value="on_page_load">Run on Page Load</option>
1262
1311
  ) : null}
1263
1312
  </select>
@@ -1277,6 +1326,7 @@ const ButtonOrLinkSettingsRows = ({
1277
1326
  <option value="">Standard</option>
1278
1327
  <option value="btn-lg">Large</option>
1279
1328
  <option value="btn-sm">Small</option>
1329
+ <option value="btn-sm btn-xs">Extra Small</option>
1280
1330
  <option value="btn-block">Block</option>
1281
1331
  <option value="btn-block btn-lg">Large block</option>
1282
1332
  </select>
@@ -1439,22 +1489,6 @@ const Tooltip = ({ children }) => {
1439
1489
  );
1440
1490
  };
1441
1491
 
1442
- export const buildTableCaches = (allTables) => {
1443
- const tableIdCache = {};
1444
- const tableNameCache = {};
1445
- const fieldCache = {};
1446
- for (const table of allTables) {
1447
- tableIdCache[table.id] = table;
1448
- tableNameCache[table.name] = table;
1449
- for (const field of table.foreign_keys) {
1450
- if (!fieldCache[field.reftable_name])
1451
- fieldCache[field.reftable_name] = [];
1452
- fieldCache[field.reftable_name].push(field);
1453
- }
1454
- }
1455
- return { tableIdCache, tableNameCache, fieldCache };
1456
- };
1457
-
1458
1492
  export const removeWhitespaces = (str) => {
1459
1493
  return str.replace(/\s/g, "X");
1460
1494
  };
@@ -1497,79 +1531,145 @@ export const buildBootstrapOptions = (values) => {
1497
1531
  ));
1498
1532
  };
1499
1533
 
1500
- export const prepCacheAndFinder = ({
1501
- tables,
1502
- views,
1503
- max_relations_layer_depth,
1504
- }) => {
1505
- if (tables && views) {
1506
- const caches = buildTableCaches(tables);
1507
- const finder = new relationHelpers.RelationsFinder(
1508
- caches,
1509
- views,
1510
- max_relations_layer_depth || 6
1511
- );
1512
- return { caches, finder };
1513
- } else return { caches: null, finder: null };
1534
+ export const arrayChunks = (xs, n) => {
1535
+ const arrayOfArrays = [];
1536
+ for (var i = 0; i < bigarray.length; i += n) {
1537
+ arrayOfArrays.push(bigarray.slice(i, i + n));
1538
+ }
1539
+ return arrayOfArrays;
1514
1540
  };
1515
1541
 
1516
1542
  /**
1517
- * @param {string[]} paths
1543
+ * @param {string[]} relations
1518
1544
  * @param {string} sourceTbl name of the topview table
1519
1545
  * @returns either a same table relation, a parent relation, a child relation, or the first relation
1520
1546
  */
1521
- export const initialRelation = (paths, sourceTbl) => {
1547
+ export const initialRelation = (relations) => {
1522
1548
  let sameTblRel = null;
1523
1549
  let parentRel = null;
1524
1550
  let childRel = null;
1525
- for (const path of paths) {
1526
- if (!sameTblRel && path === `.${sourceTbl}`) sameTblRel = path;
1527
- else {
1528
- const tokens = path.split(".");
1529
- if (
1530
- !parentRel &&
1531
- tokens.length === 3 &&
1532
- tokens[1] === sourceTbl &&
1533
- tokens[2].indexOf("$") === -1
1534
- )
1535
- parentRel = path;
1536
- else {
1537
- const lastToken = tokens[tokens.length - 1];
1538
- if (
1539
- lastToken.indexOf("$") > 0 &&
1540
- (!childRel || childRel.split(".").length > tokens.length)
1541
- )
1542
- childRel = path;
1543
- }
1551
+ for (const relation of relations) {
1552
+ switch (relation.type) {
1553
+ case RelationType.OWN:
1554
+ sameTblRel = relation;
1555
+ break;
1556
+ case RelationType.PARENT_SHOW:
1557
+ parentRel = relation;
1558
+ break;
1559
+ case RelationType.CHILD_LIST:
1560
+ case RelationType.ONE_TO_ONE_SHOW:
1561
+ childRel = relation;
1562
+ break;
1544
1563
  }
1545
1564
  }
1546
- return sameTblRel || parentRel || childRel || paths[0];
1565
+ return sameTblRel || parentRel || childRel || relations[0];
1547
1566
  };
1548
1567
 
1549
1568
  /**
1550
- * update the builder wide relations cache relations cache
1551
- * if there is no entry for the given tableName and viewname
1552
- * @param {any} relationsCache cache from the context
1553
- * @param {Function} setRelationsCache set cache in context
1554
- * @param {any} options builder options
1555
- * @param {RelationsFinder} finder
1556
- * @param {string} viewname subview name
1569
+ * builder intern path method
1570
+ * @param path
1571
+ * @param tableNameCache
1572
+ * @returns
1557
1573
  */
1558
- export const updateRelationsCache = (
1559
- relationsCache,
1560
- setRelationsCache,
1561
- options,
1562
- finder,
1563
- viewname
1564
- ) => {
1565
- if (!relationsCache[options.tableName])
1566
- relationsCache[options.tableName] = {};
1567
- if (!relationsCache[options.tableName][viewname]) {
1568
- relationsCache[options.tableName][viewname] = finder.findRelations(
1569
- options.tableName,
1570
- viewname,
1571
- options.excluded_subview_templates
1572
- );
1573
- setRelationsCache({ ...relationsCache });
1574
+ export const buildRelationArray = (path, tableNameCache) => {
1575
+ if (path === ".")
1576
+ return [{ type: "Independent", table: "None (no relation)" }];
1577
+ const tokens = path.split(".");
1578
+ if (tokens.length === 2)
1579
+ return [{ type: "Own", table: `${tokens[1]} (same table)` }];
1580
+ else if (tokens.length >= 3) {
1581
+ const result = [];
1582
+ let currentTbl = tokens[1];
1583
+ for (const relation of tokens.slice(2)) {
1584
+ if (relation.indexOf("$") > 0) {
1585
+ const [inboundTbl, inboundKey] = relation.split("$");
1586
+ result.push({ type: "Inbound", table: inboundTbl, key: inboundKey });
1587
+ currentTbl = inboundTbl;
1588
+ } else {
1589
+ const srcTbl = tableNameCache[currentTbl];
1590
+ const fk = srcTbl.foreign_keys.find((fk) => fk.name === relation);
1591
+ if (fk) {
1592
+ const targetTbl = tableNameCache[fk.reftable_name];
1593
+ result.push({
1594
+ type: "Foreign",
1595
+ table: targetTbl.name,
1596
+ key: relation,
1597
+ });
1598
+ currentTbl = targetTbl.name;
1599
+ }
1600
+ }
1601
+ }
1602
+ return result;
1603
+ }
1604
+ };
1605
+
1606
+ export const buildLayers = (relations, tableName, tableNameCache) => {
1607
+ const result = { table: tableName, inboundKeys: [], fkeys: [] };
1608
+ for (const relation of relations) {
1609
+ const relType = relation.type;
1610
+ let currentLevel = result;
1611
+ if (relType === RelationType.INDEPENDENT) {
1612
+ currentLevel.fkeys.push({
1613
+ name: "none (no relation)",
1614
+ table: "",
1615
+ inboundKeys: [],
1616
+ fkeys: [],
1617
+ relPath: relation.relationString,
1618
+ });
1619
+ } else if (relType === RelationType.OWN) {
1620
+ currentLevel.fkeys.push({
1621
+ name: "same table",
1622
+ table: relation.targetTblName,
1623
+ inboundKeys: [],
1624
+ fkeys: [],
1625
+ relPath: relation.relationString,
1626
+ });
1627
+ } else {
1628
+ let currentTbl = relation.sourceTblName;
1629
+ for (const pathElement of relation.path) {
1630
+ if (pathElement.inboundKey) {
1631
+ currentTbl = pathElement.table;
1632
+ const existing = currentLevel.inboundKeys.find(
1633
+ (key) =>
1634
+ key.name === pathElement.inboundKey && key.table === currentTbl
1635
+ );
1636
+ if (existing) {
1637
+ currentLevel = existing;
1638
+ } else {
1639
+ const nextLevel = {
1640
+ name: pathElement.inboundKey,
1641
+ table: currentTbl,
1642
+ inboundKeys: [],
1643
+ fkeys: [],
1644
+ };
1645
+ currentLevel.inboundKeys.push(nextLevel);
1646
+ currentLevel = nextLevel;
1647
+ }
1648
+ } else if (pathElement.fkey) {
1649
+ const tblObj = tableNameCache[currentTbl];
1650
+ const fkey = tblObj.foreign_keys.find(
1651
+ (key) => key.name === pathElement.fkey
1652
+ );
1653
+ currentTbl = fkey.reftable_name;
1654
+ const existing = currentLevel.fkeys.find(
1655
+ (key) => key.name === pathElement.fkey
1656
+ );
1657
+ if (existing) {
1658
+ currentLevel = existing;
1659
+ } else {
1660
+ const nextLevel = {
1661
+ name: pathElement.fkey,
1662
+ table: currentTbl,
1663
+ inboundKeys: [],
1664
+ fkeys: [],
1665
+ };
1666
+ currentLevel.fkeys.push(nextLevel);
1667
+ currentLevel = nextLevel;
1668
+ }
1669
+ }
1670
+ }
1671
+ }
1672
+ currentLevel.relPath = relation.relationString;
1574
1673
  }
1674
+ return result;
1575
1675
  };
@@ -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"}
@@ -253,10 +258,12 @@ const layoutToNodes = (layout, query, actions, parent = "ROOT") => {
253
258
  <Tabs
254
259
  key={ix}
255
260
  titles={segment.titles}
261
+ showif={segment.showif}
256
262
  ntabs={segment.ntabs}
257
263
  independent={segment.independent}
258
264
  startClosed={segment.startClosed}
259
265
  deeplink={segment.deeplink}
266
+ acc_init_opens={segment.acc_init_opens}
260
267
  disable_inactive={segment.disable_inactive}
261
268
  serverRendered={segment.serverRendered}
262
269
  tabId={segment.tabId}
@@ -282,6 +289,27 @@ const layoutToNodes = (layout, query, actions, parent = "ROOT") => {
282
289
  )}
283
290
  />
284
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
+ });
285
313
  } else if (segment.besides) {
286
314
  return (
287
315
  <Columns
@@ -316,7 +344,7 @@ const layoutToNodes = (layout, query, actions, parent = "ROOT") => {
316
344
  segment.above.forEach((child) => {
317
345
  if (child) go(child, parent);
318
346
  });
319
- } else if (segment.besides) {
347
+ } else if (segment.besides && !segment.list_columns) {
320
348
  const node = query
321
349
  .parseReactElement(
322
350
  <Columns
@@ -338,7 +366,13 @@ const layoutToNodes = (layout, query, actions, parent = "ROOT") => {
338
366
  actions.addNodeTree(node, parent);
339
367
  } else {
340
368
  const tag = toTag(segment);
341
- 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) {
342
376
  const node = query.parseReactElement(tag).toNodeTree();
343
377
  //console.log("other", node);
344
378
  actions.addNodeTree(node, parent);
@@ -362,7 +396,7 @@ export /**
362
396
  * @subcategory components
363
397
  * @namespace
364
398
  */
365
- const craftToSaltcorn = (nodes, startFrom = "ROOT") => {
399
+ const craftToSaltcorn = (nodes, startFrom = "ROOT", options) => {
366
400
  //console.log(JSON.stringify(nodes, null, 2));
367
401
  var columns = [];
368
402
  /**
@@ -414,10 +448,33 @@ const craftToSaltcorn = (nodes, startFrom = "ROOT") => {
414
448
  related.fields.forEach((f) => {
415
449
  c[f.column_name || f.name || f] = node.props[f.name || f];
416
450
  });
451
+ if (s.isFormula) c.isFormula = s.isFormula;
417
452
  columns.push(c);
418
453
  }
419
454
  return s;
420
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
+ }
421
478
  if (node.isCanvas) {
422
479
  if (node.displayName === Container.craft.displayName)
423
480
  return {
@@ -457,6 +514,7 @@ const craftToSaltcorn = (nodes, startFrom = "ROOT") => {
457
514
  gradStartColor: node.props.gradStartColor,
458
515
  gradEndColor: node.props.gradEndColor,
459
516
  gradDirection: node.props.gradDirection,
517
+ click_action: node.props.click_action,
460
518
  rotate: node.props.rotate,
461
519
  style: node.props.style,
462
520
  };
@@ -529,10 +587,12 @@ const craftToSaltcorn = (nodes, startFrom = "ROOT") => {
529
587
  type: "tabs",
530
588
  contents,
531
589
  titles: node.props.titles,
590
+ showif: node.props.showif,
532
591
  tabsStyle: node.props.tabsStyle,
533
592
  field: node.props.field,
534
593
  independent: node.props.independent,
535
594
  startClosed: node.props.startClosed,
595
+ acc_init_opens: node.props.acc_init_opens,
536
596
  deeplink: node.props.deeplink,
537
597
  disable_inactive: node.props.disable_inactive,
538
598
  serverRendered: node.props.serverRendered,
@@ -573,6 +633,7 @@ const craftToSaltcorn = (nodes, startFrom = "ROOT") => {
573
633
  action_textcol: node.props.action_textcol,
574
634
  minRole: node.props.minRole,
575
635
  confirm: node.props.confirm,
636
+ spinner: node.props.spinner,
576
637
  nsteps: node.props.nsteps,
577
638
  step_only_ifs: node.props.step_only_ifs,
578
639
  step_action_names: node.props.step_action_names,
@@ -585,6 +646,7 @@ const craftToSaltcorn = (nodes, startFrom = "ROOT") => {
585
646
  block: node.props.block,
586
647
  configuration: node.props.configuration,
587
648
  confirm: node.props.confirm,
649
+ spinner: node.props.spinner,
588
650
  action_name: node.props.name,
589
651
  ...(node.props.name !== "Clear" && node.props.action_row_variable
590
652
  ? {