@springmicro/forms 0.1.3 → 0.2.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/.eslintrc.cjs +4 -0
- package/dist/index.js +4567 -4761
- package/dist/index.umd.cjs +38 -38
- package/package.json +9 -3
- package/src/builder/bottom-drawer.tsx +429 -0
- package/src/builder/form-builder.tsx +256 -0
- package/src/builder/modal.tsx +39 -0
- package/src/builder/nodes/node-base.tsx +94 -0
- package/src/builder/nodes/node-child-helpers.tsx +273 -0
- package/src/builder/nodes/node-parent.tsx +187 -0
- package/src/builder/nodes/node-types/array-node.tsx +134 -0
- package/src/builder/nodes/node-types/date-node.tsx +60 -0
- package/src/builder/nodes/node-types/file-node.tsx +67 -0
- package/src/builder/nodes/node-types/integer-node.tsx +60 -0
- package/src/builder/nodes/node-types/object-node.tsx +67 -0
- package/src/builder/nodes/node-types/text-node.tsx +66 -0
- package/src/types/form-builder.ts +135 -0
- package/src/types/utils.type.ts +1 -0
- package/src/utils/form-builder.ts +424 -0
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { EditingStateType } from "../../types/form-builder";
|
|
2
|
+
import { UseStateType } from "../../types/utils.type";
|
|
3
|
+
import { Box, SxProps } from "@mui/material";
|
|
4
|
+
import React from "react";
|
|
5
|
+
import { Draggable } from "react-beautiful-dnd";
|
|
6
|
+
|
|
7
|
+
export type NodeBaseProps = {
|
|
8
|
+
children?: React.ReactNode;
|
|
9
|
+
sx?: SxProps;
|
|
10
|
+
nodeId: string;
|
|
11
|
+
index: number;
|
|
12
|
+
editingState: UseStateType<EditingStateType>;
|
|
13
|
+
icon: React.ComponentType<any>;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default function NodeBase({
|
|
17
|
+
children,
|
|
18
|
+
sx,
|
|
19
|
+
nodeId,
|
|
20
|
+
index,
|
|
21
|
+
editingState,
|
|
22
|
+
icon: Icon,
|
|
23
|
+
}: NodeBaseProps) {
|
|
24
|
+
const [editingId, setEditing] = editingState;
|
|
25
|
+
const editingThisNode = editingId && editingId === nodeId;
|
|
26
|
+
|
|
27
|
+
const edit = () => {
|
|
28
|
+
if (editingThisNode) return;
|
|
29
|
+
setEditing(nodeId);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<Draggable draggableId={nodeId} index={index}>
|
|
34
|
+
{(provided) => (
|
|
35
|
+
<div ref={provided.innerRef} {...provided.draggableProps}>
|
|
36
|
+
<Box
|
|
37
|
+
id={nodeId}
|
|
38
|
+
sx={{
|
|
39
|
+
display: "flex",
|
|
40
|
+
flexDirection: "row",
|
|
41
|
+
width: "100%",
|
|
42
|
+
backgroundColor: editingThisNode ? "#ececec" : "#e3e3e3",
|
|
43
|
+
border: editingThisNode
|
|
44
|
+
? "2px solid #c0c0c0"
|
|
45
|
+
: "2px solid #e3e3e3",
|
|
46
|
+
borderRadius: 1,
|
|
47
|
+
transition: "all 0.2s",
|
|
48
|
+
cursor: editingThisNode ? "default" : "pointer",
|
|
49
|
+
...(sx ?? {}), // Can be used for custom styles or overrides.
|
|
50
|
+
}}
|
|
51
|
+
>
|
|
52
|
+
<Handle {...provided.dragHandleProps} />
|
|
53
|
+
<Box
|
|
54
|
+
sx={{ flexGrow: 1, pr: 2, pb: 2, display: "flex" }}
|
|
55
|
+
onClick={edit}
|
|
56
|
+
>
|
|
57
|
+
<Box sx={{ width: 30, height: "100%", pt: 2 }} onClick={edit}>
|
|
58
|
+
<Icon sx={{ color: "#888" }} />
|
|
59
|
+
</Box>
|
|
60
|
+
{children}
|
|
61
|
+
</Box>
|
|
62
|
+
</Box>
|
|
63
|
+
</div>
|
|
64
|
+
)}
|
|
65
|
+
</Draggable>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// The draggable part of the icon.
|
|
70
|
+
function Handle(props: any) {
|
|
71
|
+
return (
|
|
72
|
+
<Box
|
|
73
|
+
sx={{ p: 2, gap: 0.33, display: "flex", flexDirection: "row" }}
|
|
74
|
+
{...props}
|
|
75
|
+
>
|
|
76
|
+
<Box
|
|
77
|
+
sx={{
|
|
78
|
+
backgroundColor: "#888",
|
|
79
|
+
borderRadius: 2,
|
|
80
|
+
width: 3,
|
|
81
|
+
height: "100%",
|
|
82
|
+
}}
|
|
83
|
+
/>
|
|
84
|
+
<Box
|
|
85
|
+
sx={{
|
|
86
|
+
backgroundColor: "#888",
|
|
87
|
+
borderRadius: 2,
|
|
88
|
+
width: 3,
|
|
89
|
+
height: "100%",
|
|
90
|
+
}}
|
|
91
|
+
/>
|
|
92
|
+
</Box>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { FieldArrayType, FormNodeType } from "../../types/form-builder";
|
|
2
|
+
import {
|
|
3
|
+
Checkbox,
|
|
4
|
+
FormControlLabel,
|
|
5
|
+
IconButton,
|
|
6
|
+
TextField,
|
|
7
|
+
Tooltip,
|
|
8
|
+
} from "@mui/material";
|
|
9
|
+
import { Box, SxProps } from "@mui/material";
|
|
10
|
+
import CloseIcon from "@mui/icons-material/Close";
|
|
11
|
+
import React from "react";
|
|
12
|
+
import { formatTitle } from "../../utils/form-builder";
|
|
13
|
+
import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* ---------------------- NODE CHILD BASE ----------------------
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export type NodeChildBaseProps = {
|
|
20
|
+
children?: React.ReactNode;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function NodeChildBase({ children }: NodeChildBaseProps) {
|
|
24
|
+
return (
|
|
25
|
+
<Box sx={{ display: "flex", flexDirection: "column", width: "100%" }}>
|
|
26
|
+
{children}
|
|
27
|
+
</Box>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* ---------------------- NODE ACCORDIAN ----------------------
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
export type NodeAccordianProps = {
|
|
36
|
+
open?: boolean;
|
|
37
|
+
children?: React.ReactNode;
|
|
38
|
+
close: () => void;
|
|
39
|
+
deleteNode: () => void;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export function NodeAccordian({
|
|
43
|
+
open,
|
|
44
|
+
children,
|
|
45
|
+
close,
|
|
46
|
+
deleteNode,
|
|
47
|
+
}: NodeAccordianProps) {
|
|
48
|
+
return (
|
|
49
|
+
<Box
|
|
50
|
+
className={open ? "open" : ""}
|
|
51
|
+
sx={{
|
|
52
|
+
overflow: "hidden",
|
|
53
|
+
transition: "max-height .45s cubic-bezier(0, 1, 0, 1) -.1s",
|
|
54
|
+
maxHeight: 0,
|
|
55
|
+
"&.open": {
|
|
56
|
+
maxHeight: 9999,
|
|
57
|
+
transitionTimingFunction: "cubic-bezier(0.5, 0, 1, 0)",
|
|
58
|
+
transitionDelay: "0s",
|
|
59
|
+
},
|
|
60
|
+
width: "100%",
|
|
61
|
+
}}
|
|
62
|
+
>
|
|
63
|
+
<Box
|
|
64
|
+
sx={{
|
|
65
|
+
pt: 2,
|
|
66
|
+
display: "flex",
|
|
67
|
+
flexDirection: "column",
|
|
68
|
+
}}
|
|
69
|
+
>
|
|
70
|
+
{children}
|
|
71
|
+
</Box>
|
|
72
|
+
<Box sx={{ pt: 1 }}>
|
|
73
|
+
<BottomSection close={close} deleteNode={deleteNode} />
|
|
74
|
+
</Box>
|
|
75
|
+
</Box>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* ---------------------- NODE TITLE ----------------------
|
|
81
|
+
*/
|
|
82
|
+
|
|
83
|
+
export function NodeTitle({
|
|
84
|
+
children,
|
|
85
|
+
isPropname,
|
|
86
|
+
isEditing,
|
|
87
|
+
onClick,
|
|
88
|
+
}: {
|
|
89
|
+
children: React.ReactNode;
|
|
90
|
+
isPropname?: boolean;
|
|
91
|
+
isEditing?: boolean;
|
|
92
|
+
onClick: () => void;
|
|
93
|
+
}) {
|
|
94
|
+
return (
|
|
95
|
+
<Box
|
|
96
|
+
sx={{
|
|
97
|
+
width: "100%",
|
|
98
|
+
pt: 2,
|
|
99
|
+
cursor: "pointer",
|
|
100
|
+
transition: "background-color 0.5s",
|
|
101
|
+
borderBottomLeftRadius: 10,
|
|
102
|
+
borderBottomRightRadius: 10,
|
|
103
|
+
":hover": {
|
|
104
|
+
backgroundColor: isEditing ? "#dddddd" : "transparent",
|
|
105
|
+
},
|
|
106
|
+
}}
|
|
107
|
+
onClick={isEditing ? onClick : () => {}}
|
|
108
|
+
>
|
|
109
|
+
<Box
|
|
110
|
+
sx={{
|
|
111
|
+
fontWeight: 400,
|
|
112
|
+
fontSize: 18,
|
|
113
|
+
backgroundColor: !isPropname ? "transparent" : "#d5d5d5",
|
|
114
|
+
borderRadius: 2,
|
|
115
|
+
px: 1,
|
|
116
|
+
display: "inline-block",
|
|
117
|
+
}}
|
|
118
|
+
>
|
|
119
|
+
{children}
|
|
120
|
+
</Box>
|
|
121
|
+
</Box>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* ---------------------- STYLED TEXT FIELD ----------------------
|
|
127
|
+
*/
|
|
128
|
+
|
|
129
|
+
export const textfieldStyle = (sx?: SxProps) => ({
|
|
130
|
+
minWidth: 250,
|
|
131
|
+
flexGrow: 1,
|
|
132
|
+
...{
|
|
133
|
+
"& .MuiOutlinedInput-root": {
|
|
134
|
+
"& fieldset": {
|
|
135
|
+
borderColor: "#C0C3C7",
|
|
136
|
+
},
|
|
137
|
+
"&:hover fieldset": {
|
|
138
|
+
borderColor: "#A2AAB2",
|
|
139
|
+
},
|
|
140
|
+
"&.Mui-focused fieldset": {
|
|
141
|
+
borderColor: "#6F7E8C",
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
...(sx ?? {}),
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
export function StyledTextField(props?: any, children?: React.ReactNode) {
|
|
149
|
+
return (
|
|
150
|
+
<TextField {...props} sx={textfieldStyle(props.sx)}>
|
|
151
|
+
{children}
|
|
152
|
+
</TextField>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* ---------------------- BOTTOM SECTION ----------------------
|
|
158
|
+
*/
|
|
159
|
+
|
|
160
|
+
export function BottomSection({
|
|
161
|
+
close,
|
|
162
|
+
deleteNode,
|
|
163
|
+
}: {
|
|
164
|
+
close: () => void;
|
|
165
|
+
deleteNode: () => void;
|
|
166
|
+
}) {
|
|
167
|
+
return (
|
|
168
|
+
<Box
|
|
169
|
+
sx={{
|
|
170
|
+
width: "100%",
|
|
171
|
+
display: "flex",
|
|
172
|
+
justifyContent: "space-between",
|
|
173
|
+
backgroundColor: "#00000009",
|
|
174
|
+
borderRadius: 1,
|
|
175
|
+
py: 0.5,
|
|
176
|
+
}}
|
|
177
|
+
>
|
|
178
|
+
<Box>
|
|
179
|
+
<IconButton onClick={deleteNode}>
|
|
180
|
+
<DeleteOutlineIcon sx={{ color: "red" }} />
|
|
181
|
+
</IconButton>
|
|
182
|
+
</Box>
|
|
183
|
+
<Box>
|
|
184
|
+
<IconButton onClick={close}>
|
|
185
|
+
<CloseIcon />
|
|
186
|
+
</IconButton>
|
|
187
|
+
</Box>
|
|
188
|
+
</Box>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* ---------------------- FIELD INPUTS ----------------------
|
|
194
|
+
*/
|
|
195
|
+
|
|
196
|
+
export function NodeField({
|
|
197
|
+
field,
|
|
198
|
+
node,
|
|
199
|
+
update,
|
|
200
|
+
}: {
|
|
201
|
+
field: FieldArrayType<any>[0];
|
|
202
|
+
node: FormNodeType;
|
|
203
|
+
update: (
|
|
204
|
+
key: string,
|
|
205
|
+
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
|
206
|
+
type?: string
|
|
207
|
+
) => void;
|
|
208
|
+
}) {
|
|
209
|
+
const prop = field.prop as keyof FormNodeType;
|
|
210
|
+
const textfield = (
|
|
211
|
+
<StyledTextField
|
|
212
|
+
label={field.title ?? formatTitle(prop)}
|
|
213
|
+
value={node[prop] as string}
|
|
214
|
+
onChange={(e: any) => update(prop, e)}
|
|
215
|
+
{...field.props}
|
|
216
|
+
/>
|
|
217
|
+
);
|
|
218
|
+
if (!field.tooltip) return textfield;
|
|
219
|
+
return (
|
|
220
|
+
<Tooltip title={field.tooltip} arrow>
|
|
221
|
+
{textfield}
|
|
222
|
+
</Tooltip>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function NodeCheckbox({
|
|
227
|
+
field,
|
|
228
|
+
node,
|
|
229
|
+
update,
|
|
230
|
+
}: {
|
|
231
|
+
field: FieldArrayType<any>[0];
|
|
232
|
+
node: FormNodeType;
|
|
233
|
+
update: (
|
|
234
|
+
key: string,
|
|
235
|
+
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
|
236
|
+
type?: string
|
|
237
|
+
) => void;
|
|
238
|
+
}) {
|
|
239
|
+
const prop = field.prop as keyof FormNodeType;
|
|
240
|
+
const checkbox = (
|
|
241
|
+
<FormControlLabel
|
|
242
|
+
control={
|
|
243
|
+
<Checkbox
|
|
244
|
+
checked={node[prop] as boolean}
|
|
245
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
246
|
+
update(prop, e, "check");
|
|
247
|
+
}}
|
|
248
|
+
/>
|
|
249
|
+
}
|
|
250
|
+
label={field.title ?? formatTitle(prop)}
|
|
251
|
+
/>
|
|
252
|
+
);
|
|
253
|
+
if (!field.tooltip) return checkbox;
|
|
254
|
+
return (
|
|
255
|
+
<Tooltip title={field.tooltip} arrow>
|
|
256
|
+
{checkbox}
|
|
257
|
+
</Tooltip>
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* ---------------------- FIELD CONTAINERS ----------------------
|
|
263
|
+
*/
|
|
264
|
+
|
|
265
|
+
export function FieldContainer({ children }: { children?: React.ReactNode }) {
|
|
266
|
+
return (
|
|
267
|
+
<Box
|
|
268
|
+
sx={{ display: "flex", flexDirection: "row", gap: 2, flexWrap: "wrap" }}
|
|
269
|
+
>
|
|
270
|
+
{children}
|
|
271
|
+
</Box>
|
|
272
|
+
);
|
|
273
|
+
}
|