@saltcorn/builder 1.6.0-beta.12 → 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/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@saltcorn/builder",
3
- "version": "1.6.0-beta.12",
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 none",
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.12",
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
- const doSave = (query, keepalive) => {
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,//this is conditional bec body size is limited to 64KB
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((state, query) => ({
221
- selected: getSelectedNodes(state.events.selected)[0] || null,
222
- selectedNodes: getSelectedNodes(state.events.selected),
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].events.selected,
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 rightPos = window.innerWidth - right;
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: `${top > 0 ? top : bottom}px`,
68
- left: `${left}px`,
77
+ top: `${topAdj}px`,
78
+ left: `${leftAdj}px`,
69
79
  right: `${rightPos}px`,
70
- topn: top,
71
- leftn: left,
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>
@@ -165,7 +165,7 @@ const AggregationSettings = () => {
165
165
  <label>
166
166
  {options.mode === "filter"
167
167
  ? t("Field")
168
- : t("Child table field")}
168
+ : t("On field")}
169
169
  </label>
170
170
  </td>
171
171
  <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
- {title && title.length > 0 && (
136
- <div className="card-header right-section">
137
- {isFormula?.title ? (
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
- <div className='title-right'>
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
- </div>
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
- recursivelyCloneToElems(query)(data.linkedNodes["Col" + ix])
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 = recursivelyCloneToElems(query)(data.linkedNodes["listcol"]);
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 Table
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 Columns of Card/Container
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 === "Card" || parentName === "Container") {
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
- data: { parent },
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
- if (options.setMonaco) return;
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
- options.setMonaco = true;
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}