@saltcorn/builder 1.6.0-alpha.10 → 1.6.0-alpha.12

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": "1.6.0-alpha.10",
3
+ "version": "1.6.0-alpha.12",
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",
@@ -30,7 +30,7 @@
30
30
  "@fortawesome/free-solid-svg-icons": "5.15.2",
31
31
  "@fortawesome/react-fontawesome": "0.1.14",
32
32
  "@monaco-editor/react": "4.7.0",
33
- "@saltcorn/common-code": "1.6.0-alpha.10",
33
+ "@saltcorn/common-code": "1.6.0-alpha.12",
34
34
  "@tippyjs/react": "4.2.6",
35
35
  "babel-jest": "^29.7.0",
36
36
  "babel-loader": "9.2.1",
@@ -12,6 +12,7 @@ import React, {
12
12
  useRef,
13
13
  memo,
14
14
  } from "react";
15
+ import { createPortal } from "react-dom";
15
16
  import useTranslation from "../hooks/useTranslation";
16
17
  import { Editor, Frame, Element, Selector, useEditor, DefaultEventHandlers } from "@craftjs/core";
17
18
  import { Layers, useLayer } from "@craftjs/layers"
@@ -67,6 +68,7 @@ import {
67
68
  faCaretSquareRight,
68
69
  } from "@fortawesome/free-regular-svg-icons";
69
70
  import { Accordion, ErrorBoundary } from "./elements/utils";
71
+ import { Display, Tablet, Phone } from "react-bootstrap-icons";
70
72
  import { InitNewElement, Library, LibraryElem } from "./Library";
71
73
  import { RenderNode } from "./RenderNode";
72
74
  import { ListColumn } from "./elements/ListColumn";
@@ -104,7 +106,7 @@ const getFirstSelected = (selected) => {
104
106
  * @subcategory components
105
107
  * @namespace
106
108
  */
107
- const SettingsPanel = () => {
109
+ const SettingsPanel = ({ isEnlarged, setIsEnlarged }) => {
108
110
  const { t } = useTranslation();
109
111
  const options = useContext(optionsCtx);
110
112
 
@@ -160,6 +162,15 @@ const SettingsPanel = () => {
160
162
  const tagName = target.tagName.toLowerCase();
161
163
  const hasSelection = selectedCount > 0;
162
164
  if ((tagName === "body" || tagName === "button") && hasSelection) {
165
+ if (!selected && selectedCount > 1 && (keyCode === 8 || keyCode === 46)) {
166
+ const currentSelected = query.getEvent("selected");
167
+ const nodeIds = getSelectedNodes(currentSelected)
168
+ .map((nodeId) => (typeof nodeId === "string" ? nodeId : nodeId?.id))
169
+ .filter((nodeId) => nodeId && nodeId !== "ROOT");
170
+ nodeIds.forEach((nodeId) => {
171
+ try { actions.delete(nodeId); } catch (e) { /* node may already be deleted */ }
172
+ });
173
+ }
163
174
  if (selected) {
164
175
  if ((keyCode === 8 || keyCode === 46) && selected.id === "ROOT") {
165
176
  deleteChildren();
@@ -298,6 +309,14 @@ const SettingsPanel = () => {
298
309
  actions.history.redo();
299
310
  }
300
311
  }
312
+ if ((tagName === "body" || tagName === "button") &&
313
+ (event.ctrlKey || event.metaKey) && event.keyCode == 65) {
314
+ event.preventDefault();
315
+ const rootChildren = query.node("ROOT").childNodes();
316
+ if (rootChildren.length > 0) {
317
+ actions.selectNode(rootChildren);
318
+ }
319
+ }
301
320
  };
302
321
  useEffect(() => {
303
322
  window.addEventListener("keydown", handleUserKeyPress);
@@ -336,13 +355,23 @@ const SettingsPanel = () => {
336
355
 
337
356
  return (
338
357
  <div className="settings-panel card mt-1">
339
- <div className="card-header px-2 py-1">
340
- {selected && selected.displayName ? (
341
- <Fragment>
342
- <b>{selected.displayName}</b> settings
343
- </Fragment>
344
- ) : (
345
- t("Settings")
358
+ <div className="card-header px-2 py-1 d-flex justify-content-between align-items-center">
359
+ <div>
360
+ {selected && selected.displayName ? (
361
+ <Fragment>
362
+ <b>{selected.displayName}</b> settings
363
+ </Fragment>
364
+ ) : (
365
+ t("Settings")
366
+ )}
367
+ </div>
368
+ {setIsEnlarged && (
369
+ <FontAwesomeIcon
370
+ icon={isEnlarged ? faCaretSquareRight : faCaretSquareLeft}
371
+ className="fa-lg builder-expand-toggle-right"
372
+ onClick={() => setIsEnlarged(!isEnlarged)}
373
+ title={isEnlarged ? t("Shrink") : t("Enlarge")}
374
+ />
346
375
  )}
347
376
  </div>
348
377
  <div className="card-body p-2">
@@ -426,7 +455,7 @@ function useWindowDimensions() {
426
455
  * @namespace
427
456
  */
428
457
 
429
- const hiddenColumnParents = new Set(["Card", "Container", "Tabs", "Table", "DropMenu"]);
458
+ const hiddenColumnParents = new Set(["Card", "Container", "Tabs", "Table", "DropMenu", "ListColumn"]);
430
459
 
431
460
  const CustomLayerComponent = memo(({ children }) => {
432
461
  const {
@@ -443,7 +472,7 @@ const CustomLayerComponent = memo(({ children }) => {
443
472
  };
444
473
  });
445
474
 
446
- const { displayName, hasNodes, isHiddenColumn, connectors: editorConnectors } = useEditor((state) => {
475
+ const { displayName, hasNodes, isHiddenColumn, selected, connectors: editorConnectors } = useEditor((state) => {
447
476
  const node = state.nodes[id];
448
477
  const data = node?.data;
449
478
 
@@ -469,28 +498,33 @@ const CustomLayerComponent = memo(({ children }) => {
469
498
  }
470
499
  }
471
500
 
501
+ const isSelected = state.events?.selected?.has?.(id) || (state.events?.selected === id);
502
+
472
503
  return {
473
504
  displayName: name,
474
505
  hasNodes: hasChildren,
475
- isHiddenColumn: shouldHide
506
+ isHiddenColumn: shouldHide,
507
+ selected: isSelected
476
508
  };
477
509
  });
478
510
 
511
+ const isRoot = id === "ROOT";
512
+
479
513
  // Auto-expand hidden linked-node Columns so their children are always
480
514
  // visible through the transparent wrapper. Uses setExpandedState(true)
481
515
  // instead of toggleLayer() — it's idempotent (no-op when already true),
482
516
  // so it won't conflict with craft.js internals or cause toggle loops.
483
517
  useEffect(() => {
484
- if (isHiddenColumn && !expanded) {
518
+ if ((isHiddenColumn || isRoot) && !expanded) {
485
519
  setExpandedState(true);
486
520
  }
487
- }, [isHiddenColumn, expanded, setExpandedState]);
521
+ }, [isHiddenColumn, isRoot, expanded, setExpandedState]);
488
522
 
489
- if (isHiddenColumn) {
523
+ if (isHiddenColumn || isRoot) {
490
524
  return (
491
525
  <div
492
526
  ref={(dom) => { layer(dom); if (dom) editorConnectors.drop(dom, id); }}
493
- style={{ marginLeft: "-20px" }}
527
+ style={{ marginLeft: "-14px" }}
494
528
  >
495
529
  {children}
496
530
  </div>
@@ -501,9 +535,9 @@ const CustomLayerComponent = memo(({ children }) => {
501
535
  <div ref={(dom) => { layer(dom); if (dom) editorConnectors.drop(dom, id); }}>
502
536
  <div
503
537
  ref={(dom) => { drag(dom); layerHeader(dom); }}
504
- className={`builder-layer-node ${hovered ? "hovered" : ""}`}
538
+ className={`builder-layer-node ${hovered ? "hovered" : ""} ${selected ? "selected" : ""}`}
505
539
  style={{
506
- paddingLeft: `${depth * 20 + 10}px`,
540
+ paddingLeft: `${depth * 14 + 10}px`,
507
541
  }}
508
542
  >
509
543
  <span className="layer-name" style={{ flexGrow: 1 }}>{displayName}</span>
@@ -545,6 +579,38 @@ const AddColumnButton = () => {
545
579
  );
546
580
  };
547
581
 
582
+ const DEVICE_WIDTHS = {
583
+ desktop: null,
584
+ tablet: 768,
585
+ mobile: 375,
586
+ };
587
+
588
+ const DevicePreviewToolbar = ({ previewDevice, setPreviewDevice }) => {
589
+ const { t } = useTranslation();
590
+ const devices = [
591
+ { key: "desktop", icon: Display, label: t("Desktop") },
592
+ { key: "tablet", icon: Tablet, label: t("Tablet") },
593
+ { key: "mobile", icon: Phone, label: t("Mobile") },
594
+ ];
595
+
596
+ return (
597
+ <div className="device-preview-toolbar">
598
+ {devices.map(({ key, icon: Icon, label }) => (
599
+ <button
600
+ key={key}
601
+ className={`btn btn-sm ${
602
+ previewDevice === key ? "btn-primary" : "btn-outline-secondary"
603
+ } device-preview-btn`}
604
+ onClick={() => setPreviewDevice(key)}
605
+ title={label}
606
+ >
607
+ <Icon size={16} />
608
+ </button>
609
+ ))}
610
+ </div>
611
+ );
612
+ };
613
+
548
614
  /**
549
615
  * @returns {Fragment}
550
616
  * @category saltcorn-builder
@@ -644,6 +710,7 @@ const Builder = ({ options, layout, mode }) => {
644
710
  const [isEnlarged, setIsEnlarged] = useState(false);
645
711
  const [isLeftEnlarged, setIsLeftEnlarged] = useState(false);
646
712
  const [relationsCache, setRelationsCache] = useState({});
713
+ const [previewDevice, setPreviewDevice] = useState("desktop");
647
714
  const { windowWidth, windowHeight } = useWindowDimensions();
648
715
 
649
716
  const [builderHeight, setBuilderHeight] = useState(0);
@@ -703,7 +770,7 @@ const Builder = ({ options, layout, mode }) => {
703
770
  >
704
771
  <Provider value={options}>
705
772
  <PreviewCtx.Provider
706
- value={{ previews, setPreviews, uploadedFiles, setUploadedFiles }}
773
+ value={{ previews, setPreviews, uploadedFiles, setUploadedFiles, previewDevice }}
707
774
  >
708
775
  <RelationsCtx.Provider
709
776
  value={{
@@ -782,40 +849,51 @@ const Builder = ({ options, layout, mode }) => {
782
849
  options.mode !== "list" ? "emptymsg" : ""
783
850
  }`}
784
851
  >
785
- <div>
786
- <Frame>
787
- {options.mode === "list" ? (
788
- <Element canvas is={ListColumns}></Element>
789
- ) : (
790
- <Element canvas is={Column}></Element>
791
- )}
792
- </Frame>
793
- {options.mode === "list" ? <AddColumnButton /> : null}
852
+ <div className="device-preview-scroll-area">
853
+ <div
854
+ className={`device-preview-canvas-wrapper ${
855
+ previewDevice !== "desktop" ? "device-preview-constrained" : ""
856
+ }`}
857
+ style={{
858
+ maxWidth: DEVICE_WIDTHS[previewDevice]
859
+ ? `${DEVICE_WIDTHS[previewDevice]}px`
860
+ : "none",
861
+ }}
862
+ >
863
+ <Frame>
864
+ {options.mode === "list" ? (
865
+ <Element canvas is={ListColumns}></Element>
866
+ ) : (
867
+ <Element canvas is={Column}></Element>
868
+ )}
869
+ </Frame>
870
+ {options.mode === "list" ? <AddColumnButton /> : null}
871
+ </div>
794
872
  </div>
795
873
  </div>
796
874
  <div className="col-sm-auto builder-sidebar">
797
875
  <div style={{ width: isEnlarged ? "28rem" : "16rem" }}>
798
- <NextButton layout={layout} />
799
- <HistoryPanel />
800
- <FontAwesomeIcon
801
- icon={faSave}
802
- className={savingState.isSaving ? "d-inline" : "d-none"}
803
- />
804
- <FontAwesomeIcon
805
- icon={faExclamationTriangle}
806
- color="#ff0033"
807
- className={savingState.error ? "d-inline" : "d-none"}
808
- />
809
- <FontAwesomeIcon
810
- icon={
811
- isEnlarged ? faCaretSquareRight : faCaretSquareLeft
812
- }
813
- className={
814
- "float-end me-2 mt-1 fa-lg builder-expand-toggle-right"
815
- }
816
- onClick={() => setIsEnlarged(!isEnlarged)}
817
- title={isEnlarged ? t("Shrink") : t("Enlarge")}
876
+ <DevicePreviewToolbar
877
+ previewDevice={previewDevice}
878
+ setPreviewDevice={setPreviewDevice}
818
879
  />
880
+ {document.getElementById("builder-header-actions") &&
881
+ createPortal(
882
+ <Fragment>
883
+ <FontAwesomeIcon
884
+ icon={faSave}
885
+ className={savingState.isSaving ? "d-inline" : "d-none"}
886
+ />
887
+ <FontAwesomeIcon
888
+ icon={faExclamationTriangle}
889
+ color="#ff0033"
890
+ className={savingState.error ? "d-inline" : "d-none"}
891
+ />
892
+ <HistoryPanel />
893
+ <NextButton layout={layout} />
894
+ </Fragment>,
895
+ document.getElementById("builder-header-actions")
896
+ )}
819
897
  <div
820
898
  className={` ${
821
899
  savingState.error ? "d-block" : "d-none"
@@ -823,7 +901,7 @@ const Builder = ({ options, layout, mode }) => {
823
901
  >
824
902
  {t("your work is not being saved")}
825
903
  </div>
826
- <SettingsPanel />
904
+ <SettingsPanel isEnlarged={isEnlarged} setIsEnlarged={setIsEnlarged} />
827
905
  </div>
828
906
  </div>
829
907
  </div>
@@ -82,6 +82,7 @@ const RenderNode = ({ render }) => {
82
82
 
83
83
  const hiddenColumnParents = new Set(["Card", "Container", "Tabs", "Table", "DropMenu"]);
84
84
  useEffect(() => {
85
+ if (!isActive) return;
85
86
  if (name === "Column" && parent && parent !== "ROOT") {
86
87
  const parentNode = query.node(parent).get();
87
88
  const parentName = parentNode?.data?.displayName;
@@ -91,7 +92,13 @@ const RenderNode = ({ render }) => {
91
92
  parentLinked &&
92
93
  Object.values(parentLinked).includes(id)
93
94
  ) {
94
- actions.selectNode(parent);
95
+ const currentlySelected = query.getEvent("selected").all();
96
+ const otherSelected = currentlySelected.filter((nid) => nid !== id);
97
+ if (otherSelected.length > 0) {
98
+ actions.selectNode([...otherSelected, parent]);
99
+ } else {
100
+ actions.selectNode(parent);
101
+ }
95
102
  }
96
103
  }
97
104
  }, [isActive]);
@@ -56,6 +56,8 @@ export const ArrayManager = ({
56
56
  managedArrays,
57
57
  manageContents,
58
58
  initialAddProps,
59
+ contentsKey = "contents",
60
+ onLayoutChange,
59
61
  }) => {
60
62
  const { t } = useTranslation();
61
63
  const { actions, query, connectors } = useEditor((state, query) => {
@@ -77,13 +79,14 @@ export const ArrayManager = ({
77
79
  node.id,
78
80
  options
79
81
  );
80
- layout.contents.splice(rmIx, 1);
82
+ layout[contentsKey].splice(rmIx, 1);
81
83
 
82
84
  managedArrays.forEach((arrNm) => {
83
- layout[arrNm].splice(rmIx, 1);
85
+ if (layout[arrNm]) layout[arrNm].splice(rmIx, 1);
84
86
  });
85
87
  layout[countProp] = node[countProp] - 1;
86
- layout[currentProp] = node[currentProp] - 1;
88
+ layout[currentProp] = Math.max(0, node[currentProp] - 1);
89
+ if (onLayoutChange) onLayoutChange(layout, "delete");
87
90
  actions.delete(node.id);
88
91
  layoutToNodes(layout, query, actions, parentId, options, sibIx);
89
92
  } else {
@@ -119,7 +122,7 @@ export const ArrayManager = ({
119
122
  options
120
123
  );
121
124
 
122
- swapElements(layout.contents, curIx, curIx + delta);
125
+ swapElements(layout[contentsKey], curIx, curIx + delta);
123
126
 
124
127
  managedArrays.forEach((arrNm) => {
125
128
  if (arrNm.includes(".")) {
@@ -130,6 +133,7 @@ export const ArrayManager = ({
130
133
  swapElements(layout[arrNm], curIx, curIx + delta);
131
134
  });
132
135
  layout[currentProp] = node[currentProp] + delta;
136
+ if (onLayoutChange) onLayoutChange(layout, "move");
133
137
  actions.delete(node.id);
134
138
  layoutToNodes(layout, query, actions, parentId, options, sibIx);
135
139
  } else
@@ -156,7 +160,7 @@ export const ArrayManager = ({
156
160
  options
157
161
  );
158
162
 
159
- layout.contents.push(null);
163
+ layout[contentsKey].push(null);
160
164
  managedArrays.forEach((arrNm) => {
161
165
  if (initialAddProps?.[arrNm])
162
166
  layout[arrNm][node[countProp]] = initialAddProps?.[arrNm];
@@ -164,6 +168,7 @@ export const ArrayManager = ({
164
168
  layout[currentProp] = +node[countProp];
165
169
  layout[countProp] = +node[countProp] + 1;
166
170
 
171
+ if (onLayoutChange) onLayoutChange(layout, "add");
167
172
  actions.delete(node.id);
168
173
  layoutToNodes(layout, query, actions, parentId, options, sibIx);
169
174
  } else