@saltcorn/builder 1.6.0-alpha.9 → 1.6.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/builder_bundle.js +76 -104004
- package/dist/builder_bundle.js.LICENSE.txt +2 -0
- package/package.json +3 -2
- package/src/components/Builder.js +367 -186
- package/src/components/RenderNode.js +21 -3
- package/src/components/Toolbox.js +100 -22
- package/src/components/elements/Action.js +10 -120
- package/src/components/elements/ArrayManager.js +10 -5
- package/src/components/elements/BoxModelEditor.js +24 -23
- package/src/components/elements/Card.js +26 -1
- package/src/components/elements/Columns.js +158 -110
- package/src/components/elements/Container.js +43 -8
- package/src/components/elements/CustomLayer.js +285 -0
- package/src/components/elements/DropDownFilter.js +8 -1
- package/src/components/elements/DropMenu.js +10 -4
- package/src/components/elements/HTMLCode.js +3 -1
- package/src/components/elements/MonacoEditor.js +120 -15
- package/src/components/elements/Prompt.js +285 -0
- package/src/components/elements/SearchBar.js +28 -5
- package/src/components/elements/Table.js +10 -12
- package/src/components/elements/Text.js +59 -15
- package/src/components/elements/View.js +2 -1
- package/src/components/elements/ViewLink.js +1 -0
- package/src/components/elements/utils.js +133 -30
- package/src/components/storage.js +33 -7
- package/src/index.js +10 -0
- package/src/utils/responsive_utils.js +139 -0
|
@@ -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,7 +23,7 @@ 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
28
|
const {
|
|
29
29
|
selected,
|
|
@@ -31,6 +31,15 @@ const SearchBar = ({ has_dropdown, children, show_badges }) => {
|
|
|
31
31
|
} = useNode((node) => ({ selected: node.events.selected }));
|
|
32
32
|
const [showDropdown, setDropdown] = useState(false);
|
|
33
33
|
const [dropWidth, setDropWidth] = useState(200);
|
|
34
|
+
|
|
35
|
+
const renderContents = () => {
|
|
36
|
+
const actualChildren = contents || children;
|
|
37
|
+
if (!actualChildren) return null;
|
|
38
|
+
if (React.isValidElement(actualChildren)) return actualChildren;
|
|
39
|
+
if (Array.isArray(actualChildren)) return actualChildren;
|
|
40
|
+
return actualChildren;
|
|
41
|
+
};
|
|
42
|
+
|
|
34
43
|
return (
|
|
35
44
|
<div
|
|
36
45
|
className={`input-group ${selected ? "selected-node" : ""}`}
|
|
@@ -73,10 +82,19 @@ const SearchBar = ({ has_dropdown, children, show_badges }) => {
|
|
|
73
82
|
}`}
|
|
74
83
|
style={{ width: dropWidth, left: 0 }}
|
|
75
84
|
>
|
|
76
|
-
<
|
|
85
|
+
<Element canvas id="searchbar-contents" is={Column}>
|
|
86
|
+
{renderContents()}
|
|
87
|
+
</Element>
|
|
77
88
|
</div>
|
|
78
89
|
</Fragment>
|
|
79
90
|
)}
|
|
91
|
+
{!has_dropdown && (
|
|
92
|
+
<div style={{ display: "none" }}>
|
|
93
|
+
<Element canvas id="searchbar-contents" is={Column}>
|
|
94
|
+
{renderContents()}
|
|
95
|
+
</Element>
|
|
96
|
+
</div>
|
|
97
|
+
)}
|
|
80
98
|
</div>
|
|
81
99
|
);
|
|
82
100
|
};
|
|
@@ -146,11 +164,16 @@ SearchBar.craft = {
|
|
|
146
164
|
has_dropdown: false,
|
|
147
165
|
show_badges: false,
|
|
148
166
|
autofocus: false,
|
|
167
|
+
contents: [],
|
|
149
168
|
},
|
|
150
169
|
related: {
|
|
151
170
|
settings: SearchBarSettings,
|
|
152
171
|
segment_type: "search_bar",
|
|
153
|
-
|
|
154
|
-
|
|
172
|
+
fields: [
|
|
173
|
+
{ name: "has_dropdown" },
|
|
174
|
+
{ name: "show_badges" },
|
|
175
|
+
{ name: "autofocus" },
|
|
176
|
+
{ label: "Contents", name: "contents", type: "Nodes", nodeID: "searchbar-contents" },
|
|
177
|
+
],
|
|
155
178
|
},
|
|
156
179
|
};
|
|
@@ -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
|
/**
|
|
@@ -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";
|
|
@@ -89,6 +91,8 @@ 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 },
|
|
@@ -99,6 +103,19 @@ const Text = ({
|
|
|
99
103
|
dragged: state.events.dragged,
|
|
100
104
|
}));
|
|
101
105
|
const [editable, setEditable] = useState(false);
|
|
106
|
+
const { previewDevice } = useContext(PreviewCtx);
|
|
107
|
+
|
|
108
|
+
const baseStyle = {
|
|
109
|
+
...(font ? { fontFamily: font } : {}),
|
|
110
|
+
...reactifyStyles(style || {}),
|
|
111
|
+
};
|
|
112
|
+
const activeFontSize = getDeviceValue(
|
|
113
|
+
baseStyle.fontSize,
|
|
114
|
+
tabletFontSize,
|
|
115
|
+
mobileFontSize,
|
|
116
|
+
previewDevice
|
|
117
|
+
);
|
|
118
|
+
if (activeFontSize) baseStyle.fontSize = activeFontSize;
|
|
102
119
|
|
|
103
120
|
useEffect(() => {
|
|
104
121
|
!selected && setEditable(false);
|
|
@@ -112,10 +129,7 @@ const Text = ({
|
|
|
112
129
|
} ${selected ? "selected-node" : ""}`}
|
|
113
130
|
ref={(dom) => connect(drag(dom))}
|
|
114
131
|
onDoubleClick={(e) => selected && setEditable(true)}
|
|
115
|
-
style={
|
|
116
|
-
...(font ? { fontFamily: font } : {}),
|
|
117
|
-
...reactifyStyles(style || {}),
|
|
118
|
-
}}
|
|
132
|
+
style={baseStyle}
|
|
119
133
|
>
|
|
120
134
|
<DynamicFontAwesomeIcon icon={icon} className="me-1" />
|
|
121
135
|
{isFormula.text ? (
|
|
@@ -158,6 +172,7 @@ export /**
|
|
|
158
172
|
*/
|
|
159
173
|
const TextSettings = () => {
|
|
160
174
|
const { t } = useTranslation();
|
|
175
|
+
const { previewDevice } = useContext(PreviewCtx);
|
|
161
176
|
const node = useNode((node) => ({
|
|
162
177
|
id: node.id,
|
|
163
178
|
text: node.data.props.text,
|
|
@@ -170,6 +185,8 @@ const TextSettings = () => {
|
|
|
170
185
|
icon: node.data.props.icon,
|
|
171
186
|
font: node.data.props.font,
|
|
172
187
|
style: node.data.props.style,
|
|
188
|
+
mobileFontSize: node.data.props.mobileFontSize,
|
|
189
|
+
tabletFontSize: node.data.props.tabletFontSize,
|
|
173
190
|
}));
|
|
174
191
|
const {
|
|
175
192
|
actions: { setProp },
|
|
@@ -183,6 +200,8 @@ const TextSettings = () => {
|
|
|
183
200
|
font,
|
|
184
201
|
style,
|
|
185
202
|
customClass,
|
|
203
|
+
mobileFontSize,
|
|
204
|
+
tabletFontSize,
|
|
186
205
|
} = node;
|
|
187
206
|
const { mode, fields, icons } = useContext(optionsCtx);
|
|
188
207
|
const setAProp = setAPropGen(setProp);
|
|
@@ -255,7 +274,7 @@ const TextSettings = () => {
|
|
|
255
274
|
className="w-100"
|
|
256
275
|
value={icon}
|
|
257
276
|
icons={icons}
|
|
258
|
-
onChange={(value) => setProp((prop) => (prop.icon = value))}
|
|
277
|
+
onChange={(value) => { if ((value || "") !== (icon || "")) setProp((prop) => (prop.icon = value), 500); }}
|
|
259
278
|
isMulti={false}
|
|
260
279
|
/>
|
|
261
280
|
</td>
|
|
@@ -269,16 +288,41 @@ const TextSettings = () => {
|
|
|
269
288
|
node={node}
|
|
270
289
|
setProp={setProp}
|
|
271
290
|
/>
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
291
|
+
{previewDevice === "desktop" ? (
|
|
292
|
+
<SettingsRow
|
|
293
|
+
field={{
|
|
294
|
+
name: "font-size",
|
|
295
|
+
label: t("Font size"),
|
|
296
|
+
type: "DimUnits",
|
|
297
|
+
}}
|
|
298
|
+
node={node}
|
|
299
|
+
setProp={setProp}
|
|
300
|
+
isStyle={true}
|
|
301
|
+
/>
|
|
302
|
+
) : (
|
|
303
|
+
<SettingsRow
|
|
304
|
+
field={{
|
|
305
|
+
name: "font-size",
|
|
306
|
+
label: `${t("Font size")} (${previewDevice})`,
|
|
307
|
+
type: "DimUnits",
|
|
308
|
+
}}
|
|
309
|
+
node={{
|
|
310
|
+
...node,
|
|
311
|
+
style: {
|
|
312
|
+
"font-size": previewDevice === "mobile" ? mobileFontSize : tabletFontSize,
|
|
313
|
+
},
|
|
314
|
+
}}
|
|
315
|
+
setProp={(fn) => {
|
|
316
|
+
// Write to mobileFontSize/tabletFontSize instead of style
|
|
317
|
+
const proxy = { style: {} };
|
|
318
|
+
fn(proxy);
|
|
319
|
+
const val = proxy.style["font-size"];
|
|
320
|
+
const propName = previewDevice === "mobile" ? "mobileFontSize" : "tabletFontSize";
|
|
321
|
+
setProp((prop) => { prop[propName] = val; });
|
|
322
|
+
}}
|
|
323
|
+
isStyle={true}
|
|
324
|
+
/>
|
|
325
|
+
)}
|
|
282
326
|
<SettingsRow
|
|
283
327
|
field={{
|
|
284
328
|
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">
|