@saltcorn/builder 0.9.4-beta.2 → 0.9.4-beta.20
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 +23 -11
- package/package.json +3 -1
- package/src/components/Builder.js +92 -66
- package/src/components/Library.js +46 -22
- package/src/components/RenderNode.js +5 -0
- package/src/components/Toolbox.js +173 -165
- package/src/components/elements/Action.js +12 -0
- package/src/components/elements/Aggregation.js +193 -104
- package/src/components/elements/BoxModelEditor.js +8 -8
- package/src/components/elements/Column.js +16 -2
- package/src/components/elements/Columns.js +4 -4
- package/src/components/elements/Container.js +26 -2
- package/src/components/elements/DropMenu.js +31 -2
- package/src/components/elements/Field.js +22 -20
- package/src/components/elements/HTMLCode.js +1 -1
- package/src/components/elements/JoinField.js +25 -1
- package/src/components/elements/Link.js +1 -0
- package/src/components/elements/ListColumn.js +177 -0
- package/src/components/elements/ListColumns.js +62 -0
- package/src/components/elements/RelationBadges.js +53 -44
- package/src/components/elements/Tabs.js +118 -40
- package/src/components/elements/Text.js +4 -2
- package/src/components/elements/View.js +125 -68
- package/src/components/elements/ViewLink.js +100 -48
- package/src/components/elements/utils.js +198 -98
- package/src/components/storage.js +67 -5
- package/tests/relations_finder.test.js +58 -92
- package/tests/test_data.js +0 -163
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saltcorn/builder",
|
|
3
|
-
"version": "0.9.4-beta.
|
|
3
|
+
"version": "0.9.4-beta.20",
|
|
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",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"@babel/preset-react": "7.9.4",
|
|
21
21
|
"@craftjs/core": "0.1.0-beta.20",
|
|
22
22
|
"@craftjs/utils": "0.1.0-beta.20",
|
|
23
|
+
"@saltcorn/common-code": "0.9.4-beta.20",
|
|
23
24
|
"saltcorn-craft-layers-noeye": "0.1.0-beta.22",
|
|
24
25
|
"@fonticonpicker/react-fonticonpicker": "1.2.0",
|
|
25
26
|
"@fortawesome/fontawesome-svg-core": "1.2.34",
|
|
@@ -38,6 +39,7 @@
|
|
|
38
39
|
"react-bootstrap-icons": "1.5.0",
|
|
39
40
|
"react-contenteditable": "3.3.5",
|
|
40
41
|
"react-dom": "16.13.1",
|
|
42
|
+
"react-select": "4.3.1",
|
|
41
43
|
"react-test-renderer": "16.13.1",
|
|
42
44
|
"react-transition-group": "4.4.1",
|
|
43
45
|
"@tippyjs/react": "4.2.6",
|
|
@@ -18,7 +18,7 @@ import { JoinField } from "./elements/JoinField";
|
|
|
18
18
|
import { Aggregation } from "./elements/Aggregation";
|
|
19
19
|
import { LineBreak } from "./elements/LineBreak";
|
|
20
20
|
import { ViewLink } from "./elements/ViewLink";
|
|
21
|
-
import { Columns } from "./elements/Columns";
|
|
21
|
+
import { Columns, ntimes } from "./elements/Columns";
|
|
22
22
|
import { SearchBar } from "./elements/SearchBar";
|
|
23
23
|
import { HTMLCode } from "./elements/HTMLCode";
|
|
24
24
|
import { Action } from "./elements/Action";
|
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
ToolboxEdit,
|
|
38
38
|
ToolboxPage,
|
|
39
39
|
ToolboxFilter,
|
|
40
|
+
ToolboxList,
|
|
40
41
|
} from "./Toolbox";
|
|
41
42
|
import { craftToSaltcorn, layoutToNodes } from "./storage";
|
|
42
43
|
import { Card } from "./elements/Card";
|
|
@@ -52,7 +53,13 @@ import {
|
|
|
52
53
|
faRedo,
|
|
53
54
|
faTrashAlt,
|
|
54
55
|
faSave,
|
|
56
|
+
faExclamationTriangle,
|
|
57
|
+
faPlus,
|
|
55
58
|
} from "@fortawesome/free-solid-svg-icons";
|
|
59
|
+
import {
|
|
60
|
+
faCaretSquareLeft,
|
|
61
|
+
faCaretSquareRight,
|
|
62
|
+
} from "@fortawesome/free-regular-svg-icons";
|
|
56
63
|
import {
|
|
57
64
|
Accordion,
|
|
58
65
|
ErrorBoundary,
|
|
@@ -60,6 +67,8 @@ import {
|
|
|
60
67
|
} from "./elements/utils";
|
|
61
68
|
import { InitNewElement, Library } from "./Library";
|
|
62
69
|
import { RenderNode } from "./RenderNode";
|
|
70
|
+
import { ListColumn } from "./elements/ListColumn";
|
|
71
|
+
import { ListColumns } from "./elements/ListColumns";
|
|
63
72
|
const { Provider } = optionsCtx;
|
|
64
73
|
|
|
65
74
|
/**
|
|
@@ -248,58 +257,20 @@ const SettingsPanel = () => {
|
|
|
248
257
|
);
|
|
249
258
|
};
|
|
250
259
|
|
|
251
|
-
|
|
252
|
-
* @returns {button}
|
|
253
|
-
* @category saltcorn-builder
|
|
254
|
-
* @subcategory components
|
|
255
|
-
* @namespace
|
|
256
|
-
*/
|
|
257
|
-
const SaveButton = () => {
|
|
260
|
+
const AddColumnButton = () => {
|
|
258
261
|
const { query, actions } = useEditor(() => {});
|
|
259
262
|
const options = useContext(optionsCtx);
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
const data = craftToSaltcorn(JSON.parse(query.serialize()));
|
|
266
|
-
const urlroot = options.page_id ? "pageedit" : "viewedit";
|
|
267
|
-
fetch(`/${urlroot}/savebuilder/${options.page_id || options.view_id}`, {
|
|
268
|
-
method: "POST", // or 'PUT'
|
|
269
|
-
headers: {
|
|
270
|
-
"Content-Type": "application/json",
|
|
271
|
-
"CSRF-Token": options.csrfToken,
|
|
272
|
-
},
|
|
273
|
-
body: JSON.stringify(data),
|
|
274
|
-
});
|
|
263
|
+
const addColumn = () => {
|
|
264
|
+
actions.addNodeTree(
|
|
265
|
+
query.parseReactElement(<ListColumn />).toNodeTree(),
|
|
266
|
+
"ROOT"
|
|
267
|
+
);
|
|
275
268
|
};
|
|
276
|
-
return
|
|
277
|
-
<button
|
|
278
|
-
className="
|
|
279
|
-
|
|
280
|
-
>
|
|
281
|
-
Save
|
|
269
|
+
return (
|
|
270
|
+
<button className="btn btn-primary mt-2" onClick={addColumn}>
|
|
271
|
+
<FontAwesomeIcon icon={faPlus} className="me-2" />
|
|
272
|
+
Add column
|
|
282
273
|
</button>
|
|
283
|
-
) : (
|
|
284
|
-
""
|
|
285
|
-
);
|
|
286
|
-
};
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* @returns {a|""}
|
|
290
|
-
* @category saltcorn-builder
|
|
291
|
-
* @subcategory components
|
|
292
|
-
* @namespace
|
|
293
|
-
*/
|
|
294
|
-
const ViewPageLink = () => {
|
|
295
|
-
const { query, actions } = useEditor(() => {});
|
|
296
|
-
const options = useContext(optionsCtx);
|
|
297
|
-
return options.page_id ? (
|
|
298
|
-
<a target="_blank" className="d-block" href={`/page/${options.page_name}`}>
|
|
299
|
-
View page in new tab »
|
|
300
|
-
</a>
|
|
301
|
-
) : (
|
|
302
|
-
""
|
|
303
274
|
);
|
|
304
275
|
};
|
|
305
276
|
|
|
@@ -352,14 +323,18 @@ const NextButton = ({ layout }) => {
|
|
|
352
323
|
const options = useContext(optionsCtx);
|
|
353
324
|
|
|
354
325
|
useEffect(() => {
|
|
355
|
-
layoutToNodes(layout, query, actions);
|
|
326
|
+
layoutToNodes(layout, query, actions, "ROOT", options);
|
|
356
327
|
}, []);
|
|
357
328
|
|
|
358
329
|
/**
|
|
359
330
|
* @returns {void}
|
|
360
331
|
*/
|
|
361
332
|
const onClick = () => {
|
|
362
|
-
const { columns, layout } = craftToSaltcorn(
|
|
333
|
+
const { columns, layout } = craftToSaltcorn(
|
|
334
|
+
JSON.parse(query.serialize()),
|
|
335
|
+
"ROOT",
|
|
336
|
+
options
|
|
337
|
+
);
|
|
363
338
|
document
|
|
364
339
|
.querySelector("form#scbuildform input[name=columns]")
|
|
365
340
|
.setAttribute("value", encodeURIComponent(JSON.stringify(columns)));
|
|
@@ -390,9 +365,10 @@ const Builder = ({ options, layout, mode }) => {
|
|
|
390
365
|
const [previews, setPreviews] = useState({});
|
|
391
366
|
const [uploadedFiles, setUploadedFiles] = useState([]);
|
|
392
367
|
const nodekeys = useRef([]);
|
|
393
|
-
const [
|
|
368
|
+
const [savingState, setSavingState] = useState({ isSaving: false });
|
|
369
|
+
const [isEnlarged, setIsEnlarged] = useState(false);
|
|
370
|
+
const [isLeftEnlarged, setIsLeftEnlarged] = useState(false);
|
|
394
371
|
const [relationsCache, setRelationsCache] = useState({});
|
|
395
|
-
|
|
396
372
|
return (
|
|
397
373
|
<ErrorBoundary>
|
|
398
374
|
<Editor onRender={RenderNode}>
|
|
@@ -407,28 +383,51 @@ const Builder = ({ options, layout, mode }) => {
|
|
|
407
383
|
}}
|
|
408
384
|
>
|
|
409
385
|
<div className="row" style={{ marginTop: "-5px" }}>
|
|
410
|
-
<div
|
|
386
|
+
<div
|
|
387
|
+
className={`col-sm-auto left-builder-col ${
|
|
388
|
+
isLeftEnlarged
|
|
389
|
+
? "builder-left-enlarged"
|
|
390
|
+
: "builder-left-shrunk"
|
|
391
|
+
}`}
|
|
392
|
+
>
|
|
411
393
|
<div className="componets-and-library-accordion toolbox-card">
|
|
412
394
|
<InitNewElement
|
|
413
395
|
nodekeys={nodekeys}
|
|
414
|
-
|
|
396
|
+
setSavingState={setSavingState}
|
|
397
|
+
savingState={savingState}
|
|
415
398
|
/>
|
|
416
399
|
<Accordion>
|
|
417
400
|
<div className="card mt-1" accordiontitle="Components">
|
|
418
401
|
{{
|
|
419
|
-
show: <ToolboxShow />,
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
402
|
+
show: <ToolboxShow expanded={isLeftEnlarged} />,
|
|
403
|
+
list: <ToolboxList expanded={isLeftEnlarged} />,
|
|
404
|
+
edit: <ToolboxEdit expanded={isLeftEnlarged} />,
|
|
405
|
+
page: <ToolboxPage expanded={isLeftEnlarged} />,
|
|
406
|
+
filter: <ToolboxFilter expanded={isLeftEnlarged} />,
|
|
423
407
|
}[mode] || <div>Missing mode</div>}
|
|
424
408
|
</div>
|
|
425
409
|
<div accordiontitle="Library">
|
|
426
|
-
<Library />
|
|
410
|
+
<Library expanded={isLeftEnlarged} />
|
|
427
411
|
</div>
|
|
428
412
|
</Accordion>
|
|
429
413
|
</div>
|
|
430
|
-
<div
|
|
431
|
-
|
|
414
|
+
<div
|
|
415
|
+
className="card toolbox-card pe-0"
|
|
416
|
+
style={isLeftEnlarged ? { width: "13.4rem" } : {}}
|
|
417
|
+
>
|
|
418
|
+
<div className="card-header p-2 d-flex justify-content-between">
|
|
419
|
+
<div>Layers</div>
|
|
420
|
+
<FontAwesomeIcon
|
|
421
|
+
icon={
|
|
422
|
+
isLeftEnlarged
|
|
423
|
+
? faCaretSquareLeft
|
|
424
|
+
: faCaretSquareRight
|
|
425
|
+
}
|
|
426
|
+
className={"float-end fa-lg"}
|
|
427
|
+
onClick={() => setIsLeftEnlarged(!isLeftEnlarged)}
|
|
428
|
+
title={isLeftEnlarged ? "Shrink" : "Enlarge"}
|
|
429
|
+
/>
|
|
430
|
+
</div>
|
|
432
431
|
{showLayers && (
|
|
433
432
|
<div className="card-body p-0 builder-layers">
|
|
434
433
|
<Layers expandRootOnLoad={true} />
|
|
@@ -438,7 +437,9 @@ const Builder = ({ options, layout, mode }) => {
|
|
|
438
437
|
</div>
|
|
439
438
|
<div
|
|
440
439
|
id="builder-main-canvas"
|
|
441
|
-
className={`col builder-mode-${options.mode}
|
|
440
|
+
className={`col builder-mode-${options.mode} ${
|
|
441
|
+
options.mode !== "list" ? "emptymsg" : ""
|
|
442
|
+
}`}
|
|
442
443
|
>
|
|
443
444
|
<div>
|
|
444
445
|
<Frame
|
|
@@ -465,20 +466,45 @@ const Builder = ({ options, layout, mode }) => {
|
|
|
465
466
|
Tabs,
|
|
466
467
|
Table,
|
|
467
468
|
ToggleFilter,
|
|
469
|
+
ListColumn,
|
|
470
|
+
ListColumns,
|
|
468
471
|
}}
|
|
469
472
|
>
|
|
470
|
-
|
|
473
|
+
{options.mode === "list" ? (
|
|
474
|
+
<Element canvas is={ListColumns}></Element>
|
|
475
|
+
) : (
|
|
476
|
+
<Element canvas is={Column}></Element>
|
|
477
|
+
)}
|
|
471
478
|
</Frame>
|
|
479
|
+
{options.mode === "list" ? <AddColumnButton /> : null}
|
|
472
480
|
</div>
|
|
473
481
|
</div>
|
|
474
482
|
<div className="col-sm-auto builder-sidebar">
|
|
475
|
-
<div style={{ width: "16rem" }}>
|
|
483
|
+
<div style={{ width: isEnlarged ? "28rem" : "16rem" }}>
|
|
476
484
|
<NextButton layout={layout} />
|
|
477
485
|
<HistoryPanel />
|
|
478
486
|
<FontAwesomeIcon
|
|
479
487
|
icon={faSave}
|
|
480
|
-
className={isSaving ? "d-inline" : "d-none"}
|
|
488
|
+
className={savingState.isSaving ? "d-inline" : "d-none"}
|
|
489
|
+
/>
|
|
490
|
+
<FontAwesomeIcon
|
|
491
|
+
icon={faExclamationTriangle}
|
|
492
|
+
color="#ff0033"
|
|
493
|
+
className={savingState.error ? "d-inline" : "d-none"}
|
|
494
|
+
/>
|
|
495
|
+
<FontAwesomeIcon
|
|
496
|
+
icon={isEnlarged ? faCaretSquareRight : faCaretSquareLeft}
|
|
497
|
+
className={"float-end me-2 mt-1 fa-lg"}
|
|
498
|
+
onClick={() => setIsEnlarged(!isEnlarged)}
|
|
499
|
+
title={isEnlarged ? "Shrink" : "Enlarge"}
|
|
481
500
|
/>
|
|
501
|
+
<div
|
|
502
|
+
className={` ${
|
|
503
|
+
savingState.error ? "d-block" : "d-none"
|
|
504
|
+
} my-2 fw-bold`}
|
|
505
|
+
>
|
|
506
|
+
your work is not being saved
|
|
507
|
+
</div>
|
|
482
508
|
<SettingsPanel />
|
|
483
509
|
</div>
|
|
484
510
|
</div>
|
|
@@ -20,18 +20,7 @@ import faIcons from "./elements/faicons";
|
|
|
20
20
|
import { craftToSaltcorn, layoutToNodes } from "./storage";
|
|
21
21
|
import optionsCtx from "./context";
|
|
22
22
|
import { WrapElem } from "./Toolbox";
|
|
23
|
-
import { isEqual, throttle } from "lodash";
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
*
|
|
27
|
-
* @param {object[]} xs
|
|
28
|
-
* @returns {object[]}
|
|
29
|
-
*/
|
|
30
|
-
const twoByTwos = (xs) => {
|
|
31
|
-
if (xs.length <= 2) return [xs];
|
|
32
|
-
const [x, y, ...rest] = xs;
|
|
33
|
-
return [[x, y], ...twoByTwos(rest)];
|
|
34
|
-
};
|
|
23
|
+
import { isEqual, throttle, chunk } from "lodash";
|
|
35
24
|
|
|
36
25
|
export /**
|
|
37
26
|
* @param {object} props
|
|
@@ -94,7 +83,7 @@ export /**
|
|
|
94
83
|
* @subcategory components
|
|
95
84
|
* @namespace
|
|
96
85
|
*/
|
|
97
|
-
const InitNewElement = ({ nodekeys,
|
|
86
|
+
const InitNewElement = ({ nodekeys, savingState, setSavingState }) => {
|
|
98
87
|
const [saveTimeout, setSaveTimeout] = useState(false);
|
|
99
88
|
const savedData = useRef(false);
|
|
100
89
|
const { actions, query, connectors } = useEditor((state, query) => {
|
|
@@ -104,7 +93,11 @@ const InitNewElement = ({ nodekeys, setIsSaving }) => {
|
|
|
104
93
|
const doSave = (query) => {
|
|
105
94
|
if (!query.serialize) return;
|
|
106
95
|
|
|
107
|
-
const data = craftToSaltcorn(
|
|
96
|
+
const data = craftToSaltcorn(
|
|
97
|
+
JSON.parse(query.serialize()),
|
|
98
|
+
"ROOT",
|
|
99
|
+
options
|
|
100
|
+
);
|
|
108
101
|
const urlroot = options.page_id ? "pageedit" : "viewedit";
|
|
109
102
|
if (savedData.current === false) {
|
|
110
103
|
//do not save on first call
|
|
@@ -114,7 +107,7 @@ const InitNewElement = ({ nodekeys, setIsSaving }) => {
|
|
|
114
107
|
}
|
|
115
108
|
if (isEqual(savedData.current, JSON.stringify(data.layout))) return;
|
|
116
109
|
savedData.current = JSON.stringify(data.layout);
|
|
117
|
-
|
|
110
|
+
setSavingState({ isSaving: true });
|
|
118
111
|
|
|
119
112
|
fetch(`/${urlroot}/savebuilder/${options.page_id || options.view_id}`, {
|
|
120
113
|
method: "POST", // or 'PUT'
|
|
@@ -123,9 +116,32 @@ const InitNewElement = ({ nodekeys, setIsSaving }) => {
|
|
|
123
116
|
"CSRF-Token": options.csrfToken,
|
|
124
117
|
},
|
|
125
118
|
body: JSON.stringify(data),
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
|
|
119
|
+
})
|
|
120
|
+
.then((response) => {
|
|
121
|
+
response.json().then((data) => {
|
|
122
|
+
if (typeof data?.error === "string") {
|
|
123
|
+
// don't log duplicates
|
|
124
|
+
if (!savingState.error)
|
|
125
|
+
window.notifyAlert({ type: "danger", text: data.error });
|
|
126
|
+
setSavingState({ isSaving: false, error: data.error });
|
|
127
|
+
} else setSavingState({ isSaving: false });
|
|
128
|
+
});
|
|
129
|
+
})
|
|
130
|
+
.catch((e) => {
|
|
131
|
+
const text =
|
|
132
|
+
e.message === "Failed to fetch"
|
|
133
|
+
? "Network connection lost"
|
|
134
|
+
: e || "Unable to save";
|
|
135
|
+
// don't log duplicates
|
|
136
|
+
if (savingState.error) setSavingState({ isSaving: false, error: text });
|
|
137
|
+
else {
|
|
138
|
+
window.notifyAlert({ type: "danger", text: text });
|
|
139
|
+
setSavingState({
|
|
140
|
+
isSaving: false,
|
|
141
|
+
error: text,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
});
|
|
129
145
|
};
|
|
130
146
|
const throttledSave = useThrottle(() => {
|
|
131
147
|
doSave(query);
|
|
@@ -148,7 +164,8 @@ const InitNewElement = ({ nodekeys, setIsSaving }) => {
|
|
|
148
164
|
layout.layout ? layout.layout : layout,
|
|
149
165
|
query,
|
|
150
166
|
actions,
|
|
151
|
-
node.parent
|
|
167
|
+
node.parent,
|
|
168
|
+
options
|
|
152
169
|
);
|
|
153
170
|
setTimeout(() => {
|
|
154
171
|
actions.delete(id);
|
|
@@ -184,7 +201,7 @@ export /**
|
|
|
184
201
|
* @subcategory components
|
|
185
202
|
* @namespace
|
|
186
203
|
*/
|
|
187
|
-
const Library = () => {
|
|
204
|
+
const Library = ({ expanded }) => {
|
|
188
205
|
const { actions, selected, query, connectors } = useEditor((state, query) => {
|
|
189
206
|
return {
|
|
190
207
|
selected: state.events.selected,
|
|
@@ -200,7 +217,11 @@ const Library = () => {
|
|
|
200
217
|
* @returns {void}
|
|
201
218
|
*/
|
|
202
219
|
const addSelected = () => {
|
|
203
|
-
const layout = craftToSaltcorn(
|
|
220
|
+
const layout = craftToSaltcorn(
|
|
221
|
+
JSON.parse(query.serialize()),
|
|
222
|
+
selected,
|
|
223
|
+
options
|
|
224
|
+
);
|
|
204
225
|
const data = { layout, icon, name: newName };
|
|
205
226
|
fetch(`/library/savefrombuilder`, {
|
|
206
227
|
method: "POST", // or 'PUT'
|
|
@@ -216,7 +237,10 @@ const Library = () => {
|
|
|
216
237
|
setRecent([...recent, data]);
|
|
217
238
|
};
|
|
218
239
|
|
|
219
|
-
const elemRows =
|
|
240
|
+
const elemRows = chunk(
|
|
241
|
+
[...(options.library || []), ...recent],
|
|
242
|
+
expanded ? 3 : 2
|
|
243
|
+
);
|
|
220
244
|
return (
|
|
221
245
|
<div className="builder-library">
|
|
222
246
|
<div className="dropdown">
|
|
@@ -80,6 +80,11 @@ const RenderNode = ({ render }) => {
|
|
|
80
80
|
currentDOM.style.left = left;
|
|
81
81
|
}, [dom, getPos]);
|
|
82
82
|
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (name === "Column" && parent && parent !== "ROOT")
|
|
85
|
+
actions.selectNode(parent);
|
|
86
|
+
}, [isActive]);
|
|
87
|
+
|
|
83
88
|
useEffect(() => {
|
|
84
89
|
document
|
|
85
90
|
.getElementById("builder-main-canvas")
|