@saltcorn/builder 1.6.0-alpha.8 → 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 +7 -7
- 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 +11 -121
- 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 +19 -7
- 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",
|
|
@@ -162,6 +162,7 @@ const ViewSettings = () => {
|
|
|
162
162
|
}
|
|
163
163
|
if (viewname && viewname.includes(".")) viewname = viewname.split(".")[0];
|
|
164
164
|
|
|
165
|
+
let cacheWasPopulated = false;
|
|
165
166
|
if (
|
|
166
167
|
finder &&
|
|
167
168
|
!(relationsCache[tableName] && relationsCache[tableName][viewname])
|
|
@@ -178,8 +179,13 @@ const ViewSettings = () => {
|
|
|
178
179
|
);
|
|
179
180
|
relationsCache[tableName] = relationsCache[tableName] || {};
|
|
180
181
|
relationsCache[tableName][viewname] = { relations, layers };
|
|
181
|
-
|
|
182
|
+
cacheWasPopulated = true;
|
|
182
183
|
}
|
|
184
|
+
useEffect(() => {
|
|
185
|
+
if (cacheWasPopulated) {
|
|
186
|
+
setRelationsCache({ ...relationsCache });
|
|
187
|
+
}
|
|
188
|
+
});
|
|
183
189
|
const [relationsData, setRelationsData] = finder
|
|
184
190
|
? React.useState(relationsCache[tableName][viewname])
|
|
185
191
|
: [undefined, undefined];
|
|
@@ -193,18 +199,23 @@ const ViewSettings = () => {
|
|
|
193
199
|
subView.display_type
|
|
194
200
|
);
|
|
195
201
|
}
|
|
196
|
-
|
|
202
|
+
const needsInitialRelation =
|
|
197
203
|
options.mode !== "filter" &&
|
|
198
204
|
subView?.table_id &&
|
|
199
205
|
!safeRelation &&
|
|
200
206
|
!hasLegacyRelation &&
|
|
201
|
-
relationsData?.relations.length > 0
|
|
202
|
-
) {
|
|
207
|
+
relationsData?.relations.length > 0;
|
|
208
|
+
if (needsInitialRelation) {
|
|
203
209
|
safeRelation = initialRelation(relationsData.relations);
|
|
204
|
-
setProp((prop) => {
|
|
205
|
-
prop.relation = safeRelation.relationString;
|
|
206
|
-
});
|
|
207
210
|
}
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
if (needsInitialRelation) {
|
|
213
|
+
const rel = initialRelation(relationsData.relations);
|
|
214
|
+
setProp((prop) => {
|
|
215
|
+
prop.relation = rel.relationString;
|
|
216
|
+
}, 500);
|
|
217
|
+
}
|
|
218
|
+
}, [needsInitialRelation]);
|
|
208
219
|
const helpContext = { view_name: viewname };
|
|
209
220
|
if (options.tableName) helpContext.srcTable = options.tableName;
|
|
210
221
|
const set_view_name = (e) => {
|
|
@@ -394,6 +405,7 @@ const ViewSettings = () => {
|
|
|
394
405
|
value={extra_state_fml}
|
|
395
406
|
propKey="extra_state_fml"
|
|
396
407
|
onChange={setAProp("extra_state_fml")}
|
|
408
|
+
stateExpr
|
|
397
409
|
/>
|
|
398
410
|
{errorString ? (
|
|
399
411
|
<small className="text-danger font-monospace d-block">
|