@saltcorn/builder 1.6.0-alpha.9 → 1.6.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/builder_bundle.js +76 -104004
- package/dist/builder_bundle.js.LICENSE.txt +2 -0
- package/package.json +3 -2
- package/src/components/Builder.js +371 -187
- package/src/components/RenderNode.js +38 -10
- package/src/components/Toolbox.js +100 -22
- package/src/components/elements/Action.js +10 -120
- package/src/components/elements/Aggregation.js +17 -9
- package/src/components/elements/ArrayManager.js +10 -5
- package/src/components/elements/BoxModelEditor.js +24 -23
- package/src/components/elements/Card.js +26 -1
- package/src/components/elements/Columns.js +158 -110
- package/src/components/elements/Container.js +43 -8
- package/src/components/elements/CustomLayer.js +288 -0
- package/src/components/elements/DropDownFilter.js +8 -1
- package/src/components/elements/DropMenu.js +10 -4
- package/src/components/elements/HTMLCode.js +3 -1
- package/src/components/elements/MonacoEditor.js +120 -15
- package/src/components/elements/Prompt.js +285 -0
- package/src/components/elements/SearchBar.js +30 -6
- package/src/components/elements/Table.js +10 -12
- package/src/components/elements/Text.js +104 -20
- package/src/components/elements/View.js +2 -1
- package/src/components/elements/ViewLink.js +1 -0
- package/src/components/elements/utils.js +133 -30
- package/src/components/storage.js +33 -7
- package/src/index.js +10 -0
- package/src/utils/responsive_utils.js +139 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @category saltcorn-builder
|
|
3
|
+
* @module components/elements/Prompt
|
|
4
|
+
* @subcategory components / elements
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React, { useState, useContext, Fragment } from "react";
|
|
8
|
+
import { useNode, useEditor } from "@craftjs/core";
|
|
9
|
+
import useTranslation from "../../hooks/useTranslation";
|
|
10
|
+
import optionsCtx from "../context";
|
|
11
|
+
import StorageCtx from "../storage_context";
|
|
12
|
+
|
|
13
|
+
const PROMPT_ICONS = {
|
|
14
|
+
container: "fas fa-box",
|
|
15
|
+
view: "fas fa-eye",
|
|
16
|
+
field: "fas fa-i-cursor",
|
|
17
|
+
action: "fas fa-bolt",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const PROMPT_LABELS = {
|
|
21
|
+
container: "Container",
|
|
22
|
+
view: "View",
|
|
23
|
+
field: "Field",
|
|
24
|
+
action: "Action",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const Prompt = ({ promptType, promptText }) => {
|
|
28
|
+
const {
|
|
29
|
+
connectors: { connect, drag },
|
|
30
|
+
selected,
|
|
31
|
+
actions: { setProp },
|
|
32
|
+
id,
|
|
33
|
+
parent,
|
|
34
|
+
} = useNode((state) => ({
|
|
35
|
+
selected: state.events.selected,
|
|
36
|
+
parent: state.data.parent,
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
const { query, actions: editorActions } = useEditor();
|
|
40
|
+
const options = useContext(optionsCtx);
|
|
41
|
+
const { layoutToNodes } = useContext(StorageCtx);
|
|
42
|
+
const { t } = useTranslation();
|
|
43
|
+
|
|
44
|
+
const [generating, setGenerating] = useState(false);
|
|
45
|
+
|
|
46
|
+
const icon = PROMPT_ICONS[promptType] || "fas fa-robot";
|
|
47
|
+
|
|
48
|
+
const handleGenerate = async (e) => {
|
|
49
|
+
e.stopPropagation();
|
|
50
|
+
if (!promptText.trim()) return;
|
|
51
|
+
|
|
52
|
+
setGenerating(true);
|
|
53
|
+
setProp((props) => {
|
|
54
|
+
props.generateError = null;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const combinedPrompt = `[${promptType}]: ${promptText}`;
|
|
59
|
+
|
|
60
|
+
const res = await fetch("/viewedit/copilot-generate-layout", {
|
|
61
|
+
method: "POST",
|
|
62
|
+
headers: {
|
|
63
|
+
"Content-Type": "application/json",
|
|
64
|
+
"CSRF-Token": options.csrfToken,
|
|
65
|
+
"X-Requested-With": "XMLHttpRequest",
|
|
66
|
+
},
|
|
67
|
+
body: JSON.stringify({
|
|
68
|
+
prompt: combinedPrompt,
|
|
69
|
+
mode: options.mode,
|
|
70
|
+
table: options.tableName,
|
|
71
|
+
}),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const data = await res.json();
|
|
75
|
+
if (data.error) {
|
|
76
|
+
setProp((props) => {
|
|
77
|
+
props.generateError = data.error;
|
|
78
|
+
});
|
|
79
|
+
} else if (data.layout) {
|
|
80
|
+
editorActions.delete(id);
|
|
81
|
+
layoutToNodes(data.layout, query, editorActions, parent, options);
|
|
82
|
+
}
|
|
83
|
+
} catch (err) {
|
|
84
|
+
setProp((props) => {
|
|
85
|
+
props.generateError = err.message || "Generation failed";
|
|
86
|
+
});
|
|
87
|
+
} finally {
|
|
88
|
+
setGenerating(false);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<div
|
|
94
|
+
ref={(dom) => connect(drag(dom))}
|
|
95
|
+
className={`prompt-placeholder ${selected ? "selected-node" : ""}`}
|
|
96
|
+
style={{
|
|
97
|
+
border: "2px dashed #6c8ebf",
|
|
98
|
+
borderRadius: "8px",
|
|
99
|
+
padding: "12px",
|
|
100
|
+
margin: "4px 0",
|
|
101
|
+
backgroundColor: "#e8f0fe",
|
|
102
|
+
minHeight: "60px",
|
|
103
|
+
}}
|
|
104
|
+
>
|
|
105
|
+
<div
|
|
106
|
+
style={{
|
|
107
|
+
display: "flex",
|
|
108
|
+
alignItems: "center",
|
|
109
|
+
gap: "6px",
|
|
110
|
+
marginBottom: "6px",
|
|
111
|
+
fontWeight: "bold",
|
|
112
|
+
fontSize: "13px",
|
|
113
|
+
color: "#1a73e8",
|
|
114
|
+
}}
|
|
115
|
+
>
|
|
116
|
+
<i className={icon}></i>
|
|
117
|
+
<span>{t("Prompt")}</span>
|
|
118
|
+
</div>
|
|
119
|
+
<textarea
|
|
120
|
+
rows="3"
|
|
121
|
+
className="form-control form-control-sm"
|
|
122
|
+
style={{
|
|
123
|
+
fontSize: "13px",
|
|
124
|
+
backgroundColor: "transparent",
|
|
125
|
+
border: "1px solid #b0c4de",
|
|
126
|
+
resize: "vertical",
|
|
127
|
+
marginBottom: "8px",
|
|
128
|
+
}}
|
|
129
|
+
value={promptText}
|
|
130
|
+
placeholder={t("Describe what you want to generate...")}
|
|
131
|
+
onChange={(e) =>
|
|
132
|
+
setProp((props) => {
|
|
133
|
+
props.promptText = e.target.value;
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
onClick={(e) => e.stopPropagation()}
|
|
137
|
+
/>
|
|
138
|
+
<button
|
|
139
|
+
className="btn btn-sm btn-success w-100"
|
|
140
|
+
onClick={handleGenerate}
|
|
141
|
+
disabled={generating || !promptText.trim()}
|
|
142
|
+
style={{ fontSize: "12px" }}
|
|
143
|
+
>
|
|
144
|
+
{generating ? (
|
|
145
|
+
<Fragment>
|
|
146
|
+
<span
|
|
147
|
+
className="spinner-border spinner-border-sm me-1"
|
|
148
|
+
role="status"
|
|
149
|
+
></span>
|
|
150
|
+
{t("Generating...")}
|
|
151
|
+
</Fragment>
|
|
152
|
+
) : (
|
|
153
|
+
<Fragment>
|
|
154
|
+
<i className="fas fa-robot me-1"></i>
|
|
155
|
+
{t("Generate")}
|
|
156
|
+
</Fragment>
|
|
157
|
+
)}
|
|
158
|
+
</button>
|
|
159
|
+
</div>
|
|
160
|
+
);
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const PromptSettings = () => {
|
|
164
|
+
const { t } = useTranslation();
|
|
165
|
+
const {
|
|
166
|
+
actions: { setProp },
|
|
167
|
+
promptType,
|
|
168
|
+
promptText,
|
|
169
|
+
generateError,
|
|
170
|
+
id,
|
|
171
|
+
parent,
|
|
172
|
+
} = useNode((node) => ({
|
|
173
|
+
promptType: node.data.props.promptType,
|
|
174
|
+
promptText: node.data.props.promptText,
|
|
175
|
+
generateError: node.data.props.generateError,
|
|
176
|
+
id: node.id,
|
|
177
|
+
parent: node.data.parent,
|
|
178
|
+
}));
|
|
179
|
+
|
|
180
|
+
const { query, actions: editorActions } = useEditor();
|
|
181
|
+
const options = useContext(optionsCtx);
|
|
182
|
+
const { layoutToNodes } = useContext(StorageCtx);
|
|
183
|
+
|
|
184
|
+
const [generating, setGenerating] = useState(false);
|
|
185
|
+
|
|
186
|
+
const handleGenerate = async () => {
|
|
187
|
+
if (!promptText.trim()) return;
|
|
188
|
+
|
|
189
|
+
setGenerating(true);
|
|
190
|
+
setProp((props) => {
|
|
191
|
+
props.generateError = null;
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
const combinedPrompt = `[${promptType}]: ${promptText}`;
|
|
196
|
+
|
|
197
|
+
const res = await fetch("/viewedit/copilot-generate-layout", {
|
|
198
|
+
method: "POST",
|
|
199
|
+
headers: {
|
|
200
|
+
"Content-Type": "application/json",
|
|
201
|
+
"CSRF-Token": options.csrfToken,
|
|
202
|
+
"X-Requested-With": "XMLHttpRequest",
|
|
203
|
+
},
|
|
204
|
+
body: JSON.stringify({
|
|
205
|
+
prompt: combinedPrompt,
|
|
206
|
+
mode: options.mode,
|
|
207
|
+
table: options.tableName,
|
|
208
|
+
}),
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const data = await res.json();
|
|
212
|
+
if (data.error) {
|
|
213
|
+
setProp((props) => {
|
|
214
|
+
props.generateError = data.error;
|
|
215
|
+
});
|
|
216
|
+
} else if (data.layout) {
|
|
217
|
+
editorActions.delete(id);
|
|
218
|
+
layoutToNodes(data.layout, query, editorActions, parent, options);
|
|
219
|
+
}
|
|
220
|
+
} catch (err) {
|
|
221
|
+
setProp((props) => {
|
|
222
|
+
props.generateError = err.message || "Generation failed";
|
|
223
|
+
});
|
|
224
|
+
} finally {
|
|
225
|
+
setGenerating(false);
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
return (
|
|
230
|
+
<div>
|
|
231
|
+
<div className="mb-2">
|
|
232
|
+
<label className="form-label">{t("Prompt")}</label>
|
|
233
|
+
<textarea
|
|
234
|
+
rows="4"
|
|
235
|
+
className="form-control"
|
|
236
|
+
value={promptText}
|
|
237
|
+
placeholder={t("Describe what you want to generate...")}
|
|
238
|
+
onChange={(e) =>
|
|
239
|
+
setProp((props) => {
|
|
240
|
+
props.promptText = e.target.value;
|
|
241
|
+
})
|
|
242
|
+
}
|
|
243
|
+
/>
|
|
244
|
+
{generateError && (
|
|
245
|
+
<div className="text-danger small mt-1">{generateError}</div>
|
|
246
|
+
)}
|
|
247
|
+
</div>
|
|
248
|
+
<div className="mb-2">
|
|
249
|
+
<button
|
|
250
|
+
className="btn btn-sm btn-success w-100"
|
|
251
|
+
onClick={handleGenerate}
|
|
252
|
+
disabled={generating || !promptText.trim()}
|
|
253
|
+
>
|
|
254
|
+
{generating ? (
|
|
255
|
+
<Fragment>
|
|
256
|
+
<span
|
|
257
|
+
className="spinner-border spinner-border-sm me-1"
|
|
258
|
+
role="status"
|
|
259
|
+
></span>
|
|
260
|
+
{t("Generating...")}
|
|
261
|
+
</Fragment>
|
|
262
|
+
) : (
|
|
263
|
+
<Fragment>
|
|
264
|
+
<i className="fas fa-robot me-1"></i>
|
|
265
|
+
{t("Generate")}
|
|
266
|
+
</Fragment>
|
|
267
|
+
)}
|
|
268
|
+
</button>
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
271
|
+
);
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
Prompt.craft = {
|
|
275
|
+
displayName: "Prompt",
|
|
276
|
+
defaultProps: {
|
|
277
|
+
promptType: "container",
|
|
278
|
+
promptText: "",
|
|
279
|
+
},
|
|
280
|
+
related: {
|
|
281
|
+
settings: PromptSettings,
|
|
282
|
+
segment_type: "prompt",
|
|
283
|
+
fields: ["promptType", "promptText"],
|
|
284
|
+
},
|
|
285
|
+
};
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import React, { Fragment, useState, useEffect, useContext } from "react";
|
|
8
8
|
import useTranslation from "../../hooks/useTranslation";
|
|
9
9
|
import optionsCtx from "../context";
|
|
10
|
-
import { useNode } from "@craftjs/core";
|
|
10
|
+
import { useNode, Element } from "@craftjs/core";
|
|
11
11
|
import { Column } from "./Column";
|
|
12
12
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
13
13
|
import { faCaretDown } from "@fortawesome/free-solid-svg-icons";
|
|
@@ -23,14 +23,24 @@ export /**
|
|
|
23
23
|
* @category saltcorn-builder
|
|
24
24
|
* @subcategory components
|
|
25
25
|
*/
|
|
26
|
-
const SearchBar = ({ has_dropdown, children, show_badges }) => {
|
|
26
|
+
const SearchBar = ({ has_dropdown, children, contents, show_badges }) => {
|
|
27
27
|
const { t } = useTranslation();
|
|
28
|
+
const options = useContext(optionsCtx);
|
|
28
29
|
const {
|
|
29
30
|
selected,
|
|
30
31
|
connectors: { connect, drag },
|
|
31
32
|
} = useNode((node) => ({ selected: node.events.selected }));
|
|
32
33
|
const [showDropdown, setDropdown] = useState(false);
|
|
33
34
|
const [dropWidth, setDropWidth] = useState(200);
|
|
35
|
+
|
|
36
|
+
const renderContents = () => {
|
|
37
|
+
const actualChildren = contents || children;
|
|
38
|
+
if (!actualChildren) return null;
|
|
39
|
+
if (React.isValidElement(actualChildren)) return actualChildren;
|
|
40
|
+
if (Array.isArray(actualChildren)) return actualChildren;
|
|
41
|
+
return actualChildren;
|
|
42
|
+
};
|
|
43
|
+
|
|
34
44
|
return (
|
|
35
45
|
<div
|
|
36
46
|
className={`input-group ${selected ? "selected-node" : ""}`}
|
|
@@ -71,12 +81,21 @@ const SearchBar = ({ has_dropdown, children, show_badges }) => {
|
|
|
71
81
|
className={`dropdown-menu searchbar-dropdown ${
|
|
72
82
|
showDropdown ? "show" : ""
|
|
73
83
|
}`}
|
|
74
|
-
style={{ width: dropWidth, left: 0 }}
|
|
84
|
+
style={{ width: dropWidth, ...(options?.isRTL ? { right: 0 } : { left: 0 }) }}
|
|
75
85
|
>
|
|
76
|
-
<
|
|
86
|
+
<Element canvas id="searchbar-contents" is={Column}>
|
|
87
|
+
{renderContents()}
|
|
88
|
+
</Element>
|
|
77
89
|
</div>
|
|
78
90
|
</Fragment>
|
|
79
91
|
)}
|
|
92
|
+
{!has_dropdown && (
|
|
93
|
+
<div style={{ display: "none" }}>
|
|
94
|
+
<Element canvas id="searchbar-contents" is={Column}>
|
|
95
|
+
{renderContents()}
|
|
96
|
+
</Element>
|
|
97
|
+
</div>
|
|
98
|
+
)}
|
|
80
99
|
</div>
|
|
81
100
|
);
|
|
82
101
|
};
|
|
@@ -146,11 +165,16 @@ SearchBar.craft = {
|
|
|
146
165
|
has_dropdown: false,
|
|
147
166
|
show_badges: false,
|
|
148
167
|
autofocus: false,
|
|
168
|
+
contents: [],
|
|
149
169
|
},
|
|
150
170
|
related: {
|
|
151
171
|
settings: SearchBarSettings,
|
|
152
172
|
segment_type: "search_bar",
|
|
153
|
-
|
|
154
|
-
|
|
173
|
+
fields: [
|
|
174
|
+
{ name: "has_dropdown" },
|
|
175
|
+
{ name: "show_badges" },
|
|
176
|
+
{ name: "autofocus" },
|
|
177
|
+
{ label: "Contents", name: "contents", type: "Nodes", nodeID: "searchbar-contents" },
|
|
178
|
+
],
|
|
155
179
|
},
|
|
156
180
|
};
|
|
@@ -124,19 +124,17 @@ const TableSettings = () => {
|
|
|
124
124
|
showIf: { bs_style: true },
|
|
125
125
|
},
|
|
126
126
|
];
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
ntimes(v, (i) => {
|
|
134
|
-
if (!prop.contents[i]) prop.contents[i] = [];
|
|
135
|
-
});
|
|
127
|
+
const Settings = SettingsFromFields(fields, {
|
|
128
|
+
onChange: (fnm, v, setProp) => {
|
|
129
|
+
if (fnm === "rows")
|
|
130
|
+
setProp((prop) => {
|
|
131
|
+
ntimes(v, (i) => {
|
|
132
|
+
if (!prop.contents[i]) prop.contents[i] = [];
|
|
136
133
|
});
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
);
|
|
134
|
+
});
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
return <Settings />;
|
|
140
138
|
};
|
|
141
139
|
|
|
142
140
|
/**
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* @subcategory components / elements
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import React, { useState, useContext, useEffect, Fragment } from "react";
|
|
7
|
+
import React, { useState, useContext, useEffect, useRef, Fragment } from "react";
|
|
8
8
|
import { useNode } from "@craftjs/core";
|
|
9
9
|
import {
|
|
10
10
|
blockProps,
|
|
@@ -19,8 +19,10 @@ import {
|
|
|
19
19
|
SettingsRow,
|
|
20
20
|
setAPropGen,
|
|
21
21
|
} from "./utils";
|
|
22
|
+
import { getDeviceValue } from "../../utils/responsive_utils";
|
|
22
23
|
import ContentEditable from "react-contenteditable";
|
|
23
24
|
import optionsCtx from "../context";
|
|
25
|
+
import PreviewCtx from "../preview_context";
|
|
24
26
|
import { CKEditor } from "ckeditor4-react";
|
|
25
27
|
import FontIconPicker from "@fonticonpicker/react-fonticonpicker";
|
|
26
28
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
@@ -80,7 +82,7 @@ export /**
|
|
|
80
82
|
* @subcategory components
|
|
81
83
|
*/
|
|
82
84
|
const Text = ({
|
|
83
|
-
text,
|
|
85
|
+
text: propText,
|
|
84
86
|
block,
|
|
85
87
|
inline,
|
|
86
88
|
isFormula,
|
|
@@ -89,17 +91,52 @@ const Text = ({
|
|
|
89
91
|
font,
|
|
90
92
|
style,
|
|
91
93
|
customClass,
|
|
94
|
+
mobileFontSize,
|
|
95
|
+
tabletFontSize,
|
|
92
96
|
}) => {
|
|
93
97
|
const {
|
|
94
98
|
connectors: { connect, drag },
|
|
95
99
|
selected,
|
|
100
|
+
nodeText,
|
|
96
101
|
actions: { setProp },
|
|
97
102
|
} = useNode((state) => ({
|
|
98
103
|
selected: state.events.selected,
|
|
99
104
|
dragged: state.events.dragged,
|
|
105
|
+
nodeText: state.data.props.text,
|
|
100
106
|
}));
|
|
107
|
+
// Use nodeText from store (reacts to undo) with fallback to prop
|
|
108
|
+
const text = nodeText !== undefined ? nodeText : propText;
|
|
101
109
|
const [editable, setEditable] = useState(false);
|
|
110
|
+
const { previewDevice } = useContext(PreviewCtx);
|
|
111
|
+
const ckInitRef = useRef(true);
|
|
112
|
+
const lastSavedTextRef = useRef(text);
|
|
113
|
+
const skipDestroyRef = useRef(false);
|
|
102
114
|
|
|
115
|
+
const baseStyle = {
|
|
116
|
+
...(font ? { fontFamily: font } : {}),
|
|
117
|
+
...reactifyStyles(style || {}),
|
|
118
|
+
};
|
|
119
|
+
const activeFontSize = getDeviceValue(
|
|
120
|
+
baseStyle.fontSize,
|
|
121
|
+
tabletFontSize,
|
|
122
|
+
mobileFontSize,
|
|
123
|
+
previewDevice
|
|
124
|
+
);
|
|
125
|
+
if (activeFontSize) baseStyle.fontSize = activeFontSize;
|
|
126
|
+
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
if (editable) {
|
|
129
|
+
ckInitRef.current = true;
|
|
130
|
+
lastSavedTextRef.current = text;
|
|
131
|
+
}
|
|
132
|
+
}, [editable]);
|
|
133
|
+
// Close CKEditor when text changes externally (e.g. undo/redo)
|
|
134
|
+
useEffect(() => {
|
|
135
|
+
if (editable && text !== lastSavedTextRef.current) {
|
|
136
|
+
skipDestroyRef.current = true;
|
|
137
|
+
setEditable(false);
|
|
138
|
+
}
|
|
139
|
+
}, [text]);
|
|
103
140
|
useEffect(() => {
|
|
104
141
|
!selected && setEditable(false);
|
|
105
142
|
}, [selected]);
|
|
@@ -112,10 +149,7 @@ const Text = ({
|
|
|
112
149
|
} ${selected ? "selected-node" : ""}`}
|
|
113
150
|
ref={(dom) => connect(drag(dom))}
|
|
114
151
|
onDoubleClick={(e) => selected && setEditable(true)}
|
|
115
|
-
style={
|
|
116
|
-
...(font ? { fontFamily: font } : {}),
|
|
117
|
-
...reactifyStyles(style || {}),
|
|
118
|
-
}}
|
|
152
|
+
style={baseStyle}
|
|
119
153
|
>
|
|
120
154
|
<DynamicFontAwesomeIcon icon={icon} className="me-1" />
|
|
121
155
|
{isFormula.text ? (
|
|
@@ -135,9 +169,29 @@ const Text = ({
|
|
|
135
169
|
<CKEditor
|
|
136
170
|
initData={text || ""}
|
|
137
171
|
style={{ display: "inline" }}
|
|
138
|
-
onChange={(e) =>
|
|
139
|
-
|
|
140
|
-
|
|
172
|
+
onChange={(e) => {
|
|
173
|
+
if (ckInitRef.current) {
|
|
174
|
+
ckInitRef.current = false;
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (e?.editor) {
|
|
178
|
+
const newText = e.editor.getData();
|
|
179
|
+
setProp((props) => (props.text = newText), 500);
|
|
180
|
+
lastSavedTextRef.current = newText;
|
|
181
|
+
}
|
|
182
|
+
}}
|
|
183
|
+
onBeforeDestroy={(e) => {
|
|
184
|
+
if (skipDestroyRef.current) {
|
|
185
|
+
skipDestroyRef.current = false;
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (e?.editor) {
|
|
189
|
+
const newText = e.editor.getData();
|
|
190
|
+
if (newText !== lastSavedTextRef.current) {
|
|
191
|
+
setProp((props) => (props.text = newText));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}}
|
|
141
195
|
config={ckConfig}
|
|
142
196
|
type="inline"
|
|
143
197
|
/>
|
|
@@ -158,6 +212,7 @@ export /**
|
|
|
158
212
|
*/
|
|
159
213
|
const TextSettings = () => {
|
|
160
214
|
const { t } = useTranslation();
|
|
215
|
+
const { previewDevice } = useContext(PreviewCtx);
|
|
161
216
|
const node = useNode((node) => ({
|
|
162
217
|
id: node.id,
|
|
163
218
|
text: node.data.props.text,
|
|
@@ -170,6 +225,8 @@ const TextSettings = () => {
|
|
|
170
225
|
icon: node.data.props.icon,
|
|
171
226
|
font: node.data.props.font,
|
|
172
227
|
style: node.data.props.style,
|
|
228
|
+
mobileFontSize: node.data.props.mobileFontSize,
|
|
229
|
+
tabletFontSize: node.data.props.tabletFontSize,
|
|
173
230
|
}));
|
|
174
231
|
const {
|
|
175
232
|
actions: { setProp },
|
|
@@ -183,6 +240,8 @@ const TextSettings = () => {
|
|
|
183
240
|
font,
|
|
184
241
|
style,
|
|
185
242
|
customClass,
|
|
243
|
+
mobileFontSize,
|
|
244
|
+
tabletFontSize,
|
|
186
245
|
} = node;
|
|
187
246
|
const { mode, fields, icons } = useContext(optionsCtx);
|
|
188
247
|
const setAProp = setAPropGen(setProp);
|
|
@@ -255,7 +314,7 @@ const TextSettings = () => {
|
|
|
255
314
|
className="w-100"
|
|
256
315
|
value={icon}
|
|
257
316
|
icons={icons}
|
|
258
|
-
onChange={(value) => setProp((prop) => (prop.icon = value))}
|
|
317
|
+
onChange={(value) => { if ((value || "") !== (icon || "")) setProp((prop) => (prop.icon = value), 500); }}
|
|
259
318
|
isMulti={false}
|
|
260
319
|
/>
|
|
261
320
|
</td>
|
|
@@ -269,16 +328,41 @@ const TextSettings = () => {
|
|
|
269
328
|
node={node}
|
|
270
329
|
setProp={setProp}
|
|
271
330
|
/>
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
331
|
+
{previewDevice === "desktop" ? (
|
|
332
|
+
<SettingsRow
|
|
333
|
+
field={{
|
|
334
|
+
name: "font-size",
|
|
335
|
+
label: t("Font size"),
|
|
336
|
+
type: "DimUnits",
|
|
337
|
+
}}
|
|
338
|
+
node={node}
|
|
339
|
+
setProp={setProp}
|
|
340
|
+
isStyle={true}
|
|
341
|
+
/>
|
|
342
|
+
) : (
|
|
343
|
+
<SettingsRow
|
|
344
|
+
field={{
|
|
345
|
+
name: "font-size",
|
|
346
|
+
label: `${t("Font size")} (${previewDevice})`,
|
|
347
|
+
type: "DimUnits",
|
|
348
|
+
}}
|
|
349
|
+
node={{
|
|
350
|
+
...node,
|
|
351
|
+
style: {
|
|
352
|
+
"font-size": previewDevice === "mobile" ? mobileFontSize : tabletFontSize,
|
|
353
|
+
},
|
|
354
|
+
}}
|
|
355
|
+
setProp={(fn) => {
|
|
356
|
+
// Write to mobileFontSize/tabletFontSize instead of style
|
|
357
|
+
const proxy = { style: {} };
|
|
358
|
+
fn(proxy);
|
|
359
|
+
const val = proxy.style["font-size"];
|
|
360
|
+
const propName = previewDevice === "mobile" ? "mobileFontSize" : "tabletFontSize";
|
|
361
|
+
setProp((prop) => { prop[propName] = val; });
|
|
362
|
+
}}
|
|
363
|
+
isStyle={true}
|
|
364
|
+
/>
|
|
365
|
+
)}
|
|
282
366
|
<SettingsRow
|
|
283
367
|
field={{
|
|
284
368
|
name: "font-weight",
|
|
@@ -213,7 +213,7 @@ const ViewSettings = () => {
|
|
|
213
213
|
const rel = initialRelation(relationsData.relations);
|
|
214
214
|
setProp((prop) => {
|
|
215
215
|
prop.relation = rel.relationString;
|
|
216
|
-
});
|
|
216
|
+
}, 500);
|
|
217
217
|
}
|
|
218
218
|
}, [needsInitialRelation]);
|
|
219
219
|
const helpContext = { view_name: viewname };
|
|
@@ -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">
|