@saltcorn/builder 0.5.6-beta.2 → 0.6.0-alpha.0
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 +19 -11
- package/dist/builder_bundle.js.map +1 -0
- package/package.json +6 -3
- package/src/components/Builder.js +188 -86
- package/src/components/Library.js +184 -0
- package/src/components/Toolbox.js +130 -69
- package/src/components/elements/Action.js +68 -108
- package/src/components/elements/Aggregation.js +0 -1
- package/src/components/elements/Column.js +4 -1
- package/src/components/elements/Columns.js +1 -2
- package/src/components/elements/Container.js +50 -84
- package/src/components/elements/DropDownFilter.js +16 -1
- package/src/components/elements/Field.js +1 -1
- package/src/components/elements/Image.js +92 -33
- package/src/components/elements/Link.js +139 -133
- package/src/components/elements/SearchBar.js +1 -1
- package/src/components/elements/Text.js +60 -26
- package/src/components/elements/ToggleFilter.js +12 -14
- package/src/components/elements/View.js +46 -19
- package/src/components/elements/ViewLink.js +62 -72
- package/src/components/elements/utils.js +206 -31
- package/src/components/storage.js +19 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saltcorn/builder",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0-alpha.0",
|
|
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",
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
"@babel/core": "^7.9.6",
|
|
17
17
|
"@babel/preset-env": "^7.9.6",
|
|
18
18
|
"@babel/preset-react": "^7.9.4",
|
|
19
|
-
"@craftjs/core": "0.1.0-beta.
|
|
20
|
-
"
|
|
19
|
+
"@craftjs/core": "0.1.0-beta.20",
|
|
20
|
+
"saltcorn-craft-layers-noeye": "0.1.0-beta.22",
|
|
21
21
|
"@fonticonpicker/react-fonticonpicker": "^1.2.0",
|
|
22
22
|
"@fortawesome/fontawesome-svg-core": "^1.2.34",
|
|
23
23
|
"@fortawesome/free-regular-svg-icons": "^5.15.2",
|
|
@@ -37,5 +37,8 @@
|
|
|
37
37
|
},
|
|
38
38
|
"publishConfig": {
|
|
39
39
|
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"styled-components": "^4.4.1"
|
|
40
43
|
}
|
|
41
44
|
}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, {
|
|
2
|
+
useEffect,
|
|
3
|
+
useContext,
|
|
4
|
+
useState,
|
|
5
|
+
Fragment,
|
|
6
|
+
useRef,
|
|
7
|
+
} from "react";
|
|
2
8
|
import { Editor, Frame, Element, Selector, useEditor } from "@craftjs/core";
|
|
3
9
|
import { Text } from "./elements/Text";
|
|
4
10
|
import { Field } from "./elements/Field";
|
|
@@ -29,12 +35,20 @@ import { Link } from "./elements/Link";
|
|
|
29
35
|
import { View } from "./elements/View";
|
|
30
36
|
import { Container } from "./elements/Container";
|
|
31
37
|
import { Column } from "./elements/Column";
|
|
32
|
-
import { Layers } from "
|
|
33
|
-
|
|
38
|
+
import { Layers } from "saltcorn-craft-layers-noeye";
|
|
39
|
+
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
40
|
+
import {
|
|
41
|
+
faCopy,
|
|
42
|
+
faUndo,
|
|
43
|
+
faRedo,
|
|
44
|
+
faTrashAlt,
|
|
45
|
+
} from "@fortawesome/free-solid-svg-icons";
|
|
46
|
+
import { Accordion, ErrorBoundary } from "./elements/utils";
|
|
47
|
+
import { InitNewElement, Library } from "./Library";
|
|
34
48
|
const { Provider } = optionsCtx;
|
|
35
49
|
|
|
36
50
|
const SettingsPanel = () => {
|
|
37
|
-
const { actions, selected } = useEditor((state, query) => {
|
|
51
|
+
const { actions, selected, query } = useEditor((state, query) => {
|
|
38
52
|
const currentNodeId = state.events.selected;
|
|
39
53
|
let selected;
|
|
40
54
|
|
|
@@ -59,9 +73,25 @@ const SettingsPanel = () => {
|
|
|
59
73
|
selected,
|
|
60
74
|
};
|
|
61
75
|
});
|
|
76
|
+
|
|
62
77
|
const deleteThis = () => {
|
|
63
78
|
actions.delete(selected.id);
|
|
64
79
|
};
|
|
80
|
+
const handleUserKeyPress = ({ keyCode, target }) => {
|
|
81
|
+
if (
|
|
82
|
+
(keyCode === 8 || keyCode === 46) &&
|
|
83
|
+
target.tagName.toLowerCase() === "body" &&
|
|
84
|
+
selected
|
|
85
|
+
) {
|
|
86
|
+
deleteThis();
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
window.addEventListener("keydown", handleUserKeyPress);
|
|
91
|
+
return () => {
|
|
92
|
+
window.removeEventListener("keydown", handleUserKeyPress);
|
|
93
|
+
};
|
|
94
|
+
}, [handleUserKeyPress]);
|
|
65
95
|
const hasChildren =
|
|
66
96
|
selected && selected.children && selected.children.length > 0;
|
|
67
97
|
const deleteChildren = () => {
|
|
@@ -69,28 +99,67 @@ const SettingsPanel = () => {
|
|
|
69
99
|
actions.delete(child);
|
|
70
100
|
});
|
|
71
101
|
};
|
|
102
|
+
const recursivelyCloneToElems = (nodeId, ix) => {
|
|
103
|
+
const {
|
|
104
|
+
data: { type, props, nodes },
|
|
105
|
+
} = query.node(nodeId).get();
|
|
106
|
+
const children = (nodes || []).map(recursivelyCloneToElems);
|
|
107
|
+
return React.createElement(
|
|
108
|
+
type,
|
|
109
|
+
{ ...props, ...(typeof ix !== "undefined" ? { key: ix } : {}) },
|
|
110
|
+
children
|
|
111
|
+
);
|
|
112
|
+
};
|
|
113
|
+
const duplicate = () => {
|
|
114
|
+
const {
|
|
115
|
+
data: { parent },
|
|
116
|
+
} = query.node(selected.id).get();
|
|
117
|
+
const elem = recursivelyCloneToElems(selected.id);
|
|
118
|
+
actions.addNodeTree(
|
|
119
|
+
query.parseReactElement(elem).toNodeTree(),
|
|
120
|
+
parent || "ROOT"
|
|
121
|
+
);
|
|
122
|
+
};
|
|
72
123
|
return (
|
|
73
|
-
<div className="settings-panel card mt-
|
|
74
|
-
<div className="card-header">
|
|
75
|
-
{selected && selected.displayName
|
|
76
|
-
|
|
77
|
-
|
|
124
|
+
<div className="settings-panel card mt-1">
|
|
125
|
+
<div className="card-header px-2 py-1">
|
|
126
|
+
{selected && selected.displayName ? (
|
|
127
|
+
<Fragment>
|
|
128
|
+
<b>{selected.displayName}</b> settings
|
|
129
|
+
</Fragment>
|
|
130
|
+
) : (
|
|
131
|
+
"Settings"
|
|
132
|
+
)}
|
|
78
133
|
</div>
|
|
79
134
|
<div className="card-body p-2">
|
|
80
135
|
{selected ? (
|
|
81
136
|
<Fragment>
|
|
82
|
-
{}
|
|
83
|
-
{selected.settings && React.createElement(selected.settings)}
|
|
84
137
|
{selected.isDeletable && (
|
|
85
|
-
<button className="btn btn-
|
|
138
|
+
<button className="btn btn-sm btn-danger" onClick={deleteThis}>
|
|
139
|
+
<FontAwesomeIcon icon={faTrashAlt} className="mr-1" />
|
|
86
140
|
Delete
|
|
87
141
|
</button>
|
|
88
142
|
)}
|
|
89
|
-
{hasChildren && !selected.isDeletable
|
|
90
|
-
<button
|
|
143
|
+
{hasChildren && !selected.isDeletable ? (
|
|
144
|
+
<button
|
|
145
|
+
className="btn btn-sm btn-danger"
|
|
146
|
+
onClick={deleteChildren}
|
|
147
|
+
>
|
|
148
|
+
<FontAwesomeIcon icon={faTrashAlt} className="mr-1" />
|
|
91
149
|
Delete contents
|
|
92
150
|
</button>
|
|
151
|
+
) : (
|
|
152
|
+
<button
|
|
153
|
+
title="Duplicate element with its children"
|
|
154
|
+
className="btn btn-sm btn-secondary ml-2"
|
|
155
|
+
onClick={duplicate}
|
|
156
|
+
>
|
|
157
|
+
<FontAwesomeIcon icon={faCopy} className="mr-1" />
|
|
158
|
+
Clone
|
|
159
|
+
</button>
|
|
93
160
|
)}
|
|
161
|
+
<hr className="my-2" />
|
|
162
|
+
{selected.settings && React.createElement(selected.settings)}
|
|
94
163
|
</Fragment>
|
|
95
164
|
) : (
|
|
96
165
|
"No element selected"
|
|
@@ -130,19 +199,47 @@ const ViewPageLink = () => {
|
|
|
130
199
|
const { query, actions } = useEditor(() => {});
|
|
131
200
|
const options = useContext(optionsCtx);
|
|
132
201
|
return options.page_id ? (
|
|
133
|
-
<a
|
|
134
|
-
target="_blank"
|
|
135
|
-
className="d-block mt-2"
|
|
136
|
-
href={`/page/${options.page_name}`}
|
|
137
|
-
>
|
|
202
|
+
<a target="_blank" className="ml-3" href={`/page/${options.page_name}`}>
|
|
138
203
|
View page
|
|
139
204
|
</a>
|
|
140
205
|
) : (
|
|
141
206
|
""
|
|
142
207
|
);
|
|
143
208
|
};
|
|
209
|
+
const HistoryPanel = () => {
|
|
210
|
+
const { canUndo, canRedo, actions } = useEditor((state, query) => ({
|
|
211
|
+
canUndo: query.history.canUndo(),
|
|
212
|
+
canRedo: query.history.canRedo(),
|
|
213
|
+
}));
|
|
214
|
+
|
|
215
|
+
return (
|
|
216
|
+
<div className="mt-2">
|
|
217
|
+
{canUndo && (
|
|
218
|
+
<button
|
|
219
|
+
className="btn btn-sm btn-secondary mr-2"
|
|
220
|
+
title="Undo"
|
|
221
|
+
onClick={() => actions.history.undo()}
|
|
222
|
+
>
|
|
223
|
+
<FontAwesomeIcon icon={faUndo} />
|
|
224
|
+
</button>
|
|
225
|
+
)}
|
|
226
|
+
{canRedo && (
|
|
227
|
+
<button
|
|
228
|
+
className="btn btn-sm btn-secondary"
|
|
229
|
+
title="Redo"
|
|
230
|
+
onClick={() => actions.history.redo()}
|
|
231
|
+
>
|
|
232
|
+
<FontAwesomeIcon icon={faRedo} />
|
|
233
|
+
</button>
|
|
234
|
+
)}
|
|
235
|
+
</div>
|
|
236
|
+
);
|
|
237
|
+
};
|
|
238
|
+
|
|
144
239
|
const NextButton = ({ layout }) => {
|
|
145
240
|
const { query, actions } = useEditor(() => {});
|
|
241
|
+
const options = useContext(optionsCtx);
|
|
242
|
+
|
|
146
243
|
useEffect(() => {
|
|
147
244
|
layoutToNodes(layout, query, actions);
|
|
148
245
|
}, []);
|
|
@@ -158,7 +255,7 @@ const NextButton = ({ layout }) => {
|
|
|
158
255
|
};
|
|
159
256
|
return (
|
|
160
257
|
<button className="btn btn-primary builder-save" onClick={onClick}>
|
|
161
|
-
Next »
|
|
258
|
+
{options.next_button_label || "Next"} »
|
|
162
259
|
</button>
|
|
163
260
|
);
|
|
164
261
|
};
|
|
@@ -166,83 +263,88 @@ const NextButton = ({ layout }) => {
|
|
|
166
263
|
const Builder = ({ options, layout, mode }) => {
|
|
167
264
|
const [showLayers, setShowLayers] = useState(true);
|
|
168
265
|
const [previews, setPreviews] = useState({});
|
|
266
|
+
const nodekeys = useRef([]);
|
|
169
267
|
|
|
170
268
|
return (
|
|
171
|
-
<
|
|
172
|
-
<
|
|
173
|
-
<
|
|
174
|
-
<
|
|
175
|
-
<div className="
|
|
176
|
-
<div className="
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
<Frame
|
|
188
|
-
resolver={{
|
|
189
|
-
Text,
|
|
190
|
-
Empty,
|
|
191
|
-
Columns,
|
|
192
|
-
JoinField,
|
|
193
|
-
Field,
|
|
194
|
-
ViewLink,
|
|
195
|
-
Action,
|
|
196
|
-
HTMLCode,
|
|
197
|
-
LineBreak,
|
|
198
|
-
Aggregation,
|
|
199
|
-
Card,
|
|
200
|
-
Image,
|
|
201
|
-
Link,
|
|
202
|
-
View,
|
|
203
|
-
SearchBar,
|
|
204
|
-
Container,
|
|
205
|
-
Column,
|
|
206
|
-
DropDownFilter,
|
|
207
|
-
Tabs,
|
|
208
|
-
ToggleFilter,
|
|
209
|
-
}}
|
|
210
|
-
>
|
|
211
|
-
<Element canvas is={Column}></Element>
|
|
212
|
-
</Frame>
|
|
213
|
-
</div>
|
|
214
|
-
</div>
|
|
215
|
-
<div className="col-sm-auto builder-sidebar">
|
|
216
|
-
<div style={{ width: "16rem" }}>
|
|
217
|
-
<div className="card">
|
|
218
|
-
<div className="card-header">
|
|
219
|
-
Layers
|
|
220
|
-
<div className="float-right">
|
|
221
|
-
<input
|
|
222
|
-
type="checkbox"
|
|
223
|
-
checked={showLayers}
|
|
224
|
-
onChange={(e) => setShowLayers(e.target.checked)}
|
|
225
|
-
/>
|
|
269
|
+
<ErrorBoundary>
|
|
270
|
+
<Editor>
|
|
271
|
+
<Provider value={options}>
|
|
272
|
+
<PreviewCtx.Provider value={{ previews, setPreviews }}>
|
|
273
|
+
<div className="row" style={{ marginTop: "-5px" }}>
|
|
274
|
+
<div className="col-sm-auto">
|
|
275
|
+
<div className="componets-and-library-accordion toolbox-card">
|
|
276
|
+
<InitNewElement nodekeys={nodekeys} />
|
|
277
|
+
<Accordion>
|
|
278
|
+
<div className="card " accordiontitle="Components">
|
|
279
|
+
{{
|
|
280
|
+
show: <ToolboxShow />,
|
|
281
|
+
edit: <ToolboxEdit />,
|
|
282
|
+
page: <ToolboxPage />,
|
|
283
|
+
filter: <ToolboxFilter />,
|
|
284
|
+
}[mode] || <div>Missing mode</div>}
|
|
226
285
|
</div>
|
|
227
|
-
|
|
286
|
+
<div accordiontitle="Library">
|
|
287
|
+
<Library />
|
|
288
|
+
</div>
|
|
289
|
+
</Accordion>
|
|
290
|
+
</div>
|
|
291
|
+
<div className="card toolbox-card">
|
|
292
|
+
<div className="card-header">Layers</div>
|
|
228
293
|
{showLayers && (
|
|
229
294
|
<div className="card-body p-0 builder-layers">
|
|
230
295
|
<Layers expandRootOnLoad={true} />
|
|
231
296
|
</div>
|
|
232
297
|
)}
|
|
233
298
|
</div>
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
299
|
+
</div>
|
|
300
|
+
<div
|
|
301
|
+
id="builder-main-canvas"
|
|
302
|
+
className={`col builder-mode-${options.mode}`}
|
|
303
|
+
>
|
|
304
|
+
<div>
|
|
305
|
+
<Frame
|
|
306
|
+
resolver={{
|
|
307
|
+
Text,
|
|
308
|
+
Empty,
|
|
309
|
+
Columns,
|
|
310
|
+
JoinField,
|
|
311
|
+
Field,
|
|
312
|
+
ViewLink,
|
|
313
|
+
Action,
|
|
314
|
+
HTMLCode,
|
|
315
|
+
LineBreak,
|
|
316
|
+
Aggregation,
|
|
317
|
+
Card,
|
|
318
|
+
Image,
|
|
319
|
+
Link,
|
|
320
|
+
View,
|
|
321
|
+
SearchBar,
|
|
322
|
+
Container,
|
|
323
|
+
Column,
|
|
324
|
+
DropDownFilter,
|
|
325
|
+
Tabs,
|
|
326
|
+
ToggleFilter,
|
|
327
|
+
}}
|
|
328
|
+
>
|
|
329
|
+
<Element canvas is={Column}></Element>
|
|
330
|
+
</Frame>
|
|
331
|
+
</div>
|
|
332
|
+
</div>
|
|
333
|
+
<div className="col-sm-auto builder-sidebar">
|
|
334
|
+
<div style={{ width: "16rem" }}>
|
|
335
|
+
<SaveButton />
|
|
336
|
+
<NextButton layout={layout} />
|
|
337
|
+
<ViewPageLink />
|
|
338
|
+
<HistoryPanel />
|
|
339
|
+
<SettingsPanel />
|
|
340
|
+
</div>
|
|
239
341
|
</div>
|
|
240
342
|
</div>
|
|
241
|
-
</
|
|
242
|
-
</
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
</
|
|
343
|
+
</PreviewCtx.Provider>
|
|
344
|
+
</Provider>
|
|
345
|
+
<div className="d-none preview-scratchpad"></div>
|
|
346
|
+
</Editor>
|
|
347
|
+
</ErrorBoundary>
|
|
246
348
|
);
|
|
247
349
|
};
|
|
248
350
|
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import React, { useEffect, useContext, useState, Fragment } from "react";
|
|
2
|
+
import { useEditor, useNode } from "@craftjs/core";
|
|
3
|
+
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
4
|
+
import { faPlus, faTimes } from "@fortawesome/free-solid-svg-icons";
|
|
5
|
+
import FontIconPicker from "@fonticonpicker/react-fonticonpicker";
|
|
6
|
+
import faIcons from "./elements/faicons";
|
|
7
|
+
import { craftToSaltcorn, layoutToNodes } from "./storage";
|
|
8
|
+
import optionsCtx from "./context";
|
|
9
|
+
import { WrapElem } from "./Toolbox";
|
|
10
|
+
|
|
11
|
+
const twoByTwos = (xs) => {
|
|
12
|
+
if (xs.length <= 2) return [xs];
|
|
13
|
+
const [x, y, ...rest] = xs;
|
|
14
|
+
return [[x, y], ...twoByTwos(rest)];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const LibraryElem = ({ name, layout }) => {
|
|
18
|
+
const {
|
|
19
|
+
selected,
|
|
20
|
+
connectors: { connect, drag },
|
|
21
|
+
} = useNode((node) => ({ selected: node.events.selected }));
|
|
22
|
+
return (
|
|
23
|
+
<Fragment>
|
|
24
|
+
<span
|
|
25
|
+
className={selected ? "selected-node" : ""}
|
|
26
|
+
ref={(dom) => connect(drag(dom))}
|
|
27
|
+
>
|
|
28
|
+
LibElem
|
|
29
|
+
</span>
|
|
30
|
+
<br />
|
|
31
|
+
</Fragment>
|
|
32
|
+
);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
LibraryElem.craft = {
|
|
36
|
+
displayName: "LibraryElem",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const InitNewElement = ({ nodekeys }) => {
|
|
40
|
+
const { actions, query, connectors } = useEditor((state, query) => {
|
|
41
|
+
return {};
|
|
42
|
+
});
|
|
43
|
+
const onNodesChange = (arg, arg1) => {
|
|
44
|
+
const nodes = arg.getSerializedNodes();
|
|
45
|
+
const newNodeIds = [];
|
|
46
|
+
Object.keys(nodes).forEach((id) => {
|
|
47
|
+
if (!nodekeys.current.includes(id)) {
|
|
48
|
+
newNodeIds.push(id);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
nodekeys.current = Object.keys(nodes);
|
|
52
|
+
if (newNodeIds.length === 1) {
|
|
53
|
+
const id = newNodeIds[0];
|
|
54
|
+
const node = nodes[id];
|
|
55
|
+
if (node.displayName === "LibraryElem") {
|
|
56
|
+
const layout = node.props.layout;
|
|
57
|
+
layoutToNodes(
|
|
58
|
+
layout.layout ? layout.layout : layout,
|
|
59
|
+
query,
|
|
60
|
+
actions,
|
|
61
|
+
node.parent
|
|
62
|
+
);
|
|
63
|
+
setTimeout(() => {
|
|
64
|
+
actions.delete(id);
|
|
65
|
+
}, 0);
|
|
66
|
+
} else {
|
|
67
|
+
actions.selectNode(id);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
const nodes = query.getSerializedNodes();
|
|
73
|
+
nodekeys.current = Object.keys(nodes);
|
|
74
|
+
actions.setOptions((options) => {
|
|
75
|
+
const oldf = options.onNodesChange(
|
|
76
|
+
(options.onNodesChange = oldf
|
|
77
|
+
? (q) => {
|
|
78
|
+
oldf(q);
|
|
79
|
+
onNodesChange(q);
|
|
80
|
+
}
|
|
81
|
+
: onNodesChange)
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
}, []);
|
|
85
|
+
|
|
86
|
+
return [];
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export const Library = () => {
|
|
90
|
+
const { actions, selected, query, connectors } = useEditor((state, query) => {
|
|
91
|
+
return {
|
|
92
|
+
selected: state.events.selected,
|
|
93
|
+
};
|
|
94
|
+
});
|
|
95
|
+
const options = useContext(optionsCtx);
|
|
96
|
+
const [adding, setAdding] = useState(false);
|
|
97
|
+
const [newName, setNewName] = useState("");
|
|
98
|
+
const [icon, setIcon] = useState();
|
|
99
|
+
const [recent, setRecent] = useState([]);
|
|
100
|
+
|
|
101
|
+
const addSelected = () => {
|
|
102
|
+
const layout = craftToSaltcorn(JSON.parse(query.serialize()), selected);
|
|
103
|
+
const data = { layout, icon, name: newName };
|
|
104
|
+
fetch(`/library/savefrombuilder`, {
|
|
105
|
+
method: "POST", // or 'PUT'
|
|
106
|
+
headers: {
|
|
107
|
+
"Content-Type": "application/json",
|
|
108
|
+
"CSRF-Token": options.csrfToken,
|
|
109
|
+
},
|
|
110
|
+
body: JSON.stringify(data),
|
|
111
|
+
});
|
|
112
|
+
setAdding(false);
|
|
113
|
+
setIcon();
|
|
114
|
+
setNewName("");
|
|
115
|
+
setRecent([...recent, data]);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const elemRows = twoByTwos([...(options.library || []), ...recent]);
|
|
119
|
+
return (
|
|
120
|
+
<div className="builder-library">
|
|
121
|
+
<div className="dropdown">
|
|
122
|
+
<button
|
|
123
|
+
className="btn btn-sm btn-secondary dropdown-toggle mt-2"
|
|
124
|
+
type="button"
|
|
125
|
+
id="dropdownMenuButton"
|
|
126
|
+
aria-haspopup="true"
|
|
127
|
+
aria-expanded="false"
|
|
128
|
+
disabled={!selected}
|
|
129
|
+
onClick={() => setAdding(!adding)}
|
|
130
|
+
>
|
|
131
|
+
<FontAwesomeIcon icon={faPlus} className="mr-1" />
|
|
132
|
+
Add
|
|
133
|
+
</button>
|
|
134
|
+
<div
|
|
135
|
+
className={`dropdown-menu py-3 px-4 ${adding ? "show" : ""}`}
|
|
136
|
+
aria-labelledby="dropdownMenuButton"
|
|
137
|
+
>
|
|
138
|
+
<label>Name</label>
|
|
139
|
+
<input
|
|
140
|
+
type="text"
|
|
141
|
+
className="form-control"
|
|
142
|
+
value={newName}
|
|
143
|
+
onChange={(e) => setNewName(e.target.value)}
|
|
144
|
+
/>
|
|
145
|
+
<br />
|
|
146
|
+
<label>Icon</label>
|
|
147
|
+
<FontIconPicker
|
|
148
|
+
className="w-100"
|
|
149
|
+
value={icon}
|
|
150
|
+
icons={faIcons}
|
|
151
|
+
onChange={setIcon}
|
|
152
|
+
isMulti={false}
|
|
153
|
+
/>
|
|
154
|
+
<button className={`btn btn-primary mt-3`} onClick={addSelected}>
|
|
155
|
+
<FontAwesomeIcon icon={faPlus} className="mr-1" />
|
|
156
|
+
Add
|
|
157
|
+
</button>
|
|
158
|
+
<button
|
|
159
|
+
className={`btn btn-outline-secondary ml-2 mt-3`}
|
|
160
|
+
onClick={() => setAdding(false)}
|
|
161
|
+
>
|
|
162
|
+
<FontAwesomeIcon icon={faTimes} />
|
|
163
|
+
</button>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
<div className="card mt-2">
|
|
167
|
+
{elemRows.map((els, ix) => (
|
|
168
|
+
<div className="toolbar-row" key={ix}>
|
|
169
|
+
{els.map((l, ix) => (
|
|
170
|
+
<WrapElem
|
|
171
|
+
key={ix}
|
|
172
|
+
connectors={connectors}
|
|
173
|
+
icon={l.icon}
|
|
174
|
+
label={l.name}
|
|
175
|
+
>
|
|
176
|
+
<LibraryElem name={l.name} layout={l.layout}></LibraryElem>
|
|
177
|
+
</WrapElem>
|
|
178
|
+
))}
|
|
179
|
+
</div>
|
|
180
|
+
))}
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
);
|
|
184
|
+
};
|