@saltcorn/builder 1.6.0-alpha.9 → 1.6.0-beta.2
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/dist/builder_bundle.js +76 -104004
- package/dist/builder_bundle.js.LICENSE.txt +2 -0
- package/package.json +3 -2
- package/src/components/Builder.js +371 -187
- package/src/components/RenderNode.js +38 -10
- package/src/components/Toolbox.js +100 -22
- package/src/components/elements/Action.js +10 -120
- package/src/components/elements/Aggregation.js +17 -9
- package/src/components/elements/ArrayManager.js +10 -5
- package/src/components/elements/BoxModelEditor.js +24 -23
- package/src/components/elements/Card.js +26 -1
- package/src/components/elements/Columns.js +158 -110
- package/src/components/elements/Container.js +43 -8
- package/src/components/elements/CustomLayer.js +288 -0
- package/src/components/elements/DropDownFilter.js +8 -1
- package/src/components/elements/DropMenu.js +10 -4
- package/src/components/elements/HTMLCode.js +3 -1
- package/src/components/elements/MonacoEditor.js +120 -15
- package/src/components/elements/Prompt.js +285 -0
- package/src/components/elements/SearchBar.js +30 -6
- package/src/components/elements/Table.js +10 -12
- package/src/components/elements/Text.js +104 -20
- package/src/components/elements/View.js +2 -1
- package/src/components/elements/ViewLink.js +1 -0
- package/src/components/elements/utils.js +133 -30
- package/src/components/storage.js +33 -7
- package/src/index.js +10 -0
- package/src/utils/responsive_utils.js +139 -0
|
@@ -9,12 +9,12 @@ import React, {
|
|
|
9
9
|
useContext,
|
|
10
10
|
useState,
|
|
11
11
|
Fragment,
|
|
12
|
-
useRef
|
|
13
|
-
memo,
|
|
12
|
+
useRef
|
|
14
13
|
} from "react";
|
|
14
|
+
import { createPortal } from "react-dom";
|
|
15
15
|
import useTranslation from "../hooks/useTranslation";
|
|
16
16
|
import { Editor, Frame, Element, Selector, useEditor, DefaultEventHandlers } from "@craftjs/core";
|
|
17
|
-
import { Layers
|
|
17
|
+
import { Layers } from "@craftjs/layers"
|
|
18
18
|
import { Text } from "./elements/Text";
|
|
19
19
|
import { Field } from "./elements/Field";
|
|
20
20
|
import { JoinField } from "./elements/JoinField";
|
|
@@ -49,7 +49,6 @@ import { Link } from "./elements/Link";
|
|
|
49
49
|
import { View } from "./elements/View";
|
|
50
50
|
import { Container } from "./elements/Container";
|
|
51
51
|
import { Column } from "./elements/Column";
|
|
52
|
-
// import { Layers } from "saltcorn-craft-layers-noeye";
|
|
53
52
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
54
53
|
import {
|
|
55
54
|
faCopy,
|
|
@@ -58,20 +57,22 @@ import {
|
|
|
58
57
|
faTrashAlt,
|
|
59
58
|
faSave,
|
|
60
59
|
faExclamationTriangle,
|
|
61
|
-
faPlus
|
|
62
|
-
faChevronDown,
|
|
63
|
-
faChevronUp
|
|
60
|
+
faPlus
|
|
64
61
|
} from "@fortawesome/free-solid-svg-icons";
|
|
65
62
|
import {
|
|
66
63
|
faCaretSquareLeft,
|
|
67
64
|
faCaretSquareRight,
|
|
68
65
|
} from "@fortawesome/free-regular-svg-icons";
|
|
69
66
|
import { Accordion, ErrorBoundary } from "./elements/utils";
|
|
67
|
+
import { Display, Tablet, Phone } from "react-bootstrap-icons";
|
|
70
68
|
import { InitNewElement, Library, LibraryElem } from "./Library";
|
|
71
69
|
import { RenderNode } from "./RenderNode";
|
|
72
70
|
import { ListColumn } from "./elements/ListColumn";
|
|
73
71
|
import { ListColumns } from "./elements/ListColumns";
|
|
72
|
+
import { Prompt } from "./elements/Prompt";
|
|
74
73
|
import { recursivelyCloneToElems } from "./elements/Clone";
|
|
74
|
+
import { Page } from "./elements/Page";
|
|
75
|
+
import CustomLayer from "./elements/CustomLayer";
|
|
75
76
|
|
|
76
77
|
const { Provider } = optionsCtx;
|
|
77
78
|
|
|
@@ -92,11 +93,6 @@ const getSelectedNodes = (selected) => {
|
|
|
92
93
|
return [selected];
|
|
93
94
|
};
|
|
94
95
|
|
|
95
|
-
const getFirstSelected = (selected) => {
|
|
96
|
-
const nodes = getSelectedNodes(selected);
|
|
97
|
-
return nodes.length > 0 ? nodes[0] : null;
|
|
98
|
-
};
|
|
99
|
-
|
|
100
96
|
/**
|
|
101
97
|
*
|
|
102
98
|
* @returns {div}
|
|
@@ -104,7 +100,7 @@ const getFirstSelected = (selected) => {
|
|
|
104
100
|
* @subcategory components
|
|
105
101
|
* @namespace
|
|
106
102
|
*/
|
|
107
|
-
const SettingsPanel = () => {
|
|
103
|
+
const SettingsPanel = ({ isEnlarged, setIsEnlarged }) => {
|
|
108
104
|
const { t } = useTranslation();
|
|
109
105
|
const options = useContext(optionsCtx);
|
|
110
106
|
|
|
@@ -160,6 +156,15 @@ const SettingsPanel = () => {
|
|
|
160
156
|
const tagName = target.tagName.toLowerCase();
|
|
161
157
|
const hasSelection = selectedCount > 0;
|
|
162
158
|
if ((tagName === "body" || tagName === "button") && hasSelection) {
|
|
159
|
+
if (!selected && selectedCount > 1 && (keyCode === 8 || keyCode === 46)) {
|
|
160
|
+
const currentSelected = query.getEvent("selected");
|
|
161
|
+
const nodeIds = getSelectedNodes(currentSelected)
|
|
162
|
+
.map((nodeId) => (typeof nodeId === "string" ? nodeId : nodeId?.id))
|
|
163
|
+
.filter((nodeId) => nodeId && nodeId !== "ROOT");
|
|
164
|
+
nodeIds.forEach((nodeId) => {
|
|
165
|
+
try { actions.delete(nodeId); } catch (e) { /* node may already be deleted */ }
|
|
166
|
+
});
|
|
167
|
+
}
|
|
163
168
|
if (selected) {
|
|
164
169
|
if ((keyCode === 8 || keyCode === 46) && selected.id === "ROOT") {
|
|
165
170
|
deleteChildren();
|
|
@@ -298,6 +303,14 @@ const SettingsPanel = () => {
|
|
|
298
303
|
actions.history.redo();
|
|
299
304
|
}
|
|
300
305
|
}
|
|
306
|
+
if ((tagName === "body" || tagName === "button") &&
|
|
307
|
+
(event.ctrlKey || event.metaKey) && event.keyCode == 65) {
|
|
308
|
+
event.preventDefault();
|
|
309
|
+
const rootChildren = query.node("ROOT").childNodes();
|
|
310
|
+
if (rootChildren.length > 0) {
|
|
311
|
+
actions.selectNode(rootChildren);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
301
314
|
};
|
|
302
315
|
useEffect(() => {
|
|
303
316
|
window.addEventListener("keydown", handleUserKeyPress);
|
|
@@ -334,15 +347,181 @@ const SettingsPanel = () => {
|
|
|
334
347
|
);
|
|
335
348
|
};
|
|
336
349
|
|
|
350
|
+
const [generating, setGenerating] = useState(false);
|
|
351
|
+
const [generateError, setGenerateError] = useState(null);
|
|
352
|
+
|
|
353
|
+
// Regenerate modal state
|
|
354
|
+
const [showRegenerateModal, setShowRegenerateModal] = useState(false);
|
|
355
|
+
const [regeneratePrompt, setRegeneratePrompt] = useState("");
|
|
356
|
+
const [regenerating, setRegenerating] = useState(false);
|
|
357
|
+
const [regenerateError, setRegenerateError] = useState(null);
|
|
358
|
+
|
|
359
|
+
const handleRegenerate = async () => {
|
|
360
|
+
if (!regeneratePrompt.trim() || !selected) return;
|
|
361
|
+
setRegenerating(true);
|
|
362
|
+
setRegenerateError(null);
|
|
363
|
+
try {
|
|
364
|
+
const selectedNode = query.node(selected.id).get();
|
|
365
|
+
const existingJson = craftToSaltcorn(
|
|
366
|
+
JSON.parse(query.serialize()),
|
|
367
|
+
selected.id
|
|
368
|
+
);
|
|
369
|
+
const res = await fetch("/viewedit/copilot-generate-layout", {
|
|
370
|
+
method: "POST",
|
|
371
|
+
headers: {
|
|
372
|
+
"Content-Type": "application/json",
|
|
373
|
+
"CSRF-Token": options.csrfToken,
|
|
374
|
+
"X-Requested-With": "XMLHttpRequest",
|
|
375
|
+
},
|
|
376
|
+
body: JSON.stringify({
|
|
377
|
+
prompt: regeneratePrompt,
|
|
378
|
+
mode: options.mode,
|
|
379
|
+
table: options.tableName,
|
|
380
|
+
existing: existingJson,
|
|
381
|
+
}),
|
|
382
|
+
});
|
|
383
|
+
const data = await res.json();
|
|
384
|
+
if (data.error) {
|
|
385
|
+
setRegenerateError(data.error);
|
|
386
|
+
} else if (data.layout) {
|
|
387
|
+
const parentId = selectedNode.data.parent || "ROOT";
|
|
388
|
+
const siblings = query.node(parentId).childNodes();
|
|
389
|
+
const sibIx = siblings.findIndex((sib) => sib === selected.id);
|
|
390
|
+
actions.delete(selected.id);
|
|
391
|
+
layoutToNodes(data.layout, query, actions, parentId, options, sibIx);
|
|
392
|
+
setShowRegenerateModal(false);
|
|
393
|
+
setRegeneratePrompt("");
|
|
394
|
+
}
|
|
395
|
+
} catch (err) {
|
|
396
|
+
setRegenerateError(err.message || "Regeneration failed");
|
|
397
|
+
} finally {
|
|
398
|
+
setRegenerating(false);
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
// Find prompt nodes: check children of selected, or siblings if selected is a Prompt
|
|
403
|
+
const findPromptContext = () => {
|
|
404
|
+
if (!selected) return { promptNodes: [], targetParent: null };
|
|
405
|
+
const isSelectedPrompt = selected.displayName === "Prompt";
|
|
406
|
+
|
|
407
|
+
if (isSelectedPrompt && selected.parent) {
|
|
408
|
+
// Selected node is a Prompt — find all Prompt siblings in same parent
|
|
409
|
+
try {
|
|
410
|
+
const siblingIds = query.node(selected.parent).childNodes();
|
|
411
|
+
const promptIds = siblingIds.filter((id) => {
|
|
412
|
+
const n = query.node(id).get();
|
|
413
|
+
return n?.data?.displayName === "Prompt";
|
|
414
|
+
});
|
|
415
|
+
return { promptNodes: promptIds, targetParent: selected.parent };
|
|
416
|
+
} catch {
|
|
417
|
+
return { promptNodes: [], targetParent: null };
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Selected node is a container — check its direct children
|
|
422
|
+
if (selected.children && selected.children.length > 0) {
|
|
423
|
+
const promptIds = selected.children.filter((id) => {
|
|
424
|
+
try {
|
|
425
|
+
const n = query.node(id).get();
|
|
426
|
+
return n?.data?.displayName === "Prompt";
|
|
427
|
+
} catch {
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
if (promptIds.length > 0) {
|
|
432
|
+
return { promptNodes: promptIds, targetParent: selected.id };
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Check linked nodes (e.g. Card's inner Column)
|
|
437
|
+
try {
|
|
438
|
+
const nodeData = query.node(selected.id).get();
|
|
439
|
+
const linkedNodes = nodeData?.data?.linkedNodes;
|
|
440
|
+
if (linkedNodes) {
|
|
441
|
+
for (const linkedId of Object.values(linkedNodes)) {
|
|
442
|
+
const linkedChildIds = query.node(linkedId).childNodes();
|
|
443
|
+
const promptIds = linkedChildIds.filter((id) => {
|
|
444
|
+
try {
|
|
445
|
+
const n = query.node(id).get();
|
|
446
|
+
return n?.data?.displayName === "Prompt";
|
|
447
|
+
} catch {
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
if (promptIds.length > 0) {
|
|
452
|
+
return { promptNodes: promptIds, targetParent: linkedId };
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
} catch {
|
|
457
|
+
// ignore
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return { promptNodes: [], targetParent: null };
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
const { promptNodes, targetParent } = selected
|
|
464
|
+
? findPromptContext()
|
|
465
|
+
: { promptNodes: [], targetParent: null };
|
|
466
|
+
const hasPromptNodes = promptNodes.length > 0;
|
|
467
|
+
|
|
468
|
+
const handleGenerate = async () => {
|
|
469
|
+
setGenerating(true);
|
|
470
|
+
setGenerateError(null);
|
|
471
|
+
try {
|
|
472
|
+
const prompts = promptNodes.map((childId) => {
|
|
473
|
+
const n = query.node(childId).get();
|
|
474
|
+
const { promptType, promptText } = n.data.props;
|
|
475
|
+
return `[${promptType}]: ${promptText}`;
|
|
476
|
+
});
|
|
477
|
+
const combinedPrompt = prompts.join("\n");
|
|
478
|
+
|
|
479
|
+
const res = await fetch("/viewedit/copilot-generate-layout", {
|
|
480
|
+
method: "POST",
|
|
481
|
+
headers: {
|
|
482
|
+
"Content-Type": "application/json",
|
|
483
|
+
"CSRF-Token": options.csrfToken,
|
|
484
|
+
"X-Requested-With": "XMLHttpRequest",
|
|
485
|
+
},
|
|
486
|
+
body: JSON.stringify({
|
|
487
|
+
prompt: combinedPrompt,
|
|
488
|
+
mode: options.mode,
|
|
489
|
+
table: options.tableName,
|
|
490
|
+
}),
|
|
491
|
+
});
|
|
492
|
+
const data = await res.json();
|
|
493
|
+
if (data.error) {
|
|
494
|
+
setGenerateError(data.error);
|
|
495
|
+
} else if (data.layout) {
|
|
496
|
+
promptNodes.forEach((id) => actions.delete(id));
|
|
497
|
+
layoutToNodes(data.layout, query, actions, targetParent, options);
|
|
498
|
+
}
|
|
499
|
+
} catch (err) {
|
|
500
|
+
setGenerateError(err.message || "Generation failed");
|
|
501
|
+
} finally {
|
|
502
|
+
setGenerating(false);
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
|
|
337
506
|
return (
|
|
338
507
|
<div className="settings-panel card mt-1">
|
|
339
|
-
<div className="card-header px-2 py-1">
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
<
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
508
|
+
<div className="card-header px-2 py-1 d-flex justify-content-between align-items-center">
|
|
509
|
+
<div>
|
|
510
|
+
{selected && selected.displayName ? (
|
|
511
|
+
<Fragment>
|
|
512
|
+
<b>{selected.displayName}</b> settings
|
|
513
|
+
</Fragment>
|
|
514
|
+
) : (
|
|
515
|
+
t("Settings")
|
|
516
|
+
)}
|
|
517
|
+
</div>
|
|
518
|
+
{setIsEnlarged && (
|
|
519
|
+
<FontAwesomeIcon
|
|
520
|
+
icon={isEnlarged ? faCaretSquareRight : faCaretSquareLeft}
|
|
521
|
+
className="fa-lg builder-expand-toggle-right"
|
|
522
|
+
onClick={() => setIsEnlarged(!isEnlarged)}
|
|
523
|
+
title={isEnlarged ? t("Shrink") : t("Enlarge")}
|
|
524
|
+
/>
|
|
346
525
|
)}
|
|
347
526
|
</div>
|
|
348
527
|
<div className="card-body p-2">
|
|
@@ -373,15 +552,76 @@ const SettingsPanel = () => {
|
|
|
373
552
|
) : (
|
|
374
553
|
<button
|
|
375
554
|
title={t("Duplicate element with its children")}
|
|
376
|
-
className="btn btn-sm btn-secondary ms-
|
|
555
|
+
className="btn btn-sm btn-secondary ms-1 duplicate-element-builder"
|
|
377
556
|
onClick={duplicate}
|
|
378
557
|
>
|
|
379
558
|
<FontAwesomeIcon icon={faCopy} className="me-1" />
|
|
380
559
|
{t("Clone")}
|
|
381
560
|
</button>
|
|
382
561
|
)}
|
|
383
|
-
|
|
384
|
-
|
|
562
|
+
{options.has_copilot_generate && selected.isDeletable && (
|
|
563
|
+
<button
|
|
564
|
+
className="btn btn-sm btn-secondary ms-1"
|
|
565
|
+
onClick={() => setShowRegenerateModal(true)}
|
|
566
|
+
>
|
|
567
|
+
{t("Edit with AI")}
|
|
568
|
+
</button>
|
|
569
|
+
)}
|
|
570
|
+
<div className="mt-2">
|
|
571
|
+
{selected.settings && React.createElement(selected.settings)}
|
|
572
|
+
</div>
|
|
573
|
+
{showRegenerateModal && (
|
|
574
|
+
<div className="modal d-block" tabIndex="-1" style={{ backgroundColor: "rgba(0,0,0,0.5)" }}>
|
|
575
|
+
<div className="modal-dialog">
|
|
576
|
+
<div className="modal-content">
|
|
577
|
+
<div className="modal-header">
|
|
578
|
+
<h5 className="modal-title">{t("Generate Element")}</h5>
|
|
579
|
+
<button
|
|
580
|
+
type="button"
|
|
581
|
+
className="btn-close"
|
|
582
|
+
onClick={() => {
|
|
583
|
+
setShowRegenerateModal(false);
|
|
584
|
+
setRegenerateError(null);
|
|
585
|
+
}}
|
|
586
|
+
></button>
|
|
587
|
+
</div>
|
|
588
|
+
<div className="modal-body">
|
|
589
|
+
<p className="text-muted small">
|
|
590
|
+
{t("Describe how you want to regenerate the selected element.")}
|
|
591
|
+
</p>
|
|
592
|
+
<textarea
|
|
593
|
+
className="form-control"
|
|
594
|
+
rows={3}
|
|
595
|
+
value={regeneratePrompt}
|
|
596
|
+
onChange={(e) => setRegeneratePrompt(e.target.value)}
|
|
597
|
+
placeholder={t("Enter your prompt...")}
|
|
598
|
+
/>
|
|
599
|
+
{regenerateError && (
|
|
600
|
+
<div className="alert alert-danger mt-2 mb-0">{regenerateError}</div>
|
|
601
|
+
)}
|
|
602
|
+
</div>
|
|
603
|
+
<div className="modal-footer">
|
|
604
|
+
<button
|
|
605
|
+
className="btn btn-secondary"
|
|
606
|
+
onClick={() => {
|
|
607
|
+
setShowRegenerateModal(false);
|
|
608
|
+
setRegenerateError(null);
|
|
609
|
+
}}
|
|
610
|
+
>
|
|
611
|
+
{t("Cancel")}
|
|
612
|
+
</button>
|
|
613
|
+
<button
|
|
614
|
+
className="btn btn-primary"
|
|
615
|
+
onClick={handleRegenerate}
|
|
616
|
+
disabled={regenerating || !regeneratePrompt.trim()}
|
|
617
|
+
>
|
|
618
|
+
{regenerating ? t("Generating...") : t("Generate")}
|
|
619
|
+
</button>
|
|
620
|
+
</div>
|
|
621
|
+
</div>
|
|
622
|
+
</div>
|
|
623
|
+
</div>
|
|
624
|
+
)}
|
|
385
625
|
</Fragment>
|
|
386
626
|
) : (
|
|
387
627
|
t("No element selected")
|
|
@@ -417,113 +657,6 @@ function useWindowDimensions() {
|
|
|
417
657
|
return windowDimensions;
|
|
418
658
|
}
|
|
419
659
|
|
|
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
|
-
|
|
527
660
|
const AddColumnButton = () => {
|
|
528
661
|
const { t } = useTranslation();
|
|
529
662
|
const { query, actions } = useEditor(() => {});
|
|
@@ -545,6 +678,38 @@ const AddColumnButton = () => {
|
|
|
545
678
|
);
|
|
546
679
|
};
|
|
547
680
|
|
|
681
|
+
const DEVICE_WIDTHS = {
|
|
682
|
+
desktop: null,
|
|
683
|
+
tablet: 768,
|
|
684
|
+
mobile: 576,
|
|
685
|
+
};
|
|
686
|
+
|
|
687
|
+
const DevicePreviewToolbar = ({ previewDevice, setPreviewDevice }) => {
|
|
688
|
+
const { t } = useTranslation();
|
|
689
|
+
const devices = [
|
|
690
|
+
{ key: "desktop", icon: Display, label: t("Desktop") },
|
|
691
|
+
{ key: "tablet", icon: Tablet, label: t("Tablet") },
|
|
692
|
+
{ key: "mobile", icon: Phone, label: t("Mobile") },
|
|
693
|
+
];
|
|
694
|
+
|
|
695
|
+
return (
|
|
696
|
+
<div className="device-preview-toolbar">
|
|
697
|
+
{devices.map(({ key, icon: Icon, label }) => (
|
|
698
|
+
<button
|
|
699
|
+
key={key}
|
|
700
|
+
className={`btn btn-sm ${
|
|
701
|
+
previewDevice === key ? "btn-primary" : "btn-outline-secondary"
|
|
702
|
+
} device-preview-btn`}
|
|
703
|
+
onClick={() => setPreviewDevice(key)}
|
|
704
|
+
title={label}
|
|
705
|
+
>
|
|
706
|
+
<Icon size={16} />
|
|
707
|
+
</button>
|
|
708
|
+
))}
|
|
709
|
+
</div>
|
|
710
|
+
);
|
|
711
|
+
};
|
|
712
|
+
|
|
548
713
|
/**
|
|
549
714
|
* @returns {Fragment}
|
|
550
715
|
* @category saltcorn-builder
|
|
@@ -559,26 +724,26 @@ const HistoryPanel = () => {
|
|
|
559
724
|
}));
|
|
560
725
|
|
|
561
726
|
return (
|
|
562
|
-
<
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
</
|
|
727
|
+
<div className="d-flex gap-1">
|
|
728
|
+
<button
|
|
729
|
+
className="btn btn-sm btn-secondary redo-builder"
|
|
730
|
+
title={t("Redo")}
|
|
731
|
+
onClick={() => actions.history.redo()}
|
|
732
|
+
disabled={!canRedo}
|
|
733
|
+
style={!canRedo ? { opacity: 0.4, pointerEvents: "none" } : {}}
|
|
734
|
+
>
|
|
735
|
+
<FontAwesomeIcon icon={faRedo} />
|
|
736
|
+
</button>
|
|
737
|
+
<button
|
|
738
|
+
className="btn btn-sm btn-secondary undo-builder"
|
|
739
|
+
title={t("Undo")}
|
|
740
|
+
onClick={() => actions.history.undo()}
|
|
741
|
+
disabled={!canUndo}
|
|
742
|
+
style={!canUndo ? { opacity: 0.4, pointerEvents: "none" } : {}}
|
|
743
|
+
>
|
|
744
|
+
<FontAwesomeIcon icon={faUndo} />
|
|
745
|
+
</button>
|
|
746
|
+
</div>
|
|
582
747
|
);
|
|
583
748
|
};
|
|
584
749
|
|
|
@@ -595,7 +760,7 @@ const NextButton = ({ layout }) => {
|
|
|
595
760
|
const options = useContext(optionsCtx);
|
|
596
761
|
|
|
597
762
|
useEffect(() => {
|
|
598
|
-
layoutToNodes(layout, query, actions, "ROOT", options);
|
|
763
|
+
layoutToNodes(layout, query, actions.history.ignore(), "ROOT", options);
|
|
599
764
|
}, []);
|
|
600
765
|
|
|
601
766
|
/**
|
|
@@ -644,6 +809,7 @@ const Builder = ({ options, layout, mode }) => {
|
|
|
644
809
|
const [isEnlarged, setIsEnlarged] = useState(false);
|
|
645
810
|
const [isLeftEnlarged, setIsLeftEnlarged] = useState(false);
|
|
646
811
|
const [relationsCache, setRelationsCache] = useState({});
|
|
812
|
+
const [previewDevice, setPreviewDevice] = useState("desktop");
|
|
647
813
|
const { windowWidth, windowHeight } = useWindowDimensions();
|
|
648
814
|
|
|
649
815
|
const [builderHeight, setBuilderHeight] = useState(0);
|
|
@@ -660,6 +826,9 @@ const Builder = ({ options, layout, mode }) => {
|
|
|
660
826
|
|
|
661
827
|
const canvasHeight =
|
|
662
828
|
Math.max(windowHeight - builderTop, builderHeight, 600) - 10;
|
|
829
|
+
|
|
830
|
+
const smallSidebarWidth = options.isRTL ? '17.5rem' : '16.5rem'
|
|
831
|
+
|
|
663
832
|
return (
|
|
664
833
|
<ErrorBoundary>
|
|
665
834
|
<Editor
|
|
@@ -699,11 +868,13 @@ const Builder = ({ options, layout, mode }) => {
|
|
|
699
868
|
ListColumn,
|
|
700
869
|
ListColumns,
|
|
701
870
|
LibraryElem,
|
|
871
|
+
Prompt,
|
|
872
|
+
Page
|
|
702
873
|
}}
|
|
703
874
|
>
|
|
704
875
|
<Provider value={options}>
|
|
705
876
|
<PreviewCtx.Provider
|
|
706
|
-
value={{ previews, setPreviews, uploadedFiles, setUploadedFiles }}
|
|
877
|
+
value={{ previews, setPreviews, uploadedFiles, setUploadedFiles, previewDevice }}
|
|
707
878
|
>
|
|
708
879
|
<RelationsCtx.Provider
|
|
709
880
|
value={{
|
|
@@ -717,7 +888,7 @@ const Builder = ({ options, layout, mode }) => {
|
|
|
717
888
|
layoutToNodes,
|
|
718
889
|
}}
|
|
719
890
|
>
|
|
720
|
-
<div className="row" ref={ref} style={{ marginTop: "-5px" }}>
|
|
891
|
+
<div className="row" ref={ref} style={{ marginTop: "-5px" }} dir={options.isRTL ? "rtl" : "ltr"}>
|
|
721
892
|
<div
|
|
722
893
|
className={`col-sm-auto left-builder-col ${
|
|
723
894
|
isLeftEnlarged
|
|
@@ -767,9 +938,9 @@ const Builder = ({ options, layout, mode }) => {
|
|
|
767
938
|
</div>
|
|
768
939
|
{showLayers && (
|
|
769
940
|
<div className="card-body p-0 builder-layers">
|
|
770
|
-
<Layers
|
|
941
|
+
<Layers
|
|
771
942
|
expandRootOnLoad={true}
|
|
772
|
-
renderLayer={
|
|
943
|
+
renderLayer={CustomLayer}
|
|
773
944
|
/>
|
|
774
945
|
</div>
|
|
775
946
|
)}
|
|
@@ -782,40 +953,53 @@ const Builder = ({ options, layout, mode }) => {
|
|
|
782
953
|
options.mode !== "list" ? "emptymsg" : ""
|
|
783
954
|
}`}
|
|
784
955
|
>
|
|
785
|
-
<div>
|
|
786
|
-
<
|
|
787
|
-
{
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
956
|
+
<div className="device-preview-scroll-area">
|
|
957
|
+
<div
|
|
958
|
+
className={`device-preview-canvas-wrapper ${
|
|
959
|
+
previewDevice !== "desktop" && options.mode !== "list" ? "device-preview-constrained" : ""
|
|
960
|
+
}`}
|
|
961
|
+
style={{
|
|
962
|
+
maxWidth: options.mode !== "list" && DEVICE_WIDTHS[previewDevice]
|
|
963
|
+
? `${DEVICE_WIDTHS[previewDevice]}px`
|
|
964
|
+
: "none",
|
|
965
|
+
}}
|
|
966
|
+
>
|
|
967
|
+
<Frame>
|
|
968
|
+
{options.mode === "list" ? (
|
|
969
|
+
<Element canvas is={ListColumns}></Element>
|
|
970
|
+
) : (
|
|
971
|
+
<Element canvas is={Column}></Element>
|
|
972
|
+
)}
|
|
973
|
+
</Frame>
|
|
974
|
+
{options.mode === "list" ? <AddColumnButton /> : null}
|
|
975
|
+
</div>
|
|
794
976
|
</div>
|
|
795
977
|
</div>
|
|
796
978
|
<div className="col-sm-auto builder-sidebar">
|
|
797
|
-
<div style={{ width: isEnlarged ? "28rem" :
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
979
|
+
<div style={{ width: isEnlarged ? "28rem" : smallSidebarWidth }}>
|
|
980
|
+
{document.getElementById("builder-header-actions") &&
|
|
981
|
+
createPortal(
|
|
982
|
+
<Fragment>
|
|
983
|
+
<FontAwesomeIcon
|
|
984
|
+
icon={faSave}
|
|
985
|
+
className={savingState.isSaving ? "d-inline" : "d-none"}
|
|
986
|
+
/>
|
|
987
|
+
<FontAwesomeIcon
|
|
988
|
+
icon={faExclamationTriangle}
|
|
989
|
+
color="#ff0033"
|
|
990
|
+
className={savingState.error ? "d-inline" : "d-none"}
|
|
991
|
+
/>
|
|
992
|
+
<HistoryPanel />
|
|
993
|
+
{options.mode !== "list" && (
|
|
994
|
+
<DevicePreviewToolbar
|
|
995
|
+
previewDevice={previewDevice}
|
|
996
|
+
setPreviewDevice={setPreviewDevice}
|
|
997
|
+
/>
|
|
998
|
+
)}
|
|
999
|
+
<NextButton layout={layout} />
|
|
1000
|
+
</Fragment>,
|
|
1001
|
+
document.getElementById("builder-header-actions")
|
|
1002
|
+
)}
|
|
819
1003
|
<div
|
|
820
1004
|
className={` ${
|
|
821
1005
|
savingState.error ? "d-block" : "d-none"
|
|
@@ -823,7 +1007,7 @@ const Builder = ({ options, layout, mode }) => {
|
|
|
823
1007
|
>
|
|
824
1008
|
{t("your work is not being saved")}
|
|
825
1009
|
</div>
|
|
826
|
-
<SettingsPanel />
|
|
1010
|
+
<SettingsPanel isEnlarged={isEnlarged} setIsEnlarged={setIsEnlarged} />
|
|
827
1011
|
</div>
|
|
828
1012
|
</div>
|
|
829
1013
|
</div>
|