@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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saltcorn/builder",
3
- "version": "0.7.3-beta.7",
3
+ "version": "0.7.4-beta.1",
4
4
  "description": "Drag and drop view builder for Saltcorn, open-source no-code platform",
5
5
  "main": "index.js",
6
6
  "homepage": "https://saltcorn.com",
@@ -35,8 +35,10 @@
35
35
  "react-contenteditable": "3.3.5",
36
36
  "react-dom": "16.13.1",
37
37
  "react-transition-group": "4.4.1",
38
+ "@tippyjs/react": "4.2.6",
38
39
  "webpack": "4.43.0",
39
- "webpack-cli": "3.3.11"
40
+ "webpack-cli": "3.3.11",
41
+ "lodash": "4.17.11"
40
42
  },
41
43
  "publishConfig": {
42
44
  "access": "public"
@@ -49,6 +49,7 @@ import {
49
49
  faUndo,
50
50
  faRedo,
51
51
  faTrashAlt,
52
+ faSave,
52
53
  } from "@fortawesome/free-solid-svg-icons";
53
54
  import {
54
55
  Accordion,
@@ -387,6 +388,7 @@ const Builder = ({ options, layout, mode }) => {
387
388
  const [previews, setPreviews] = useState({});
388
389
  const [uploadedFiles, setUploadedFiles] = useState([]);
389
390
  const nodekeys = useRef([]);
391
+ const [isSaving, setIsSaving] = useState(false);
390
392
 
391
393
  return (
392
394
  <ErrorBoundary>
@@ -398,7 +400,10 @@ const Builder = ({ options, layout, mode }) => {
398
400
  <div className="row" style={{ marginTop: "-5px" }}>
399
401
  <div className="col-sm-auto left-builder-col">
400
402
  <div className="componets-and-library-accordion toolbox-card">
401
- <InitNewElement nodekeys={nodekeys} />
403
+ <InitNewElement
404
+ nodekeys={nodekeys}
405
+ setIsSaving={setIsSaving}
406
+ />
402
407
  <Accordion>
403
408
  <div className="card mt-1" accordiontitle="Components">
404
409
  {{
@@ -458,9 +463,13 @@ const Builder = ({ options, layout, mode }) => {
458
463
  </div>
459
464
  <div className="col-sm-auto builder-sidebar">
460
465
  <div style={{ width: "16rem" }}>
461
- <SaveButton />
462
466
  <NextButton layout={layout} />
463
467
  <HistoryPanel />
468
+ <FontAwesomeIcon
469
+ icon={faSave}
470
+ title={isSaving ? "Saving..." : "All changes saved"}
471
+ className={isSaving ? "text-muted" : ""}
472
+ />
464
473
  <ViewPageLink />
465
474
  <SettingsPanel />
466
475
  </div>
@@ -4,7 +4,13 @@
4
4
  * @subcategory components
5
5
  */
6
6
 
7
- import React, { useEffect, useContext, useState, Fragment } from "react";
7
+ import React, {
8
+ useEffect,
9
+ useContext,
10
+ useState,
11
+ Fragment,
12
+ useRef,
13
+ } from "react";
8
14
  import { useEditor, useNode } from "@craftjs/core";
9
15
  import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
10
16
  import { faPlus, faTimes } from "@fortawesome/free-solid-svg-icons";
@@ -13,10 +19,11 @@ import faIcons from "./elements/faicons";
13
19
  import { craftToSaltcorn, layoutToNodes } from "./storage";
14
20
  import optionsCtx from "./context";
15
21
  import { WrapElem } from "./Toolbox";
22
+ import { isEqual } from "lodash";
16
23
 
17
24
  /**
18
- *
19
- * @param {object[]} xs
25
+ *
26
+ * @param {object[]} xs
20
27
  * @returns {object[]}
21
28
  */
22
29
  const twoByTwos = (xs) => {
@@ -67,10 +74,38 @@ export /**
67
74
  * @subcategory components
68
75
  * @namespace
69
76
  */
70
- const InitNewElement = ({ nodekeys }) => {
77
+ const InitNewElement = ({ nodekeys, setIsSaving }) => {
78
+ const [saveTimeout, setSaveTimeout] = useState(false);
79
+ const savedData = useRef(false);
71
80
  const { actions, query, connectors } = useEditor((state, query) => {
72
81
  return {};
73
82
  });
83
+ const options = useContext(optionsCtx);
84
+ const doSave = (query) => {
85
+ if (!query.serialize) return;
86
+
87
+ const data = craftToSaltcorn(JSON.parse(query.serialize()));
88
+ const urlroot = options.page_id ? "pageedit" : "viewedit";
89
+ if (savedData.current === false) {
90
+ //do not save on first call
91
+ savedData.current = JSON.stringify(data.layout);
92
+ setIsSaving(false);
93
+ return;
94
+ }
95
+ if (isEqual(savedData.current, JSON.stringify(data.layout))) return;
96
+ savedData.current = JSON.stringify(data.layout);
97
+
98
+ fetch(`/${urlroot}/savebuilder/${options.page_id || options.view_id}`, {
99
+ method: "POST", // or 'PUT'
100
+ headers: {
101
+ "Content-Type": "application/json",
102
+ "CSRF-Token": options.csrfToken,
103
+ },
104
+ body: JSON.stringify(data),
105
+ }).then(() => {
106
+ setIsSaving(false);
107
+ });
108
+ };
74
109
  const onNodesChange = (arg, arg1) => {
75
110
  const nodes = arg.getSerializedNodes();
76
111
  const newNodeIds = [];
@@ -98,6 +133,14 @@ const InitNewElement = ({ nodekeys }) => {
98
133
  actions.selectNode(id);
99
134
  }
100
135
  }
136
+ if (saveTimeout) clearTimeout(saveTimeout);
137
+ setIsSaving(true);
138
+ setSaveTimeout(
139
+ setTimeout(() => {
140
+ doSave(query);
141
+ setSaveTimeout(false);
142
+ }, 500)
143
+ );
101
144
  };
102
145
  useEffect(() => {
103
146
  const nodes = query.getSerializedNodes();
@@ -180,7 +223,7 @@ const Library = () => {
180
223
  type="text"
181
224
  className="form-control"
182
225
  value={newName}
183
- onChange={(e) => setNewName(e.target.value)}
226
+ onChange={(e) => e?.target && setNewName(e.target.value)}
184
227
  />
185
228
  <br />
186
229
  <label>Icon</label>
@@ -16,6 +16,7 @@ import {
16
16
  setInitialConfig,
17
17
  ButtonOrLinkSettingsRows,
18
18
  DynamicFontAwesomeIcon,
19
+ setAPropGen,
19
20
  } from "./utils";
20
21
 
21
22
  export /**
@@ -110,6 +111,8 @@ const ActionSettings = () => {
110
111
  const options = useContext(optionsCtx);
111
112
  const getCfgFields = (fv) => (options.actionConfigForms || {})[fv];
112
113
  const cfgFields = getCfgFields(name);
114
+ const setAProp = setAPropGen(setProp);
115
+
113
116
  return (
114
117
  <div>
115
118
  <table className="w-100">
@@ -123,12 +126,10 @@ const ActionSettings = () => {
123
126
  value={name}
124
127
  className="form-control form-select"
125
128
  onChange={(e) => {
126
- setProp((prop) => (prop.name = e.target.value));
127
- setInitialConfig(
128
- setProp,
129
- e.target.value,
130
- getCfgFields(e.target.value)
131
- );
129
+ if (!e.target) return;
130
+ const value = e.target.value;
131
+ setProp((prop) => (prop.name = value));
132
+ setInitialConfig(setProp, value, getCfgFields(value));
132
133
  }}
133
134
  >
134
135
  {options.actions.map((f, ix) => (
@@ -150,9 +151,7 @@ const ActionSettings = () => {
150
151
  type="text"
151
152
  className="form-control"
152
153
  value={action_label}
153
- onChange={(e) =>
154
- setProp((prop) => (prop.action_label = e.target.value))
155
- }
154
+ onChange={setAProp("action_label")}
156
155
  />
157
156
  </OrFormula>
158
157
  </td>
@@ -172,9 +171,7 @@ const ActionSettings = () => {
172
171
  name="block"
173
172
  type="checkbox"
174
173
  checked={confirm}
175
- onChange={(e) =>
176
- setProp((prop) => (prop.confirm = e.target.checked))
177
- }
174
+ onChange={setAProp("confirm", { checked: true })}
178
175
  />
179
176
  <label className="form-check-label">User confirmation?</label>
180
177
  </div>
@@ -7,7 +7,7 @@
7
7
  import React, { useContext } from "react";
8
8
  import { useNode } from "@craftjs/core";
9
9
  import optionsCtx from "../context";
10
- import { blockProps, BlockSetting, TextStyleRow } from "./utils";
10
+ import { blockProps, BlockSetting, TextStyleRow, setAPropGen } from "./utils";
11
11
 
12
12
  export /**
13
13
  * @param {object} props
@@ -61,6 +61,8 @@ const AggregationSettings = () => {
61
61
  textStyle: node.data.props.textStyle,
62
62
  }));
63
63
  const options = useContext(optionsCtx);
64
+ const setAProp = setAPropGen(setProp);
65
+
64
66
  return (
65
67
  <table>
66
68
  <tbody>
@@ -72,13 +74,15 @@ const AggregationSettings = () => {
72
74
  <select
73
75
  className="form-control form-select"
74
76
  value={agg_relation}
75
- onChange={(e) =>
77
+ onChange={(e) => {
78
+ if (!e.target) return;
79
+ const value = e.target.value;
76
80
  setProp((prop) => {
77
- prop.agg_relation = e.target.value;
78
- const fs = options.agg_field_opts[e.target.value];
81
+ prop.agg_relation = value;
82
+ const fs = options.agg_field_opts[value];
79
83
  if (fs && fs.length > 0) prop.agg_field = fs[0];
80
- })
81
- }
84
+ });
85
+ }}
82
86
  >
83
87
  {options.child_field_list.map((f, ix) => (
84
88
  <option key={ix} value={f}>
@@ -96,9 +100,7 @@ const AggregationSettings = () => {
96
100
  <select
97
101
  className="form-control form-select"
98
102
  value={agg_field}
99
- onChange={(e) =>
100
- setProp((prop) => (prop.agg_field = e.target.value))
101
- }
103
+ onChange={setAProp("agg_field")}
102
104
  >
103
105
  {(options.agg_field_opts[agg_relation] || []).map((f, ix) => (
104
106
  <option key={ix} value={f}>
@@ -116,7 +118,7 @@ const AggregationSettings = () => {
116
118
  <select
117
119
  value={stat}
118
120
  className="form-control form-select"
119
- onChange={(e) => setProp((prop) => (prop.stat = e.target.value))}
121
+ onChange={setAProp("stat")}
120
122
  >
121
123
  <option value={"Count"}>Count</option>
122
124
  <option value={"Avg"}>Avg</option>
@@ -141,9 +143,7 @@ const AggregationSettings = () => {
141
143
  type="text"
142
144
  className="form-control"
143
145
  value={aggwhere}
144
- onChange={(e) =>
145
- setProp((prop) => (prop.aggwhere = e.target.value))
146
- }
146
+ onChange={setAProp("aggwhere")}
147
147
  />
148
148
  </td>
149
149
  </tr>
@@ -118,12 +118,14 @@ const ColumnsSettings = () => {
118
118
  step="1"
119
119
  min="1"
120
120
  max="4"
121
- onChange={(e) =>
121
+ onChange={(e) => {
122
+ if (!e.target) return;
123
+ const value = e.target.value;
122
124
  setProp((prop) => {
123
- prop.ncols = e.target.value;
124
- prop.widths = resetWidths(e.target.value);
125
- })
126
- }
125
+ prop.ncols = value;
126
+ prop.widths = resetWidths(value);
127
+ });
128
+ }}
127
129
  />
128
130
  </td>
129
131
  </tr>
@@ -148,9 +150,11 @@ const ColumnsSettings = () => {
148
150
  step="1"
149
151
  min="1"
150
152
  max={12 - (sum(widths) - widths[ix]) - 1}
151
- onChange={(e) =>
152
- setProp((prop) => (prop.widths[ix] = +e.target.value))
153
- }
153
+ onChange={(e) => {
154
+ if (!e.target) return;
155
+ const value = e.target.value;
156
+ setProp((prop) => (prop.widths[ix] = +value));
157
+ }}
154
158
  />
155
159
  ) : (
156
160
  `${12 - sum(widths)}`
@@ -161,9 +165,11 @@ const ColumnsSettings = () => {
161
165
  <select
162
166
  className="form-control form-select"
163
167
  value={breakpoints[ix]}
164
- onChange={(e) =>
165
- setProp((prop) => (prop.breakpoints[ix] = e.target.value))
166
- }
168
+ onChange={(e) => {
169
+ if (!e.target) return;
170
+ const value = e.target.value;
171
+ setProp((prop) => (prop.breakpoints[ix] = value));
172
+ }}
167
173
  >
168
174
  <option disabled>Breakpoint</option>
169
175
  <option value="">None</option>
@@ -18,6 +18,8 @@ import {
18
18
  SettingsRow,
19
19
  reactifyStyles,
20
20
  bstyleopt,
21
+ setAPropGen,
22
+ FormulaTooltip,
21
23
  } from "./utils";
22
24
  import {
23
25
  BorderOuter,
@@ -269,12 +271,7 @@ const ContainerSettings = () => {
269
271
  * @param {string} key
270
272
  * @returns {function}
271
273
  */
272
- const setAProp = (key) => (e) => {
273
- if (e.target) {
274
- const target_value = e.target.value;
275
- setProp((prop) => (prop[key] = target_value));
276
- }
277
- };
274
+ const setAProp = setAPropGen(setProp);
278
275
  return (
279
276
  <Accordion>
280
277
  <div accordiontitle="Box" className="w-100">
@@ -350,9 +347,7 @@ const ContainerSettings = () => {
350
347
  name="block"
351
348
  type="checkbox"
352
349
  checked={fullPageWidth}
353
- onChange={(e) =>
354
- setProp((prop) => (prop.fullPageWidth = e.target.checked))
355
- }
350
+ onChange={setAProp("fullPageWidth", { checked: true })}
356
351
  />
357
352
  <label className="form-check-label">
358
353
  Expand to full page width
@@ -536,8 +531,10 @@ const ContainerSettings = () => {
536
531
  onChange={setAProp("imgResponsiveWidths")}
537
532
  />
538
533
  <small>
539
- <i>List of widths to serve resized images,
540
- e.g. 300, 400, 600</i>
534
+ <i>
535
+ List of widths to serve resized images, e.g. 300, 400,
536
+ 600
537
+ </i>
541
538
  </small>
542
539
  </td>
543
540
  </tr>
@@ -611,9 +608,7 @@ const ContainerSettings = () => {
611
608
  name="setTextColor"
612
609
  type="checkbox"
613
610
  checked={setTextColor}
614
- onChange={(e) =>
615
- setProp((prop) => (prop.setTextColor = e.target.checked))
616
- }
611
+ onChange={setAProp("setTextColor", { checked: true })}
617
612
  />
618
613
  </label>
619
614
  </td>
@@ -763,7 +758,7 @@ const ContainerSettings = () => {
763
758
  onChange={setAProp("showIfFormula")}
764
759
  />
765
760
  <div style={{ marginTop: "-5px" }}>
766
- <small className="text-muted font-monospace">FORMULA</small>
761
+ <small className="text-muted font-monospace">FORMULA <FormulaTooltip /></small>
767
762
  </div>
768
763
  </td>
769
764
  </tr>
@@ -782,15 +777,17 @@ const ContainerSettings = () => {
782
777
  ? true
783
778
  : showForRole[id]
784
779
  }
785
- onChange={(e) =>
780
+ onChange={(e) => {
781
+ if (!e?.target) return;
782
+ const checked = e.target.checked;
786
783
  setProp((prop) => {
787
784
  if (!prop.showForRole || prop.showForRole.length === 0)
788
785
  options.roles.forEach(
789
786
  (r) => (prop.showForRole[r.id] = true)
790
787
  );
791
- prop.showForRole[id] = e.target.checked;
792
- })
793
- }
788
+ prop.showForRole[id] = checked;
789
+ });
790
+ }}
794
791
  />
795
792
  <label className="form-check-label">{role}</label>
796
793
  </div>
@@ -806,11 +803,7 @@ const ContainerSettings = () => {
806
803
  name="block"
807
804
  type="checkbox"
808
805
  checked={show_for_owner}
809
- onChange={(e) =>
810
- setProp(
811
- (prop) => (prop.show_for_owner = e.target.checked)
812
- )
813
- }
806
+ onChange={setAProp("show_for_owner", { checked: true })}
814
807
  />
815
808
  <label className="form-check-label">Owner</label>
816
809
  </div>
@@ -7,7 +7,7 @@
7
7
  import React, { useContext, Fragment } from "react";
8
8
  import { useNode } from "@craftjs/core";
9
9
  import optionsCtx from "../context";
10
- import { blockProps, BlockSetting, TextStyleRow } from "./utils";
10
+ import { blockProps, BlockSetting, TextStyleRow, setAPropGen } from "./utils";
11
11
 
12
12
  export /**
13
13
  * @param {object} props
@@ -59,6 +59,8 @@ const DropDownFilterSettings = () => {
59
59
  where: node.data.props.where,
60
60
  }));
61
61
  const options = useContext(optionsCtx);
62
+ const setAProp = setAPropGen(setProp);
63
+
62
64
  return (
63
65
  <table className="w-100">
64
66
  <tbody>
@@ -70,13 +72,18 @@ const DropDownFilterSettings = () => {
70
72
  <select
71
73
  value={name}
72
74
  className="form-control form-select"
73
- onChange={(e) => setProp((prop) => (prop.name = e.target.value))}
75
+ onChange={setAProp("name")}
74
76
  >
75
77
  {options.fields.map((f, ix) => (
76
78
  <option key={ix} value={f.name}>
77
79
  {f.label}
78
80
  </option>
79
81
  ))}
82
+ {options.parent_field_list.map((f, ix) => (
83
+ <option key={ix} value={f}>
84
+ {f}
85
+ </option>
86
+ ))}
80
87
  </select>
81
88
  </td>
82
89
  </tr>
@@ -88,9 +95,7 @@ const DropDownFilterSettings = () => {
88
95
  <input
89
96
  value={neutral_label}
90
97
  className="form-control"
91
- onChange={(e) =>
92
- setProp((prop) => (prop.neutral_label = e.target.value))
93
- }
98
+ onChange={setAProp("neutral_label")}
94
99
  />
95
100
  </td>
96
101
  </tr>
@@ -102,7 +107,7 @@ const DropDownFilterSettings = () => {
102
107
  <input
103
108
  value={where}
104
109
  className="form-control"
105
- onChange={(e) => setProp((prop) => (prop.where = e.target.value))}
110
+ onChange={setAProp("where")}
106
111
  />
107
112
  </td>
108
113
  </tr>
@@ -121,9 +126,7 @@ const DropDownFilterSettings = () => {
121
126
  name="block"
122
127
  type="checkbox"
123
128
  checked={full_width}
124
- onChange={(e) =>
125
- setProp((prop) => (prop.full_width = e.target.checked))
126
- }
129
+ onChange={setAProp("full_width", { checked: true })}
127
130
  />
128
131
  <label className="form-check-label">Full width</label>
129
132
  </div>
@@ -134,8 +137,8 @@ const DropDownFilterSettings = () => {
134
137
  );
135
138
  };
136
139
 
137
- /**
138
- * @type {object}
140
+ /**
141
+ * @type {object}
139
142
  */
140
143
  DropDownFilter.craft = {
141
144
  displayName: "DropDownFilter",
@@ -140,15 +140,17 @@ const FieldSettings = () => {
140
140
  value={name}
141
141
  className="form-control form-select"
142
142
  onChange={(e) => {
143
- setProp((prop) => (prop.name = e.target.value));
144
- const newfvs = options.field_view_options[e.target.value];
143
+ if (!e.target) return;
144
+ const value = e.target.value;
145
+ setProp((prop) => (prop.name = value));
146
+ const newfvs = options.field_view_options[value];
145
147
  if (newfvs && newfvs.length > 0) {
146
148
  setProp((prop) => (prop.fieldview = newfvs[0]));
147
149
  refetchPreview({
148
- name: e.target.value,
150
+ name: value,
149
151
  fieldview: newfvs[0],
150
152
  });
151
- } else refetchPreview({ name: e.target.value });
153
+ } else refetchPreview({ name: value });
152
154
  }}
153
155
  >
154
156
  {options.fields.map((f, ix) => (
@@ -170,13 +172,12 @@ const FieldSettings = () => {
170
172
  value={fieldview}
171
173
  className="form-control form-select"
172
174
  onChange={(e) => {
173
- setProp((prop) => (prop.fieldview = e.target.value));
174
- setInitialConfig(
175
- setProp,
176
- e.target.value,
177
- getCfgFields(e.target.value)
178
- );
179
- refetchPreview({ fieldview: e.target.value });
175
+ if (!e.target) return;
176
+ const value = e.target.value;
177
+
178
+ setProp((prop) => (prop.fieldview = value));
179
+ setInitialConfig(setProp, value, getCfgFields(value));
180
+ refetchPreview({ fieldview: value });
180
181
  }}
181
182
  >
182
183
  {(fvs || []).map((fvnm, ix) => (
@@ -16,6 +16,7 @@ import {
16
16
  reactifyStyles,
17
17
  Accordion,
18
18
  OrFormula,
19
+ setAPropGen,
19
20
  } from "./utils";
20
21
 
21
22
  export /**
@@ -120,12 +121,7 @@ const ImageSettings = () => {
120
121
  });
121
122
  }
122
123
  };
123
- const setAProp = (key) => (e) => {
124
- if (e.target) {
125
- const target_value = e.target.value;
126
- setProp((prop) => (prop[key] = target_value));
127
- }
128
- };
124
+ const setAProp = setAPropGen(setProp);
129
125
  return (
130
126
  <Accordion>
131
127
  <table accordiontitle="Select image">
@@ -256,7 +252,7 @@ const ImageSettings = () => {
256
252
  )}
257
253
  {srctype !== "Upload" && (
258
254
  <tr>
259
- <td>
255
+ <td style={{ verticalAlign: "top" }}>
260
256
  <label>Responsive widths</label>
261
257
  </td>
262
258
 
@@ -122,15 +122,17 @@ const JoinFieldSettings = () => {
122
122
  value={name}
123
123
  className="form-control form-select"
124
124
  onChange={(e) => {
125
- setProp((prop) => (prop.name = e.target.value));
126
- const newfvs = options.field_view_options[e.target.value];
125
+ if (!e.target) return;
126
+ const value = e.target.value;
127
+ setProp((prop) => (prop.name = value));
128
+ const newfvs = options.field_view_options[value];
127
129
  if (newfvs && newfvs.length > 0) {
128
130
  setProp((prop) => (prop.fieldview = newfvs[0]));
129
131
  refetchPreview({
130
- name: e.target.value,
132
+ name: value,
131
133
  fieldview: newfvs[0],
132
134
  });
133
- } else refetchPreview({ name: e.target.value });
135
+ } else refetchPreview({ name: value });
134
136
  }}
135
137
  >
136
138
  {options.parent_field_list.map((f, ix) => (
@@ -152,8 +154,10 @@ const JoinFieldSettings = () => {
152
154
  value={fieldview}
153
155
  className="form-control form-select"
154
156
  onChange={(e) => {
155
- setProp((prop) => (prop.fieldview = e.target.value));
156
- refetchPreview({ fieldview: e.target.value });
157
+ if (!e.target) return;
158
+ const value = e.target.value;
159
+ setProp((prop) => (prop.fieldview = value));
160
+ refetchPreview({ fieldview: value });
157
161
  }}
158
162
  >
159
163
  {(fvs || []).map((fvnm, ix) => (