@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saltcorn/builder",
3
- "version": "0.5.6-beta.2",
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.17",
20
- "@craftjs/layers": "0.1.0-beta.17",
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, { useEffect, useContext, useState, Fragment } from "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 "@craftjs/layers";
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-2">
74
- <div className="card-header">
75
- {selected && selected.displayName
76
- ? `Settings: ${selected.displayName}`
77
- : "Settings"}
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-danger mt-2" onClick={deleteThis}>
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 className="btn btn-danger mt-2" onClick={deleteChildren}>
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 &raquo;
258
+ {options.next_button_label || "Next"} &raquo;
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
- <Editor>
172
- <Provider value={options}>
173
- <PreviewCtx.Provider value={{ previews, setPreviews }}>
174
- <div className="row">
175
- <div className="col-sm-auto">
176
- <div className="card">
177
- {{
178
- show: <ToolboxShow />,
179
- edit: <ToolboxEdit />,
180
- page: <ToolboxPage />,
181
- filter: <ToolboxFilter />,
182
- }[mode] || <div>Missing mode</div>}
183
- </div>
184
- </div>
185
- <div id="builder-main-canvas" className="col">
186
- <div>
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
- </div>
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
- <SettingsPanel />
235
- <br />
236
- <SaveButton />
237
- <NextButton layout={layout} />
238
- <ViewPageLink />
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
- </div>
242
- </PreviewCtx.Provider>
243
- </Provider>
244
- <div className="d-none preview-scratchpad"></div>
245
- </Editor>
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
+ };