@saltcorn/builder 0.9.3-beta.8 → 0.9.3-rc.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 +10 -10
- package/package.json +1 -1
- package/src/components/Builder.js +82 -73
- package/src/components/Library.js +28 -10
- package/src/components/elements/Aggregation.js +2 -2
- package/src/components/elements/Field.js +22 -5
- package/src/components/elements/Image.js +229 -227
- package/src/components/elements/JoinField.js +25 -4
- package/src/components/elements/ToggleFilter.js +6 -8
- package/src/components/elements/View.js +42 -19
- package/src/components/elements/ViewLink.js +25 -11
- package/src/components/elements/utils.js +71 -5
- package/src/components/relations_context.js +3 -0
- package/tests/relations_finder.test.js +8 -0
package/package.json
CHANGED
|
@@ -31,6 +31,7 @@ import { DropMenu } from "./elements/DropMenu";
|
|
|
31
31
|
import { ToggleFilter } from "./elements/ToggleFilter";
|
|
32
32
|
import optionsCtx from "./context";
|
|
33
33
|
import PreviewCtx from "./preview_context";
|
|
34
|
+
import RelationsCtx from "./relations_context";
|
|
34
35
|
import {
|
|
35
36
|
ToolboxShow,
|
|
36
37
|
ToolboxEdit,
|
|
@@ -390,6 +391,7 @@ const Builder = ({ options, layout, mode }) => {
|
|
|
390
391
|
const [uploadedFiles, setUploadedFiles] = useState([]);
|
|
391
392
|
const nodekeys = useRef([]);
|
|
392
393
|
const [isSaving, setIsSaving] = useState(false);
|
|
394
|
+
const [relationsCache, setRelationsCache] = useState({});
|
|
393
395
|
|
|
394
396
|
return (
|
|
395
397
|
<ErrorBoundary>
|
|
@@ -398,83 +400,90 @@ const Builder = ({ options, layout, mode }) => {
|
|
|
398
400
|
<PreviewCtx.Provider
|
|
399
401
|
value={{ previews, setPreviews, uploadedFiles, setUploadedFiles }}
|
|
400
402
|
>
|
|
401
|
-
<
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
403
|
+
<RelationsCtx.Provider
|
|
404
|
+
value={{
|
|
405
|
+
relationsCache,
|
|
406
|
+
setRelationsCache,
|
|
407
|
+
}}
|
|
408
|
+
>
|
|
409
|
+
<div className="row" style={{ marginTop: "-5px" }}>
|
|
410
|
+
<div className="col-sm-auto left-builder-col">
|
|
411
|
+
<div className="componets-and-library-accordion toolbox-card">
|
|
412
|
+
<InitNewElement
|
|
413
|
+
nodekeys={nodekeys}
|
|
414
|
+
setIsSaving={setIsSaving}
|
|
415
|
+
/>
|
|
416
|
+
<Accordion>
|
|
417
|
+
<div className="card mt-1" accordiontitle="Components">
|
|
418
|
+
{{
|
|
419
|
+
show: <ToolboxShow />,
|
|
420
|
+
edit: <ToolboxEdit />,
|
|
421
|
+
page: <ToolboxPage />,
|
|
422
|
+
filter: <ToolboxFilter />,
|
|
423
|
+
}[mode] || <div>Missing mode</div>}
|
|
424
|
+
</div>
|
|
425
|
+
<div accordiontitle="Library">
|
|
426
|
+
<Library />
|
|
427
|
+
</div>
|
|
428
|
+
</Accordion>
|
|
429
|
+
</div>
|
|
430
|
+
<div className="card toolbox-card pe-0">
|
|
431
|
+
<div className="card-header">Layers</div>
|
|
432
|
+
{showLayers && (
|
|
433
|
+
<div className="card-body p-0 builder-layers">
|
|
434
|
+
<Layers expandRootOnLoad={true} />
|
|
435
|
+
</div>
|
|
436
|
+
)}
|
|
437
|
+
</div>
|
|
421
438
|
</div>
|
|
422
|
-
<div
|
|
423
|
-
|
|
424
|
-
{
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
439
|
+
<div
|
|
440
|
+
id="builder-main-canvas"
|
|
441
|
+
className={`col builder-mode-${options.mode}`}
|
|
442
|
+
>
|
|
443
|
+
<div>
|
|
444
|
+
<Frame
|
|
445
|
+
resolver={{
|
|
446
|
+
Text,
|
|
447
|
+
Empty,
|
|
448
|
+
Columns,
|
|
449
|
+
JoinField,
|
|
450
|
+
Field,
|
|
451
|
+
ViewLink,
|
|
452
|
+
Action,
|
|
453
|
+
HTMLCode,
|
|
454
|
+
LineBreak,
|
|
455
|
+
Aggregation,
|
|
456
|
+
Card,
|
|
457
|
+
Image,
|
|
458
|
+
Link,
|
|
459
|
+
View,
|
|
460
|
+
SearchBar,
|
|
461
|
+
Container,
|
|
462
|
+
Column,
|
|
463
|
+
DropDownFilter,
|
|
464
|
+
DropMenu,
|
|
465
|
+
Tabs,
|
|
466
|
+
Table,
|
|
467
|
+
ToggleFilter,
|
|
468
|
+
}}
|
|
469
|
+
>
|
|
470
|
+
<Element canvas is={Column}></Element>
|
|
471
|
+
</Frame>
|
|
472
|
+
</div>
|
|
429
473
|
</div>
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
Columns,
|
|
441
|
-
JoinField,
|
|
442
|
-
Field,
|
|
443
|
-
ViewLink,
|
|
444
|
-
Action,
|
|
445
|
-
HTMLCode,
|
|
446
|
-
LineBreak,
|
|
447
|
-
Aggregation,
|
|
448
|
-
Card,
|
|
449
|
-
Image,
|
|
450
|
-
Link,
|
|
451
|
-
View,
|
|
452
|
-
SearchBar,
|
|
453
|
-
Container,
|
|
454
|
-
Column,
|
|
455
|
-
DropDownFilter,
|
|
456
|
-
DropMenu,
|
|
457
|
-
Tabs,
|
|
458
|
-
Table,
|
|
459
|
-
ToggleFilter,
|
|
460
|
-
}}
|
|
461
|
-
>
|
|
462
|
-
<Element canvas is={Column}></Element>
|
|
463
|
-
</Frame>
|
|
464
|
-
</div>
|
|
465
|
-
</div>
|
|
466
|
-
<div className="col-sm-auto builder-sidebar">
|
|
467
|
-
<div style={{ width: "16rem" }}>
|
|
468
|
-
<NextButton layout={layout} />
|
|
469
|
-
<HistoryPanel />
|
|
470
|
-
<FontAwesomeIcon
|
|
471
|
-
icon={faSave}
|
|
472
|
-
className={isSaving ? "d-inline" : "d-none"}
|
|
473
|
-
/>
|
|
474
|
-
<SettingsPanel />
|
|
474
|
+
<div className="col-sm-auto builder-sidebar">
|
|
475
|
+
<div style={{ width: "16rem" }}>
|
|
476
|
+
<NextButton layout={layout} />
|
|
477
|
+
<HistoryPanel />
|
|
478
|
+
<FontAwesomeIcon
|
|
479
|
+
icon={faSave}
|
|
480
|
+
className={isSaving ? "d-inline" : "d-none"}
|
|
481
|
+
/>
|
|
482
|
+
<SettingsPanel />
|
|
483
|
+
</div>
|
|
475
484
|
</div>
|
|
476
485
|
</div>
|
|
477
|
-
</
|
|
486
|
+
</RelationsCtx.Provider>
|
|
478
487
|
</PreviewCtx.Provider>
|
|
479
488
|
</Provider>
|
|
480
489
|
<div className="d-none preview-scratchpad"></div>
|
|
@@ -10,6 +10,7 @@ import React, {
|
|
|
10
10
|
useState,
|
|
11
11
|
Fragment,
|
|
12
12
|
useRef,
|
|
13
|
+
useMemo,
|
|
13
14
|
} from "react";
|
|
14
15
|
import { useEditor, useNode } from "@craftjs/core";
|
|
15
16
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
@@ -19,7 +20,7 @@ import faIcons from "./elements/faicons";
|
|
|
19
20
|
import { craftToSaltcorn, layoutToNodes } from "./storage";
|
|
20
21
|
import optionsCtx from "./context";
|
|
21
22
|
import { WrapElem } from "./Toolbox";
|
|
22
|
-
import { isEqual } from "lodash";
|
|
23
|
+
import { isEqual, throttle } from "lodash";
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
*
|
|
@@ -66,6 +67,25 @@ LibraryElem.craft = {
|
|
|
66
67
|
displayName: "LibraryElem",
|
|
67
68
|
};
|
|
68
69
|
|
|
70
|
+
// https://www.developerway.com/posts/debouncing-in-react
|
|
71
|
+
const useThrottle = (callback) => {
|
|
72
|
+
const ref = useRef();
|
|
73
|
+
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
ref.current = callback;
|
|
76
|
+
}, [callback]);
|
|
77
|
+
|
|
78
|
+
const debouncedCallback = useMemo(() => {
|
|
79
|
+
const func = () => {
|
|
80
|
+
ref.current?.();
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return throttle(func, 3000);
|
|
84
|
+
}, []);
|
|
85
|
+
|
|
86
|
+
return debouncedCallback;
|
|
87
|
+
};
|
|
88
|
+
|
|
69
89
|
export /**
|
|
70
90
|
* @param {object} props
|
|
71
91
|
* @param {object} props.nodekeys
|
|
@@ -89,11 +109,12 @@ const InitNewElement = ({ nodekeys, setIsSaving }) => {
|
|
|
89
109
|
if (savedData.current === false) {
|
|
90
110
|
//do not save on first call
|
|
91
111
|
savedData.current = JSON.stringify(data.layout);
|
|
92
|
-
|
|
112
|
+
|
|
93
113
|
return;
|
|
94
114
|
}
|
|
95
115
|
if (isEqual(savedData.current, JSON.stringify(data.layout))) return;
|
|
96
116
|
savedData.current = JSON.stringify(data.layout);
|
|
117
|
+
setIsSaving(true);
|
|
97
118
|
|
|
98
119
|
fetch(`/${urlroot}/savebuilder/${options.page_id || options.view_id}`, {
|
|
99
120
|
method: "POST", // or 'PUT'
|
|
@@ -106,6 +127,9 @@ const InitNewElement = ({ nodekeys, setIsSaving }) => {
|
|
|
106
127
|
setIsSaving(false);
|
|
107
128
|
});
|
|
108
129
|
};
|
|
130
|
+
const throttledSave = useThrottle(() => {
|
|
131
|
+
doSave(query);
|
|
132
|
+
});
|
|
109
133
|
const onNodesChange = (arg, arg1) => {
|
|
110
134
|
const nodes = arg.getSerializedNodes();
|
|
111
135
|
const newNodeIds = [];
|
|
@@ -133,14 +157,8 @@ const InitNewElement = ({ nodekeys, setIsSaving }) => {
|
|
|
133
157
|
actions.selectNode(id);
|
|
134
158
|
}
|
|
135
159
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
setSaveTimeout(
|
|
139
|
-
setTimeout(() => {
|
|
140
|
-
doSave(query);
|
|
141
|
-
setSaveTimeout(false);
|
|
142
|
-
}, 500)
|
|
143
|
-
);
|
|
160
|
+
|
|
161
|
+
throttledSave();
|
|
144
162
|
};
|
|
145
163
|
useEffect(() => {
|
|
146
164
|
const nodes = query.getSerializedNodes();
|
|
@@ -140,12 +140,12 @@ const AggregationSettings = () => {
|
|
|
140
140
|
{ valAttr: true }
|
|
141
141
|
)}
|
|
142
142
|
{options.fields
|
|
143
|
-
.filter((f) => f.type.name === "Date")
|
|
143
|
+
.filter((f) => f.type === "Date" || f.type.name === "Date")
|
|
144
144
|
.map((f) => (
|
|
145
145
|
<option value={`Latest ${f.name}`}>Latest {f.name}</option>
|
|
146
146
|
))}
|
|
147
147
|
{options.fields
|
|
148
|
-
.filter((f) => f.type.name === "Date")
|
|
148
|
+
.filter((f) => f.type === "Date" || f.type.name === "Date")
|
|
149
149
|
.map((f) => (
|
|
150
150
|
<option value={`Earliest ${f.name}`}>
|
|
151
151
|
Earliest {f.name}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* @subcategory components / elements
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import React, { useContext, useEffect, Fragment } from "react";
|
|
7
|
+
import React, { useContext, useEffect, useState, Fragment } from "react";
|
|
8
8
|
import { useNode } from "@craftjs/core";
|
|
9
9
|
import optionsCtx from "../context";
|
|
10
10
|
import previewCtx from "../preview_context";
|
|
@@ -115,9 +115,26 @@ const FieldSettings = () => {
|
|
|
115
115
|
const fvs = options.field_view_options[name];
|
|
116
116
|
const handlesTextStyle = (options.handlesTextStyle || {})[name];
|
|
117
117
|
const blockDisplay = (options.blockDisplay || {})[name];
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const cfgFields =
|
|
118
|
+
|
|
119
|
+
const [fetchedCfgFields, setFetchedCfgFields] = useState([]);
|
|
120
|
+
const cfgFields = fetchedCfgFields;
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
fetch(`/field/fieldviewcfgform/${options.tableName}?accept=json`, {
|
|
123
|
+
method: "POST",
|
|
124
|
+
headers: {
|
|
125
|
+
"Content-Type": "application/json",
|
|
126
|
+
"CSRF-Token": options.csrfToken,
|
|
127
|
+
"X-Requested-With": "XMLHttpRequest",
|
|
128
|
+
},
|
|
129
|
+
body: JSON.stringify({ field_name: name, fieldview, type: "Field" }),
|
|
130
|
+
})
|
|
131
|
+
.then(function (response) {
|
|
132
|
+
if (response.status < 399) return response.json();
|
|
133
|
+
else return [];
|
|
134
|
+
})
|
|
135
|
+
.then(setFetchedCfgFields);
|
|
136
|
+
}, [name, fieldview]);
|
|
137
|
+
|
|
121
138
|
const refetchPreview = fetchFieldPreview({
|
|
122
139
|
options,
|
|
123
140
|
name,
|
|
@@ -189,7 +206,7 @@ const FieldSettings = () => {
|
|
|
189
206
|
const value = e.target.value;
|
|
190
207
|
|
|
191
208
|
setProp((prop) => (prop.fieldview = value));
|
|
192
|
-
setInitialConfig(setProp, value, getCfgFields(value));
|
|
209
|
+
//setInitialConfig(setProp, value, getCfgFields(value));
|
|
193
210
|
refetchPreview({ fieldview: value });
|
|
194
211
|
}}
|
|
195
212
|
>
|