@saltcorn/builder 1.6.0-alpha.9 → 1.6.0-beta.1
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 +367 -186
- package/src/components/RenderNode.js +21 -3
- package/src/components/Toolbox.js +100 -22
- package/src/components/elements/Action.js +10 -120
- 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 +285 -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 +28 -5
- package/src/components/elements/Table.js +10 -12
- package/src/components/elements/Text.js +59 -15
- 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);
|
|
@@ -699,11 +865,13 @@ const Builder = ({ options, layout, mode }) => {
|
|
|
699
865
|
ListColumn,
|
|
700
866
|
ListColumns,
|
|
701
867
|
LibraryElem,
|
|
868
|
+
Prompt,
|
|
869
|
+
Page
|
|
702
870
|
}}
|
|
703
871
|
>
|
|
704
872
|
<Provider value={options}>
|
|
705
873
|
<PreviewCtx.Provider
|
|
706
|
-
value={{ previews, setPreviews, uploadedFiles, setUploadedFiles }}
|
|
874
|
+
value={{ previews, setPreviews, uploadedFiles, setUploadedFiles, previewDevice }}
|
|
707
875
|
>
|
|
708
876
|
<RelationsCtx.Provider
|
|
709
877
|
value={{
|
|
@@ -767,9 +935,9 @@ const Builder = ({ options, layout, mode }) => {
|
|
|
767
935
|
</div>
|
|
768
936
|
{showLayers && (
|
|
769
937
|
<div className="card-body p-0 builder-layers">
|
|
770
|
-
<Layers
|
|
938
|
+
<Layers
|
|
771
939
|
expandRootOnLoad={true}
|
|
772
|
-
renderLayer={
|
|
940
|
+
renderLayer={CustomLayer}
|
|
773
941
|
/>
|
|
774
942
|
</div>
|
|
775
943
|
)}
|
|
@@ -782,40 +950,53 @@ const Builder = ({ options, layout, mode }) => {
|
|
|
782
950
|
options.mode !== "list" ? "emptymsg" : ""
|
|
783
951
|
}`}
|
|
784
952
|
>
|
|
785
|
-
<div>
|
|
786
|
-
<
|
|
787
|
-
{
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
953
|
+
<div className="device-preview-scroll-area">
|
|
954
|
+
<div
|
|
955
|
+
className={`device-preview-canvas-wrapper ${
|
|
956
|
+
previewDevice !== "desktop" && options.mode !== "list" ? "device-preview-constrained" : ""
|
|
957
|
+
}`}
|
|
958
|
+
style={{
|
|
959
|
+
maxWidth: options.mode !== "list" && DEVICE_WIDTHS[previewDevice]
|
|
960
|
+
? `${DEVICE_WIDTHS[previewDevice]}px`
|
|
961
|
+
: "none",
|
|
962
|
+
}}
|
|
963
|
+
>
|
|
964
|
+
<Frame>
|
|
965
|
+
{options.mode === "list" ? (
|
|
966
|
+
<Element canvas is={ListColumns}></Element>
|
|
967
|
+
) : (
|
|
968
|
+
<Element canvas is={Column}></Element>
|
|
969
|
+
)}
|
|
970
|
+
</Frame>
|
|
971
|
+
{options.mode === "list" ? <AddColumnButton /> : null}
|
|
972
|
+
</div>
|
|
794
973
|
</div>
|
|
795
974
|
</div>
|
|
796
975
|
<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
|
-
|
|
976
|
+
<div style={{ width: isEnlarged ? "28rem" : "16.5rem" }}>
|
|
977
|
+
{document.getElementById("builder-header-actions") &&
|
|
978
|
+
createPortal(
|
|
979
|
+
<Fragment>
|
|
980
|
+
<FontAwesomeIcon
|
|
981
|
+
icon={faSave}
|
|
982
|
+
className={savingState.isSaving ? "d-inline" : "d-none"}
|
|
983
|
+
/>
|
|
984
|
+
<FontAwesomeIcon
|
|
985
|
+
icon={faExclamationTriangle}
|
|
986
|
+
color="#ff0033"
|
|
987
|
+
className={savingState.error ? "d-inline" : "d-none"}
|
|
988
|
+
/>
|
|
989
|
+
<HistoryPanel />
|
|
990
|
+
{options.mode !== "list" && (
|
|
991
|
+
<DevicePreviewToolbar
|
|
992
|
+
previewDevice={previewDevice}
|
|
993
|
+
setPreviewDevice={setPreviewDevice}
|
|
994
|
+
/>
|
|
995
|
+
)}
|
|
996
|
+
<NextButton layout={layout} />
|
|
997
|
+
</Fragment>,
|
|
998
|
+
document.getElementById("builder-header-actions")
|
|
999
|
+
)}
|
|
819
1000
|
<div
|
|
820
1001
|
className={` ${
|
|
821
1002
|
savingState.error ? "d-block" : "d-none"
|
|
@@ -823,7 +1004,7 @@ const Builder = ({ options, layout, mode }) => {
|
|
|
823
1004
|
>
|
|
824
1005
|
{t("your work is not being saved")}
|
|
825
1006
|
</div>
|
|
826
|
-
<SettingsPanel />
|
|
1007
|
+
<SettingsPanel isEnlarged={isEnlarged} setIsEnlarged={setIsEnlarged} />
|
|
827
1008
|
</div>
|
|
828
1009
|
</div>
|
|
829
1010
|
</div>
|