@saltcorn/builder 1.6.0-alpha.7 → 1.6.0-alpha.9

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.
Files changed (40) hide show
  1. package/dist/builder_bundle.js +104014 -2
  2. package/dist/builder_bundle.js.LICENSE.txt +65 -0
  3. package/package.json +32 -28
  4. package/src/components/Builder.js +334 -122
  5. package/src/components/Library.js +25 -13
  6. package/src/components/RenderNode.js +6 -6
  7. package/src/components/Toolbox.js +333 -269
  8. package/src/components/elements/Action.js +144 -29
  9. package/src/components/elements/Aggregation.js +20 -23
  10. package/src/components/elements/ArrayManager.js +7 -5
  11. package/src/components/elements/BoxModelEditor.js +19 -17
  12. package/src/components/elements/Card.js +47 -34
  13. package/src/components/elements/Clone.js +74 -2
  14. package/src/components/elements/Column.js +1 -1
  15. package/src/components/elements/Columns.js +27 -25
  16. package/src/components/elements/Container.js +170 -90
  17. package/src/components/elements/DropDownFilter.js +10 -8
  18. package/src/components/elements/DropMenu.js +8 -5
  19. package/src/components/elements/Field.js +9 -7
  20. package/src/components/elements/HTMLCode.js +3 -1
  21. package/src/components/elements/Image.js +20 -15
  22. package/src/components/elements/JoinField.js +15 -11
  23. package/src/components/elements/Link.js +18 -16
  24. package/src/components/elements/ListColumn.js +7 -3
  25. package/src/components/elements/ListColumns.js +4 -1
  26. package/src/components/elements/MonacoEditor.js +4 -2
  27. package/src/components/elements/Page.js +7 -4
  28. package/src/components/elements/RelationBadges.js +16 -11
  29. package/src/components/elements/RelationOnDemandPicker.js +18 -12
  30. package/src/components/elements/SearchBar.js +10 -6
  31. package/src/components/elements/Table.js +72 -65
  32. package/src/components/elements/Tabs.js +18 -15
  33. package/src/components/elements/Text.js +19 -14
  34. package/src/components/elements/ToggleFilter.js +28 -25
  35. package/src/components/elements/View.js +36 -18
  36. package/src/components/elements/ViewLink.js +15 -11
  37. package/src/components/elements/utils.js +224 -55
  38. package/src/components/storage.js +27 -129
  39. package/src/hooks/useTranslation.js +11 -0
  40. package/src/index.js +6 -3
@@ -10,8 +10,11 @@ import React, {
10
10
  useState,
11
11
  Fragment,
12
12
  useRef,
13
+ memo,
13
14
  } from "react";
14
- import { Editor, Frame, Element, Selector, useEditor } from "@craftjs/core";
15
+ import useTranslation from "../hooks/useTranslation";
16
+ import { Editor, Frame, Element, Selector, useEditor, DefaultEventHandlers } from "@craftjs/core";
17
+ import { Layers, useLayer } from "@craftjs/layers"
15
18
  import { Text } from "./elements/Text";
16
19
  import { Field } from "./elements/Field";
17
20
  import { JoinField } from "./elements/JoinField";
@@ -46,7 +49,7 @@ import { Link } from "./elements/Link";
46
49
  import { View } from "./elements/View";
47
50
  import { Container } from "./elements/Container";
48
51
  import { Column } from "./elements/Column";
49
- import { Layers } from "saltcorn-craft-layers-noeye";
52
+ // import { Layers } from "saltcorn-craft-layers-noeye";
50
53
  import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
51
54
  import {
52
55
  faCopy,
@@ -56,13 +59,15 @@ import {
56
59
  faSave,
57
60
  faExclamationTriangle,
58
61
  faPlus,
62
+ faChevronDown,
63
+ faChevronUp
59
64
  } from "@fortawesome/free-solid-svg-icons";
60
65
  import {
61
66
  faCaretSquareLeft,
62
67
  faCaretSquareRight,
63
68
  } from "@fortawesome/free-regular-svg-icons";
64
69
  import { Accordion, ErrorBoundary } from "./elements/utils";
65
- import { InitNewElement, Library } from "./Library";
70
+ import { InitNewElement, Library, LibraryElem } from "./Library";
66
71
  import { RenderNode } from "./RenderNode";
67
72
  import { ListColumn } from "./elements/ListColumn";
68
73
  import { ListColumns } from "./elements/ListColumns";
@@ -70,6 +75,28 @@ import { recursivelyCloneToElems } from "./elements/Clone";
70
75
 
71
76
  const { Provider } = optionsCtx;
72
77
 
78
+ const getSelectedNodes = (selected) => {
79
+ if (!selected) return [];
80
+ if (typeof selected.all === "function") {
81
+ return selected.all();
82
+ }
83
+ if (Array.isArray(selected.all)) {
84
+ return selected.all;
85
+ }
86
+ if (typeof selected.values === "function") {
87
+ return Array.from(selected.values());
88
+ }
89
+ if (typeof selected.has === "function") {
90
+ return [...selected];
91
+ }
92
+ return [selected];
93
+ };
94
+
95
+ const getFirstSelected = (selected) => {
96
+ const nodes = getSelectedNodes(selected);
97
+ return nodes.length > 0 ? nodes[0] : null;
98
+ };
99
+
73
100
  /**
74
101
  *
75
102
  * @returns {div}
@@ -78,10 +105,12 @@ const { Provider } = optionsCtx;
78
105
  * @namespace
79
106
  */
80
107
  const SettingsPanel = () => {
108
+ const { t } = useTranslation();
81
109
  const options = useContext(optionsCtx);
82
110
 
83
- const { actions, selected, query } = useEditor((state, query) => {
84
- const currentNodeId = state.events.selected;
111
+ const { actions, selected, selectedCount, query } = useEditor((state, query) => {
112
+ const selectedNodes = getSelectedNodes(state.events.selected);
113
+ const currentNodeId = selectedNodes.length === 1 ? selectedNodes[0] : null;
85
114
  let selected;
86
115
 
87
116
  if (currentNodeId) {
@@ -104,6 +133,7 @@ const SettingsPanel = () => {
104
133
 
105
134
  return {
106
135
  selected,
136
+ selectedCount: selectedNodes.length,
107
137
  };
108
138
  });
109
139
 
@@ -128,74 +158,126 @@ const SettingsPanel = () => {
128
158
  const handleUserKeyPress = (event) => {
129
159
  const { keyCode, target } = event;
130
160
  const tagName = target.tagName.toLowerCase();
131
- if ((tagName === "body" || tagName === "button") && selected) {
132
- //8 backsp, 46 del
133
- if ((keyCode === 8 || keyCode === 46) && selected.id === "ROOT") {
134
- deleteChildren();
135
- }
136
- if (keyCode === 8) {
137
- //backspace
138
- const prevSib = otherSibling(-1);
139
- const parent = selected.parent;
140
- deleteThis();
141
- if (prevSib) actions.selectNode(prevSib);
142
- else actions.selectNode(parent);
143
- }
144
- if (keyCode === 46) {
145
- //del
146
- const nextSib = otherSibling(1);
147
- deleteThis();
148
- if (nextSib) actions.selectNode(nextSib);
149
- }
150
- if (keyCode === 37 && selected.parent)
151
- //left
152
- actions.selectNode(selected.parent);
153
-
154
- if (keyCode === 39) {
155
- //right
156
- if (selected.children && selected.children.length > 0) {
157
- actions.selectNode(selected.children[0]);
158
- } else if (selected.displayName === "Columns") {
159
- const node = query.node(selected.id).get();
160
- const child = node?.data?.linkedNodes?.Col0;
161
- if (child) actions.selectNode(child);
161
+ const hasSelection = selectedCount > 0;
162
+ if ((tagName === "body" || tagName === "button") && hasSelection) {
163
+ if (selected) {
164
+ if ((keyCode === 8 || keyCode === 46) && selected.id === "ROOT") {
165
+ deleteChildren();
166
+ }
167
+ if (keyCode === 8) {
168
+ //backspace
169
+ const prevSib = otherSibling(-1);
170
+ const parent = selected.parent;
171
+ deleteThis();
172
+ if (prevSib) actions.selectNode(prevSib);
173
+ else actions.selectNode(parent);
174
+ }
175
+ if (keyCode === 46) {
176
+ //del
177
+ const nextSib = otherSibling(1);
178
+ deleteThis();
179
+ if (nextSib) actions.selectNode(nextSib);
180
+ }
181
+ if (keyCode === 37 && selected.parent)
182
+ //left
183
+ actions.selectNode(selected.parent);
184
+
185
+ if (keyCode === 39) {
186
+ //right
187
+ if (selected.children && selected.children.length > 0) {
188
+ actions.selectNode(selected.children[0]);
189
+ } else if (selected.displayName === "Columns") {
190
+ const node = query.node(selected.id).get();
191
+ const child = node?.data?.linkedNodes?.Col0;
192
+ if (child) actions.selectNode(child);
193
+ }
194
+ }
195
+ if (keyCode === 38 && selected.parent) {
196
+ //up
197
+ const prevSib = otherSibling(-1);
198
+ if (prevSib) actions.selectNode(prevSib);
199
+ event.preventDefault();
200
+ }
201
+ if (keyCode === 40 && selected.parent) {
202
+ //down
203
+ const nextSib = otherSibling(1);
204
+ if (nextSib) actions.selectNode(nextSib);
205
+ event.preventDefault();
162
206
  }
163
207
  }
164
- if (keyCode === 38 && selected.parent) {
165
- //up
166
- const prevSib = otherSibling(-1);
167
- if (prevSib) actions.selectNode(prevSib);
168
- event.preventDefault();
169
- }
170
- if (keyCode === 40 && selected.parent) {
171
- //down
172
- const nextSib = otherSibling(1);
173
- if (nextSib) actions.selectNode(nextSib);
174
- event.preventDefault();
175
- }
176
- // Ctrl+C or Cmd+C pressed?
177
- if ((event.ctrlKey || event.metaKey) && event.keyCode == 67 && selected) {
178
- // copy elem in json format to clipboard
179
- const { layout } = craftToSaltcorn(
180
- JSON.parse(query.serialize()),
181
- selected?.id,
182
- options
183
- );
184
- navigator.clipboard.writeText(JSON.stringify(layout, null, 2));
208
+ if ((event.ctrlKey || event.metaKey) && event.keyCode == 67) {
209
+ const serialized = JSON.parse(query.serialize());
210
+ const serializedIds = new Set(Object.keys(serialized));
211
+ const currentSelected = query.getEvent("selected");
212
+ const rawSelected = getSelectedNodes(currentSelected);
213
+ if (rawSelected.length === 0 && selected?.id) rawSelected.push(selected.id);
214
+ const selectedNodes = rawSelected
215
+ .map((nodeId) => (typeof nodeId === "string" ? nodeId : nodeId?.id))
216
+ .filter(
217
+ (nodeId) =>
218
+ nodeId && nodeId !== "ROOT" && serializedIds.has(nodeId)
219
+ );
220
+ if (selectedNodes.length === 0) return;
221
+
222
+ if (selectedNodes.length === 1) {
223
+ const { layout } = craftToSaltcorn(
224
+ serialized,
225
+ selectedNodes[0],
226
+ options
227
+ );
228
+ navigator.clipboard.writeText(JSON.stringify(layout, null, 2));
229
+ } else {
230
+ const layouts = selectedNodes.map((nodeId) => {
231
+ const { layout } = craftToSaltcorn(
232
+ serialized,
233
+ nodeId,
234
+ options
235
+ );
236
+ return layout;
237
+ });
238
+ navigator.clipboard.writeText(
239
+ JSON.stringify({ above: layouts }, null, 2)
240
+ );
241
+ }
185
242
  }
186
- if ((event.ctrlKey || event.metaKey) && event.keyCode == 88 && selected) {
187
- // cut elem in json format to clipboard
188
- const { layout } = craftToSaltcorn(
189
- JSON.parse(query.serialize()),
190
- selected?.id,
191
- options
192
- );
193
- navigator.clipboard.writeText(JSON.stringify(layout, null, 2));
194
- deleteThis();
243
+ if ((event.ctrlKey || event.metaKey) && event.keyCode == 88) {
244
+ const serialized = JSON.parse(query.serialize());
245
+ const serializedIds = new Set(Object.keys(serialized));
246
+ const currentSelected = query.getEvent("selected");
247
+ const rawSelected = getSelectedNodes(currentSelected);
248
+ if (rawSelected.length === 0 && selected?.id) rawSelected.push(selected.id);
249
+ const selectedNodes = rawSelected
250
+ .map((nodeId) => (typeof nodeId === "string" ? nodeId : nodeId?.id))
251
+ .filter(
252
+ (nodeId) =>
253
+ nodeId && nodeId !== "ROOT" && serializedIds.has(nodeId)
254
+ );
255
+ if (selectedNodes.length === 0) return;
256
+
257
+ if (selectedNodes.length === 1) {
258
+ const { layout } = craftToSaltcorn(
259
+ serialized,
260
+ selectedNodes[0],
261
+ options
262
+ );
263
+ navigator.clipboard.writeText(JSON.stringify(layout, null, 2));
264
+ actions.delete(selectedNodes[0]);
265
+ } else {
266
+ const layouts = selectedNodes.map((nodeId) => {
267
+ const { layout } = craftToSaltcorn(
268
+ serialized,
269
+ nodeId,
270
+ options
271
+ );
272
+ return layout;
273
+ });
274
+ navigator.clipboard.writeText(
275
+ JSON.stringify({ above: layouts }, null, 2)
276
+ );
277
+ selectedNodes.forEach((nodeId) => actions.delete(nodeId));
278
+ }
195
279
  }
196
280
  if ((event.ctrlKey || event.metaKey) && event.keyCode == 86) {
197
- // paste elem from clipboard into container element
198
-
199
281
  navigator.clipboard.readText().then((clipText) => {
200
282
  const layout = JSON.parse(clipText);
201
283
  layoutToNodes(
@@ -245,7 +327,6 @@ const SettingsPanel = () => {
245
327
  const siblings = query.node(selected.parent).childNodes();
246
328
  const sibIx = siblings.findIndex((sib) => sib === selected.id);
247
329
  const elem = recursivelyCloneToElems(query)(selected.id);
248
- //console.log(elem);
249
330
  actions.addNodeTree(
250
331
  query.parseReactElement(elem).toNodeTree(),
251
332
  parent || "ROOT",
@@ -261,11 +342,16 @@ const SettingsPanel = () => {
261
342
  <b>{selected.displayName}</b> settings
262
343
  </Fragment>
263
344
  ) : (
264
- "Settings"
345
+ t("Settings")
265
346
  )}
266
347
  </div>
267
348
  <div className="card-body p-2">
268
- {selected ? (
349
+ {selectedCount > 1 ? (
350
+ <div>
351
+ <p><strong>{selectedCount} {t("elements selected")}</strong></p>
352
+ <p className="text-muted small">{t("Multi-selection active. Use Shift+Click to add/remove elements.")}</p>
353
+ </div>
354
+ ) : selected ? (
269
355
  <Fragment>
270
356
  {selected.isDeletable && (
271
357
  <button
@@ -273,7 +359,7 @@ const SettingsPanel = () => {
273
359
  onClick={deleteThis}
274
360
  >
275
361
  <FontAwesomeIcon icon={faTrashAlt} className="me-1" />
276
- Delete
362
+ {t("Delete")}
277
363
  </button>
278
364
  )}
279
365
  {hasChildren && !selected.isDeletable ? (
@@ -282,23 +368,23 @@ const SettingsPanel = () => {
282
368
  onClick={deleteChildren}
283
369
  >
284
370
  <FontAwesomeIcon icon={faTrashAlt} className="me-1" />
285
- Delete contents
371
+ {t("Delete contents")}
286
372
  </button>
287
373
  ) : (
288
374
  <button
289
- title="Duplicate element with its children"
375
+ title={t("Duplicate element with its children")}
290
376
  className="btn btn-sm btn-secondary ms-2 duplicate-element-builder"
291
377
  onClick={duplicate}
292
378
  >
293
379
  <FontAwesomeIcon icon={faCopy} className="me-1" />
294
- Clone
380
+ {t("Clone")}
295
381
  </button>
296
382
  )}
297
383
  <hr className="my-2" />
298
384
  {selected.settings && React.createElement(selected.settings)}
299
385
  </Fragment>
300
386
  ) : (
301
- "No element selected"
387
+ t("No element selected")
302
388
  )}
303
389
  </div>
304
390
  </div>
@@ -331,7 +417,115 @@ function useWindowDimensions() {
331
417
  return windowDimensions;
332
418
  }
333
419
 
420
+ /**
421
+ * Custom Layer Component for Craft.js Layers panel
422
+ * Must be defined outside Builder component and memoized to prevent infinite re-renders
423
+ * Added defensive checks for layer properties
424
+ * @category saltcorn-builder
425
+ * @subcategory components
426
+ * @namespace
427
+ */
428
+
429
+ const hiddenColumnParents = new Set(["Card", "Container", "Tabs", "Table"]);
430
+
431
+ const CustomLayerComponent = memo(({ children }) => {
432
+ const {
433
+ id,
434
+ depth,
435
+ expanded,
436
+ hovered,
437
+ actions: { toggleLayer, setExpandedState },
438
+ connectors: { layer, drag },
439
+ } = useLayer((layer) => {
440
+ return {
441
+ hovered: layer?.event?.hovered,
442
+ expanded: layer?.expanded,
443
+ };
444
+ });
445
+
446
+ const { displayName, hasNodes, isHiddenColumn } = useEditor((state) => {
447
+ const node = state.nodes[id];
448
+ const data = node?.data;
449
+
450
+ let name = data?.displayName || data?.name || id;
451
+ if (name === "ROOT" || name === "Canvas") {
452
+ name = data?.name || name;
453
+ }
454
+
455
+ const nodes = data?.nodes;
456
+ const linkedNodes = data?.linkedNodes;
457
+ const hasChildren = (nodes && nodes.length > 0) || (linkedNodes && Object.keys(linkedNodes).length > 0);
458
+
459
+ // Check if this Column is a linked node of a Card/Container/Tabs/Table
460
+ let shouldHide = false;
461
+ if (name === "Column" && data?.parent) {
462
+ const parentNode = state.nodes[data.parent];
463
+ const parentName = parentNode?.data?.displayName || parentNode?.data?.name;
464
+ if (hiddenColumnParents.has(parentName)) {
465
+ const parentLinked = parentNode?.data?.linkedNodes;
466
+ if (parentLinked && Object.values(parentLinked).includes(id)) {
467
+ shouldHide = true;
468
+ }
469
+ }
470
+ }
471
+
472
+ return {
473
+ displayName: name,
474
+ hasNodes: hasChildren,
475
+ isHiddenColumn: shouldHide
476
+ };
477
+ });
478
+
479
+ // Auto-expand hidden linked-node Columns so their children are always
480
+ // visible through the transparent wrapper. Uses setExpandedState(true)
481
+ // instead of toggleLayer() — it's idempotent (no-op when already true),
482
+ // so it won't conflict with craft.js internals or cause toggle loops.
483
+ useEffect(() => {
484
+ if (isHiddenColumn && !expanded) {
485
+ setExpandedState(true);
486
+ }
487
+ }, [isHiddenColumn, expanded, setExpandedState]);
488
+
489
+ if (isHiddenColumn) {
490
+ return (
491
+ <div
492
+ ref={(dom) => { layer(dom); drag(dom); }}
493
+ style={{ marginLeft: "-20px" }}
494
+ >
495
+ {children}
496
+ </div>
497
+ );
498
+ }
499
+
500
+ return (
501
+ <div>
502
+ <div
503
+ ref={(dom) => { layer(dom); drag(dom); }}
504
+ className={`builder-layer-node ${hovered ? "hovered" : ""}`}
505
+ style={{
506
+ paddingLeft: `${depth * 20 + 10}px`,
507
+ }}
508
+ >
509
+ <span className="layer-name" style={{ flexGrow: 1 }}>{displayName}</span>
510
+
511
+ {hasNodes && (
512
+ <span
513
+ onClick={(e) => {
514
+ e.stopPropagation();
515
+ toggleLayer();
516
+ }}
517
+ >
518
+ <FontAwesomeIcon icon={expanded ? faChevronUp : faChevronDown} fontSize={14} className="float-end fa-lg" />
519
+ </span>
520
+ )}
521
+ </div>
522
+ {children}
523
+ </div>
524
+ );
525
+ });
526
+
334
527
  const AddColumnButton = () => {
528
+ const { t } = useTranslation();
335
529
  const { query, actions } = useEditor(() => {});
336
530
  const options = useContext(optionsCtx);
337
531
  const addColumn = () => {
@@ -346,7 +540,7 @@ const AddColumnButton = () => {
346
540
  onClick={addColumn}
347
541
  >
348
542
  <FontAwesomeIcon icon={faPlus} className="me-2" />
349
- Add column
543
+ {t("Add column")}
350
544
  </button>
351
545
  );
352
546
  };
@@ -358,6 +552,7 @@ const AddColumnButton = () => {
358
552
  * @namespace
359
553
  */
360
554
  const HistoryPanel = () => {
555
+ const { t } = useTranslation();
361
556
  const { canUndo, canRedo, actions } = useEditor((state, query) => ({
362
557
  canUndo: query.history.canUndo(),
363
558
  canRedo: query.history.canRedo(),
@@ -368,7 +563,7 @@ const HistoryPanel = () => {
368
563
  {canUndo && (
369
564
  <button
370
565
  className="btn btn-sm btn-secondary ms-2 me-2 undo-builder"
371
- title="Undo"
566
+ title={t("Undo")}
372
567
  onClick={() => actions.history.undo()}
373
568
  >
374
569
  <FontAwesomeIcon icon={faUndo} />
@@ -377,7 +572,7 @@ const HistoryPanel = () => {
377
572
  {canRedo && (
378
573
  <button
379
574
  className="btn btn-sm btn-secondary redo-builder"
380
- title="Redo"
575
+ title={t("Redo")}
381
576
  onClick={() => actions.history.redo()}
382
577
  >
383
578
  <FontAwesomeIcon icon={faRedo} />
@@ -437,7 +632,10 @@ const NextButton = ({ layout }) => {
437
632
  * @subcategory components
438
633
  * @namespace
439
634
  */
635
+
636
+
440
637
  const Builder = ({ options, layout, mode }) => {
638
+ const { t } = useTranslation();
441
639
  const [showLayers, setShowLayers] = useState(true);
442
640
  const [previews, setPreviews] = useState({});
443
641
  const [uploadedFiles, setUploadedFiles] = useState([]);
@@ -453,18 +651,56 @@ const Builder = ({ options, layout, mode }) => {
453
651
 
454
652
  const ref = useRef(null);
455
653
 
456
- useEffect(() => {
457
- if (!ref.current) return;
458
- setBuilderHeight(ref.current.clientHeight);
459
- const rect = ref.current.getBoundingClientRect();
460
- setBuilderTop(rect.top);
461
- });
654
+ useEffect(() => {
655
+ if (!ref.current) return;
656
+ setBuilderHeight(ref.current.clientHeight);
657
+ const rect = ref.current.getBoundingClientRect();
658
+ setBuilderTop(rect.top);
659
+ });
462
660
 
463
661
  const canvasHeight =
464
662
  Math.max(windowHeight - builderTop, builderHeight, 600) - 10;
465
663
  return (
466
664
  <ErrorBoundary>
467
- <Editor onRender={RenderNode}>
665
+ <Editor
666
+ onRender={RenderNode}
667
+ indicator={{
668
+ success: "#28a745",
669
+ thickness: 2,
670
+ className: "builder-drop-indicator",
671
+ }}
672
+ handlers={(store) => new DefaultEventHandlers({
673
+ store,
674
+ isMultiSelectEnabled: (e) => e?.shiftKey || false
675
+ })}
676
+ resolver={{
677
+ Text,
678
+ Empty,
679
+ Columns,
680
+ JoinField,
681
+ Field,
682
+ ViewLink,
683
+ Action,
684
+ HTMLCode,
685
+ LineBreak,
686
+ Aggregation,
687
+ Card,
688
+ Image,
689
+ Link,
690
+ View,
691
+ SearchBar,
692
+ Container,
693
+ Column,
694
+ DropDownFilter,
695
+ DropMenu,
696
+ Tabs,
697
+ Table,
698
+ ToggleFilter,
699
+ ListColumn,
700
+ ListColumns,
701
+ LibraryElem,
702
+ }}
703
+ >
468
704
  <Provider value={options}>
469
705
  <PreviewCtx.Provider
470
706
  value={{ previews, setPreviews, uploadedFiles, setUploadedFiles }}
@@ -496,16 +732,16 @@ const Builder = ({ options, layout, mode }) => {
496
732
  savingState={savingState}
497
733
  />
498
734
  <Accordion>
499
- <div className="card mt-1" accordiontitle="Components">
735
+ <div className="card mt-1" accordiontitle={t("Components")}>
500
736
  {{
501
737
  show: <ToolboxShow expanded={isLeftEnlarged} />,
502
738
  list: <ToolboxList expanded={isLeftEnlarged} />,
503
739
  edit: <ToolboxEdit expanded={isLeftEnlarged} />,
504
740
  page: <ToolboxPage expanded={isLeftEnlarged} />,
505
741
  filter: <ToolboxFilter expanded={isLeftEnlarged} />,
506
- }[mode] || <div>Missing mode</div>}
742
+ }[mode] || <div>{t("Missing mode")}</div>}
507
743
  </div>
508
- <div accordiontitle="Library">
744
+ <div accordiontitle={t("Library")}>
509
745
  <Library expanded={isLeftEnlarged} />
510
746
  </div>
511
747
  </Accordion>
@@ -515,7 +751,7 @@ const Builder = ({ options, layout, mode }) => {
515
751
  style={isLeftEnlarged ? { width: "13.4rem" } : {}}
516
752
  >
517
753
  <div className="card-header p-2 d-flex justify-content-between">
518
- <div>Layers</div>
754
+ <div>{t("Layers")}</div>
519
755
  <FontAwesomeIcon
520
756
  icon={
521
757
  isLeftEnlarged
@@ -526,12 +762,15 @@ const Builder = ({ options, layout, mode }) => {
526
762
  "float-end fa-lg builder-expand-toggle-left"
527
763
  }
528
764
  onClick={() => setIsLeftEnlarged(!isLeftEnlarged)}
529
- title={isLeftEnlarged ? "Shrink" : "Enlarge"}
765
+ title={isLeftEnlarged ? t("Shrink") : t("Enlarge")}
530
766
  />
531
767
  </div>
532
768
  {showLayers && (
533
769
  <div className="card-body p-0 builder-layers">
534
- <Layers expandRootOnLoad={true} />
770
+ <Layers
771
+ expandRootOnLoad={true}
772
+ renderLayer={CustomLayerComponent}
773
+ />
535
774
  </div>
536
775
  )}
537
776
  </div>
@@ -544,34 +783,7 @@ const Builder = ({ options, layout, mode }) => {
544
783
  }`}
545
784
  >
546
785
  <div>
547
- <Frame
548
- resolver={{
549
- Text,
550
- Empty,
551
- Columns,
552
- JoinField,
553
- Field,
554
- ViewLink,
555
- Action,
556
- HTMLCode,
557
- LineBreak,
558
- Aggregation,
559
- Card,
560
- Image,
561
- Link,
562
- View,
563
- SearchBar,
564
- Container,
565
- Column,
566
- DropDownFilter,
567
- DropMenu,
568
- Tabs,
569
- Table,
570
- ToggleFilter,
571
- ListColumn,
572
- ListColumns,
573
- }}
574
- >
786
+ <Frame>
575
787
  {options.mode === "list" ? (
576
788
  <Element canvas is={ListColumns}></Element>
577
789
  ) : (
@@ -602,14 +814,14 @@ const Builder = ({ options, layout, mode }) => {
602
814
  "float-end me-2 mt-1 fa-lg builder-expand-toggle-right"
603
815
  }
604
816
  onClick={() => setIsEnlarged(!isEnlarged)}
605
- title={isEnlarged ? "Shrink" : "Enlarge"}
817
+ title={isEnlarged ? t("Shrink") : t("Enlarge")}
606
818
  />
607
819
  <div
608
820
  className={` ${
609
821
  savingState.error ? "d-block" : "d-none"
610
822
  } my-2 fw-bold`}
611
823
  >
612
- your work is not being saved
824
+ {t("your work is not being saved")}
613
825
  </div>
614
826
  <SettingsPanel />
615
827
  </div>