@springmicro/forms 0.7.5 → 0.7.7

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.
@@ -1,256 +1,256 @@
1
- import React from "react";
2
- import NodeParent from "./nodes/node-parent";
3
- import BottomDrawer from "./bottom-drawer";
4
- import {
5
- CountdownType,
6
- EditingStateType,
7
- FormNodeType,
8
- FormType,
9
- ObjectNode,
10
- } from "../types/form-builder";
11
- import { DragDropContext, Droppable } from "react-beautiful-dnd";
12
- import { serializeBuilderToForm } from "../utils/form-builder";
13
- import { RJSFSchema, UiSchema } from "@rjsf/utils";
14
- import { UseStateType } from "../types/utils.type";
15
- import { Box } from "@mui/material";
16
-
17
- export type FormBuilderProps = {
18
- nodes: UseStateType<FormNodeType[]>;
19
- form: UseStateType<FormType>;
20
- saveCallback: (data: { form: RJSFSchema; ui: UiSchema }) => Promise<boolean>;
21
- };
22
-
23
- const countdownSeconds = 5;
24
-
25
- export default function FormBuilder({
26
- nodes: providedNodes,
27
- form: providedForm,
28
- saveCallback,
29
- }: FormBuilderProps) {
30
- const [form, setForm] = providedForm;
31
- const [baseNodes, setBaseNodes] = providedNodes;
32
- const [path, setPath] = React.useState<number[]>([]);
33
- const [nodes, setNodes] = React.useState<FormNodeType[]>([...baseNodes]);
34
- const editingState = React.useState<EditingStateType>(false);
35
-
36
- /**
37
- * ------------------------- PATH MANAGEMENT -----------------------------
38
- */
39
-
40
- React.useEffect(() => {
41
- if (path.length === 0) {
42
- setBaseNodes([...nodes]);
43
- } else {
44
- setBaseNodes((bn) => {
45
- const basePathSearch = [...bn]; // The new base of nodes.
46
- let pathSearch = basePathSearch; // Runs through the path and modifies basePathSearch.
47
- for (let i = 0; i < path.length - 1; i++) {
48
- // Sets pathSearch to every child in the path except for the last one.
49
- pathSearch = (pathSearch[path[i]] as ObjectNode).children;
50
- }
51
- (pathSearch[path[path.length - 1]] as ObjectNode).children = nodes; // Sets the value pathSearch.children in the last value of the path to nodes. (Where the user is currently editing)
52
- return [...basePathSearch];
53
- });
54
- }
55
- }, [nodes]);
56
-
57
- React.useEffect(() => {
58
- let pathSearch = [...baseNodes];
59
- for (let i = 0; i < path.length; i++) {
60
- pathSearch = (pathSearch[path[i]] as ObjectNode).children;
61
- }
62
- setNodes(pathSearch);
63
- }, [path]);
64
-
65
- function updatePath(newPathNum?: number) {
66
- editingState[1](false);
67
- if (newPathNum === undefined) {
68
- // If undefined, go up a layer in the path.
69
- setPath((p) => p.filter((_, i) => i !== p.length - 1));
70
- } else if (newPathNum < 0) {
71
- // If negative, reset to base path.
72
- setPath([]);
73
- } else {
74
- // If 0 or higher, push to the end of the path.
75
- setPath((p) => [...p, newPathNum]);
76
- }
77
- }
78
-
79
- /**
80
- * ------------------------- AUTOSAVE -----------------------------
81
- */
82
-
83
- const timerRef = React.useRef<NodeJS.Timeout>();
84
- const [countdown, setCountdown] = React.useState<CountdownType>(undefined);
85
-
86
- function resetCountdown() {
87
- if (timerRef.current) {
88
- clearTimeout(timerRef.current);
89
- }
90
-
91
- setCountdown(countdownSeconds);
92
- timerRef.current = setTimeout(() => {
93
- Save();
94
- }, countdownSeconds * 1000);
95
- }
96
-
97
- const AutosaveNodes: React.Dispatch<React.SetStateAction<FormNodeType[]>> = (
98
- action: React.SetStateAction<FormNodeType[]>
99
- ) => {
100
- setNodes((prevNodes) => {
101
- let currentState: FormNodeType[] = prevNodes;
102
- if (typeof action === "function") {
103
- currentState = action(currentState);
104
- } else {
105
- currentState = [...action];
106
- }
107
-
108
- resetCountdown();
109
- return currentState;
110
- });
111
- };
112
-
113
- const AutosaveForm: React.Dispatch<React.SetStateAction<FormType>> = (
114
- action: React.SetStateAction<FormType>
115
- ) => {
116
- setForm((prevForm) => {
117
- let currentState: FormType = prevForm;
118
- if (typeof action === "function") {
119
- currentState = action(currentState);
120
- } else {
121
- currentState = { ...prevForm, ...action };
122
- }
123
-
124
- resetCountdown();
125
- return currentState;
126
- });
127
- };
128
-
129
- React.useEffect(() => {
130
- if (typeof countdown === "string" || countdown === undefined) return;
131
- if (countdown > 0) {
132
- const interval = setInterval(() => {
133
- setCountdown((prevCount) => (prevCount as number) - 1);
134
- }, 1000);
135
- return () => clearInterval(interval);
136
- }
137
- }, [countdown]);
138
-
139
- React.useEffect(() => {
140
- // Adds a warning when user tries to close tab with unsaved changes
141
- if (countdown !== "Saved." && countdown !== undefined) {
142
- const handleBeforeUnload = (event: BeforeUnloadEvent) => {
143
- const confirmationMessage =
144
- "You have unsaved changes. Are you sure you want to leave?";
145
- event.returnValue = confirmationMessage; // Gecko, Trident, Chrome 34+
146
- return confirmationMessage; // Gecko, WebKit, Chrome <34
147
- };
148
-
149
- window.addEventListener("beforeunload", handleBeforeUnload);
150
-
151
- return () => {
152
- window.removeEventListener("beforeunload", handleBeforeUnload);
153
- };
154
- }
155
- }, [countdown]);
156
-
157
- // Runs after autosave countdown finishes.
158
- function Save() {
159
- setCountdown("Saving...");
160
- setBaseNodes((bn) => {
161
- // Gets the most up-to-date baseNodes through the setBaseNodes function because it updates in a weird way.
162
- const Form = serializeBuilderToForm(form, bn);
163
- saveCallback({ ...Form }).then((res) => {
164
- console.log("PROMISE RES:", res);
165
- if (res) {
166
- setCountdown("Saved.");
167
- setTimeout(() => {
168
- setCountdown(undefined);
169
- }, 1500); // Waits 1.5s before removing text
170
- } else {
171
- setCountdown("Save failed."); // Do not remove this text until another change has been made by the user.
172
- }
173
- });
174
-
175
- return bn; // doesn't need to return a new object because it's not intended to update any state.
176
- });
177
- setTimeout(() => {}, Math.random() * 700 + 300); // Simulated API wait time
178
- }
179
-
180
- return (
181
- <Box
182
- sx={{
183
- p: 2,
184
- width: "100%",
185
- height: "100%",
186
- borderRadius: 2,
187
- border: "1px solid #999",
188
- display: "flex",
189
- flexDirection: "column",
190
- }}
191
- >
192
- <Box
193
- id="fb-scrollable"
194
- sx={{
195
- display: "flex",
196
- flexGrow: 1,
197
- overflow: "auto",
198
- borderTopLeftRadius: 0.5,
199
- borderTopRightRadius: 0.5,
200
- flexDirection: "column",
201
- scrollBehavior: "smooth",
202
- }}
203
- >
204
- <Box sx={{ display: "flex", flexDirection: "column" }}>
205
- <DragDropContext
206
- onDragEnd={(result) => {
207
- AutosaveNodes((prevNodes) => {
208
- if (!result.destination) return prevNodes;
209
-
210
- const items = Array.from(prevNodes);
211
- const [reorderedItem] = items.splice(result.source.index, 1);
212
- items.splice(result.destination.index, 0, reorderedItem);
213
-
214
- return items;
215
- });
216
- }}
217
- >
218
- <Droppable droppableId="nodes">
219
- {(provided) => (
220
- <Box
221
- ref={provided.innerRef}
222
- {...provided.droppableProps}
223
- sx={{ display: "flex", flexDirection: "column", gap: 1 }}
224
- >
225
- {nodes.map((node, i) => (
226
- <NodeParent
227
- key={node.nodeId}
228
- node={node}
229
- nodesState={[nodes, AutosaveNodes]}
230
- index={i}
231
- editingState={editingState}
232
- updatePath={() => {
233
- updatePath(i);
234
- }}
235
- deleteNode={() => {
236
- AutosaveNodes((n) => n.filter((_, j) => j !== i));
237
- }}
238
- />
239
- ))}
240
- {provided.placeholder}
241
- </Box>
242
- )}
243
- </Droppable>
244
- </DragDropContext>
245
- </Box>
246
- </Box>
247
- <BottomDrawer
248
- nodesState={[nodes, AutosaveNodes]}
249
- formState={[form, AutosaveForm]}
250
- countdown={countdown}
251
- path={[path, updatePath]}
252
- setEditing={editingState[1]}
253
- />
254
- </Box>
255
- );
256
- }
1
+ import React from "react";
2
+ import NodeParent from "./nodes/node-parent";
3
+ import BottomDrawer from "./bottom-drawer";
4
+ import {
5
+ CountdownType,
6
+ EditingStateType,
7
+ FormNodeType,
8
+ FormType,
9
+ ObjectNode,
10
+ } from "../types/form-builder";
11
+ import { DragDropContext, Droppable } from "react-beautiful-dnd";
12
+ import { serializeBuilderToForm } from "../utils/form-builder";
13
+ import { RJSFSchema, UiSchema } from "@rjsf/utils";
14
+ import { UseStateType } from "../types/utils.type";
15
+ import { Box } from "@mui/material";
16
+
17
+ export type FormBuilderProps = {
18
+ nodes: UseStateType<FormNodeType[]>;
19
+ form: UseStateType<FormType>;
20
+ saveCallback: (data: { form: RJSFSchema; ui: UiSchema }) => Promise<boolean>;
21
+ };
22
+
23
+ const countdownSeconds = 5;
24
+
25
+ export default function FormBuilder({
26
+ nodes: providedNodes,
27
+ form: providedForm,
28
+ saveCallback,
29
+ }: FormBuilderProps) {
30
+ const [form, setForm] = providedForm;
31
+ const [baseNodes, setBaseNodes] = providedNodes;
32
+ const [path, setPath] = React.useState<number[]>([]);
33
+ const [nodes, setNodes] = React.useState<FormNodeType[]>([...baseNodes]);
34
+ const editingState = React.useState<EditingStateType>(false);
35
+
36
+ /**
37
+ * ------------------------- PATH MANAGEMENT -----------------------------
38
+ */
39
+
40
+ React.useEffect(() => {
41
+ if (path.length === 0) {
42
+ setBaseNodes([...nodes]);
43
+ } else {
44
+ setBaseNodes((bn) => {
45
+ const basePathSearch = [...bn]; // The new base of nodes.
46
+ let pathSearch = basePathSearch; // Runs through the path and modifies basePathSearch.
47
+ for (let i = 0; i < path.length - 1; i++) {
48
+ // Sets pathSearch to every child in the path except for the last one.
49
+ pathSearch = (pathSearch[path[i]] as ObjectNode).children;
50
+ }
51
+ (pathSearch[path[path.length - 1]] as ObjectNode).children = nodes; // Sets the value pathSearch.children in the last value of the path to nodes. (Where the user is currently editing)
52
+ return [...basePathSearch];
53
+ });
54
+ }
55
+ }, [nodes]);
56
+
57
+ React.useEffect(() => {
58
+ let pathSearch = [...baseNodes];
59
+ for (let i = 0; i < path.length; i++) {
60
+ pathSearch = (pathSearch[path[i]] as ObjectNode).children;
61
+ }
62
+ setNodes(pathSearch);
63
+ }, [path]);
64
+
65
+ function updatePath(newPathNum?: number) {
66
+ editingState[1](false);
67
+ if (newPathNum === undefined) {
68
+ // If undefined, go up a layer in the path.
69
+ setPath((p) => p.filter((_, i) => i !== p.length - 1));
70
+ } else if (newPathNum < 0) {
71
+ // If negative, reset to base path.
72
+ setPath([]);
73
+ } else {
74
+ // If 0 or higher, push to the end of the path.
75
+ setPath((p) => [...p, newPathNum]);
76
+ }
77
+ }
78
+
79
+ /**
80
+ * ------------------------- AUTOSAVE -----------------------------
81
+ */
82
+
83
+ const timerRef = React.useRef<NodeJS.Timeout>();
84
+ const [countdown, setCountdown] = React.useState<CountdownType>(undefined);
85
+
86
+ function resetCountdown() {
87
+ if (timerRef.current) {
88
+ clearTimeout(timerRef.current);
89
+ }
90
+
91
+ setCountdown(countdownSeconds);
92
+ timerRef.current = setTimeout(() => {
93
+ Save();
94
+ }, countdownSeconds * 1000);
95
+ }
96
+
97
+ const AutosaveNodes: React.Dispatch<React.SetStateAction<FormNodeType[]>> = (
98
+ action: React.SetStateAction<FormNodeType[]>
99
+ ) => {
100
+ setNodes((prevNodes) => {
101
+ let currentState: FormNodeType[] = prevNodes;
102
+ if (typeof action === "function") {
103
+ currentState = action(currentState);
104
+ } else {
105
+ currentState = [...action];
106
+ }
107
+
108
+ resetCountdown();
109
+ return currentState;
110
+ });
111
+ };
112
+
113
+ const AutosaveForm: React.Dispatch<React.SetStateAction<FormType>> = (
114
+ action: React.SetStateAction<FormType>
115
+ ) => {
116
+ setForm((prevForm) => {
117
+ let currentState: FormType = prevForm;
118
+ if (typeof action === "function") {
119
+ currentState = action(currentState);
120
+ } else {
121
+ currentState = { ...prevForm, ...action };
122
+ }
123
+
124
+ resetCountdown();
125
+ return currentState;
126
+ });
127
+ };
128
+
129
+ React.useEffect(() => {
130
+ if (typeof countdown === "string" || countdown === undefined) return;
131
+ if (countdown > 0) {
132
+ const interval = setInterval(() => {
133
+ setCountdown((prevCount) => (prevCount as number) - 1);
134
+ }, 1000);
135
+ return () => clearInterval(interval);
136
+ }
137
+ }, [countdown]);
138
+
139
+ React.useEffect(() => {
140
+ // Adds a warning when user tries to close tab with unsaved changes
141
+ if (countdown !== "Saved." && countdown !== undefined) {
142
+ const handleBeforeUnload = (event: BeforeUnloadEvent) => {
143
+ const confirmationMessage =
144
+ "You have unsaved changes. Are you sure you want to leave?";
145
+ event.returnValue = confirmationMessage; // Gecko, Trident, Chrome 34+
146
+ return confirmationMessage; // Gecko, WebKit, Chrome <34
147
+ };
148
+
149
+ window.addEventListener("beforeunload", handleBeforeUnload);
150
+
151
+ return () => {
152
+ window.removeEventListener("beforeunload", handleBeforeUnload);
153
+ };
154
+ }
155
+ }, [countdown]);
156
+
157
+ // Runs after autosave countdown finishes.
158
+ function Save() {
159
+ setCountdown("Saving...");
160
+ setBaseNodes((bn) => {
161
+ // Gets the most up-to-date baseNodes through the setBaseNodes function because it updates in a weird way.
162
+ const Form = serializeBuilderToForm(form, bn);
163
+ saveCallback({ ...Form }).then((res) => {
164
+ console.log("PROMISE RES:", res);
165
+ if (res) {
166
+ setCountdown("Saved.");
167
+ setTimeout(() => {
168
+ setCountdown(undefined);
169
+ }, 1500); // Waits 1.5s before removing text
170
+ } else {
171
+ setCountdown("Save failed."); // Do not remove this text until another change has been made by the user.
172
+ }
173
+ });
174
+
175
+ return bn; // doesn't need to return a new object because it's not intended to update any state.
176
+ });
177
+ setTimeout(() => {}, Math.random() * 700 + 300); // Simulated API wait time
178
+ }
179
+
180
+ return (
181
+ <Box
182
+ sx={{
183
+ p: 2,
184
+ width: "100%",
185
+ height: "100%",
186
+ borderRadius: 2,
187
+ border: "1px solid #999",
188
+ display: "flex",
189
+ flexDirection: "column",
190
+ }}
191
+ >
192
+ <Box
193
+ id="fb-scrollable"
194
+ sx={{
195
+ display: "flex",
196
+ flexGrow: 1,
197
+ overflow: "auto",
198
+ borderTopLeftRadius: 0.5,
199
+ borderTopRightRadius: 0.5,
200
+ flexDirection: "column",
201
+ scrollBehavior: "smooth",
202
+ }}
203
+ >
204
+ <Box sx={{ display: "flex", flexDirection: "column" }}>
205
+ <DragDropContext
206
+ onDragEnd={(result) => {
207
+ AutosaveNodes((prevNodes) => {
208
+ if (!result.destination) return prevNodes;
209
+
210
+ const items = Array.from(prevNodes);
211
+ const [reorderedItem] = items.splice(result.source.index, 1);
212
+ items.splice(result.destination.index, 0, reorderedItem);
213
+
214
+ return items;
215
+ });
216
+ }}
217
+ >
218
+ <Droppable droppableId="nodes">
219
+ {(provided) => (
220
+ <Box
221
+ ref={provided.innerRef}
222
+ {...provided.droppableProps}
223
+ sx={{ display: "flex", flexDirection: "column", gap: 1 }}
224
+ >
225
+ {nodes.map((node, i) => (
226
+ <NodeParent
227
+ key={node.nodeId}
228
+ node={node}
229
+ nodesState={[nodes, AutosaveNodes]}
230
+ index={i}
231
+ editingState={editingState}
232
+ updatePath={() => {
233
+ updatePath(i);
234
+ }}
235
+ deleteNode={() => {
236
+ AutosaveNodes((n) => n.filter((_, j) => j !== i));
237
+ }}
238
+ />
239
+ ))}
240
+ {provided.placeholder}
241
+ </Box>
242
+ )}
243
+ </Droppable>
244
+ </DragDropContext>
245
+ </Box>
246
+ </Box>
247
+ <BottomDrawer
248
+ nodesState={[nodes, AutosaveNodes]}
249
+ formState={[form, AutosaveForm]}
250
+ countdown={countdown}
251
+ path={[path, updatePath]}
252
+ setEditing={editingState[1]}
253
+ />
254
+ </Box>
255
+ );
256
+ }
@@ -1,39 +1,39 @@
1
- import { Box, Modal as MuiModal, SxProps } from "@mui/material";
2
-
3
- const style: SxProps = {
4
- position: "absolute",
5
- top: "50%",
6
- left: "50%",
7
- transform: "translate(-50%, -50%)",
8
- width: 400,
9
- bgcolor: "white",
10
- borderRadius: 2,
11
- boxShadow: 24,
12
- p: 4,
13
- gap: 2,
14
- display: "flex",
15
- flexDirection: "column",
16
- };
17
-
18
- export type ModalProps = {
19
- open: boolean;
20
- onClose?: () => void;
21
- children: React.ReactNode;
22
- sx?: SxProps;
23
- innerBoxSx?: SxProps;
24
- };
25
-
26
- export default function Modal({
27
- open,
28
- onClose,
29
- children,
30
- sx,
31
- innerBoxSx,
32
- }: ModalProps) {
33
- const innerSx = { ...style, ...innerBoxSx };
34
- return (
35
- <MuiModal open={open} onClose={onClose} sx={sx}>
36
- <Box sx={innerSx}>{children}</Box>
37
- </MuiModal>
38
- );
39
- }
1
+ import { Box, Modal as MuiModal, SxProps } from "@mui/material";
2
+
3
+ const style: SxProps = {
4
+ position: "absolute",
5
+ top: "50%",
6
+ left: "50%",
7
+ transform: "translate(-50%, -50%)",
8
+ width: 400,
9
+ bgcolor: "white",
10
+ borderRadius: 2,
11
+ boxShadow: 24,
12
+ p: 4,
13
+ gap: 2,
14
+ display: "flex",
15
+ flexDirection: "column",
16
+ };
17
+
18
+ export type ModalProps = {
19
+ open: boolean;
20
+ onClose?: () => void;
21
+ children: React.ReactNode;
22
+ sx?: SxProps;
23
+ innerBoxSx?: SxProps;
24
+ };
25
+
26
+ export default function Modal({
27
+ open,
28
+ onClose,
29
+ children,
30
+ sx,
31
+ innerBoxSx,
32
+ }: ModalProps) {
33
+ const innerSx = { ...style, ...innerBoxSx };
34
+ return (
35
+ <MuiModal open={open} onClose={onClose} sx={sx}>
36
+ <Box sx={innerSx}>{children}</Box>
37
+ </MuiModal>
38
+ );
39
+ }