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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saltcorn/builder",
3
- "version": "0.9.4-beta.2",
3
+ "version": "0.9.4-beta.21",
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-beta.21",
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,7 +53,13 @@ import {
52
53
  faRedo,
53
54
  faTrashAlt,
54
55
  faSave,
56
+ faExclamationTriangle,
57
+ faPlus,
55
58
  } from "@fortawesome/free-solid-svg-icons";
59
+ import {
60
+ faCaretSquareLeft,
61
+ faCaretSquareRight,
62
+ } from "@fortawesome/free-regular-svg-icons";
56
63
  import {
57
64
  Accordion,
58
65
  ErrorBoundary,
@@ -60,6 +67,8 @@ import {
60
67
  } from "./elements/utils";
61
68
  import { InitNewElement, Library } from "./Library";
62
69
  import { RenderNode } from "./RenderNode";
70
+ import { ListColumn } from "./elements/ListColumn";
71
+ import { ListColumns } from "./elements/ListColumns";
63
72
  const { Provider } = optionsCtx;
64
73
 
65
74
  /**
@@ -248,58 +257,20 @@ const SettingsPanel = () => {
248
257
  );
249
258
  };
250
259
 
251
- /**
252
- * @returns {button}
253
- * @category saltcorn-builder
254
- * @subcategory components
255
- * @namespace
256
- */
257
- const SaveButton = () => {
260
+ const AddColumnButton = () => {
258
261
  const { query, actions } = useEditor(() => {});
259
262
  const options = useContext(optionsCtx);
260
-
261
- /**
262
- * @returns {void}
263
- */
264
- const onClick = () => {
265
- const data = craftToSaltcorn(JSON.parse(query.serialize()));
266
- const urlroot = options.page_id ? "pageedit" : "viewedit";
267
- fetch(`/${urlroot}/savebuilder/${options.page_id || options.view_id}`, {
268
- method: "POST", // or 'PUT'
269
- headers: {
270
- "Content-Type": "application/json",
271
- "CSRF-Token": options.csrfToken,
272
- },
273
- body: JSON.stringify(data),
274
- });
263
+ const addColumn = () => {
264
+ actions.addNodeTree(
265
+ query.parseReactElement(<ListColumn />).toNodeTree(),
266
+ "ROOT"
267
+ );
275
268
  };
276
- return options.page_id || options.view_id ? (
277
- <button
278
- className="btn btn-sm btn-outline-secondary me-2 builder-save-ajax"
279
- onClick={onClick}
280
- >
281
- Save
269
+ return (
270
+ <button className="btn btn-primary mt-2" onClick={addColumn}>
271
+ <FontAwesomeIcon icon={faPlus} className="me-2" />
272
+ Add column
282
273
  </button>
283
- ) : (
284
- ""
285
- );
286
- };
287
-
288
- /**
289
- * @returns {a|""}
290
- * @category saltcorn-builder
291
- * @subcategory components
292
- * @namespace
293
- */
294
- const ViewPageLink = () => {
295
- const { query, actions } = useEditor(() => {});
296
- const options = useContext(optionsCtx);
297
- return options.page_id ? (
298
- <a target="_blank" className="d-block" href={`/page/${options.page_name}`}>
299
- View page in new tab &raquo;
300
- </a>
301
- ) : (
302
- ""
303
274
  );
304
275
  };
305
276
 
@@ -352,14 +323,18 @@ const NextButton = ({ layout }) => {
352
323
  const options = useContext(optionsCtx);
353
324
 
354
325
  useEffect(() => {
355
- layoutToNodes(layout, query, actions);
326
+ layoutToNodes(layout, query, actions, "ROOT", options);
356
327
  }, []);
357
328
 
358
329
  /**
359
330
  * @returns {void}
360
331
  */
361
332
  const onClick = () => {
362
- const { columns, layout } = craftToSaltcorn(JSON.parse(query.serialize()));
333
+ const { columns, layout } = craftToSaltcorn(
334
+ JSON.parse(query.serialize()),
335
+ "ROOT",
336
+ options
337
+ );
363
338
  document
364
339
  .querySelector("form#scbuildform input[name=columns]")
365
340
  .setAttribute("value", encodeURIComponent(JSON.stringify(columns)));
@@ -390,9 +365,10 @@ const Builder = ({ options, layout, mode }) => {
390
365
  const [previews, setPreviews] = useState({});
391
366
  const [uploadedFiles, setUploadedFiles] = useState([]);
392
367
  const nodekeys = useRef([]);
393
- const [isSaving, setIsSaving] = useState(false);
368
+ const [savingState, setSavingState] = useState({ isSaving: false });
369
+ const [isEnlarged, setIsEnlarged] = useState(false);
370
+ const [isLeftEnlarged, setIsLeftEnlarged] = useState(false);
394
371
  const [relationsCache, setRelationsCache] = useState({});
395
-
396
372
  return (
397
373
  <ErrorBoundary>
398
374
  <Editor onRender={RenderNode}>
@@ -407,28 +383,51 @@ const Builder = ({ options, layout, mode }) => {
407
383
  }}
408
384
  >
409
385
  <div className="row" style={{ marginTop: "-5px" }}>
410
- <div className="col-sm-auto left-builder-col">
386
+ <div
387
+ className={`col-sm-auto left-builder-col ${
388
+ isLeftEnlarged
389
+ ? "builder-left-enlarged"
390
+ : "builder-left-shrunk"
391
+ }`}
392
+ >
411
393
  <div className="componets-and-library-accordion toolbox-card">
412
394
  <InitNewElement
413
395
  nodekeys={nodekeys}
414
- setIsSaving={setIsSaving}
396
+ setSavingState={setSavingState}
397
+ savingState={savingState}
415
398
  />
416
399
  <Accordion>
417
400
  <div className="card mt-1" accordiontitle="Components">
418
401
  {{
419
- show: <ToolboxShow />,
420
- edit: <ToolboxEdit />,
421
- page: <ToolboxPage />,
422
- filter: <ToolboxFilter />,
402
+ show: <ToolboxShow expanded={isLeftEnlarged} />,
403
+ list: <ToolboxList expanded={isLeftEnlarged} />,
404
+ edit: <ToolboxEdit expanded={isLeftEnlarged} />,
405
+ page: <ToolboxPage expanded={isLeftEnlarged} />,
406
+ filter: <ToolboxFilter expanded={isLeftEnlarged} />,
423
407
  }[mode] || <div>Missing mode</div>}
424
408
  </div>
425
409
  <div accordiontitle="Library">
426
- <Library />
410
+ <Library expanded={isLeftEnlarged} />
427
411
  </div>
428
412
  </Accordion>
429
413
  </div>
430
- <div className="card toolbox-card pe-0">
431
- <div className="card-header">Layers</div>
414
+ <div
415
+ className="card toolbox-card pe-0"
416
+ style={isLeftEnlarged ? { width: "13.4rem" } : {}}
417
+ >
418
+ <div className="card-header p-2 d-flex justify-content-between">
419
+ <div>Layers</div>
420
+ <FontAwesomeIcon
421
+ icon={
422
+ isLeftEnlarged
423
+ ? faCaretSquareLeft
424
+ : faCaretSquareRight
425
+ }
426
+ className={"float-end fa-lg"}
427
+ onClick={() => setIsLeftEnlarged(!isLeftEnlarged)}
428
+ title={isLeftEnlarged ? "Shrink" : "Enlarge"}
429
+ />
430
+ </div>
432
431
  {showLayers && (
433
432
  <div className="card-body p-0 builder-layers">
434
433
  <Layers expandRootOnLoad={true} />
@@ -438,7 +437,9 @@ const Builder = ({ options, layout, mode }) => {
438
437
  </div>
439
438
  <div
440
439
  id="builder-main-canvas"
441
- className={`col builder-mode-${options.mode}`}
440
+ className={`col builder-mode-${options.mode} ${
441
+ options.mode !== "list" ? "emptymsg" : ""
442
+ }`}
442
443
  >
443
444
  <div>
444
445
  <Frame
@@ -465,20 +466,45 @@ const Builder = ({ options, layout, mode }) => {
465
466
  Tabs,
466
467
  Table,
467
468
  ToggleFilter,
469
+ ListColumn,
470
+ ListColumns,
468
471
  }}
469
472
  >
470
- <Element canvas is={Column}></Element>
473
+ {options.mode === "list" ? (
474
+ <Element canvas is={ListColumns}></Element>
475
+ ) : (
476
+ <Element canvas is={Column}></Element>
477
+ )}
471
478
  </Frame>
479
+ {options.mode === "list" ? <AddColumnButton /> : null}
472
480
  </div>
473
481
  </div>
474
482
  <div className="col-sm-auto builder-sidebar">
475
- <div style={{ width: "16rem" }}>
483
+ <div style={{ width: isEnlarged ? "28rem" : "16rem" }}>
476
484
  <NextButton layout={layout} />
477
485
  <HistoryPanel />
478
486
  <FontAwesomeIcon
479
487
  icon={faSave}
480
- 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"}
494
+ />
495
+ <FontAwesomeIcon
496
+ icon={isEnlarged ? faCaretSquareRight : faCaretSquareLeft}
497
+ className={"float-end me-2 mt-1 fa-lg"}
498
+ onClick={() => setIsEnlarged(!isEnlarged)}
499
+ title={isEnlarged ? "Shrink" : "Enlarge"}
481
500
  />
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>
482
508
  <SettingsPanel />
483
509
  </div>
484
510
  </div>
@@ -20,18 +20,7 @@ import faIcons from "./elements/faicons";
20
20
  import { craftToSaltcorn, layoutToNodes } from "./storage";
21
21
  import optionsCtx from "./context";
22
22
  import { WrapElem } from "./Toolbox";
23
- import { isEqual, throttle } from "lodash";
24
-
25
- /**
26
- *
27
- * @param {object[]} xs
28
- * @returns {object[]}
29
- */
30
- const twoByTwos = (xs) => {
31
- if (xs.length <= 2) return [xs];
32
- const [x, y, ...rest] = xs;
33
- return [[x, y], ...twoByTwos(rest)];
34
- };
23
+ import { isEqual, throttle, chunk } from "lodash";
35
24
 
36
25
  export /**
37
26
  * @param {object} props
@@ -94,7 +83,7 @@ export /**
94
83
  * @subcategory components
95
84
  * @namespace
96
85
  */
97
- const InitNewElement = ({ nodekeys, setIsSaving }) => {
86
+ const InitNewElement = ({ nodekeys, savingState, setSavingState }) => {
98
87
  const [saveTimeout, setSaveTimeout] = useState(false);
99
88
  const savedData = useRef(false);
100
89
  const { actions, query, connectors } = useEditor((state, query) => {
@@ -104,7 +93,11 @@ const InitNewElement = ({ nodekeys, setIsSaving }) => {
104
93
  const doSave = (query) => {
105
94
  if (!query.serialize) return;
106
95
 
107
- const data = craftToSaltcorn(JSON.parse(query.serialize()));
96
+ const data = craftToSaltcorn(
97
+ JSON.parse(query.serialize()),
98
+ "ROOT",
99
+ options
100
+ );
108
101
  const urlroot = options.page_id ? "pageedit" : "viewedit";
109
102
  if (savedData.current === false) {
110
103
  //do not save on first call
@@ -114,7 +107,7 @@ const InitNewElement = ({ nodekeys, setIsSaving }) => {
114
107
  }
115
108
  if (isEqual(savedData.current, JSON.stringify(data.layout))) return;
116
109
  savedData.current = JSON.stringify(data.layout);
117
- setIsSaving(true);
110
+ setSavingState({ isSaving: true });
118
111
 
119
112
  fetch(`/${urlroot}/savebuilder/${options.page_id || options.view_id}`, {
120
113
  method: "POST", // or 'PUT'
@@ -123,9 +116,32 @@ const InitNewElement = ({ nodekeys, setIsSaving }) => {
123
116
  "CSRF-Token": options.csrfToken,
124
117
  },
125
118
  body: JSON.stringify(data),
126
- }).then(() => {
127
- setIsSaving(false);
128
- });
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
+ });
129
145
  };
130
146
  const throttledSave = useThrottle(() => {
131
147
  doSave(query);
@@ -148,7 +164,8 @@ const InitNewElement = ({ nodekeys, setIsSaving }) => {
148
164
  layout.layout ? layout.layout : layout,
149
165
  query,
150
166
  actions,
151
- node.parent
167
+ node.parent,
168
+ options
152
169
  );
153
170
  setTimeout(() => {
154
171
  actions.delete(id);
@@ -184,7 +201,7 @@ export /**
184
201
  * @subcategory components
185
202
  * @namespace
186
203
  */
187
- const Library = () => {
204
+ const Library = ({ expanded }) => {
188
205
  const { actions, selected, query, connectors } = useEditor((state, query) => {
189
206
  return {
190
207
  selected: state.events.selected,
@@ -200,7 +217,11 @@ const Library = () => {
200
217
  * @returns {void}
201
218
  */
202
219
  const addSelected = () => {
203
- const layout = craftToSaltcorn(JSON.parse(query.serialize()), selected);
220
+ const layout = craftToSaltcorn(
221
+ JSON.parse(query.serialize()),
222
+ selected,
223
+ options
224
+ );
204
225
  const data = { layout, icon, name: newName };
205
226
  fetch(`/library/savefrombuilder`, {
206
227
  method: "POST", // or 'PUT'
@@ -216,7 +237,10 @@ const Library = () => {
216
237
  setRecent([...recent, data]);
217
238
  };
218
239
 
219
- const elemRows = twoByTwos([...(options.library || []), ...recent]);
240
+ const elemRows = chunk(
241
+ [...(options.library || []), ...recent],
242
+ expanded ? 3 : 2
243
+ );
220
244
  return (
221
245
  <div className="builder-library">
222
246
  <div className="dropdown">
@@ -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")