@saltcorn/builder 0.7.3-beta.7 → 0.7.4-beta.1

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.
@@ -10,11 +10,13 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
10
10
  import {
11
11
  faChevronDown,
12
12
  faChevronRight,
13
+ faInfoCircle
13
14
  } from "@fortawesome/free-solid-svg-icons";
14
15
  import { useNode, Element } from "@craftjs/core";
15
16
  import FontIconPicker from "@fonticonpicker/react-fonticonpicker";
16
17
  import faIcons from "./faicons";
17
18
  import { Columns, ntimes } from "./Columns";
19
+ import Tippy from '@tippyjs/react';
18
20
 
19
21
  export const DynamicFontAwesomeIcon = ({ icon, className }) => {
20
22
  if (!icon) return null;
@@ -25,8 +27,8 @@ export /**
25
27
  * @param {boolean} is_block
26
28
  * @returns {object}
27
29
  */
28
- const blockProps = (is_block) =>
29
- is_block ? { style: { display: "block" } } : {};
30
+ const blockProps = (is_block) =>
31
+ is_block ? { style: { display: "block" } } : {};
30
32
 
31
33
  export /**
32
34
  * @param {object} props
@@ -37,23 +39,23 @@ export /**
37
39
  * @subcategory components / elements / utils
38
40
  * @namespace
39
41
  */
40
- const BlockSetting = ({ block, setProp }) => (
41
- <div className="form-check">
42
- <input
43
- className="form-check-input"
44
- name="block"
45
- type="checkbox"
46
- checked={block}
47
- onChange={(e) => {
48
- if (e.target) {
49
- const target_value = e.target.checked;
50
- setProp((prop) => (prop.block = target_value));
51
- }
52
- }}
53
- />
54
- <label className="form-check-label">Block display</label>
55
- </div>
56
- );
42
+ const BlockSetting = ({ block, setProp }) => (
43
+ <div className="form-check">
44
+ <input
45
+ className="form-check-input"
46
+ name="block"
47
+ type="checkbox"
48
+ checked={block}
49
+ onChange={(e) => {
50
+ if (e.target) {
51
+ const target_value = e.target.checked;
52
+ setProp((prop) => (prop.block = target_value));
53
+ }
54
+ }}
55
+ />
56
+ <label className="form-check-label">Block display</label>
57
+ </div>
58
+ );
57
59
 
58
60
  export const BlockOrInlineSetting = ({ block, inline, textStyle, setProp }) =>
59
61
  !textStyle || !textStyle.startsWith("h") ? (
@@ -76,6 +78,18 @@ export const BlockOrInlineSetting = ({ block, inline, textStyle, setProp }) =>
76
78
  </div>
77
79
  );
78
80
 
81
+ export const FormulaTooltip = () => {
82
+ const { fields } = useContext(optionsCtx);
83
+ console.log(fields);
84
+ return <Tooltip>
85
+ <div>Formulae in Saltcorn are JavaScript expressions based on the current database row.</div>
86
+ {fields ? <Fragment> Variables in scope: &nbsp;
87
+ {fields.map((f, ix) => <Fragment><code key={ix}>{f.name}</code>{" "}</Fragment>)}</Fragment> : null}
88
+
89
+ <a className="d-block" href="https://wiki.saltcorn.com/view/ShowPage/formulas">Wiki page on formulas</a>
90
+ </Tooltip>
91
+ }
92
+
79
93
  export /**
80
94
  * @param {object} props
81
95
  * @param {function} props.setProp
@@ -88,81 +102,83 @@ export /**
88
102
  * @category saltcorn-builder
89
103
  * @subcategory components / elements / utils
90
104
  */
91
- const OrFormula = ({ setProp, isFormula, node, nodekey, children }) => {
92
- const { mode } = useContext(optionsCtx);
105
+ const OrFormula = ({ setProp, isFormula, node, nodekey, children }) => {
106
+ const { mode } = useContext(optionsCtx);
93
107
 
94
- /**
95
- * @returns {void}
96
- */
97
- const switchIsFml = () => {
98
- const isFmlAfter = !isFormula[nodekey];
99
- setProp((prop) => {
100
- prop.isFormula[nodekey] = isFmlAfter;
101
- if (isFmlAfter && prop[nodekey] && prop[nodekey][0] !== '"') {
102
- prop[nodekey] = `"${prop[nodekey]}"`;
103
- } else if (
104
- !isFmlAfter &&
105
- typeof prop[nodekey] === "string" &&
106
- prop[nodekey][0] === '"' &&
107
- prop[nodekey][prop[nodekey].length - 1] === '"'
108
- ) {
109
- prop[nodekey] = prop[nodekey].substring(1, prop[nodekey].length - 1);
108
+ /**
109
+ * @returns {void}
110
+ */
111
+ const switchIsFml = () => {
112
+ const isFmlAfter = !isFormula[nodekey];
113
+ setProp((prop) => {
114
+ prop.isFormula[nodekey] = isFmlAfter;
115
+ if (isFmlAfter && prop[nodekey] && prop[nodekey][0] !== '"') {
116
+ prop[nodekey] = `"${prop[nodekey]}"`;
117
+ } else if (
118
+ !isFmlAfter &&
119
+ typeof prop[nodekey] === "string" &&
120
+ prop[nodekey][0] === '"' &&
121
+ prop[nodekey][prop[nodekey].length - 1] === '"'
122
+ ) {
123
+ prop[nodekey] = prop[nodekey].substring(1, prop[nodekey].length - 1);
124
+ }
125
+ });
126
+ };
127
+ let errorString = false;
128
+ if (mode === "show" && isFormula[nodekey]) {
129
+ try {
130
+ Function("return " + node[nodekey]);
131
+ } catch (error) {
132
+ errorString = error.message;
110
133
  }
111
- });
112
- };
113
- let errorString = false;
114
- if (isFormula[nodekey]) {
115
- try {
116
- Function("return " + node[nodekey]);
117
- } catch (error) {
118
- errorString = error.message;
119
134
  }
120
- }
121
- return mode !== "show" ? (
122
- children
123
- ) : (
124
- <Fragment>
125
- <div className="input-group input-group-sm w-100">
126
- {isFormula[nodekey] ? (
127
- <input
128
- type="text"
129
- className="form-control text-to-display"
130
- value={node[nodekey] || ""}
131
- onChange={(e) => {
132
- if (e.target) {
133
- const target_value = e.target.value;
134
- setProp((prop) => (prop[nodekey] = target_value));
135
- }
136
- }}
137
- />
138
- ) : (
139
- children
140
- )}
135
+ return mode !== "show" ? (
136
+ children
137
+ ) : (
138
+ <Fragment>
139
+ <div className="input-group input-group-sm w-100">
140
+ {isFormula[nodekey] ? (
141
+ <input
142
+ type="text"
143
+ className="form-control text-to-display"
144
+ value={node[nodekey] || ""}
145
+ onChange={(e) => {
146
+ if (e.target) {
147
+ const target_value = e.target.value;
148
+ setProp((prop) => (prop[nodekey] = target_value));
149
+ }
150
+ }}
151
+ />
152
+ ) : (
153
+ children
154
+ )}
141
155
 
142
- <button
143
- className={`btn activate-formula ${
144
- isFormula[nodekey] ? "btn-secondary" : "btn-outline-secondary"
145
- }`}
146
- title="Calculated formula"
147
- type="button"
148
- onClick={switchIsFml}
149
- >
150
- <i className="fas fa-calculator"></i>
151
- </button>
152
- </div>
153
- {isFormula[nodekey] && (
154
- <div style={{ marginTop: "-5px" }}>
155
- <small className="text-muted font-monospace">FORMULA</small>
156
- {errorString ? (
157
- <small className="text-danger font-monospace d-block">
158
- {errorString}
159
- </small>
160
- ) : null}
156
+ <button
157
+ className={`btn activate-formula ${isFormula[nodekey] ? "btn-secondary" : "btn-outline-secondary"
158
+ }`}
159
+ title="Calculated formula"
160
+ type="button"
161
+ onClick={switchIsFml}
162
+ >
163
+ <i className="fas fa-calculator"></i>
164
+ </button>
161
165
  </div>
162
- )}
163
- </Fragment>
164
- );
165
- };
166
+ {isFormula[nodekey] && (
167
+ <div style={{ marginTop: "-5px" }}>
168
+ <small className="text-muted font-monospace">
169
+ FORMULA
170
+ <FormulaTooltip />
171
+ </small>
172
+ {errorString ? (
173
+ <small className="text-danger font-monospace d-block">
174
+ {errorString}
175
+ </small>
176
+ ) : null}
177
+ </div>
178
+ )}
179
+ </Fragment>
180
+ );
181
+ };
166
182
 
167
183
  export /**
168
184
  * @param {object} props
@@ -173,52 +189,15 @@ export /**
173
189
  * @category saltcorn-builder
174
190
  * @subcategory components / elements / utils
175
191
  */
176
- const MinRoleSetting = ({ minRole, setProp }) => {
177
- const options = useContext(optionsCtx);
178
- return (
179
- <div>
180
- <label>Minimum Role</label>
181
- <select
182
- className="form-control form-select"
183
- value={minRole}
184
- onChange={(e) => (e) => {
185
- if (e.target) {
186
- const target_value = e.target.value;
187
- setProp((prop) => (prop.minRole = target_value));
188
- }
189
- }}
190
- >
191
- {options.roles.map((r) => (
192
- <option key={r.id} value={r.id}>
193
- {r.role}
194
- </option>
195
- ))}
196
- </select>
197
- </div>
198
- );
199
- };
200
-
201
- export /**
202
- * @param {object} props
203
- * @param {string} props.minRole
204
- * @param {function} props.setProp
205
- * @returns {tr}
206
- * @namespace
207
- * @category saltcorn-builder
208
- * @subcategory components / elements / utils
209
- */
210
- const MinRoleSettingRow = ({ minRole, setProp }) => {
211
- const options = useContext(optionsCtx);
212
- return (
213
- <tr>
214
- <td>
192
+ const MinRoleSetting = ({ minRole, setProp }) => {
193
+ const options = useContext(optionsCtx);
194
+ return (
195
+ <div>
215
196
  <label>Minimum Role</label>
216
- </td>
217
- <td>
218
197
  <select
219
- value={minRole}
220
198
  className="form-control form-select"
221
- onChange={(e) => {
199
+ value={minRole}
200
+ onChange={(e) => (e) => {
222
201
  if (e.target) {
223
202
  const target_value = e.target.value;
224
203
  setProp((prop) => (prop.minRole = target_value));
@@ -231,10 +210,47 @@ const MinRoleSettingRow = ({ minRole, setProp }) => {
231
210
  </option>
232
211
  ))}
233
212
  </select>
234
- </td>
235
- </tr>
236
- );
237
- };
213
+ </div>
214
+ );
215
+ };
216
+
217
+ export /**
218
+ * @param {object} props
219
+ * @param {string} props.minRole
220
+ * @param {function} props.setProp
221
+ * @returns {tr}
222
+ * @namespace
223
+ * @category saltcorn-builder
224
+ * @subcategory components / elements / utils
225
+ */
226
+ const MinRoleSettingRow = ({ minRole, setProp }) => {
227
+ const options = useContext(optionsCtx);
228
+ return (
229
+ <tr>
230
+ <td>
231
+ <label>Minimum Role</label>
232
+ </td>
233
+ <td>
234
+ <select
235
+ value={minRole}
236
+ className="form-control form-select"
237
+ onChange={(e) => {
238
+ if (e.target) {
239
+ const target_value = e.target.value;
240
+ setProp((prop) => (prop.minRole = target_value));
241
+ }
242
+ }}
243
+ >
244
+ {options.roles.map((r) => (
245
+ <option key={r.id} value={r.id}>
246
+ {r.role}
247
+ </option>
248
+ ))}
249
+ </select>
250
+ </td>
251
+ </tr>
252
+ );
253
+ };
238
254
 
239
255
  /**
240
256
  * @param {object} props
@@ -283,14 +299,14 @@ export /**
283
299
  * @category saltcorn-builder
284
300
  * @subcategory components / elements / utils
285
301
  */
286
- const TextStyleSetting = ({ textStyle, setProp }) => {
287
- return (
288
- <div>
289
- <label>Text Style</label>
290
- <TextStyleSelect textStyle={textStyle} setProp={setProp} />
291
- </div>
292
- );
293
- };
302
+ const TextStyleSetting = ({ textStyle, setProp }) => {
303
+ return (
304
+ <div>
305
+ <label>Text Style</label>
306
+ <TextStyleSelect textStyle={textStyle} setProp={setProp} />
307
+ </div>
308
+ );
309
+ };
294
310
 
295
311
  export /**
296
312
  * @param {object} props
@@ -301,18 +317,18 @@ export /**
301
317
  * @category saltcorn-builder
302
318
  * @subcategory components / elements / utils
303
319
  */
304
- const TextStyleRow = ({ textStyle, setProp }) => {
305
- return (
306
- <tr>
307
- <td>
308
- <label>Text Style</label>
309
- </td>
310
- <td>
311
- <TextStyleSelect textStyle={textStyle} setProp={setProp} />
312
- </td>
313
- </tr>
314
- );
315
- };
320
+ const TextStyleRow = ({ textStyle, setProp }) => {
321
+ return (
322
+ <tr>
323
+ <td>
324
+ <label>Text Style</label>
325
+ </td>
326
+ <td>
327
+ <TextStyleSelect textStyle={textStyle} setProp={setProp} />
328
+ </td>
329
+ </tr>
330
+ );
331
+ };
316
332
 
317
333
  export /**
318
334
  * @param {object} props
@@ -323,36 +339,35 @@ export /**
323
339
  * @subcategory components / elements / utils
324
340
  * @namespace
325
341
  */
326
- const Accordion = ({ titles, children }) => {
327
- const [currentTab, setCurrentTab] = useState(0);
328
- return (
329
- <Fragment>
330
- {children.map((child, ix) => {
331
- const isCurrent = ix === currentTab;
332
- return (
333
- <Fragment key={ix}>
334
- <div
335
- className={`bg-${
336
- isCurrent ? "primary" : "secondary"
337
- } ps-1 text-white w-100 mt-1`}
338
- onClick={() => setCurrentTab(ix)}
339
- >
340
- <span className="w-1em">
341
- {isCurrent ? (
342
- <FontAwesomeIcon icon={faChevronDown} />
343
- ) : (
344
- <FontAwesomeIcon icon={faChevronRight} />
345
- )}
346
- </span>
347
- {child.props.accordiontitle || titles[ix]}
348
- </div>
349
- {isCurrent ? child : null}
350
- </Fragment>
351
- );
352
- })}
353
- </Fragment>
354
- );
355
- };
342
+ const Accordion = ({ titles, children }) => {
343
+ const [currentTab, setCurrentTab] = useState(0);
344
+ return (
345
+ <Fragment>
346
+ {children.map((child, ix) => {
347
+ const isCurrent = ix === currentTab;
348
+ return (
349
+ <Fragment key={ix}>
350
+ <div
351
+ className={`bg-${isCurrent ? "primary" : "secondary"
352
+ } ps-1 text-white w-100 mt-1`}
353
+ onClick={() => setCurrentTab(ix)}
354
+ >
355
+ <span className="w-1em">
356
+ {isCurrent ? (
357
+ <FontAwesomeIcon icon={faChevronDown} />
358
+ ) : (
359
+ <FontAwesomeIcon icon={faChevronRight} />
360
+ )}
361
+ </span>
362
+ {child.props.accordiontitle || titles[ix]}
363
+ </div>
364
+ {isCurrent ? child : null}
365
+ </Fragment>
366
+ );
367
+ })}
368
+ </Fragment>
369
+ );
370
+ };
356
371
 
357
372
  /**
358
373
  * @param {object} opts
@@ -402,23 +417,23 @@ const fetchPreview = ({ url, body, options, setPreviews, node_id, isView }) => {
402
417
  */
403
418
  export const fetchFieldPreview =
404
419
  (args = {}) =>
405
- (changes = {}) => {
406
- const { node_id, options, name, fieldview, setPreviews } = {
407
- ...args,
408
- ...changes,
409
- };
410
- const configuration = {
411
- ...(args.configuration || {}),
412
- ...(changes.configuration || {}),
420
+ (changes = {}) => {
421
+ const { node_id, options, name, fieldview, setPreviews } = {
422
+ ...args,
423
+ ...changes,
424
+ };
425
+ const configuration = {
426
+ ...(args.configuration || {}),
427
+ ...(changes.configuration || {}),
428
+ };
429
+ fetchPreview({
430
+ options,
431
+ node_id,
432
+ setPreviews,
433
+ url: `/field/preview/${options.tableName}/${name}/${fieldview}`,
434
+ body: { configuration },
435
+ });
413
436
  };
414
- fetchPreview({
415
- options,
416
- node_id,
417
- setPreviews,
418
- url: `/field/preview/${options.tableName}/${name}/${fieldview}`,
419
- body: { configuration },
420
- });
421
- };
422
437
 
423
438
  /**
424
439
  * @function
@@ -427,30 +442,30 @@ export const fetchFieldPreview =
427
442
  */
428
443
  export const fetchViewPreview =
429
444
  (args = {}) =>
430
- (changes = {}) => {
431
- const { node_id, options, view, setPreviews, configuration } = {
432
- ...args,
433
- ...changes,
434
- };
435
- let viewname,
436
- body = configuration ? { ...configuration } : {};
437
- if (view.includes(":")) {
438
- const [reltype, rest] = view.split(":");
439
- const [vnm] = rest.split(".");
440
- viewname = vnm;
441
- body.reltype = reltype;
442
- body.path = rest;
443
- } else viewname = view;
445
+ (changes = {}) => {
446
+ const { node_id, options, view, setPreviews, configuration } = {
447
+ ...args,
448
+ ...changes,
449
+ };
450
+ let viewname,
451
+ body = configuration ? { ...configuration } : {};
452
+ if (view.includes(":")) {
453
+ const [reltype, rest] = view.split(":");
454
+ const [vnm] = rest.split(".");
455
+ viewname = vnm;
456
+ body.reltype = reltype;
457
+ body.path = rest;
458
+ } else viewname = view;
444
459
 
445
- fetchPreview({
446
- options,
447
- node_id,
448
- setPreviews,
449
- url: `/view/${viewname}/preview`,
450
- body,
451
- isView: true,
452
- });
453
- };
460
+ fetchPreview({
461
+ options,
462
+ node_id,
463
+ setPreviews,
464
+ url: `/view/${viewname}/preview`,
465
+ body,
466
+ isView: true,
467
+ });
468
+ };
454
469
 
455
470
  export /**
456
471
  * @param {object} props
@@ -462,16 +477,16 @@ export /**
462
477
  * @subcategory components / elements / utils
463
478
  * @namespace
464
479
  */
465
- const SelectUnits = ({ vert, autoable, ...props }) => (
466
- <select {...props}>
467
- <option>px</option>
468
- <option>%</option>
469
- <option>{vert ? "vh" : "vw"}</option>
470
- <option>em</option>
471
- <option>rem</option>
472
- {autoable && <option>auto</option>}
473
- </select>
474
- );
480
+ const SelectUnits = ({ vert, autoable, ...props }) => (
481
+ <select {...props}>
482
+ <option>px</option>
483
+ <option>%</option>
484
+ <option>{vert ? "vh" : "vw"}</option>
485
+ <option>em</option>
486
+ <option>rem</option>
487
+ {autoable && <option>auto</option>}
488
+ </select>
489
+ );
475
490
 
476
491
  /**
477
492
  * @function
@@ -579,36 +594,36 @@ export /**
579
594
  * @subcategory components / elements / utils
580
595
  * @namespace
581
596
  */
582
- const ConfigForm = ({ fields, configuration, setProp, node, onChange }) => (
583
- <div>
584
- {fields.map((f, ix) => {
585
- if (f.showIf && configuration) {
586
- let noshow = false;
587
- Object.entries(f.showIf).forEach(([nm, value]) => {
588
- if (Array.isArray(value))
589
- noshow = noshow || value.includes(configuration[nm]);
590
- else noshow = noshow || value !== configuration[nm];
591
- });
592
- if (noshow) return null;
593
- }
594
- return (
595
- <div key={ix}>
596
- {!isCheckbox(f) ? <label>{f.label || f.name}</label> : null}
597
- <ConfigField
598
- field={f}
599
- configuration={configuration}
600
- setProp={setProp}
601
- onChange={onChange}
602
- />
603
- {f.sublabel ? (
604
- <i dangerouslySetInnerHTML={{ __html: f.sublabel }}></i>
605
- ) : null}
606
- </div>
607
- );
608
- })}
609
- <br />
610
- </div>
611
- );
597
+ const ConfigForm = ({ fields, configuration, setProp, node, onChange }) => (
598
+ <div>
599
+ {fields.map((f, ix) => {
600
+ if (f.showIf && configuration) {
601
+ let noshow = false;
602
+ Object.entries(f.showIf).forEach(([nm, value]) => {
603
+ if (Array.isArray(value))
604
+ noshow = noshow || value.includes(configuration[nm]);
605
+ else noshow = noshow || value !== configuration[nm];
606
+ });
607
+ if (noshow) return null;
608
+ }
609
+ return (
610
+ <div key={ix}>
611
+ {!isCheckbox(f) ? <label>{f.label || f.name}</label> : null}
612
+ <ConfigField
613
+ field={f}
614
+ configuration={configuration}
615
+ setProp={setProp}
616
+ onChange={onChange}
617
+ />
618
+ {f.sublabel ? (
619
+ <i dangerouslySetInnerHTML={{ __html: f.sublabel }}></i>
620
+ ) : null}
621
+ </div>
622
+ );
623
+ })}
624
+ <br />
625
+ </div>
626
+ );
612
627
 
613
628
  /**
614
629
  * @param {object|undefined} x
@@ -630,244 +645,243 @@ export /**
630
645
  * @subcategory components / elements / utils
631
646
  * @namespace
632
647
  */
633
- const ConfigField = ({
634
- field,
635
- configuration,
636
- setProp,
637
- onChange,
638
- props,
639
- isStyle,
640
- }) => {
641
- /**
642
- * @param {object} v
643
- * @returns {void}
644
- */
645
- const options = useContext(optionsCtx);
648
+ const ConfigField = ({
649
+ field,
650
+ configuration,
651
+ setProp,
652
+ onChange,
653
+ props,
654
+ isStyle,
655
+ }) => {
656
+ /**
657
+ * @param {object} v
658
+ * @returns {void}
659
+ */
660
+ const options = useContext(optionsCtx);
646
661
 
647
- const myOnChange = (v) => {
648
- setProp((prop) => {
649
- if (configuration) {
650
- if (!prop.configuration) prop.configuration = {};
651
- prop.configuration[field.name] = v;
652
- } else if (isStyle) {
653
- if (!prop.style) prop.style = {};
654
- prop.style[field.name] = v;
655
- } else prop[field.name] = v;
656
- });
657
- onChange && onChange(field.name, v);
658
- };
659
- const value = or_if_undef(
660
- configuration
661
- ? configuration[field.name]
662
- : isStyle
663
- ? props.style[field.name]
664
- : props[field.name],
665
- field.default
666
- );
667
- if (field.input_type === "fromtype") field.input_type = null;
668
- if (field.type && field.type.name === "String" && field.attributes.options) {
669
- field.input_type = "select";
670
- field.options =
671
- typeof field.attributes.options === "string"
672
- ? field.attributes.options.split(",").map((s) => s.trim())
673
- : field.attributes.options;
674
- if (!field.required && field.options) field.options.unshift("");
675
- }
676
- const dispatch = {
677
- String() {
678
- if (field.attributes?.options) {
679
- const options =
680
- typeof field.attributes.options === "string"
681
- ? field.attributes.options.split(",").map((s) => s.trim())
682
- : field.attributes.options;
683
- return (
684
- <select
685
- className="form-control form-select"
686
- value={value || ""}
687
- onChange={(e) => e.target && myOnChange(e.target.value)}
688
- >
689
- {options.map((o, ix) => (
690
- <option
691
- key={ix}
692
- value={typeof o === "string" ? o : o.value || o.name}
693
- >
694
- {typeof o === "string" ? o : o.label}
695
- </option>
696
- ))}
697
- </select>
698
- );
699
- } else
700
- return (
662
+ const myOnChange = (v) => {
663
+ setProp((prop) => {
664
+ if (configuration) {
665
+ if (!prop.configuration) prop.configuration = {};
666
+ prop.configuration[field.name] = v;
667
+ } else if (isStyle) {
668
+ if (!prop.style) prop.style = {};
669
+ prop.style[field.name] = v;
670
+ } else prop[field.name] = v;
671
+ });
672
+ onChange && onChange(field.name, v);
673
+ };
674
+ const value = or_if_undef(
675
+ configuration
676
+ ? configuration[field.name]
677
+ : isStyle
678
+ ? props.style[field.name]
679
+ : props[field.name],
680
+ field.default
681
+ );
682
+ if (field.input_type === "fromtype") field.input_type = null;
683
+ if (field.type && field.type.name === "String" && field.attributes.options) {
684
+ field.input_type = "select";
685
+ field.options =
686
+ typeof field.attributes.options === "string"
687
+ ? field.attributes.options.split(",").map((s) => s.trim())
688
+ : field.attributes.options;
689
+ if (!field.required && field.options) field.options.unshift("");
690
+ }
691
+ const dispatch = {
692
+ String() {
693
+ if (field.attributes?.options) {
694
+ const options =
695
+ typeof field.attributes.options === "string"
696
+ ? field.attributes.options.split(",").map((s) => s.trim())
697
+ : field.attributes.options;
698
+ return (
699
+ <select
700
+ className="form-control form-select"
701
+ value={value || ""}
702
+ onChange={(e) => e.target && myOnChange(e.target.value)}
703
+ >
704
+ {options.map((o, ix) => (
705
+ <option
706
+ key={ix}
707
+ value={typeof o === "string" ? o : o.value || o.name}
708
+ >
709
+ {typeof o === "string" ? o : o.label}
710
+ </option>
711
+ ))}
712
+ </select>
713
+ );
714
+ } else
715
+ return (
716
+ <input
717
+ type="text"
718
+ className="form-control"
719
+ value={value || ""}
720
+ onChange={(e) => e.target && myOnChange(e.target.value)}
721
+ />
722
+ );
723
+ },
724
+ Font: () => (
725
+ <select
726
+ className="form-control form-select"
727
+ value={value || ""}
728
+ onChange={(e) => e.target && myOnChange(e.target.value)}
729
+ >
730
+ <option value={""}></option>
731
+ {Object.entries(options.fonts || {}).map(([nm, ff], ix) => (
732
+ <option key={ix} value={ff}>
733
+ {nm}
734
+ </option>
735
+ ))}
736
+ </select>
737
+ ),
738
+ Integer: () => (
739
+ <input
740
+ type="number"
741
+ className="form-control"
742
+ step={field.step || 1}
743
+ min={field.min}
744
+ max={field.max}
745
+ value={value || ""}
746
+ onChange={(e) => e.target && myOnChange(e.target.value)}
747
+ />
748
+ ),
749
+ Float: () => (
750
+ <input
751
+ type="number"
752
+ className="form-control"
753
+ value={value || ""}
754
+ step={0.01}
755
+ onChange={(e) => e.target && myOnChange(e.target.value)}
756
+ />
757
+ ),
758
+ Color: () => <ColorInput value={value} onChange={(c) => myOnChange(c)} />,
759
+ Bool: () => (
760
+ <div className="form-check">
701
761
  <input
702
- type="text"
703
- className="form-control"
704
- value={value || ""}
705
- onChange={(e) => e.target && myOnChange(e.target.value)}
762
+ type="checkbox"
763
+ className="form-check-input"
764
+ checked={value}
765
+ onChange={(e) => e.target && myOnChange(e.target.checked)}
706
766
  />
707
- );
708
- },
709
- Font: () => (
710
- <select
711
- className="form-control form-select"
712
- value={value || ""}
713
- onChange={(e) => e.target && myOnChange(e.target.value)}
714
- >
715
- <option value={""}></option>
716
- {Object.entries(options.fonts || {}).map(([nm, ff], ix) => (
717
- <option key={ix} value={ff}>
718
- {nm}
719
- </option>
720
- ))}
721
- </select>
722
- ),
723
- Integer: () => (
724
- <input
725
- type="number"
726
- className="form-control"
727
- step={field.step || 1}
728
- min={field.min}
729
- max={field.max}
730
- value={value || ""}
731
- onChange={(e) => e.target && myOnChange(e.target.value)}
732
- />
733
- ),
734
- Float: () => (
735
- <input
736
- type="number"
737
- className="form-control"
738
- value={value || ""}
739
- step={0.01}
740
- onChange={(e) => e.target && myOnChange(e.target.value)}
741
- />
742
- ),
743
- Color: () => <ColorInput value={value} onChange={(c) => myOnChange(c)} />,
744
- Bool: () => (
745
- <div className="form-check">
746
- <input
747
- type="checkbox"
748
- className="form-check-input"
749
- checked={value}
750
- onChange={(e) => e.target && myOnChange(e.target.checked)}
767
+ <label className="form-check-label">{field.label}</label>
768
+ </div>
769
+ ),
770
+ textarea: () => (
771
+ <textarea
772
+ rows="6"
773
+ type="text"
774
+ className="form-control"
775
+ value={value}
776
+ onChange={(e) => e.target && myOnChange(e.target.value)}
751
777
  />
752
- <label className="form-check-label">{field.label}</label>
753
- </div>
754
- ),
755
- textarea: () => (
756
- <textarea
757
- rows="6"
758
- type="text"
759
- className="form-control"
760
- value={value}
761
- onChange={(e) => e.target && myOnChange(e.target.value)}
762
- />
763
- ),
764
- code: () => (
765
- <textarea
766
- rows="6"
767
- type="text"
768
- className="form-control"
769
- value={value}
770
- onChange={(e) => e.target && myOnChange(e.target.value)}
771
- />
772
- ),
773
- select: () => (
774
- <select
775
- className="form-control form-select"
776
- value={value || ""}
777
- onChange={(e) => e.target && myOnChange(e.target.value)}
778
- >
779
- {field.options.map((o, ix) => (
780
- <option key={ix}>{o}</option>
781
- ))}
782
- </select>
783
- ),
784
- btn_select: () => (
785
- <div className="btn-group w-100" role="group">
786
- {field.options.map((o, ix) => (
787
- <button
788
- key={ix}
789
- title={o.title || o.value}
790
- type="button"
791
- style={{ width: `${Math.floor(100 / field.options.length)}%` }}
792
- className={`btn btn-sm btn-${
793
- value !== o.value ? "outline-" : ""
794
- }secondary ${field.btnClass || ""}`}
795
- onClick={() => myOnChange(o.value)}
796
- >
797
- {o.label}
798
- </button>
799
- ))}
800
- </div>
801
- ),
802
- DimUnits: () => {
803
- let styleVal, styleDim;
804
- if (isStyle && value === "auto") {
805
- styleVal = "";
806
- styleDim = "auto";
807
- } else if (isStyle && value && typeof value === "string") {
808
- const matches = value.match(/^([0-9]+\.?[0-9]*)(.*)/);
809
- if (matches) {
810
- styleVal = matches[1];
811
- styleDim = matches[2];
778
+ ),
779
+ code: () => (
780
+ <textarea
781
+ rows="6"
782
+ type="text"
783
+ className="form-control"
784
+ value={value}
785
+ onChange={(e) => e.target && myOnChange(e.target.value)}
786
+ />
787
+ ),
788
+ select: () => (
789
+ <select
790
+ className="form-control form-select"
791
+ value={value || ""}
792
+ onChange={(e) => e.target && myOnChange(e.target.value)}
793
+ >
794
+ {field.options.map((o, ix) => (
795
+ <option key={ix}>{o}</option>
796
+ ))}
797
+ </select>
798
+ ),
799
+ btn_select: () => (
800
+ <div className="btn-group w-100" role="group">
801
+ {field.options.map((o, ix) => (
802
+ <button
803
+ key={ix}
804
+ title={o.title || o.value}
805
+ type="button"
806
+ style={{ width: `${Math.floor(100 / field.options.length)}%` }}
807
+ className={`btn btn-sm btn-${value !== o.value ? "outline-" : ""
808
+ }secondary ${field.btnClass || ""}`}
809
+ onClick={() => myOnChange(o.value)}
810
+ >
811
+ {o.label}
812
+ </button>
813
+ ))}
814
+ </div>
815
+ ),
816
+ DimUnits: () => {
817
+ let styleVal, styleDim;
818
+ if (isStyle && value === "auto") {
819
+ styleVal = "";
820
+ styleDim = "auto";
821
+ } else if (isStyle && value && typeof value === "string") {
822
+ const matches = value.match(/^([0-9]+\.?[0-9]*)(.*)/);
823
+ if (matches) {
824
+ styleVal = matches[1];
825
+ styleDim = matches[2];
826
+ }
812
827
  }
813
- }
814
- return (
815
- <Fragment>
816
- {styleDim !== "auto" && (
817
- <input
818
- type="number"
819
- value={(isStyle ? styleVal : value) || ""}
820
- className="w-50 form-control-sm d-inline dimunit"
821
- disabled={field.autoable && styleDim === "auto"}
822
- onChange={(e) =>
823
- myOnChange(
824
- isStyle
825
- ? `${e.target.value}${styleDim || "px"}`
826
- : e.target.value
827
- )
828
- }
829
- />
830
- )}
831
- <SelectUnits
832
- value={or_if_undef(
833
- configuration
834
- ? configuration[field.name + "Unit"]
835
- : isStyle
836
- ? styleDim
837
- : props[field.name + "Unit"],
838
- "px"
828
+ return (
829
+ <Fragment>
830
+ {styleDim !== "auto" && (
831
+ <input
832
+ type="number"
833
+ value={(isStyle ? styleVal : value) || ""}
834
+ className="w-50 form-control-sm d-inline dimunit"
835
+ disabled={field.autoable && styleDim === "auto"}
836
+ onChange={(e) =>
837
+ e?.target &&
838
+ myOnChange(
839
+ isStyle
840
+ ? `${e.target.value}${styleDim || "px"}`
841
+ : e.target.value
842
+ )
843
+ }
844
+ />
839
845
  )}
840
- autoable={field.autoable}
841
- className={`w-${
842
- styleDim === "auto" ? 100 : 50
843
- } form-control-sm d-inline dimunit`}
844
- vert={true}
845
- onChange={(e) => {
846
- if (!e.target) return;
847
- const target_value = e.target.value;
848
- setProp((prop) => {
849
- const myStyleVal =
850
- target_value === "auto" && field.autoable && isStyle
851
- ? ""
852
- : styleVal;
853
- if (configuration)
854
- prop.configuration[field.name + "Unit"] = target_value;
855
- else if (isStyle) {
856
- prop.style[field.name] = `${or_if_undef(
857
- myStyleVal,
858
- 0
859
- )}${target_value}`;
860
- } else prop[field.name + "Unit"] = target_value;
861
- });
862
- }}
863
- />
864
- </Fragment>
865
- );
866
- },
846
+ <SelectUnits
847
+ value={or_if_undef(
848
+ configuration
849
+ ? configuration[field.name + "Unit"]
850
+ : isStyle
851
+ ? styleDim
852
+ : props[field.name + "Unit"],
853
+ "px"
854
+ )}
855
+ autoable={field.autoable}
856
+ className={`w-${styleDim === "auto" ? 100 : 50
857
+ } form-control-sm d-inline dimunit`}
858
+ vert={true}
859
+ onChange={(e) => {
860
+ if (!e.target) return;
861
+ const target_value = e.target.value;
862
+ setProp((prop) => {
863
+ const myStyleVal =
864
+ target_value === "auto" && field.autoable && isStyle
865
+ ? ""
866
+ : styleVal;
867
+ if (configuration)
868
+ prop.configuration[field.name + "Unit"] = target_value;
869
+ else if (isStyle) {
870
+ prop.style[field.name] = `${or_if_undef(
871
+ myStyleVal,
872
+ 0
873
+ )}${target_value}`;
874
+ } else prop[field.name + "Unit"] = target_value;
875
+ });
876
+ }}
877
+ />
878
+ </Fragment>
879
+ );
880
+ },
881
+ };
882
+ const f = dispatch[field.input_type || field.type.name || field.type];
883
+ return f ? f() : null;
867
884
  };
868
- const f = dispatch[field.input_type || field.type.name || field.type];
869
- return f ? f() : null;
870
- };
871
885
 
872
886
  export /**
873
887
  * @param {object[]} fields
@@ -876,30 +890,30 @@ export /**
876
890
  * @namespace
877
891
  * @returns {table}
878
892
  */
879
- const SettingsFromFields = (fields) => () => {
880
- const node = useNode((node) => {
881
- const ps = {};
882
- fields.forEach((f) => {
883
- ps[f.name] = node.data.props[f.name];
893
+ const SettingsFromFields = (fields) => () => {
894
+ const node = useNode((node) => {
895
+ const ps = {};
896
+ fields.forEach((f) => {
897
+ ps[f.name] = node.data.props[f.name];
898
+ });
899
+ if (fields.some((f) => f.canBeFormula))
900
+ ps.isFormula = node.data.props.isFormula;
901
+ return ps;
884
902
  });
885
- if (fields.some((f) => f.canBeFormula))
886
- ps.isFormula = node.data.props.isFormula;
887
- return ps;
888
- });
889
- const {
890
- actions: { setProp },
891
- } = node;
903
+ const {
904
+ actions: { setProp },
905
+ } = node;
892
906
 
893
- return (
894
- <table className="w-100">
895
- <tbody>
896
- {fields.map((f, ix) => (
897
- <SettingsRow field={f} key={ix} node={node} setProp={setProp} />
898
- ))}
899
- </tbody>
900
- </table>
901
- );
902
- };
907
+ return (
908
+ <table className="w-100">
909
+ <tbody>
910
+ {fields.map((f, ix) => (
911
+ <SettingsRow field={f} key={ix} node={node} setProp={setProp} />
912
+ ))}
913
+ </tbody>
914
+ </table>
915
+ );
916
+ };
903
917
 
904
918
  export /**
905
919
  * @param {object} props
@@ -909,11 +923,11 @@ export /**
909
923
  * @subcategory components / elements / utils
910
924
  * @namespace
911
925
  */
912
- const SettingsSectionHeaderRow = ({ title }) => (
913
- <tr>
914
- <th colSpan="2">{title}</th>
915
- </tr>
916
- );
926
+ const SettingsSectionHeaderRow = ({ title }) => (
927
+ <tr>
928
+ <th colSpan="2">{title}</th>
929
+ </tr>
930
+ );
917
931
 
918
932
  export /**
919
933
  * @param {object} props
@@ -927,49 +941,49 @@ export /**
927
941
  * @subcategory components / elements / utils
928
942
  * @namespace
929
943
  */
930
- const SettingsRow = ({ field, node, setProp, onChange, isStyle }) => {
931
- const fullWidth = ["String", "Bool", "textarea"].includes(field.type);
932
- const needLabel = field.type !== "Bool";
933
- const inner = field.canBeFormula ? (
934
- <OrFormula
935
- nodekey={field.name}
936
- isFormula={node.isFormula}
937
- {...{ setProp, node }}
938
- >
944
+ const SettingsRow = ({ field, node, setProp, onChange, isStyle }) => {
945
+ const fullWidth = ["String", "Bool", "textarea"].includes(field.type);
946
+ const needLabel = field.type !== "Bool";
947
+ const inner = field.canBeFormula ? (
948
+ <OrFormula
949
+ nodekey={field.name}
950
+ isFormula={node.isFormula}
951
+ {...{ setProp, node }}
952
+ >
953
+ <ConfigField
954
+ field={field}
955
+ props={node}
956
+ setProp={setProp}
957
+ onChange={onChange}
958
+ />
959
+ </OrFormula>
960
+ ) : (
939
961
  <ConfigField
940
962
  field={field}
941
963
  props={node}
942
964
  setProp={setProp}
943
965
  onChange={onChange}
966
+ isStyle={isStyle}
944
967
  />
945
- </OrFormula>
946
- ) : (
947
- <ConfigField
948
- field={field}
949
- props={node}
950
- setProp={setProp}
951
- onChange={onChange}
952
- isStyle={isStyle}
953
- />
954
- );
955
- return (
956
- <tr>
957
- {fullWidth ? (
958
- <td colSpan="2">
959
- {needLabel && <label>{field.label}</label>}
960
- {inner}
961
- </td>
962
- ) : (
963
- <Fragment>
964
- <td>
965
- <label>{field.label}</label>
968
+ );
969
+ return (
970
+ <tr>
971
+ {fullWidth ? (
972
+ <td colSpan="2">
973
+ {needLabel && <label>{field.label}</label>}
974
+ {inner}
966
975
  </td>
967
- <td>{inner}</td>
968
- </Fragment>
969
- )}
970
- </tr>
971
- );
972
- };
976
+ ) : (
977
+ <Fragment>
978
+ <td>
979
+ <label>{field.label}</label>
980
+ </td>
981
+ <td>{inner}</td>
982
+ </Fragment>
983
+ )}
984
+ </tr>
985
+ );
986
+ };
973
987
 
974
988
  /**
975
989
  * @category saltcorn-builder
@@ -1046,88 +1060,83 @@ export /**
1046
1060
  * @subcategory components / elements / utils
1047
1061
  * @namespace
1048
1062
  */
1049
- const ButtonOrLinkSettingsRows = ({
1050
- setProp,
1051
- btnClass = null,
1052
- keyPrefix = "",
1053
- values,
1054
- linkFirst = false,
1055
- }) => {
1056
- const setAProp = (key) => (e) => {
1057
- if (e.target) {
1058
- const target_value = e.target.value;
1059
- setProp((prop) => (prop[key] = target_value));
1060
- }
1061
- };
1062
- const addBtnClass = (s) => (btnClass ? `${btnClass} ${s}` : s);
1063
- return [
1064
- <tr key="btnstyle">
1065
- <td>
1066
- <label>Style</label>
1067
- </td>
1068
- <td>
1069
- <select
1070
- className="form-control form-select"
1071
- value={values[keyPrefix + "style"]}
1072
- onChange={setAProp(keyPrefix + "style")}
1073
- >
1074
- {linkFirst ? (
1075
- <option value={addBtnClass("btn-link")}>Link</option>
1076
- ) : null}
1077
- <option value={addBtnClass("btn-primary")}>Primary button</option>
1078
- <option value={addBtnClass("btn-secondary")}>Secondary button</option>
1079
- <option value={addBtnClass("btn-success")}>Success button</option>
1080
- <option value={addBtnClass("btn-danger")}>Danger button</option>
1081
- <option value={addBtnClass("btn-outline-primary")}>
1082
- Primary outline button
1083
- </option>
1084
- <option value={addBtnClass("btn-outline-secondary")}>
1085
- Secondary outline button
1086
- </option>
1087
- <option value={addBtnClass("btn-custom-color")}>
1088
- Button custom color
1089
- </option>
1090
- {!linkFirst ? (
1091
- <option value={addBtnClass("btn-link")}>Link</option>
1092
- ) : null}
1093
- </select>
1094
- </td>
1095
- </tr>,
1096
- <tr key="btnsz">
1097
- <td>
1098
- <label>Size</label>
1099
- </td>
1100
- <td>
1101
- <select
1102
- className="form-control form-select"
1103
- value={values[keyPrefix + "size"]}
1104
- onChange={setAProp(keyPrefix + "size")}
1105
- >
1106
- <option value="">Standard</option>
1107
- <option value="btn-lg">Large</option>
1108
- <option value="btn-sm">Small</option>
1109
- <option value="btn-block">Block</option>
1110
- <option value="btn-block btn-lg">Large block</option>
1111
- </select>
1112
- </td>
1113
- </tr>,
1114
- <tr key="btnicon">
1115
- <td>
1116
- <label>Icon</label>
1117
- </td>
1118
- <td>
1119
- <FontIconPicker
1120
- value={values[keyPrefix + "icon"]}
1121
- onChange={(value) =>
1122
- setProp((prop) => (prop[keyPrefix + "icon"] = value))
1123
- }
1124
- isMulti={false}
1125
- icons={faIcons}
1126
- />
1127
- </td>
1128
- </tr>,
1129
- ...(values[keyPrefix + "style"] === addBtnClass("btn-custom-color")
1130
- ? [
1063
+ const ButtonOrLinkSettingsRows = ({
1064
+ setProp,
1065
+ btnClass = null,
1066
+ keyPrefix = "",
1067
+ values,
1068
+ linkFirst = false,
1069
+ }) => {
1070
+ const setAProp = setAPropGen(setProp);
1071
+ const addBtnClass = (s) => (btnClass ? `${btnClass} ${s}` : s);
1072
+ return [
1073
+ <tr key="btnstyle">
1074
+ <td>
1075
+ <label>Style</label>
1076
+ </td>
1077
+ <td>
1078
+ <select
1079
+ className="form-control form-select"
1080
+ value={values[keyPrefix + "style"]}
1081
+ onChange={setAProp(keyPrefix + "style")}
1082
+ >
1083
+ {linkFirst ? (
1084
+ <option value={addBtnClass("btn-link")}>Link</option>
1085
+ ) : null}
1086
+ <option value={addBtnClass("btn-primary")}>Primary button</option>
1087
+ <option value={addBtnClass("btn-secondary")}>Secondary button</option>
1088
+ <option value={addBtnClass("btn-success")}>Success button</option>
1089
+ <option value={addBtnClass("btn-danger")}>Danger button</option>
1090
+ <option value={addBtnClass("btn-outline-primary")}>
1091
+ Primary outline button
1092
+ </option>
1093
+ <option value={addBtnClass("btn-outline-secondary")}>
1094
+ Secondary outline button
1095
+ </option>
1096
+ <option value={addBtnClass("btn-custom-color")}>
1097
+ Button custom color
1098
+ </option>
1099
+ {!linkFirst ? (
1100
+ <option value={addBtnClass("btn-link")}>Link</option>
1101
+ ) : null}
1102
+ </select>
1103
+ </td>
1104
+ </tr>,
1105
+ <tr key="btnsz">
1106
+ <td>
1107
+ <label>Size</label>
1108
+ </td>
1109
+ <td>
1110
+ <select
1111
+ className="form-control form-select"
1112
+ value={values[keyPrefix + "size"]}
1113
+ onChange={setAProp(keyPrefix + "size")}
1114
+ >
1115
+ <option value="">Standard</option>
1116
+ <option value="btn-lg">Large</option>
1117
+ <option value="btn-sm">Small</option>
1118
+ <option value="btn-block">Block</option>
1119
+ <option value="btn-block btn-lg">Large block</option>
1120
+ </select>
1121
+ </td>
1122
+ </tr>,
1123
+ <tr key="btnicon">
1124
+ <td>
1125
+ <label>Icon</label>
1126
+ </td>
1127
+ <td>
1128
+ <FontIconPicker
1129
+ value={values[keyPrefix + "icon"]}
1130
+ onChange={(value) =>
1131
+ setProp((prop) => (prop[keyPrefix + "icon"] = value))
1132
+ }
1133
+ isMulti={false}
1134
+ icons={faIcons}
1135
+ />
1136
+ </td>
1137
+ </tr>,
1138
+ ...(values[keyPrefix + "style"] === addBtnClass("btn-custom-color")
1139
+ ? [
1131
1140
  <tr key="btnbgcol">
1132
1141
  <td>
1133
1142
  <label>Background</label>
@@ -1168,9 +1177,9 @@ const ButtonOrLinkSettingsRows = ({
1168
1177
  </td>
1169
1178
  </tr>,
1170
1179
  ]
1171
- : []),
1172
- ];
1173
- };
1180
+ : []),
1181
+ ];
1182
+ };
1174
1183
 
1175
1184
  /**
1176
1185
  * @function
@@ -1228,3 +1237,24 @@ export const recursivelyCloneToElems = (query) => (nodeId, ix) => {
1228
1237
 
1229
1238
  export const isBlock = (block, inline, textStyle) =>
1230
1239
  !textStyle || !textStyle.startsWith("h") ? block : !inline;
1240
+
1241
+ export const setAPropGen =
1242
+ (setProp) =>
1243
+ (key, opts = {}) =>
1244
+ (e) => {
1245
+ if (e.target) {
1246
+ const target_value = opts?.checked ? e.target.checked : e.target.value;
1247
+ setProp((prop) => (prop[key] = target_value));
1248
+ }
1249
+ };
1250
+
1251
+ const Tooltip = ({ children }) => {
1252
+ const [visible, setVisible] = useState(false);
1253
+ const show = () => setVisible(true);
1254
+ const hide = () => setVisible(false);
1255
+ return <Tippy content={children} visible={visible} onClickOutside={hide} interactive={true}>
1256
+ <span
1257
+ onClick={visible ? hide : show} className="ms-1"
1258
+ ><FontAwesomeIcon icon={faInfoCircle} /></span>
1259
+ </Tippy>
1260
+ }