@saltcorn/builder 1.6.0-beta.13 → 1.6.0-beta.14
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 +7 -7
- package/package.json +3 -3
- package/src/components/Builder.js +102 -21
- package/src/components/Library.js +79 -6
- package/src/components/RenderNode.js +20 -10
- package/src/components/elements/Action.js +8 -0
- package/src/components/elements/Aggregation.js +1 -1
- package/src/components/elements/Card.js +10 -10
- package/src/components/elements/Clone.js +2 -2
- package/src/components/elements/Container.js +1 -1
- package/src/components/elements/CustomLayer.js +8 -3
- package/src/components/elements/Link.js +4 -4
- package/src/components/elements/ListColumn.js +2 -4
- package/src/components/elements/MonacoEditor.js +45 -11
- package/src/components/elements/utils.js +52 -42
- package/src/components/storage.js +1 -1
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saltcorn/builder",
|
|
3
|
-
"version": "1.6.0-beta.
|
|
3
|
+
"version": "1.6.0-beta.14",
|
|
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",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"build": "webpack --mode production",
|
|
9
|
-
"builddev": "webpack --mode
|
|
9
|
+
"builddev": "webpack --mode development",
|
|
10
10
|
"watch": "webpack --watch --mode development",
|
|
11
11
|
"test": "jest tests --runInBand",
|
|
12
12
|
"tsc": "echo \"Error: no TypeScript support yet\"",
|
|
@@ -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-beta.
|
|
33
|
+
"@saltcorn/common-code": "1.6.0-beta.14",
|
|
34
34
|
"@tippyjs/react": "4.2.6",
|
|
35
35
|
"babel-jest": "^29.7.0",
|
|
36
36
|
"babel-loader": "9.2.1",
|
|
@@ -282,18 +282,6 @@ const SettingsPanel = ({ isEnlarged, setIsEnlarged }) => {
|
|
|
282
282
|
selectedNodes.forEach((nodeId) => actions.delete(nodeId));
|
|
283
283
|
}
|
|
284
284
|
}
|
|
285
|
-
if ((event.ctrlKey || event.metaKey) && event.keyCode == 86) {
|
|
286
|
-
navigator.clipboard.readText().then((clipText) => {
|
|
287
|
-
const layout = JSON.parse(clipText);
|
|
288
|
-
layoutToNodes(
|
|
289
|
-
layout,
|
|
290
|
-
query,
|
|
291
|
-
actions,
|
|
292
|
-
selected?.id || "ROOT",
|
|
293
|
-
options
|
|
294
|
-
);
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
285
|
if ((event.ctrlKey || event.metaKey) && event.keyCode == 90) {
|
|
298
286
|
// undo
|
|
299
287
|
actions.history.undo();
|
|
@@ -303,6 +291,43 @@ const SettingsPanel = ({ isEnlarged, setIsEnlarged }) => {
|
|
|
303
291
|
actions.history.redo();
|
|
304
292
|
}
|
|
305
293
|
}
|
|
294
|
+
if ((event.ctrlKey || event.metaKey) && event.keyCode == 86) {
|
|
295
|
+
const inputTags = ["input", "textarea", "select"];
|
|
296
|
+
if (!inputTags.includes(tagName) && !target.isContentEditable) {
|
|
297
|
+
navigator.clipboard.readText().then((clipText) => {
|
|
298
|
+
try {
|
|
299
|
+
const layout = JSON.parse(clipText);
|
|
300
|
+
|
|
301
|
+
let pasteTarget = "ROOT";
|
|
302
|
+
let pasteIndex = false;
|
|
303
|
+
try {
|
|
304
|
+
if (selected?.id && selected.id !== "ROOT") {
|
|
305
|
+
const selNode = query.node(selected.id).get();
|
|
306
|
+
const linkedNodes = selNode?.data?.linkedNodes;
|
|
307
|
+
if (linkedNodes && Object.keys(linkedNodes).length > 0) {
|
|
308
|
+
const firstLinkedId = Object.values(linkedNodes)[0];
|
|
309
|
+
pasteTarget = firstLinkedId;
|
|
310
|
+
pasteIndex = query.node(firstLinkedId).childNodes().length;
|
|
311
|
+
} else if (selNode?.data?.isCanvas) {
|
|
312
|
+
pasteTarget = selected.id;
|
|
313
|
+
pasteIndex = query.node(selected.id).childNodes().length;
|
|
314
|
+
} else {
|
|
315
|
+
const parentId = selNode?.data?.parent;
|
|
316
|
+
if (parentId) {
|
|
317
|
+
pasteTarget = parentId;
|
|
318
|
+
const siblings = query.node(parentId).childNodes();
|
|
319
|
+
const sibIx = siblings.findIndex((sib) => sib === selected.id);
|
|
320
|
+
if (sibIx !== -1) pasteIndex = sibIx + 1;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
} catch (_) {}
|
|
325
|
+
layoutToNodes(layout, query, actions, pasteTarget, options, pasteIndex);
|
|
326
|
+
} catch (e) {
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
}
|
|
306
331
|
if ((tagName === "body" || tagName === "button") &&
|
|
307
332
|
(event.ctrlKey || event.metaKey) && event.keyCode == 65) {
|
|
308
333
|
event.preventDefault();
|
|
@@ -318,6 +343,7 @@ const SettingsPanel = ({ isEnlarged, setIsEnlarged }) => {
|
|
|
318
343
|
window.removeEventListener("keydown", handleUserKeyPress);
|
|
319
344
|
};
|
|
320
345
|
}, [handleUserKeyPress]);
|
|
346
|
+
|
|
321
347
|
const hasChildren =
|
|
322
348
|
selected && selected.children && selected.children.length > 0;
|
|
323
349
|
|
|
@@ -726,15 +752,6 @@ const HistoryPanel = () => {
|
|
|
726
752
|
return (
|
|
727
753
|
<div className="d-flex gap-1">
|
|
728
754
|
<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
755
|
className="btn btn-sm btn-secondary undo-builder"
|
|
739
756
|
title={t("Undo")}
|
|
740
757
|
onClick={() => actions.history.undo()}
|
|
@@ -743,6 +760,15 @@ const HistoryPanel = () => {
|
|
|
743
760
|
>
|
|
744
761
|
<FontAwesomeIcon icon={faUndo} />
|
|
745
762
|
</button>
|
|
763
|
+
<button
|
|
764
|
+
className="btn btn-sm btn-secondary redo-builder"
|
|
765
|
+
title={t("Redo")}
|
|
766
|
+
onClick={() => actions.history.redo()}
|
|
767
|
+
disabled={!canRedo}
|
|
768
|
+
style={!canRedo ? { opacity: 0.4, pointerEvents: "none" } : {}}
|
|
769
|
+
>
|
|
770
|
+
<FontAwesomeIcon icon={faRedo} />
|
|
771
|
+
</button>
|
|
746
772
|
</div>
|
|
747
773
|
);
|
|
748
774
|
};
|
|
@@ -824,6 +850,61 @@ const Builder = ({ options, layout, mode }) => {
|
|
|
824
850
|
setBuilderTop(rect.top);
|
|
825
851
|
});
|
|
826
852
|
|
|
853
|
+
// Correct the CraftJS drop indicator position when body CSS zoom is applied.
|
|
854
|
+
// getBoundingClientRect() returns visual (zoomed) coords, but position:fixed
|
|
855
|
+
// top values inside a zoomed body are in layout (pre-zoom) coords, so we
|
|
856
|
+
// divide top/height by zoom. Left/width are already in viewport coords.
|
|
857
|
+
useEffect(() => {
|
|
858
|
+
const getBodyZoom = () => {
|
|
859
|
+
const bw = document.body.offsetWidth;
|
|
860
|
+
if (!bw) return 1;
|
|
861
|
+
return document.body.getBoundingClientRect().width / bw || 1;
|
|
862
|
+
};
|
|
863
|
+
|
|
864
|
+
let isCorrecting = false;
|
|
865
|
+
|
|
866
|
+
const correctIndicator = (el) => {
|
|
867
|
+
if (isCorrecting) return;
|
|
868
|
+
const zoom = getBodyZoom();
|
|
869
|
+
if (Math.abs(zoom - 1) < 0.01) return;
|
|
870
|
+
|
|
871
|
+
isCorrecting = true;
|
|
872
|
+
const top = parseFloat(el.style.top);
|
|
873
|
+
const height = parseFloat(el.style.height);
|
|
874
|
+
|
|
875
|
+
if (!isNaN(top)) el.style.top = `${top / zoom}px`;
|
|
876
|
+
if (!isNaN(height)) el.style.height = `${height / zoom}px`;
|
|
877
|
+
|
|
878
|
+
setTimeout(() => { isCorrecting = false; }, 0);
|
|
879
|
+
};
|
|
880
|
+
|
|
881
|
+
const observer = new MutationObserver((mutations) => {
|
|
882
|
+
for (const mutation of mutations) {
|
|
883
|
+
if (mutation.type === "attributes" && mutation.attributeName === "style") {
|
|
884
|
+
const el = mutation.target;
|
|
885
|
+
if (el.classList && el.classList.contains("builder-drop-indicator")) {
|
|
886
|
+
correctIndicator(el);
|
|
887
|
+
}
|
|
888
|
+
} else if (mutation.type === "childList") {
|
|
889
|
+
mutation.addedNodes.forEach((node) => {
|
|
890
|
+
if (node.nodeType === 1 && node.classList && node.classList.contains("builder-drop-indicator")) {
|
|
891
|
+
correctIndicator(node);
|
|
892
|
+
}
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
observer.observe(document.body, {
|
|
899
|
+
childList: true,
|
|
900
|
+
subtree: true,
|
|
901
|
+
attributes: true,
|
|
902
|
+
attributeFilter: ["style"],
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
return () => observer.disconnect();
|
|
906
|
+
}, []);
|
|
907
|
+
|
|
827
908
|
const canvasHeight =
|
|
828
909
|
Math.max(windowHeight - builderTop, builderHeight, 600) - 10;
|
|
829
910
|
|
|
@@ -99,9 +99,80 @@ const InitNewElement = ({ nodekeys, savingState, setSavingState }) => {
|
|
|
99
99
|
});
|
|
100
100
|
const { t } = useTranslation();
|
|
101
101
|
const options = useContext(optionsCtx);
|
|
102
|
-
|
|
102
|
+
|
|
103
|
+
const lastReload = useRef(null);
|
|
104
|
+
const rebuildInProgress = useRef(false);
|
|
105
|
+
|
|
106
|
+
const reloadEntityContentFromServer = async () => {
|
|
103
107
|
if (!query.serialize) return;
|
|
108
|
+
if (lastReload.current && new Date() - lastReload.current < 1000) return;
|
|
109
|
+
lastReload.current = new Date();
|
|
110
|
+
|
|
111
|
+
const urlroot = options.page_id ? "pageedit" : "viewedit";
|
|
112
|
+
const response = await fetch(
|
|
113
|
+
`/${urlroot}/getlayout/${options.page_id || options.view_id}`
|
|
114
|
+
);
|
|
115
|
+
const { layout } = await response.json();
|
|
116
|
+
|
|
117
|
+
if (!layout) return;
|
|
118
|
+
|
|
119
|
+
const data = craftToSaltcorn(
|
|
120
|
+
JSON.parse(query.serialize()),
|
|
121
|
+
"ROOT",
|
|
122
|
+
options
|
|
123
|
+
);
|
|
124
|
+
if (
|
|
125
|
+
isEqual(
|
|
126
|
+
JSON.parse(JSON.stringify(layout)),
|
|
127
|
+
JSON.parse(JSON.stringify(data.layout))
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
return;
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
rebuildInProgress.current = true;
|
|
134
|
+
savedData.current = JSON.stringify(layout);
|
|
135
|
+
actions.selectNode();
|
|
136
|
+
query
|
|
137
|
+
.node("ROOT")
|
|
138
|
+
.childNodes()
|
|
139
|
+
.forEach((child) => {
|
|
140
|
+
actions.delete(child);
|
|
141
|
+
});
|
|
142
|
+
layoutToNodes(layout, query, actions.history.ignore(), "ROOT", options);
|
|
143
|
+
} catch (e) {
|
|
144
|
+
console.error("rebuild error", e);
|
|
145
|
+
} finally {
|
|
146
|
+
rebuildInProgress.current = false;
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const handleVisibilityChange = () => {
|
|
151
|
+
if (document.hidden === false) reloadEntityContentFromServer();
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const handlePageShow = (event) => {
|
|
155
|
+
if (event.persisted || window.performance?.navigation.type === 2)
|
|
156
|
+
reloadEntityContentFromServer();
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
161
|
+
return () => {
|
|
162
|
+
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
163
|
+
};
|
|
164
|
+
}, [handleVisibilityChange]);
|
|
165
|
+
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
window.addEventListener("pageshow", handlePageShow);
|
|
168
|
+
return () => {
|
|
169
|
+
document.removeEventListener("pageshow", handlePageShow);
|
|
170
|
+
};
|
|
171
|
+
}, [handlePageShow]);
|
|
104
172
|
|
|
173
|
+
const doSave = (query, keepalive) => {
|
|
174
|
+
if (!query.serialize) return;
|
|
175
|
+
if (rebuildInProgress.current) return;
|
|
105
176
|
const data = craftToSaltcorn(
|
|
106
177
|
JSON.parse(query.serialize()),
|
|
107
178
|
"ROOT",
|
|
@@ -120,7 +191,7 @@ const InitNewElement = ({ nodekeys, savingState, setSavingState }) => {
|
|
|
120
191
|
|
|
121
192
|
fetch(`/${urlroot}/savebuilder/${options.page_id || options.view_id}`, {
|
|
122
193
|
method: "POST", // or 'PUT'
|
|
123
|
-
keepalive
|
|
194
|
+
keepalive, //this is conditional bec body size is limited to 64KB
|
|
124
195
|
headers: {
|
|
125
196
|
"Content-Type": "application/json",
|
|
126
197
|
"CSRF-Token": options.csrfToken,
|
|
@@ -217,10 +288,12 @@ export /**
|
|
|
217
288
|
* @namespace
|
|
218
289
|
*/
|
|
219
290
|
const Library = ({ expanded }) => {
|
|
220
|
-
const { actions, selected, selectedNodes, query, connectors } = useEditor(
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
291
|
+
const { actions, selected, selectedNodes, query, connectors } = useEditor(
|
|
292
|
+
(state, query) => ({
|
|
293
|
+
selected: getSelectedNodes(state.events.selected)[0] || null,
|
|
294
|
+
selectedNodes: getSelectedNodes(state.events.selected),
|
|
295
|
+
})
|
|
296
|
+
);
|
|
224
297
|
const { t } = useTranslation();
|
|
225
298
|
const options = useContext(optionsCtx);
|
|
226
299
|
const [adding, setAdding] = useState(false);
|
|
@@ -35,7 +35,7 @@ const RenderNode = ({ render }) => {
|
|
|
35
35
|
const { id } = useNode();
|
|
36
36
|
const options = useContext(optionsCtx);
|
|
37
37
|
const { actions, query, isActive } = useEditor((state) => ({
|
|
38
|
-
isActive: state.nodes[id]
|
|
38
|
+
isActive: state.nodes[id]?.events?.selected,
|
|
39
39
|
}));
|
|
40
40
|
|
|
41
41
|
const {
|
|
@@ -58,23 +58,33 @@ const RenderNode = ({ render }) => {
|
|
|
58
58
|
|
|
59
59
|
const currentRef = useRef();
|
|
60
60
|
|
|
61
|
+
const getZoomFactor = useCallback(() => {
|
|
62
|
+
const bw = document.body.offsetWidth;
|
|
63
|
+
if (!bw) return 1;
|
|
64
|
+
const factor = document.body.getBoundingClientRect().width / bw;
|
|
65
|
+
return factor || 1;
|
|
66
|
+
}, []);
|
|
67
|
+
|
|
61
68
|
const getPos = useCallback((dom) => {
|
|
69
|
+
const zoom = getZoomFactor();
|
|
62
70
|
const { top, left, bottom, height, width, right } = dom
|
|
63
71
|
? dom.getBoundingClientRect()
|
|
64
72
|
: { top: 0, left: 0, bottom: 0, right: 0, height: 0, width: 0 };
|
|
65
|
-
const
|
|
73
|
+
const topAdj = (top > 0 ? top : bottom) / zoom;
|
|
74
|
+
const leftAdj = left / zoom;
|
|
75
|
+
const rightPos = window.innerWidth / zoom - right / zoom;
|
|
66
76
|
return {
|
|
67
|
-
top: `${
|
|
68
|
-
left: `${
|
|
77
|
+
top: `${topAdj}px`,
|
|
78
|
+
left: `${leftAdj}px`,
|
|
69
79
|
right: `${rightPos}px`,
|
|
70
|
-
topn:
|
|
71
|
-
leftn:
|
|
80
|
+
topn: topAdj,
|
|
81
|
+
leftn: leftAdj,
|
|
72
82
|
rightn: rightPos,
|
|
73
|
-
height,
|
|
74
|
-
width,
|
|
75
|
-
bottom,
|
|
83
|
+
height: height / zoom,
|
|
84
|
+
width: width / zoom,
|
|
85
|
+
bottom: bottom / zoom,
|
|
76
86
|
};
|
|
77
|
-
}, []);
|
|
87
|
+
}, [getZoomFactor]);
|
|
78
88
|
|
|
79
89
|
const scroll = useCallback(() => {
|
|
80
90
|
const { current: currentDOM } = currentRef;
|
|
@@ -146,6 +146,7 @@ const ActionSettings = () => {
|
|
|
146
146
|
} = node;
|
|
147
147
|
const options = useContext(optionsCtx);
|
|
148
148
|
const getCfgFields = (fv) => (options.actionConfigForms || {})[fv];
|
|
149
|
+
const actionDescription = (options.actionDescriptions || {})[name];
|
|
149
150
|
const cfgFields = getCfgFields(name);
|
|
150
151
|
const setAProp = setAPropGen(setProp);
|
|
151
152
|
const use_setting_action_n =
|
|
@@ -260,6 +261,13 @@ const ActionSettings = () => {
|
|
|
260
261
|
)}
|
|
261
262
|
</td>
|
|
262
263
|
</tr>
|
|
264
|
+
{actionDescription ? (
|
|
265
|
+
<tr>
|
|
266
|
+
<td colSpan="2">
|
|
267
|
+
<small className="text-muted">{actionDescription}</small>
|
|
268
|
+
</td>
|
|
269
|
+
</tr>
|
|
270
|
+
) : null}
|
|
263
271
|
{name !== "Clear" && options.mode === "filter" ? (
|
|
264
272
|
<tr>
|
|
265
273
|
<td>
|
|
@@ -132,20 +132,20 @@ const Card = ({
|
|
|
132
132
|
{bgType === "Image" && bgFileId && imageLocation === "Top" ? (
|
|
133
133
|
<img src={`/files/serve/${bgFileId}`} className="card-img-top" />
|
|
134
134
|
) : null}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
135
|
+
<div className="card-header right-section">
|
|
136
|
+
{title && title.length > 0 ? (
|
|
137
|
+
isFormula?.title ? (
|
|
138
138
|
<span className="font-monospace">={title}</span>
|
|
139
139
|
) : (
|
|
140
140
|
title
|
|
141
|
-
)
|
|
142
|
-
|
|
141
|
+
)
|
|
142
|
+
) : null}
|
|
143
|
+
<div className="title-right">
|
|
143
144
|
<Element canvas id="titleRight" is={Column}>
|
|
144
145
|
{titleRight}
|
|
145
146
|
</Element>
|
|
146
147
|
</div>
|
|
147
|
-
|
|
148
|
-
)}
|
|
148
|
+
</div>
|
|
149
149
|
<div
|
|
150
150
|
className={`card-body ${noPadding ? "p-0" : ""}`}
|
|
151
151
|
style={
|
|
@@ -441,9 +441,9 @@ const CardSettings = () => {
|
|
|
441
441
|
className="form-control-sm form-select"
|
|
442
442
|
onChange={setAProp("imageLocation")}
|
|
443
443
|
>
|
|
444
|
-
<option>{t("Card")}</option>
|
|
445
|
-
<option>{t("Body")}</option>
|
|
446
|
-
<option>{t("Top")}</option>
|
|
444
|
+
<option value="Card">{t("Card")}</option>
|
|
445
|
+
<option value="Body">{t("Body")}</option>
|
|
446
|
+
<option value="Top">{t("Top")}</option>
|
|
447
447
|
</select>
|
|
448
448
|
</td>
|
|
449
449
|
</tr>
|
|
@@ -32,7 +32,7 @@ export const recursivelyCloneToElems = (query) => (nodeId, ix) => {
|
|
|
32
32
|
const children = (nodes || []).map(recursivelyCloneToElems(query));
|
|
33
33
|
if (data.displayName === "Columns") {
|
|
34
34
|
const cols = ntimes(data.props.ncols, (ix) =>
|
|
35
|
-
|
|
35
|
+
cloneLinkedNodeChildren(query, data.linkedNodes["Col" + ix])
|
|
36
36
|
);
|
|
37
37
|
return React.createElement(Columns, {
|
|
38
38
|
...newProps,
|
|
@@ -42,7 +42,7 @@ export const recursivelyCloneToElems = (query) => (nodeId, ix) => {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
if (data.displayName === "ListColumn") {
|
|
45
|
-
const col =
|
|
45
|
+
const col = cloneLinkedNodeChildren(query, data.linkedNodes["listcol"]);
|
|
46
46
|
|
|
47
47
|
return React.createElement(ListColumn, {
|
|
48
48
|
...newProps,
|
|
@@ -1026,7 +1026,7 @@ const ContainerSettings = () => {
|
|
|
1026
1026
|
/>
|
|
1027
1027
|
</tbody>
|
|
1028
1028
|
</table>
|
|
1029
|
-
<table className="w-100" accordiontitle={t("Show if...")}>
|
|
1029
|
+
<table className="w-100" accordiontitle={showIfFormula ? t("Show if...") + " *" : t("Show if...")}>
|
|
1030
1030
|
<tbody>
|
|
1031
1031
|
{["show", "edit", "filter", "list"].includes(options.mode) && (
|
|
1032
1032
|
<SettingsSectionHeaderRow title={t("Formula - show if true")} />
|
|
@@ -50,7 +50,7 @@ const CustomLayer = ({ children }) => {
|
|
|
50
50
|
name = data?.name || name;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
// Rename linked Columns for Tabs and
|
|
53
|
+
// Rename linked Columns for Tabs, Table, and Card sections
|
|
54
54
|
if (name === "Column" && data?.parent) {
|
|
55
55
|
const parentNode = state.nodes[data.parent];
|
|
56
56
|
const parentName = parentNode?.data?.displayName || parentNode?.data?.name;
|
|
@@ -68,6 +68,10 @@ const CustomLayer = ({ children }) => {
|
|
|
68
68
|
if (match) {
|
|
69
69
|
name = `R${parseInt(match[1], 10) + 1}C${parseInt(match[2], 10) + 1}`;
|
|
70
70
|
}
|
|
71
|
+
} else if (parentName === "Card") {
|
|
72
|
+
if (key === "titleRight") name = "Title Right";
|
|
73
|
+
else if (key === "cardbody") name = "Body";
|
|
74
|
+
else if (key === "cardfooter") name = "Footer";
|
|
71
75
|
}
|
|
72
76
|
}
|
|
73
77
|
}
|
|
@@ -79,14 +83,15 @@ const CustomLayer = ({ children }) => {
|
|
|
79
83
|
(nodes && nodes.length > 0) ||
|
|
80
84
|
(linkedNodes && Object.keys(linkedNodes).length > 0);
|
|
81
85
|
|
|
82
|
-
// Hide the ROOT Column and linked
|
|
86
|
+
// Hide the ROOT Column and Container's single linked canvas (no ambiguity there).
|
|
87
|
+
// Card's linked canvases are NOT hidden — they show as "Title Right", "Body", "Footer".
|
|
83
88
|
let shouldHide = false;
|
|
84
89
|
if (id === "ROOT") {
|
|
85
90
|
shouldHide = true;
|
|
86
91
|
} else if ((data?.displayName === "Column" || data?.name === "Column") && data?.parent) {
|
|
87
92
|
const parentNode = state.nodes[data.parent];
|
|
88
93
|
const parentName = parentNode?.data?.displayName || parentNode?.data?.name;
|
|
89
|
-
if (parentName === "
|
|
94
|
+
if (parentName === "Container") {
|
|
90
95
|
const parentLinked = parentNode?.data?.linkedNodes;
|
|
91
96
|
if (parentLinked && Object.values(parentLinked).includes(id)) {
|
|
92
97
|
shouldHide = true;
|
|
@@ -171,14 +171,14 @@ const LinkSettings = () => {
|
|
|
171
171
|
});
|
|
172
172
|
}}
|
|
173
173
|
>
|
|
174
|
-
<option>{t("URL")}</option>
|
|
175
|
-
{(options.pages || []).length > 0 && <option>{t("Page")}</option>}
|
|
174
|
+
<option value="URL">{t("URL")}</option>
|
|
175
|
+
{(options.pages || []).length > 0 && <option value="Page">{t("Page")}</option>}
|
|
176
176
|
{(options.views || []).length > 0 &&
|
|
177
177
|
["page", "filter"].includes(options.mode) && (
|
|
178
|
-
<option>{t("View")}</option>
|
|
178
|
+
<option value="View">{t("View")}</option>
|
|
179
179
|
)}
|
|
180
180
|
{(options.page_groups || []).length > 0 && (
|
|
181
|
-
<option>{t("Page Group")}</option>
|
|
181
|
+
<option value="Page Group">{t("Page Group")}</option>
|
|
182
182
|
)}
|
|
183
183
|
</select>
|
|
184
184
|
</td>
|
|
@@ -48,10 +48,8 @@ const ListColumn = ({
|
|
|
48
48
|
const { actions, query, isActve } = useEditor((state) => ({}));
|
|
49
49
|
const options = useContext(optionsCtx);
|
|
50
50
|
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
} = query.node(id).get();
|
|
54
|
-
const siblings = query.node(parent).childNodes();
|
|
51
|
+
const parent = query.node(id).get()?.data?.parent;
|
|
52
|
+
const siblings = parent ? query.node(parent).childNodes() : [];
|
|
55
53
|
const nChildren = siblings.length;
|
|
56
54
|
const childIx = siblings.findIndex((sib) => sib === id);
|
|
57
55
|
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import React, { Fragment, useState, useEffect, useRef } from "react";
|
|
2
2
|
import optionsCtx from "../context";
|
|
3
3
|
|
|
4
|
-
import Editor, { useMonaco } from "@monaco-editor/react";
|
|
4
|
+
import Editor, { useMonaco, loader } from "@monaco-editor/react";
|
|
5
|
+
|
|
6
|
+
loader.config({ paths: { vs: "/monaco" } });
|
|
5
7
|
|
|
6
8
|
export const mimeToMonacoLanguage = (mode) => {
|
|
7
9
|
if (!mode) return "typescript";
|
|
@@ -21,16 +23,19 @@ export const mimeToMonacoLanguage = (mode) => {
|
|
|
21
23
|
return map[mode] || mode;
|
|
22
24
|
};
|
|
23
25
|
|
|
24
|
-
const setMonacoLanguage = async (monaco, options, isStatements) => {
|
|
26
|
+
const setMonacoLanguage = async (monaco, options, isStatements, nojoins) => {
|
|
25
27
|
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
|
|
26
28
|
noLib: true,
|
|
27
29
|
allowNonTsExtensions: true,
|
|
28
30
|
});
|
|
29
|
-
|
|
31
|
+
// Separate cache keys so nojoins and full-join contexts fetch independently
|
|
32
|
+
const cacheKey = nojoins ? "setMonacoNoJoins" : "setMonaco";
|
|
33
|
+
if (options[cacheKey]) return;
|
|
34
|
+
options[cacheKey] = true;
|
|
30
35
|
|
|
31
|
-
|
|
36
|
+
const nojoinsPart = nojoins ? "&nojoins=yes" : "";
|
|
32
37
|
const tsres = await fetch(
|
|
33
|
-
`/admin/ts-declares?${options.tableName ? `table=${options.tableName}` : ""}&user=yes`
|
|
38
|
+
`/admin/ts-declares?${options.tableName ? `table=${options.tableName}` : ""}&user=yes${nojoinsPart}`
|
|
34
39
|
);
|
|
35
40
|
const tsds = await tsres.text();
|
|
36
41
|
|
|
@@ -105,7 +110,7 @@ const setupVirtualPrefix = (editor, monaco) => {
|
|
|
105
110
|
|
|
106
111
|
export const SingleLineEditor = React.forwardRef(
|
|
107
112
|
(
|
|
108
|
-
{ setProp, value, propKey, onChange, onInput, className, stateExpr },
|
|
113
|
+
{ setProp, value, propKey, onChange, onInput, className, stateExpr, placeholder },
|
|
109
114
|
ref
|
|
110
115
|
) => {
|
|
111
116
|
const options = React.useContext(optionsCtx);
|
|
@@ -133,9 +138,22 @@ export const SingleLineEditor = React.forwardRef(
|
|
|
133
138
|
: value;
|
|
134
139
|
|
|
135
140
|
return (
|
|
136
|
-
<div ref={ref} className="form-control p-0 pt-1">
|
|
141
|
+
<div ref={ref} className="form-control p-0 pt-1" style={{ position: "relative" }}>
|
|
142
|
+
{isEmpty && !activePrefix && placeholder && (
|
|
143
|
+
<div style={{
|
|
144
|
+
position: "absolute",
|
|
145
|
+
top: "3px",
|
|
146
|
+
left: "14px",
|
|
147
|
+
color: "#999",
|
|
148
|
+
pointerEvents: "none",
|
|
149
|
+
zIndex: 1,
|
|
150
|
+
fontSize: "14px",
|
|
151
|
+
whiteSpace: "nowrap",
|
|
152
|
+
}}>
|
|
153
|
+
{placeholder}
|
|
154
|
+
</div>
|
|
155
|
+
)}
|
|
137
156
|
<Editor
|
|
138
|
-
placeholder={"sdfffsd"}
|
|
139
157
|
className={className || ""}
|
|
140
158
|
height="22px"
|
|
141
159
|
value={editorValue}
|
|
@@ -161,14 +179,14 @@ export const SingleLineEditor = React.forwardRef(
|
|
|
161
179
|
}
|
|
162
180
|
);
|
|
163
181
|
|
|
164
|
-
export const MultiLineCodeEditor = ({ setProp, value, onChange, isModalEditor = false, mode }) => {
|
|
182
|
+
export const MultiLineCodeEditor = ({ setProp, value, onChange, isModalEditor = false, mode, placeholder, nojoins }) => {
|
|
165
183
|
const options = React.useContext(optionsCtx);
|
|
166
184
|
const resolvedLanguage = mimeToMonacoLanguage(mode);
|
|
167
185
|
const useTypeScriptSetup = resolvedLanguage === "typescript" || resolvedLanguage === "javascript";
|
|
168
186
|
|
|
169
187
|
const handleEditorWillMount = (monaco) => {
|
|
170
188
|
if (useTypeScriptSetup) {
|
|
171
|
-
setMonacoLanguage(monaco, options, true);
|
|
189
|
+
setMonacoLanguage(monaco, options, true, nojoins);
|
|
172
190
|
}
|
|
173
191
|
};
|
|
174
192
|
|
|
@@ -181,8 +199,24 @@ export const MultiLineCodeEditor = ({ setProp, value, onChange, isModalEditor =
|
|
|
181
199
|
}
|
|
182
200
|
};
|
|
183
201
|
|
|
202
|
+
const isEmpty = !value || value.trim() === "";
|
|
203
|
+
const resolvedPlaceholder = placeholder || "// enter code here";
|
|
204
|
+
|
|
184
205
|
return (
|
|
185
|
-
<div className="form-control p-0 pt-2">
|
|
206
|
+
<div className="form-control p-0 pt-2" style={{ position: "relative" }}>
|
|
207
|
+
{isEmpty && !isModalEditor && (
|
|
208
|
+
<div style={{
|
|
209
|
+
position: "absolute",
|
|
210
|
+
top: "10px",
|
|
211
|
+
left: "14px",
|
|
212
|
+
color: "#999",
|
|
213
|
+
pointerEvents: "none",
|
|
214
|
+
zIndex: 1,
|
|
215
|
+
fontSize: "14px",
|
|
216
|
+
}}>
|
|
217
|
+
{resolvedPlaceholder}
|
|
218
|
+
</div>
|
|
219
|
+
)}
|
|
186
220
|
<Editor
|
|
187
221
|
height={isModalEditor ? "100%" : "150px"}
|
|
188
222
|
value={value}
|