@saltcorn/builder 1.6.0-alpha.13 → 1.6.0-alpha.15

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@saltcorn/builder",
3
- "version": "1.6.0-alpha.13",
3
+ "version": "1.6.0-alpha.15",
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",
@@ -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-alpha.13",
33
+ "@saltcorn/common-code": "1.6.0-alpha.15",
34
34
  "@tippyjs/react": "4.2.6",
35
35
  "babel-jest": "^29.7.0",
36
36
  "babel-loader": "9.2.1",
@@ -5,7 +5,7 @@
5
5
  */
6
6
  /*global notifyAlert, apply_showif*/
7
7
 
8
- import React, { Fragment, useContext, useEffect, useState } from "react";
8
+ import React, { Fragment, useContext, useEffect } from "react";
9
9
  import { useNode } from "@craftjs/core";
10
10
  import useTranslation from "../../hooks/useTranslation";
11
11
  import optionsCtx from "../context";
@@ -25,7 +25,7 @@ import {
25
25
  } from "./utils";
26
26
  import { ntimes } from "./Columns";
27
27
  import { ArrayManager } from "./ArrayManager";
28
- import { MultiLineCodeEditor } from "./MonacoEditor";
28
+
29
29
  import Select from "react-select";
30
30
 
31
31
  export /**
@@ -146,13 +146,6 @@ const ActionSettings = () => {
146
146
  const options = useContext(optionsCtx);
147
147
  const getCfgFields = (fv) => (options.actionConfigForms || {})[fv];
148
148
  const cfgFields = getCfgFields(name);
149
- const cfgFieldsForForm =
150
- name === "run_js_code"
151
- ? (cfgFields || []).filter((f) => f.name !== "code")
152
- : cfgFields;
153
-
154
- const runJsCodeModalOnly = false;
155
- const [codeModalOpen, setCodeModalOpen] = useState(false);
156
149
  const setAProp = setAPropGen(setProp);
157
150
  const use_setting_action_n =
158
151
  setting_action_n || setting_action_n === 0 ? setting_action_n : 0;
@@ -457,116 +450,12 @@ const ActionSettings = () => {
457
450
  ) : null}
458
451
  </Fragment>
459
452
  ) : cfgFields ? (
460
- <Fragment>
461
- {name === "run_js_code" && runJsCodeModalOnly ? (
462
- <div className="builder-config-field" data-field-name="code">
463
- <label>{t("Code")}</label>
464
- <button
465
- type="button"
466
- className="btn btn-secondary btn-sm"
467
- onClick={() => setCodeModalOpen(true)}
468
- >
469
- {t("Open Code Popup")}
470
- </button>
471
- </div>
472
- ) : null}
473
- {name === "run_js_code" && !runJsCodeModalOnly ? (
474
- <Fragment>
475
- <ConfigForm
476
- fields={(cfgFields || []).filter((f) => f.name === "code")}
477
- configuration={configuration}
478
- setProp={setProp}
479
- node={node}
480
- openPopup={() => setCodeModalOpen(true)}
481
- />
482
- {/* <div className="builder-config-field mt-2" data-field-name="code-modal-trigger">
483
- <button
484
- type="button"
485
- className="btn btn-secondary btn-sm"
486
- onClick={() => setCodeModalOpen(true)}
487
- >
488
- {t("Open Code Popup")}
489
- </button>
490
- </div> */}
491
- <ConfigForm
492
- fields={cfgFieldsForForm}
493
- configuration={configuration}
494
- setProp={setProp}
495
- node={node}
496
- />
497
- </Fragment>
498
- ) : (
499
- <ConfigForm
500
- fields={runJsCodeModalOnly ? cfgFieldsForForm : cfgFields}
501
- configuration={configuration}
502
- setProp={setProp}
503
- node={node}
504
- />
505
- )}
506
- {name === "run_js_code" && codeModalOpen ? (
507
- <div
508
- className={`modal fade ${codeModalOpen ? "show" : ""}`}
509
- style={{
510
- display: codeModalOpen ? "block" : "none",
511
- zIndex: 1055,
512
- }}
513
- tabIndex={-1}
514
- role="dialog"
515
- aria-labelledby="codeModalLabel"
516
- aria-hidden={!codeModalOpen}
517
- >
518
- <div
519
- className="modal-backdrop fade show"
520
- style={{ zIndex: 1050 }}
521
- onClick={() => setCodeModalOpen(false)}
522
- aria-hidden="true"
523
- />
524
- <div
525
- className="modal-dialog modal-dialog-centered modal-lg"
526
- role="document"
527
- style={{ zIndex: 1060 }}
528
- onClick={(e) => e.stopPropagation()}
529
- >
530
- <div className="modal-content code-modal">
531
- <div className="modal-header">
532
- <h5 className="modal-title" id="codeModalLabel">
533
- {t("Code")}
534
- </h5>
535
- <button
536
- type="button"
537
- className="btn-close"
538
- aria-label="Close"
539
- onClick={() => setCodeModalOpen(false)}
540
- />
541
- </div>
542
- <div className="modal-body">
543
- <MultiLineCodeEditor
544
- setProp={setProp}
545
- value={configuration?.code ?? ""}
546
- onChange={(code) =>
547
- setProp((prop) => {
548
- if (!prop.configuration)
549
- prop.configuration = {};
550
- prop.configuration.code = code;
551
- })
552
- }
553
- isModalEditor
554
- />
555
- </div>
556
- <div className="modal-footer">
557
- <button
558
- type="button"
559
- className="btn btn-secondary"
560
- onClick={() => setCodeModalOpen(false)}
561
- >
562
- {t("Close")}
563
- </button>
564
- </div>
565
- </div>
566
- </div>
567
- </div>
568
- ) : null}
569
- </Fragment>
453
+ <ConfigForm
454
+ fields={cfgFields}
455
+ configuration={configuration}
456
+ setProp={setProp}
457
+ node={node}
458
+ />
570
459
  ) : null}
571
460
  {cfg_link ? (
572
461
  <a className="d-block mt-2" target="_blank" href={cfg_link}>
@@ -4,7 +4,9 @@
4
4
  * @subcategory components / elements
5
5
  */
6
6
 
7
- import React, { Fragment, useState, useContext, useEffect } from "react";
7
+ /* globals validate_bool_expression_elem */
8
+
9
+ import React, { Fragment, useState, useContext, useEffect, useRef } from "react";
8
10
  import { useNode } from "@craftjs/core";
9
11
  import useTranslation from "../../hooks/useTranslation";
10
12
  import optionsCtx from "../context";
@@ -73,6 +75,7 @@ const DropDownFilterSettings = () => {
73
75
  }));
74
76
  const options = useContext(optionsCtx);
75
77
  const setAProp = setAPropGen(setProp);
78
+ const editorRef = useRef(null);
76
79
  let select_all_options;
77
80
  const field = options.fields.find((f) => f.name === name);
78
81
  if (field?.type === "String" && field.attributes?.options)
@@ -121,9 +124,13 @@ const DropDownFilterSettings = () => {
121
124
  </td>
122
125
  <td>
123
126
  <SingleLineEditor
127
+ ref={editorRef}
124
128
  value={where}
125
129
  setProp={setProp}
126
130
  propKey="where"
131
+ onInput={(value) =>
132
+ validate_bool_expression_elem(value, editorRef.current)
133
+ }
127
134
  />
128
135
  </td>
129
136
  </tr>
@@ -45,7 +45,9 @@ const fields = (mode) => {
45
45
  {
46
46
  label: "HTML Code",
47
47
  name: "text",
48
- type: "textarea",
48
+ input_type: "code",
49
+ type: "code",
50
+ attributes: { mode: "text/html" },
49
51
  segment_name: "contents",
50
52
  onSave: (segment, node_props) => {
51
53
  const div = document.createElement("div");
@@ -3,6 +3,24 @@ import optionsCtx from "../context";
3
3
 
4
4
  import Editor, { useMonaco } from "@monaco-editor/react";
5
5
 
6
+ export const mimeToMonacoLanguage = (mode) => {
7
+ if (!mode) return "typescript";
8
+ const map = {
9
+ "text/javascript": "typescript",
10
+ "application/javascript": "typescript",
11
+ "text/html": "html",
12
+ "text/css": "css",
13
+ "application/json": "json",
14
+ "text/x-sql": "sql",
15
+ "text/x-python": "python",
16
+ "text/x-yaml": "yaml",
17
+ "text/xml": "xml",
18
+ "text/x-markdown": "markdown",
19
+ "text/typescript": "typescript",
20
+ };
21
+ return map[mode] || mode;
22
+ };
23
+
6
24
  const setMonacoLanguage = async (monaco, options, isStatements) => {
7
25
  monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
8
26
  noLib: true,
@@ -27,33 +45,108 @@ const setMonacoLanguage = async (monaco, options, isStatements) => {
27
45
  });
28
46
  };
29
47
 
48
+ // add hidden-prefix
49
+ // line 1 holds the typed prefix, the user edits from line 2 onwards
50
+ const setupVirtualPrefix = (editor, monaco) => {
51
+ const model = editor.getModel();
52
+ editor.setHiddenAreas([{ startLineNumber: 1, endLineNumber: 1 }]);
53
+ editor.onDidChangeCursorPosition((e) => {
54
+ if (e.position.lineNumber < 2)
55
+ editor.setPosition({ lineNumber: 2, column: 1 });
56
+ });
57
+ editor.onKeyDown((e) => {
58
+ const pos = editor.getPosition();
59
+ if (
60
+ pos.lineNumber === 2 &&
61
+ pos.column === 1 &&
62
+ e.keyCode === monaco.KeyCode.Backspace
63
+ ) {
64
+ e.preventDefault();
65
+ e.stopPropagation();
66
+ }
67
+ });
68
+ // copy without the prefix line
69
+ editor.addAction({
70
+ id: "copy-editable-only",
71
+ label: "Copy Only User Content",
72
+ keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyC],
73
+ run: (ed) => {
74
+ const selection = ed.getSelection();
75
+ if (selection.isEmpty()) return;
76
+ const safeSelection = selection.intersectRanges(
77
+ new monaco.Range(
78
+ 2,
79
+ 1,
80
+ model.getLineCount(),
81
+ model.getLineMaxColumn(model.getLineCount())
82
+ )
83
+ );
84
+ if (safeSelection)
85
+ navigator.clipboard.writeText(model.getValueInRange(safeSelection));
86
+ },
87
+ });
88
+ // select all without the prefix line
89
+ editor.addAction({
90
+ id: "select-editable-only",
91
+ label: "Select Only User Content",
92
+ keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyA],
93
+ run: (ed) => {
94
+ ed.setSelection(
95
+ new monaco.Range(
96
+ 2,
97
+ 1,
98
+ model.getLineCount(),
99
+ model.getLineMaxColumn(model.getLineCount())
100
+ )
101
+ );
102
+ },
103
+ });
104
+ };
105
+
30
106
  export const SingleLineEditor = React.forwardRef(
31
- ({ setProp, value, propKey, onChange, onInput, className }, ref) => {
107
+ (
108
+ { setProp, value, propKey, onChange, onInput, className, stateExpr },
109
+ ref
110
+ ) => {
32
111
  const options = React.useContext(optionsCtx);
112
+ const activePrefix = stateExpr ? "const _: Row =" : null;
33
113
 
34
114
  const handleEditorWillMount = (monaco) => {
35
115
  setMonacoLanguage(monaco, options, false);
36
116
  };
37
117
 
38
118
  const handleEditorDidMount = (editor, monaco) => {
119
+ if (activePrefix) {
120
+ setupVirtualPrefix(editor, monaco);
121
+ return;
122
+ }
39
123
  if (!onInput) return;
40
124
 
41
125
  editor.onDidChangeModelContent(() => {
42
- const value = editor.getValue();
43
- onInput(value);
126
+ onInput(editor.getValue());
44
127
  });
45
128
  };
46
129
 
130
+ const isEmpty = !value || value.trim() === "";
131
+ const editorValue = activePrefix
132
+ ? `${isEmpty ? "//" : ""} ${activePrefix}\n${value || ""}`
133
+ : value;
134
+
47
135
  return (
48
136
  <div ref={ref} className="form-control p-0 pt-1">
49
137
  <Editor
50
138
  placeholder={"sdfffsd"}
51
139
  className={className || ""}
52
140
  height="22px"
53
- value={value}
54
- onChange={(value) => {
55
- onChange && onChange(value);
56
- setProp && propKey && setProp((prop) => (prop[propKey] = value));
141
+ value={editorValue}
142
+ onChange={(fullValue) => {
143
+ const userValue = activePrefix
144
+ ? (fullValue || "").substring((fullValue || "").indexOf("\n") + 1)
145
+ : fullValue;
146
+ onChange && onChange(userValue);
147
+ setProp &&
148
+ propKey &&
149
+ setProp((prop) => (prop[propKey] = userValue));
57
150
  }}
58
151
  defaultLanguage="typescript"
59
152
  //onMount={handleEditorDidMount}
@@ -68,25 +161,37 @@ export const SingleLineEditor = React.forwardRef(
68
161
  }
69
162
  );
70
163
 
71
- export const MultiLineCodeEditor = ({ setProp, value, onChange, isModalEditor = false }) => {
164
+ export const MultiLineCodeEditor = ({ setProp, value, onChange, isModalEditor = false, mode }) => {
72
165
  const options = React.useContext(optionsCtx);
166
+ const resolvedLanguage = mimeToMonacoLanguage(mode);
167
+ const useTypeScriptSetup = resolvedLanguage === "typescript" || resolvedLanguage === "javascript";
73
168
 
74
169
  const handleEditorWillMount = (monaco) => {
75
- setMonacoLanguage(monaco, options, true);
170
+ if (useTypeScriptSetup) {
171
+ setMonacoLanguage(monaco, options, true);
172
+ }
76
173
  };
174
+
175
+ const handleEditorDidMount = (editor, monaco) => {
176
+ if (!useTypeScriptSetup) {
177
+ const model = editor.getModel();
178
+ if (model) {
179
+ monaco.editor.setModelLanguage(model, resolvedLanguage);
180
+ }
181
+ }
182
+ };
183
+
77
184
  return (
78
185
  <div className="form-control p-0 pt-2">
79
186
  <Editor
80
187
  height={isModalEditor ? "100%" : "150px"}
81
188
  value={value}
82
189
  onChange={onChange}
83
- defaultLanguage="typescript"
84
- //onMount={handleEditorDidMount}
85
- //beforeMount={handleEditorWillMount}
190
+ defaultLanguage={resolvedLanguage}
86
191
  options={multiLineEditorOptions}
87
- //theme="myCoolTheme"
88
192
  beforeMount={handleEditorWillMount}
89
- className={isModalEditor ? 'code-modal-form' : ''}
193
+ onMount={handleEditorDidMount}
194
+ className={isModalEditor ? "code-modal-form" : ""}
90
195
  />
91
196
  </div>
92
197
  );
@@ -94,7 +199,7 @@ export const MultiLineCodeEditor = ({ setProp, value, onChange, isModalEditor =
94
199
 
95
200
  const multiLineEditorOptions = {
96
201
  fontSize: "14px",
97
- minHeight:"80vh",
202
+ minHeight: "80vh",
98
203
  fontWeight: "normal",
99
204
  wordWrap: "off",
100
205
  lineNumbers: "off",
@@ -405,6 +405,7 @@ const ViewSettings = () => {
405
405
  value={extra_state_fml}
406
406
  propKey="extra_state_fml"
407
407
  onChange={setAProp("extra_state_fml")}
408
+ stateExpr
408
409
  />
409
410
  {errorString ? (
410
411
  <small className="text-danger font-monospace d-block">
@@ -321,6 +321,7 @@ const ViewLinkSettings = () => {
321
321
  value={extra_state_fml}
322
322
  propKey="extra_state_fml"
323
323
  onChange={setAProp("extra_state_fml")}
324
+ stateExpr
324
325
  />
325
326
  {errorString ? (
326
327
  <small className="text-danger font-monospace d-block">
@@ -932,6 +932,95 @@ const ColorInput = ({ value, onChange }) =>
932
932
  </button>
933
933
  );
934
934
 
935
+ const CodeFieldWithModal = ({ value, onChange, setProp, mode, label, hideLabel }) => {
936
+ const [modalOpen, setModalOpen] = useState(false);
937
+ const { t } = useTranslation();
938
+ return (
939
+ <Fragment>
940
+ {!hideLabel && (
941
+ <label>
942
+ {t(label)}{" "}
943
+ <i
944
+ className="fas fa-external-link-alt ms-1"
945
+ style={{ cursor: "pointer" }}
946
+ onClick={() => setModalOpen(true)}
947
+ title={t("Open code popup")}
948
+ ></i>
949
+ </label>
950
+ )}
951
+ {hideLabel && (
952
+ <i
953
+ className="fas fa-external-link-alt ms-1"
954
+ style={{ cursor: "pointer" }}
955
+ onClick={() => setModalOpen(true)}
956
+ title={t("Open code popup")}
957
+ ></i>
958
+ )}
959
+ <MultiLineCodeEditor
960
+ setProp={setProp}
961
+ value={value}
962
+ onChange={onChange}
963
+ mode={mode}
964
+ />
965
+ {modalOpen ? (
966
+ <div
967
+ className={`modal fade show`}
968
+ style={{ display: "block", zIndex: 1055 }}
969
+ tabIndex={-1}
970
+ role="dialog"
971
+ aria-labelledby="codeModalLabel"
972
+ aria-hidden={false}
973
+ >
974
+ <div
975
+ className="modal-backdrop fade show"
976
+ style={{ zIndex: 1050 }}
977
+ onClick={() => setModalOpen(false)}
978
+ aria-hidden="true"
979
+ />
980
+ <div
981
+ className="modal-dialog modal-dialog-centered modal-lg"
982
+ role="document"
983
+ style={{ zIndex: 1060 }}
984
+ onClick={(e) => e.stopPropagation()}
985
+ >
986
+ <div className="modal-content code-modal">
987
+ <div className="modal-header">
988
+ <h5 className="modal-title" id="codeModalLabel">
989
+ {t(label)}
990
+ </h5>
991
+ <button
992
+ type="button"
993
+ className="btn-close"
994
+ aria-label="Close"
995
+ onClick={() => setModalOpen(false)}
996
+ />
997
+ </div>
998
+ <div className="modal-body">
999
+ <MultiLineCodeEditor
1000
+ setProp={setProp}
1001
+ value={value}
1002
+ onChange={onChange}
1003
+ isModalEditor
1004
+ mode={mode}
1005
+ />
1006
+ </div>
1007
+ <div className="modal-footer">
1008
+ <button
1009
+ type="button"
1010
+ className="btn btn-secondary"
1011
+ onClick={() => setModalOpen(false)}
1012
+ >
1013
+ {t("Close")}
1014
+ </button>
1015
+ </div>
1016
+ </div>
1017
+ </div>
1018
+ </div>
1019
+ ) : null}
1020
+ </Fragment>
1021
+ );
1022
+ };
1023
+
935
1024
  export /**
936
1025
  * @param {object} props
937
1026
  * @param {object[]} props.fields
@@ -953,7 +1042,6 @@ const ConfigForm = ({
953
1042
  onChange,
954
1043
  tableName,
955
1044
  fieldName,
956
- openPopup
957
1045
  }) => (
958
1046
  <div className="form-namespace">
959
1047
  {fields.map((f, ix) => {
@@ -968,7 +1056,7 @@ const ConfigForm = ({
968
1056
  }
969
1057
  return (
970
1058
  <div key={ix} className="builder-config-field" data-field-name={f.name}>
971
- {!isCheckbox(f) ? (
1059
+ {!isCheckbox(f) && f.input_type !== "code" ? (
972
1060
  <label>
973
1061
  {f.label || f.name}
974
1062
  {f.help ? (
@@ -978,7 +1066,7 @@ const ConfigForm = ({
978
1066
  table_name={tableName}
979
1067
  />
980
1068
  ) : null}
981
- {" "}{openPopup && <i class="fas fa-external-link-alt " onClick={openPopup}></i>}
1069
+ {" "}
982
1070
  </label>
983
1071
  ) : null}
984
1072
  <ConfigField
@@ -1233,25 +1321,33 @@ const ConfigField = ({
1233
1321
  onChange={(e) => e.target && myOnChange(e.target.value)}
1234
1322
  />
1235
1323
  ),
1236
- code: () =>
1237
- field?.attributes?.expression_type === "row" ||
1238
- field?.attributes?.expression_type === "query" ? (
1239
- <textarea
1240
- rows="6"
1241
- type="text"
1242
- className={`field-${field?.name} form-control`}
1243
- value={value}
1244
- name={field?.name}
1245
- onChange={(e) => e.target && myOnChange(e.target.value)}
1246
- spellCheck={false}
1247
- />
1248
- ) : (
1249
- <MultiLineCodeEditor
1250
- setProp={setProp}
1324
+ code: () => {
1325
+ if (
1326
+ field?.attributes?.expression_type === "row" ||
1327
+ field?.attributes?.expression_type === "query"
1328
+ ) {
1329
+ return (
1330
+ <textarea
1331
+ rows="6"
1332
+ type="text"
1333
+ className={`field-${field?.name} form-control`}
1334
+ value={value}
1335
+ name={field?.name}
1336
+ onChange={(e) => e.target && myOnChange(e.target.value)}
1337
+ spellCheck={false}
1338
+ />
1339
+ );
1340
+ }
1341
+ return (
1342
+ <CodeFieldWithModal
1251
1343
  value={value}
1252
1344
  onChange={myOnChange}
1345
+ setProp={setProp}
1346
+ mode={field?.attributes?.mode}
1347
+ label={field?.label || field?.name || "Code"}
1253
1348
  />
1254
- ),
1349
+ );
1350
+ },
1255
1351
  select: () => {
1256
1352
  if (field.class?.includes?.("selectizable")) {
1257
1353
  const seloptions = field.options.map((o, ix) =>
@@ -1500,7 +1596,7 @@ const SettingsRow = ({
1500
1596
  valuePostfix,
1501
1597
  }) => {
1502
1598
  const { t } = useTranslation();
1503
- const fullWidth = ["String", "Bool", "textarea"].includes(field.type);
1599
+ const fullWidth = ["String", "Bool", "textarea"].includes(field.type) || field.input_type === "code";
1504
1600
  const needLabel = field.type !== "Bool";
1505
1601
  const inner = field.canBeFormula ? (
1506
1602
  <OrFormula
@@ -1530,7 +1626,7 @@ const SettingsRow = ({
1530
1626
  <tr>
1531
1627
  {fullWidth ? (
1532
1628
  <td colSpan="2">
1533
- {needLabel && <label>{field.label}</label>}
1629
+ {needLabel && field.input_type !== "code" && <label>{field.label}</label>}
1534
1630
  {inner}
1535
1631
  {field.sublabel ? (
1536
1632
  <i