@saltcorn/builder 0.9.4-beta.8 → 0.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saltcorn/builder",
3
- "version": "0.9.4-beta.8",
3
+ "version": "0.9.4",
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",
@@ -20,6 +20,7 @@
20
20
  "@babel/preset-react": "7.9.4",
21
21
  "@craftjs/core": "0.1.0-beta.20",
22
22
  "@craftjs/utils": "0.1.0-beta.20",
23
+ "@saltcorn/common-code": "0.9.4",
23
24
  "saltcorn-craft-layers-noeye": "0.1.0-beta.22",
24
25
  "@fonticonpicker/react-fonticonpicker": "1.2.0",
25
26
  "@fortawesome/fontawesome-svg-core": "1.2.34",
@@ -38,6 +39,7 @@
38
39
  "react-bootstrap-icons": "1.5.0",
39
40
  "react-contenteditable": "3.3.5",
40
41
  "react-dom": "16.13.1",
42
+ "react-select": "4.3.1",
41
43
  "react-test-renderer": "16.13.1",
42
44
  "react-transition-group": "4.4.1",
43
45
  "@tippyjs/react": "4.2.6",
@@ -18,7 +18,7 @@ import { JoinField } from "./elements/JoinField";
18
18
  import { Aggregation } from "./elements/Aggregation";
19
19
  import { LineBreak } from "./elements/LineBreak";
20
20
  import { ViewLink } from "./elements/ViewLink";
21
- import { Columns } from "./elements/Columns";
21
+ import { Columns, ntimes } from "./elements/Columns";
22
22
  import { SearchBar } from "./elements/SearchBar";
23
23
  import { HTMLCode } from "./elements/HTMLCode";
24
24
  import { Action } from "./elements/Action";
@@ -37,6 +37,7 @@ import {
37
37
  ToolboxEdit,
38
38
  ToolboxPage,
39
39
  ToolboxFilter,
40
+ ToolboxList,
40
41
  } from "./Toolbox";
41
42
  import { craftToSaltcorn, layoutToNodes } from "./storage";
42
43
  import { Card } from "./elements/Card";
@@ -52,6 +53,8 @@ import {
52
53
  faRedo,
53
54
  faTrashAlt,
54
55
  faSave,
56
+ faExclamationTriangle,
57
+ faPlus,
55
58
  } from "@fortawesome/free-solid-svg-icons";
56
59
  import {
57
60
  faCaretSquareLeft,
@@ -64,6 +67,8 @@ import {
64
67
  } from "./elements/utils";
65
68
  import { InitNewElement, Library } from "./Library";
66
69
  import { RenderNode } from "./RenderNode";
70
+ import { ListColumn } from "./elements/ListColumn";
71
+ import { ListColumns } from "./elements/ListColumns";
67
72
  const { Provider } = optionsCtx;
68
73
 
69
74
  /**
@@ -252,58 +257,20 @@ const SettingsPanel = () => {
252
257
  );
253
258
  };
254
259
 
255
- /**
256
- * @returns {button}
257
- * @category saltcorn-builder
258
- * @subcategory components
259
- * @namespace
260
- */
261
- const SaveButton = () => {
260
+ const AddColumnButton = () => {
262
261
  const { query, actions } = useEditor(() => {});
263
262
  const options = useContext(optionsCtx);
264
-
265
- /**
266
- * @returns {void}
267
- */
268
- const onClick = () => {
269
- const data = craftToSaltcorn(JSON.parse(query.serialize()));
270
- const urlroot = options.page_id ? "pageedit" : "viewedit";
271
- fetch(`/${urlroot}/savebuilder/${options.page_id || options.view_id}`, {
272
- method: "POST", // or 'PUT'
273
- headers: {
274
- "Content-Type": "application/json",
275
- "CSRF-Token": options.csrfToken,
276
- },
277
- body: JSON.stringify(data),
278
- });
263
+ const addColumn = () => {
264
+ actions.addNodeTree(
265
+ query.parseReactElement(<ListColumn />).toNodeTree(),
266
+ "ROOT"
267
+ );
279
268
  };
280
- return options.page_id || options.view_id ? (
281
- <button
282
- className="btn btn-sm btn-outline-secondary me-2 builder-save-ajax"
283
- onClick={onClick}
284
- >
285
- Save
269
+ return (
270
+ <button className="btn btn-primary mt-2" onClick={addColumn}>
271
+ <FontAwesomeIcon icon={faPlus} className="me-2" />
272
+ Add column
286
273
  </button>
287
- ) : (
288
- ""
289
- );
290
- };
291
-
292
- /**
293
- * @returns {a|""}
294
- * @category saltcorn-builder
295
- * @subcategory components
296
- * @namespace
297
- */
298
- const ViewPageLink = () => {
299
- const { query, actions } = useEditor(() => {});
300
- const options = useContext(optionsCtx);
301
- return options.page_id ? (
302
- <a target="_blank" className="d-block" href={`/page/${options.page_name}`}>
303
- View page in new tab &raquo;
304
- </a>
305
- ) : (
306
- ""
307
274
  );
308
275
  };
309
276
 
@@ -356,14 +323,18 @@ const NextButton = ({ layout }) => {
356
323
  const options = useContext(optionsCtx);
357
324
 
358
325
  useEffect(() => {
359
- layoutToNodes(layout, query, actions);
326
+ layoutToNodes(layout, query, actions, "ROOT", options);
360
327
  }, []);
361
328
 
362
329
  /**
363
330
  * @returns {void}
364
331
  */
365
332
  const onClick = () => {
366
- const { columns, layout } = craftToSaltcorn(JSON.parse(query.serialize()));
333
+ const { columns, layout } = craftToSaltcorn(
334
+ JSON.parse(query.serialize()),
335
+ "ROOT",
336
+ options
337
+ );
367
338
  document
368
339
  .querySelector("form#scbuildform input[name=columns]")
369
340
  .setAttribute("value", encodeURIComponent(JSON.stringify(columns)));
@@ -394,7 +365,7 @@ const Builder = ({ options, layout, mode }) => {
394
365
  const [previews, setPreviews] = useState({});
395
366
  const [uploadedFiles, setUploadedFiles] = useState([]);
396
367
  const nodekeys = useRef([]);
397
- const [isSaving, setIsSaving] = useState(false);
368
+ const [savingState, setSavingState] = useState({ isSaving: false });
398
369
  const [isEnlarged, setIsEnlarged] = useState(false);
399
370
  const [isLeftEnlarged, setIsLeftEnlarged] = useState(false);
400
371
  const [relationsCache, setRelationsCache] = useState({});
@@ -422,12 +393,14 @@ const Builder = ({ options, layout, mode }) => {
422
393
  <div className="componets-and-library-accordion toolbox-card">
423
394
  <InitNewElement
424
395
  nodekeys={nodekeys}
425
- setIsSaving={setIsSaving}
396
+ setSavingState={setSavingState}
397
+ savingState={savingState}
426
398
  />
427
399
  <Accordion>
428
400
  <div className="card mt-1" accordiontitle="Components">
429
401
  {{
430
402
  show: <ToolboxShow expanded={isLeftEnlarged} />,
403
+ list: <ToolboxList expanded={isLeftEnlarged} />,
431
404
  edit: <ToolboxEdit expanded={isLeftEnlarged} />,
432
405
  page: <ToolboxPage expanded={isLeftEnlarged} />,
433
406
  filter: <ToolboxFilter expanded={isLeftEnlarged} />,
@@ -440,7 +413,7 @@ const Builder = ({ options, layout, mode }) => {
440
413
  </div>
441
414
  <div
442
415
  className="card toolbox-card pe-0"
443
- style={isLeftEnlarged ? { width: "12.35rem" } : {}}
416
+ style={isLeftEnlarged ? { width: "13.4rem" } : {}}
444
417
  >
445
418
  <div className="card-header p-2 d-flex justify-content-between">
446
419
  <div>Layers</div>
@@ -464,7 +437,9 @@ const Builder = ({ options, layout, mode }) => {
464
437
  </div>
465
438
  <div
466
439
  id="builder-main-canvas"
467
- className={`col builder-mode-${options.mode}`}
440
+ className={`col builder-mode-${options.mode} ${
441
+ options.mode !== "list" ? "emptymsg" : ""
442
+ }`}
468
443
  >
469
444
  <div>
470
445
  <Frame
@@ -491,10 +466,17 @@ const Builder = ({ options, layout, mode }) => {
491
466
  Tabs,
492
467
  Table,
493
468
  ToggleFilter,
469
+ ListColumn,
470
+ ListColumns,
494
471
  }}
495
472
  >
496
- <Element canvas is={Column}></Element>
473
+ {options.mode === "list" ? (
474
+ <Element canvas is={ListColumns}></Element>
475
+ ) : (
476
+ <Element canvas is={Column}></Element>
477
+ )}
497
478
  </Frame>
479
+ {options.mode === "list" ? <AddColumnButton /> : null}
498
480
  </div>
499
481
  </div>
500
482
  <div className="col-sm-auto builder-sidebar">
@@ -503,7 +485,12 @@ const Builder = ({ options, layout, mode }) => {
503
485
  <HistoryPanel />
504
486
  <FontAwesomeIcon
505
487
  icon={faSave}
506
- className={isSaving ? "d-inline" : "d-none"}
488
+ className={savingState.isSaving ? "d-inline" : "d-none"}
489
+ />
490
+ <FontAwesomeIcon
491
+ icon={faExclamationTriangle}
492
+ color="#ff0033"
493
+ className={savingState.error ? "d-inline" : "d-none"}
507
494
  />
508
495
  <FontAwesomeIcon
509
496
  icon={isEnlarged ? faCaretSquareRight : faCaretSquareLeft}
@@ -511,7 +498,13 @@ const Builder = ({ options, layout, mode }) => {
511
498
  onClick={() => setIsEnlarged(!isEnlarged)}
512
499
  title={isEnlarged ? "Shrink" : "Enlarge"}
513
500
  />
514
-
501
+ <div
502
+ className={` ${
503
+ savingState.error ? "d-block" : "d-none"
504
+ } my-2 fw-bold`}
505
+ >
506
+ your work is not being saved
507
+ </div>
515
508
  <SettingsPanel />
516
509
  </div>
517
510
  </div>
@@ -83,7 +83,7 @@ export /**
83
83
  * @subcategory components
84
84
  * @namespace
85
85
  */
86
- const InitNewElement = ({ nodekeys, setIsSaving }) => {
86
+ const InitNewElement = ({ nodekeys, savingState, setSavingState }) => {
87
87
  const [saveTimeout, setSaveTimeout] = useState(false);
88
88
  const savedData = useRef(false);
89
89
  const { actions, query, connectors } = useEditor((state, query) => {
@@ -93,7 +93,11 @@ const InitNewElement = ({ nodekeys, setIsSaving }) => {
93
93
  const doSave = (query) => {
94
94
  if (!query.serialize) return;
95
95
 
96
- const data = craftToSaltcorn(JSON.parse(query.serialize()));
96
+ const data = craftToSaltcorn(
97
+ JSON.parse(query.serialize()),
98
+ "ROOT",
99
+ options
100
+ );
97
101
  const urlroot = options.page_id ? "pageedit" : "viewedit";
98
102
  if (savedData.current === false) {
99
103
  //do not save on first call
@@ -103,7 +107,7 @@ const InitNewElement = ({ nodekeys, setIsSaving }) => {
103
107
  }
104
108
  if (isEqual(savedData.current, JSON.stringify(data.layout))) return;
105
109
  savedData.current = JSON.stringify(data.layout);
106
- setIsSaving(true);
110
+ setSavingState({ isSaving: true });
107
111
 
108
112
  fetch(`/${urlroot}/savebuilder/${options.page_id || options.view_id}`, {
109
113
  method: "POST", // or 'PUT'
@@ -112,9 +116,32 @@ const InitNewElement = ({ nodekeys, setIsSaving }) => {
112
116
  "CSRF-Token": options.csrfToken,
113
117
  },
114
118
  body: JSON.stringify(data),
115
- }).then(() => {
116
- setIsSaving(false);
117
- });
119
+ })
120
+ .then((response) => {
121
+ response.json().then((data) => {
122
+ if (typeof data?.error === "string") {
123
+ // don't log duplicates
124
+ if (!savingState.error)
125
+ window.notifyAlert({ type: "danger", text: data.error });
126
+ setSavingState({ isSaving: false, error: data.error });
127
+ } else setSavingState({ isSaving: false });
128
+ });
129
+ })
130
+ .catch((e) => {
131
+ const text =
132
+ e.message === "Failed to fetch"
133
+ ? "Network connection lost"
134
+ : e || "Unable to save";
135
+ // don't log duplicates
136
+ if (savingState.error) setSavingState({ isSaving: false, error: text });
137
+ else {
138
+ window.notifyAlert({ type: "danger", text: text });
139
+ setSavingState({
140
+ isSaving: false,
141
+ error: text,
142
+ });
143
+ }
144
+ });
118
145
  };
119
146
  const throttledSave = useThrottle(() => {
120
147
  doSave(query);
@@ -137,7 +164,8 @@ const InitNewElement = ({ nodekeys, setIsSaving }) => {
137
164
  layout.layout ? layout.layout : layout,
138
165
  query,
139
166
  actions,
140
- node.parent
167
+ node.parent,
168
+ options
141
169
  );
142
170
  setTimeout(() => {
143
171
  actions.delete(id);
@@ -189,7 +217,11 @@ const Library = ({ expanded }) => {
189
217
  * @returns {void}
190
218
  */
191
219
  const addSelected = () => {
192
- const layout = craftToSaltcorn(JSON.parse(query.serialize()), selected);
220
+ const layout = craftToSaltcorn(
221
+ JSON.parse(query.serialize()),
222
+ selected,
223
+ options
224
+ );
193
225
  const data = { layout, icon, name: newName };
194
226
  fetch(`/library/savefrombuilder`, {
195
227
  method: "POST", // or 'PUT'
@@ -80,6 +80,11 @@ const RenderNode = ({ render }) => {
80
80
  currentDOM.style.left = left;
81
81
  }, [dom, getPos]);
82
82
 
83
+ useEffect(() => {
84
+ if (name === "Column" && parent && parent !== "ROOT")
85
+ actions.selectNode(parent);
86
+ }, [isActive]);
87
+
83
88
  useEffect(() => {
84
89
  document
85
90
  .getElementById("builder-main-canvas")
@@ -490,7 +490,7 @@ const AggregationElem = ({ connectors, child_field_list, agg_field_opts }) => (
490
490
  >
491
491
  <Aggregation
492
492
  agg_relation={child_field_list[0]}
493
- agg_field={headOr(agg_field_opts[child_field_list[0]], "")}
493
+ agg_field={headOr(agg_field_opts[child_field_list[0]], "")?.name}
494
494
  stat={"Count"}
495
495
  textStyle={""}
496
496
  aggwhere={""}
@@ -570,6 +570,54 @@ const ToolboxShow = ({ expanded }) => {
570
570
  expanded
571
571
  );
572
572
  };
573
+ export /**
574
+ * @returns {Fragment}
575
+ * @category saltcorn-builder
576
+ * @subcategory components / Toolbox
577
+ * @namespace
578
+ */
579
+ const ToolboxList = ({ expanded }) => {
580
+ const { connectors, query } = useEditor();
581
+ const options = useContext(optionsCtx);
582
+ const {
583
+ fields,
584
+ field_view_options,
585
+ child_field_list,
586
+ agg_field_opts,
587
+ views,
588
+ images,
589
+ } = options;
590
+ return chunkToolBox(
591
+ [
592
+ <TextElem connectors={connectors} />,
593
+ <FieldElem
594
+ connectors={connectors}
595
+ fields={fields}
596
+ field_view_options={field_view_options}
597
+ />,
598
+ <JoinFieldElem connectors={connectors} options={options} />,
599
+ <ViewLinkElem connectors={connectors} options={options} />,
600
+ <ActionElem connectors={connectors} options={options} />,
601
+ <LinkElem connectors={connectors} />,
602
+ <AggregationElem
603
+ connectors={connectors}
604
+ child_field_list={child_field_list}
605
+ agg_field_opts={agg_field_opts}
606
+ />,
607
+ // <ViewElem connectors={connectors} views={views} />,
608
+ // <ContainerElem connectors={connectors} />,
609
+ // <CardElem connectors={connectors} />,
610
+ // <TabsElem connectors={connectors} />,
611
+ <HTMLElem connectors={connectors} />,
612
+ <DropMenuElem connectors={connectors} />,
613
+ // <TableElem connectors={connectors} />,
614
+ ...(options.allowMultipleElementsPerColumn
615
+ ? [<LineBreakElem connectors={connectors} />]
616
+ : []),
617
+ ],
618
+ expanded
619
+ );
620
+ };
573
621
 
574
622
  export /**
575
623
  * @returns {Fragment}
@@ -580,7 +628,13 @@ export /**
580
628
  const ToolboxFilter = ({ expanded }) => {
581
629
  const { connectors, query } = useEditor();
582
630
  const options = useContext(optionsCtx);
583
- const { fields, views, field_view_options } = options;
631
+ const {
632
+ fields,
633
+ views,
634
+ field_view_options,
635
+ child_field_list,
636
+ agg_field_opts,
637
+ } = options;
584
638
  return chunkToolBox(
585
639
  [
586
640
  <TextElem connectors={connectors} />,
@@ -595,6 +649,11 @@ const ToolboxFilter = ({ expanded }) => {
595
649
  <ToggleFilterElem connectors={connectors} fields={fields} />,
596
650
  <SearchElem connectors={connectors} />,
597
651
  <ActionElem connectors={connectors} options={options} />,
652
+ <AggregationElem
653
+ connectors={connectors}
654
+ child_field_list={child_field_list}
655
+ agg_field_opts={agg_field_opts}
656
+ />,
598
657
  <ContainerElem connectors={connectors} />,
599
658
  <CardElem connectors={connectors} />,
600
659
  <TabsElem connectors={connectors} />,
@@ -110,6 +110,7 @@ const ActionSettings = () => {
110
110
  step_only_ifs: node.data.props.step_only_ifs,
111
111
  step_action_names: node.data.props.step_action_names,
112
112
  setting_action_n: node.data.props.setting_action_n,
113
+ spinner: node.data.props.spinner,
113
114
  }));
114
115
  const {
115
116
  actions: { setProp },
@@ -127,6 +128,7 @@ const ActionSettings = () => {
127
128
  setting_action_n,
128
129
  step_only_ifs,
129
130
  step_action_names,
131
+ spinner,
130
132
  } = node;
131
133
  const options = useContext(optionsCtx);
132
134
  const getCfgFields = (fv) => (options.actionConfigForms || {})[fv];
@@ -278,6 +280,16 @@ const ActionSettings = () => {
278
280
  />
279
281
  <label className="form-check-label">User confirmation?</label>
280
282
  </div>
283
+ <div className="form-check">
284
+ <input
285
+ className="form-check-input"
286
+ name="block"
287
+ type="checkbox"
288
+ checked={spinner}
289
+ onChange={setAProp("spinner", { checked: true })}
290
+ />
291
+ <label className="form-check-label">Spinner on click</label>
292
+ </div>
281
293
  {action_style !== "on_page_load" ? (
282
294
  <BlockSetting block={block} setProp={setProp} />
283
295
  ) : null}