@saltcorn/builder 0.7.3 → 0.7.4-beta.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.
@@ -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,245 +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
- e?.target &&
824
- myOnChange(
825
- isStyle
826
- ? `${e.target.value}${styleDim || "px"}`
827
- : e.target.value
828
- )
829
- }
830
- />
831
- )}
832
- <SelectUnits
833
- value={or_if_undef(
834
- configuration
835
- ? configuration[field.name + "Unit"]
836
- : isStyle
837
- ? styleDim
838
- : props[field.name + "Unit"],
839
- "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
+ />
840
845
  )}
841
- autoable={field.autoable}
842
- className={`w-${
843
- styleDim === "auto" ? 100 : 50
844
- } form-control-sm d-inline dimunit`}
845
- vert={true}
846
- onChange={(e) => {
847
- if (!e.target) return;
848
- const target_value = e.target.value;
849
- setProp((prop) => {
850
- const myStyleVal =
851
- target_value === "auto" && field.autoable && isStyle
852
- ? ""
853
- : styleVal;
854
- if (configuration)
855
- prop.configuration[field.name + "Unit"] = target_value;
856
- else if (isStyle) {
857
- prop.style[field.name] = `${or_if_undef(
858
- myStyleVal,
859
- 0
860
- )}${target_value}`;
861
- } else prop[field.name + "Unit"] = target_value;
862
- });
863
- }}
864
- />
865
- </Fragment>
866
- );
867
- },
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;
868
884
  };
869
- const f = dispatch[field.input_type || field.type.name || field.type];
870
- return f ? f() : null;
871
- };
872
885
 
873
886
  export /**
874
887
  * @param {object[]} fields
@@ -877,30 +890,30 @@ export /**
877
890
  * @namespace
878
891
  * @returns {table}
879
892
  */
880
- const SettingsFromFields = (fields) => () => {
881
- const node = useNode((node) => {
882
- const ps = {};
883
- fields.forEach((f) => {
884
- 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;
885
902
  });
886
- if (fields.some((f) => f.canBeFormula))
887
- ps.isFormula = node.data.props.isFormula;
888
- return ps;
889
- });
890
- const {
891
- actions: { setProp },
892
- } = node;
903
+ const {
904
+ actions: { setProp },
905
+ } = node;
893
906
 
894
- return (
895
- <table className="w-100">
896
- <tbody>
897
- {fields.map((f, ix) => (
898
- <SettingsRow field={f} key={ix} node={node} setProp={setProp} />
899
- ))}
900
- </tbody>
901
- </table>
902
- );
903
- };
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
+ };
904
917
 
905
918
  export /**
906
919
  * @param {object} props
@@ -910,11 +923,11 @@ export /**
910
923
  * @subcategory components / elements / utils
911
924
  * @namespace
912
925
  */
913
- const SettingsSectionHeaderRow = ({ title }) => (
914
- <tr>
915
- <th colSpan="2">{title}</th>
916
- </tr>
917
- );
926
+ const SettingsSectionHeaderRow = ({ title }) => (
927
+ <tr>
928
+ <th colSpan="2">{title}</th>
929
+ </tr>
930
+ );
918
931
 
919
932
  export /**
920
933
  * @param {object} props
@@ -928,49 +941,49 @@ export /**
928
941
  * @subcategory components / elements / utils
929
942
  * @namespace
930
943
  */
931
- const SettingsRow = ({ field, node, setProp, onChange, isStyle }) => {
932
- const fullWidth = ["String", "Bool", "textarea"].includes(field.type);
933
- const needLabel = field.type !== "Bool";
934
- const inner = field.canBeFormula ? (
935
- <OrFormula
936
- nodekey={field.name}
937
- isFormula={node.isFormula}
938
- {...{ setProp, node }}
939
- >
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
+ ) : (
940
961
  <ConfigField
941
962
  field={field}
942
963
  props={node}
943
964
  setProp={setProp}
944
965
  onChange={onChange}
966
+ isStyle={isStyle}
945
967
  />
946
- </OrFormula>
947
- ) : (
948
- <ConfigField
949
- field={field}
950
- props={node}
951
- setProp={setProp}
952
- onChange={onChange}
953
- isStyle={isStyle}
954
- />
955
- );
956
- return (
957
- <tr>
958
- {fullWidth ? (
959
- <td colSpan="2">
960
- {needLabel && <label>{field.label}</label>}
961
- {inner}
962
- </td>
963
- ) : (
964
- <Fragment>
965
- <td>
966
- <label>{field.label}</label>
968
+ );
969
+ return (
970
+ <tr>
971
+ {fullWidth ? (
972
+ <td colSpan="2">
973
+ {needLabel && <label>{field.label}</label>}
974
+ {inner}
967
975
  </td>
968
- <td>{inner}</td>
969
- </Fragment>
970
- )}
971
- </tr>
972
- );
973
- };
976
+ ) : (
977
+ <Fragment>
978
+ <td>
979
+ <label>{field.label}</label>
980
+ </td>
981
+ <td>{inner}</td>
982
+ </Fragment>
983
+ )}
984
+ </tr>
985
+ );
986
+ };
974
987
 
975
988
  /**
976
989
  * @category saltcorn-builder
@@ -1047,83 +1060,83 @@ export /**
1047
1060
  * @subcategory components / elements / utils
1048
1061
  * @namespace
1049
1062
  */
1050
- const ButtonOrLinkSettingsRows = ({
1051
- setProp,
1052
- btnClass = null,
1053
- keyPrefix = "",
1054
- values,
1055
- linkFirst = false,
1056
- }) => {
1057
- const setAProp = setAPropGen(setProp);
1058
- const addBtnClass = (s) => (btnClass ? `${btnClass} ${s}` : s);
1059
- return [
1060
- <tr key="btnstyle">
1061
- <td>
1062
- <label>Style</label>
1063
- </td>
1064
- <td>
1065
- <select
1066
- className="form-control form-select"
1067
- value={values[keyPrefix + "style"]}
1068
- onChange={setAProp(keyPrefix + "style")}
1069
- >
1070
- {linkFirst ? (
1071
- <option value={addBtnClass("btn-link")}>Link</option>
1072
- ) : null}
1073
- <option value={addBtnClass("btn-primary")}>Primary button</option>
1074
- <option value={addBtnClass("btn-secondary")}>Secondary button</option>
1075
- <option value={addBtnClass("btn-success")}>Success button</option>
1076
- <option value={addBtnClass("btn-danger")}>Danger button</option>
1077
- <option value={addBtnClass("btn-outline-primary")}>
1078
- Primary outline button
1079
- </option>
1080
- <option value={addBtnClass("btn-outline-secondary")}>
1081
- Secondary outline button
1082
- </option>
1083
- <option value={addBtnClass("btn-custom-color")}>
1084
- Button custom color
1085
- </option>
1086
- {!linkFirst ? (
1087
- <option value={addBtnClass("btn-link")}>Link</option>
1088
- ) : null}
1089
- </select>
1090
- </td>
1091
- </tr>,
1092
- <tr key="btnsz">
1093
- <td>
1094
- <label>Size</label>
1095
- </td>
1096
- <td>
1097
- <select
1098
- className="form-control form-select"
1099
- value={values[keyPrefix + "size"]}
1100
- onChange={setAProp(keyPrefix + "size")}
1101
- >
1102
- <option value="">Standard</option>
1103
- <option value="btn-lg">Large</option>
1104
- <option value="btn-sm">Small</option>
1105
- <option value="btn-block">Block</option>
1106
- <option value="btn-block btn-lg">Large block</option>
1107
- </select>
1108
- </td>
1109
- </tr>,
1110
- <tr key="btnicon">
1111
- <td>
1112
- <label>Icon</label>
1113
- </td>
1114
- <td>
1115
- <FontIconPicker
1116
- value={values[keyPrefix + "icon"]}
1117
- onChange={(value) =>
1118
- setProp((prop) => (prop[keyPrefix + "icon"] = value))
1119
- }
1120
- isMulti={false}
1121
- icons={faIcons}
1122
- />
1123
- </td>
1124
- </tr>,
1125
- ...(values[keyPrefix + "style"] === addBtnClass("btn-custom-color")
1126
- ? [
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
+ ? [
1127
1140
  <tr key="btnbgcol">
1128
1141
  <td>
1129
1142
  <label>Background</label>
@@ -1164,9 +1177,9 @@ const ButtonOrLinkSettingsRows = ({
1164
1177
  </td>
1165
1178
  </tr>,
1166
1179
  ]
1167
- : []),
1168
- ];
1169
- };
1180
+ : []),
1181
+ ];
1182
+ };
1170
1183
 
1171
1184
  /**
1172
1185
  * @function
@@ -1227,10 +1240,21 @@ export const isBlock = (block, inline, textStyle) =>
1227
1240
 
1228
1241
  export const setAPropGen =
1229
1242
  (setProp) =>
1230
- (key, opts = {}) =>
1231
- (e) => {
1232
- if (e.target) {
1233
- const target_value = opts?.checked ? e.target.checked : e.target.value;
1234
- setProp((prop) => (prop[key] = target_value));
1235
- }
1236
- };
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
+ }