@springmicro/forms 0.7.0 → 0.7.2
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 +22 -22
- package/README.md +11 -11
- package/dist/index.d.ts +0 -0
- package/dist/index.js +4146 -4143
- package/dist/index.umd.cjs +37 -37
- package/package.json +3 -3
- package/src/builder/bottom-drawer.tsx +429 -429
- package/src/builder/form-builder.tsx +256 -256
- package/src/builder/modal.tsx +39 -39
- package/src/builder/nodes/node-base.tsx +94 -94
- package/src/builder/nodes/node-child-helpers.tsx +273 -273
- package/src/builder/nodes/node-parent.tsx +187 -187
- package/src/builder/nodes/node-types/array-node.tsx +134 -134
- package/src/builder/nodes/node-types/date-node.tsx +60 -60
- package/src/builder/nodes/node-types/file-node.tsx +67 -67
- package/src/builder/nodes/node-types/integer-node.tsx +60 -60
- package/src/builder/nodes/node-types/object-node.tsx +67 -67
- package/src/builder/nodes/node-types/text-node.tsx +66 -66
- package/src/fields/ArrayField.tsx +875 -875
- package/src/fields/BooleanField.tsx +110 -110
- package/src/fields/MultiSchemaField.tsx +236 -236
- package/src/fields/NullField.tsx +22 -22
- package/src/fields/NumberField.tsx +87 -87
- package/src/fields/ObjectField.tsx +338 -338
- package/src/fields/SchemaField.tsx +402 -402
- package/src/fields/StringField.tsx +67 -67
- package/src/fields/index.ts +24 -24
- package/src/index.tsx +26 -26
- package/src/interfaces/MessagesProps.interface.ts +5 -5
- package/src/interfaces/Option.interface.ts +4 -4
- package/src/styles/select.styles.ts +28 -28
- package/src/templates/ArrayFieldDescriptionTemplate.tsx +42 -42
- package/src/templates/ArrayFieldItemTemplate.tsx +78 -78
- package/src/templates/ArrayFieldTemplate.tsx +90 -90
- package/src/templates/ArrayFieldTitleTemplate.tsx +44 -44
- package/src/templates/BaseInputTemplate.tsx +94 -94
- package/src/templates/ButtonTemplates/AddButton.tsx +29 -29
- package/src/templates/ButtonTemplates/IconButton.tsx +49 -49
- package/src/templates/ButtonTemplates/SubmitButton.tsx +29 -29
- package/src/templates/ButtonTemplates/index.ts +16 -16
- package/src/templates/DescriptionField.tsx +29 -29
- package/src/templates/ErrorList.tsx +25 -25
- package/src/templates/FieldTemplate/FieldTemplate.tsx +39 -39
- package/src/templates/FieldTemplate/Label.tsx +29 -29
- package/src/templates/FieldTemplate/WrapIfAdditional.tsx +85 -85
- package/src/templates/FieldTemplate/index.ts +3 -3
- package/src/templates/ObjectFieldTemplate.tsx +79 -79
- package/src/templates/TitleField.tsx +20 -20
- package/src/templates/UnsupportedField.tsx +29 -29
- package/src/templates/index.ts +32 -32
- package/src/types/Message.type.ts +6 -6
- package/src/types/RawMessage.type.ts +15 -15
- package/src/types/form-builder.ts +135 -135
- package/src/types/utils.type.ts +1 -1
- package/src/utils/form-builder.ts +424 -424
- package/src/utils/processSelectValue.ts +50 -50
- package/src/widgets/AltDateTimeWidget.tsx +17 -17
- package/src/widgets/AltDateWidget.tsx +216 -216
- package/src/widgets/CheckboxWidget.tsx +80 -80
- package/src/widgets/CheckboxesWidget.tsx +74 -74
- package/src/widgets/ColorWidget.tsx +26 -26
- package/src/widgets/DateTimeWidget.tsx +28 -28
- package/src/widgets/DateWidget.tsx +36 -36
- package/src/widgets/EmailWidget.tsx +19 -19
- package/src/widgets/FileWidget.tsx +144 -144
- package/src/widgets/HiddenWidget.tsx +22 -22
- package/src/widgets/PasswordWidget.tsx +20 -20
- package/src/widgets/RadioWidget.tsx +87 -87
- package/src/widgets/RangeWidget.tsx +24 -24
- package/src/widgets/SelectWidget.tsx +99 -99
- package/src/widgets/TextWidget.tsx +19 -19
- package/src/widgets/TextareaWidget.tsx +64 -64
- package/src/widgets/URLWidget.tsx +19 -19
- package/src/widgets/UpDownWidget.tsx +20 -20
- package/src/widgets/index.ts +43 -43
- package/tsconfig.json +24 -24
- package/tsconfig.node.json +10 -10
- package/vite.config.ts +25 -25
|
@@ -1,424 +1,424 @@
|
|
|
1
|
-
import {
|
|
2
|
-
FormNodeKeys,
|
|
3
|
-
FormNodeType,
|
|
4
|
-
FormType,
|
|
5
|
-
FileNode,
|
|
6
|
-
IntegerNode,
|
|
7
|
-
TextNode,
|
|
8
|
-
ArrayNode,
|
|
9
|
-
DateNode,
|
|
10
|
-
ObjectNode,
|
|
11
|
-
} from "../types/form-builder";
|
|
12
|
-
import { UseStateType } from "../types/utils.type";
|
|
13
|
-
import { RJSFSchema, UiSchema } from "@rjsf/utils";
|
|
14
|
-
import { JSONSchema7 } from "json-schema";
|
|
15
|
-
import { v4 as uuidv4 } from "uuid";
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* ---------------------- GENERATE NODE DATA ----------------------
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
const baseDefaultNode = { required: true, nodeId: "" };
|
|
22
|
-
|
|
23
|
-
export const defaultNodes: {
|
|
24
|
-
[key in FormNodeKeys]: FormNodeType extends { type: infer T }
|
|
25
|
-
? T extends key
|
|
26
|
-
? FormNodeType
|
|
27
|
-
: never
|
|
28
|
-
: never;
|
|
29
|
-
} = {
|
|
30
|
-
string: {
|
|
31
|
-
...baseDefaultNode,
|
|
32
|
-
type: "string",
|
|
33
|
-
title: "New String Field",
|
|
34
|
-
},
|
|
35
|
-
file: {
|
|
36
|
-
...baseDefaultNode,
|
|
37
|
-
type: "file",
|
|
38
|
-
title: "New File Field",
|
|
39
|
-
},
|
|
40
|
-
integer: {
|
|
41
|
-
...baseDefaultNode,
|
|
42
|
-
type: "integer",
|
|
43
|
-
title: "New Integer Field",
|
|
44
|
-
},
|
|
45
|
-
date: {
|
|
46
|
-
...baseDefaultNode,
|
|
47
|
-
type: "date",
|
|
48
|
-
title: "New Date Field",
|
|
49
|
-
},
|
|
50
|
-
array: {
|
|
51
|
-
...baseDefaultNode,
|
|
52
|
-
type: "array",
|
|
53
|
-
title: "New Array Field",
|
|
54
|
-
child: { type: "string" },
|
|
55
|
-
},
|
|
56
|
-
object: {
|
|
57
|
-
...baseDefaultNode,
|
|
58
|
-
type: "object",
|
|
59
|
-
title: "New Object",
|
|
60
|
-
children: [],
|
|
61
|
-
},
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
export function generateNodeData(
|
|
65
|
-
type: FormNodeKeys,
|
|
66
|
-
children?: FormNodeType[]
|
|
67
|
-
) {
|
|
68
|
-
return {
|
|
69
|
-
...defaultNodes[type],
|
|
70
|
-
nodeId: uuidv4(),
|
|
71
|
-
children: type !== "object" ? undefined : children ?? [],
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* ---------------------- FORM JSON -> BUILDER JSON ----------------------
|
|
77
|
-
*/
|
|
78
|
-
|
|
79
|
-
export function serializeFormToBuilder(
|
|
80
|
-
rjsfForm: RJSFSchema,
|
|
81
|
-
rjsfUiSchema: UiSchema = {}
|
|
82
|
-
): {
|
|
83
|
-
form: FormType;
|
|
84
|
-
nodes: FormNodeType[];
|
|
85
|
-
} {
|
|
86
|
-
if (rjsfForm === undefined) return { form: {}, nodes: [] };
|
|
87
|
-
function generate(form: RJSFSchema, ui: UiSchema) {
|
|
88
|
-
const nodes: (FormNodeType | undefined)[] = Object.keys(
|
|
89
|
-
form.properties!
|
|
90
|
-
).map((key) => {
|
|
91
|
-
// The two parts of the current working node.
|
|
92
|
-
const formNode = form.properties![key] as JSONSchema7;
|
|
93
|
-
const uiNode = ui[key] ?? {};
|
|
94
|
-
|
|
95
|
-
const type = formNode.type;
|
|
96
|
-
const required = form.required?.includes(key);
|
|
97
|
-
|
|
98
|
-
const baseNode = {
|
|
99
|
-
title: formNode.title!,
|
|
100
|
-
nodeId: uuidv4(),
|
|
101
|
-
description: uiNode["ui:description"],
|
|
102
|
-
required,
|
|
103
|
-
propertyName: key,
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
if (type === "string") {
|
|
107
|
-
if (formNode.format === "data-url") {
|
|
108
|
-
// ==================== FILE ====================
|
|
109
|
-
const node: FileNode = {
|
|
110
|
-
type: "file",
|
|
111
|
-
...baseNode,
|
|
112
|
-
};
|
|
113
|
-
if (uiNode["ui:options"] && uiNode["ui:options"].accept)
|
|
114
|
-
node.filetype = uiNode["ui:options"].accept;
|
|
115
|
-
return node;
|
|
116
|
-
}
|
|
117
|
-
if (formNode.format === "date-time") {
|
|
118
|
-
// ==================== DATE ====================
|
|
119
|
-
const node: DateNode = {
|
|
120
|
-
type: "date",
|
|
121
|
-
...baseNode,
|
|
122
|
-
};
|
|
123
|
-
return node;
|
|
124
|
-
}
|
|
125
|
-
// ==================== TEXT ====================
|
|
126
|
-
const node: TextNode = {
|
|
127
|
-
type: "string",
|
|
128
|
-
...baseNode,
|
|
129
|
-
};
|
|
130
|
-
if (formNode.format) node.format = formNode.format;
|
|
131
|
-
if (formNode.minLength) node.minLength = formNode.minLength;
|
|
132
|
-
if (formNode.maxLength) node.maxLength = formNode.maxLength;
|
|
133
|
-
if (formNode.default) node.default = formNode.default as string;
|
|
134
|
-
return node;
|
|
135
|
-
}
|
|
136
|
-
if (type === "integer") {
|
|
137
|
-
// ==================== INTEGER ====================
|
|
138
|
-
const node: IntegerNode = {
|
|
139
|
-
type: "integer",
|
|
140
|
-
...baseNode,
|
|
141
|
-
};
|
|
142
|
-
return node;
|
|
143
|
-
}
|
|
144
|
-
if (type === "array") {
|
|
145
|
-
const items = formNode.items as JSONSchema7;
|
|
146
|
-
if (items.type === "string" && items.format === "data-url") {
|
|
147
|
-
// ==================== MULTIPLE FILES ====================
|
|
148
|
-
const node: FileNode = {
|
|
149
|
-
type: "file",
|
|
150
|
-
multiple: true,
|
|
151
|
-
...baseNode,
|
|
152
|
-
};
|
|
153
|
-
if (uiNode["ui:options"] && uiNode["ui:options"].accept)
|
|
154
|
-
node.filetype = uiNode["ui:options"].accept;
|
|
155
|
-
return node;
|
|
156
|
-
}
|
|
157
|
-
if (items.type === "string" && items.format === "date-time") {
|
|
158
|
-
// ==================== DATE ARRAY ====================
|
|
159
|
-
const node: ArrayNode = {
|
|
160
|
-
type: "array",
|
|
161
|
-
...baseNode,
|
|
162
|
-
child: { type: "date" },
|
|
163
|
-
};
|
|
164
|
-
return node;
|
|
165
|
-
}
|
|
166
|
-
if (items.type === "string" || items.type === "integer") {
|
|
167
|
-
// ==================== ARRAY ====================
|
|
168
|
-
const node: ArrayNode = {
|
|
169
|
-
type: "array",
|
|
170
|
-
...baseNode,
|
|
171
|
-
child: { type: items.type },
|
|
172
|
-
};
|
|
173
|
-
return node;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
if (type === "object") {
|
|
177
|
-
// ==================== OBJECT ====================
|
|
178
|
-
const node: ObjectNode = {
|
|
179
|
-
type: "object",
|
|
180
|
-
children: generate(formNode, uiNode).nodes, // Reruns through object as if it was base layer. '.form' is not needed because title is assigned automatically for anything nested inside an object.
|
|
181
|
-
...baseNode,
|
|
182
|
-
};
|
|
183
|
-
return node;
|
|
184
|
-
}
|
|
185
|
-
});
|
|
186
|
-
return {
|
|
187
|
-
form: { title: form.title, description: form.description },
|
|
188
|
-
nodes: nodes.filter((n) => n !== undefined) as FormNodeType[],
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
return generate(rjsfForm, rjsfUiSchema);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* ---------------------- BUILDER JSON -> FORM JSON ----------------------
|
|
197
|
-
*/
|
|
198
|
-
|
|
199
|
-
export function serializeBuilderToForm(
|
|
200
|
-
formConfig: FormType,
|
|
201
|
-
nodes: FormNodeType[]
|
|
202
|
-
): { form: RJSFSchema; ui: UiSchema } {
|
|
203
|
-
function generate(_n: FormNodeType[]) {
|
|
204
|
-
const ui: UiSchema = {};
|
|
205
|
-
|
|
206
|
-
const formObjs: {
|
|
207
|
-
required: string[];
|
|
208
|
-
properties: Record<string, JSONSchema7>;
|
|
209
|
-
} = {
|
|
210
|
-
required: [],
|
|
211
|
-
properties: {},
|
|
212
|
-
};
|
|
213
|
-
|
|
214
|
-
_n.forEach((node) => {
|
|
215
|
-
let uniqueNum = 0;
|
|
216
|
-
const basePropName = node.propertyName
|
|
217
|
-
? node.propertyName
|
|
218
|
-
: formatPropName(node.title!); // Can't use ?? because node.propertyName can be ""
|
|
219
|
-
let propName = basePropName;
|
|
220
|
-
while (formObjs.properties[propName]) {
|
|
221
|
-
uniqueNum++;
|
|
222
|
-
propName = `${basePropName}-${uniqueNum}`;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
let propUi: any = node.description
|
|
226
|
-
? {
|
|
227
|
-
"ui:description": node.description,
|
|
228
|
-
"ui:enableMarkdownInDescription": true,
|
|
229
|
-
}
|
|
230
|
-
: {};
|
|
231
|
-
|
|
232
|
-
const props: JSONSchema7 = {};
|
|
233
|
-
switch (node.type) {
|
|
234
|
-
case "string": {
|
|
235
|
-
// ==================== TEXT ====================
|
|
236
|
-
props.type = "string";
|
|
237
|
-
if (node.default) props.default = node.default;
|
|
238
|
-
if (node.format) props.format = node.format;
|
|
239
|
-
if (node.minLength)
|
|
240
|
-
props.minLength = Number.parseInt(`${node.minLength}`);
|
|
241
|
-
if (node.maxLength)
|
|
242
|
-
props.maxLength = Number.parseInt(`${node.maxLength}`);
|
|
243
|
-
break;
|
|
244
|
-
}
|
|
245
|
-
case "file": {
|
|
246
|
-
// ==================== FILE ====================
|
|
247
|
-
if (node.filetype) {
|
|
248
|
-
let filetype = node.filetype.trim().toLowerCase();
|
|
249
|
-
if (filetype.charAt(0) !== ".") filetype = "." + filetype;
|
|
250
|
-
propUi["ui:options"] = { accept: filetype };
|
|
251
|
-
}
|
|
252
|
-
if (node.multiple) {
|
|
253
|
-
// ==================== MULTIPLE FILES ====================
|
|
254
|
-
props.type = "array";
|
|
255
|
-
props.items = {
|
|
256
|
-
type: "string",
|
|
257
|
-
format: "data-url",
|
|
258
|
-
};
|
|
259
|
-
break;
|
|
260
|
-
}
|
|
261
|
-
// ==================== FILE CONT. ====================
|
|
262
|
-
props.type = "string";
|
|
263
|
-
props.format = "data-url";
|
|
264
|
-
break;
|
|
265
|
-
}
|
|
266
|
-
case "integer": {
|
|
267
|
-
// ==================== INTEGER ====================
|
|
268
|
-
props.type = "integer";
|
|
269
|
-
break;
|
|
270
|
-
}
|
|
271
|
-
case "object": {
|
|
272
|
-
// ==================== OBJECT ====================
|
|
273
|
-
props.type = "object";
|
|
274
|
-
const formData = generate(node.children);
|
|
275
|
-
props.properties = formData.form.properties;
|
|
276
|
-
props.required = formData.form.required;
|
|
277
|
-
propUi = { ...propUi, ...formData.ui };
|
|
278
|
-
break;
|
|
279
|
-
}
|
|
280
|
-
case "array": {
|
|
281
|
-
// ==================== ARRAY ====================
|
|
282
|
-
props.type = "array";
|
|
283
|
-
if (node.child.type === "date") {
|
|
284
|
-
// ==================== DATE ARRAY ====================
|
|
285
|
-
props.items = {
|
|
286
|
-
type: "string",
|
|
287
|
-
format: "date-time",
|
|
288
|
-
};
|
|
289
|
-
break;
|
|
290
|
-
}
|
|
291
|
-
if (node.child.type === "string") {
|
|
292
|
-
// ==================== TEXT ARRAY ====================
|
|
293
|
-
props.items = {
|
|
294
|
-
type: "string",
|
|
295
|
-
};
|
|
296
|
-
if (node.child.format) props.items.format = node.child.format;
|
|
297
|
-
if (node.child.default) props.items.default = node.child.default;
|
|
298
|
-
if (node.child.minLength)
|
|
299
|
-
props.items.minLength = Number.parseInt(
|
|
300
|
-
`${node.child.minLength}`
|
|
301
|
-
);
|
|
302
|
-
if (node.child.maxLength)
|
|
303
|
-
props.items.maxLength = Number.parseInt(
|
|
304
|
-
`${node.child.maxLength}`
|
|
305
|
-
);
|
|
306
|
-
break;
|
|
307
|
-
}
|
|
308
|
-
if (node.child.type === "integer") {
|
|
309
|
-
// ==================== INTEGER ARRAY ====================
|
|
310
|
-
props.items = {
|
|
311
|
-
type: "integer",
|
|
312
|
-
};
|
|
313
|
-
break;
|
|
314
|
-
}
|
|
315
|
-
if (node.child.type === "array") {
|
|
316
|
-
// ==================== NESTED ARRAY ====================
|
|
317
|
-
// props.items = {
|
|
318
|
-
// type: "array",
|
|
319
|
-
// };
|
|
320
|
-
|
|
321
|
-
// ? Potentially something that can be done in the future, may need to write a generateArray function for this.
|
|
322
|
-
|
|
323
|
-
break;
|
|
324
|
-
}
|
|
325
|
-
break;
|
|
326
|
-
}
|
|
327
|
-
case "date": {
|
|
328
|
-
// ==================== DATE ====================
|
|
329
|
-
props.type = "string";
|
|
330
|
-
props.format = "date-time";
|
|
331
|
-
break;
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
if (Object.keys(props).length > 0) {
|
|
336
|
-
if (node.title) props.title = node.title;
|
|
337
|
-
if (node.required) formObjs.required.push(propName);
|
|
338
|
-
formObjs.properties[propName] = props;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
if (Object.keys(propUi).length !== 0) ui[propName] = propUi;
|
|
342
|
-
});
|
|
343
|
-
return { form: formObjs, ui: ui };
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
const formData = generate(nodes);
|
|
347
|
-
|
|
348
|
-
const form: RJSFSchema = {
|
|
349
|
-
title: formConfig.title,
|
|
350
|
-
description: formConfig.description,
|
|
351
|
-
type: "object",
|
|
352
|
-
...formData.form,
|
|
353
|
-
};
|
|
354
|
-
return { form, ui: formData.ui };
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
/**
|
|
358
|
-
* ---------------------- BASE VALIDATION ----------------------
|
|
359
|
-
*/
|
|
360
|
-
|
|
361
|
-
export function baseValidation(key: string, value: string, node: FormNodeType) {
|
|
362
|
-
if (key === "title" && !value && !node.propertyName) return false;
|
|
363
|
-
if (key === "propertyName" && !value && !node.title) return false;
|
|
364
|
-
return true;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
/**
|
|
368
|
-
* ---------------------- GENERATE UPDATE NODE BY KEY LOCAL ----------------------
|
|
369
|
-
*/
|
|
370
|
-
|
|
371
|
-
export function generateUpdateNodeByKeyLocal(
|
|
372
|
-
isValid: (key: string, value: any) => boolean,
|
|
373
|
-
setNode: React.Dispatch<React.SetStateAction<any>>
|
|
374
|
-
) {
|
|
375
|
-
return (
|
|
376
|
-
key: string,
|
|
377
|
-
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
|
378
|
-
type?: string
|
|
379
|
-
) => {
|
|
380
|
-
updateNodeByKey(key, e, isValid, setNode, type);
|
|
381
|
-
};
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
/**
|
|
385
|
-
* ---------------------- UPDATE NODE BY KEY ----------------------
|
|
386
|
-
*/
|
|
387
|
-
|
|
388
|
-
export function updateNodeByKey(
|
|
389
|
-
key: string,
|
|
390
|
-
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
|
391
|
-
isValid: (key: string, value: any) => boolean,
|
|
392
|
-
setNode: UseStateType<any>[1],
|
|
393
|
-
type?: string
|
|
394
|
-
) {
|
|
395
|
-
if (type === "check") {
|
|
396
|
-
// @ts-ignore
|
|
397
|
-
const val = e.target.checked;
|
|
398
|
-
if (!isValid(key, val)) return;
|
|
399
|
-
setNode((n: any) => ({ ...n, [key]: val }));
|
|
400
|
-
return;
|
|
401
|
-
}
|
|
402
|
-
const val = e.target.value;
|
|
403
|
-
if (!isValid(key, val)) return;
|
|
404
|
-
setNode((n: any) => ({ ...n, [key]: val }));
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
/**
|
|
408
|
-
* ---------------------- FIELD TITLE GENERATOR ----------------------
|
|
409
|
-
*/
|
|
410
|
-
|
|
411
|
-
export const formatTitle = (field: string) =>
|
|
412
|
-
field.replace(/([A-Z])/g, " $1").replace(/^./, (str) => str.toUpperCase());
|
|
413
|
-
|
|
414
|
-
/**
|
|
415
|
-
* ---------------------- FORMAT PROP NAME ----------------------
|
|
416
|
-
*/
|
|
417
|
-
|
|
418
|
-
export const formatPropName = (propName: string) =>
|
|
419
|
-
propName
|
|
420
|
-
.replace(/[()]/g, "")
|
|
421
|
-
.replace(/([A-Z])/g, " $1")
|
|
422
|
-
.trim()
|
|
423
|
-
.replace(/ +|--+/g, "-")
|
|
424
|
-
.toLowerCase();
|
|
1
|
+
import {
|
|
2
|
+
FormNodeKeys,
|
|
3
|
+
FormNodeType,
|
|
4
|
+
FormType,
|
|
5
|
+
FileNode,
|
|
6
|
+
IntegerNode,
|
|
7
|
+
TextNode,
|
|
8
|
+
ArrayNode,
|
|
9
|
+
DateNode,
|
|
10
|
+
ObjectNode,
|
|
11
|
+
} from "../types/form-builder";
|
|
12
|
+
import { UseStateType } from "../types/utils.type";
|
|
13
|
+
import { RJSFSchema, UiSchema } from "@rjsf/utils";
|
|
14
|
+
import { JSONSchema7 } from "json-schema";
|
|
15
|
+
import { v4 as uuidv4 } from "uuid";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* ---------------------- GENERATE NODE DATA ----------------------
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const baseDefaultNode = { required: true, nodeId: "" };
|
|
22
|
+
|
|
23
|
+
export const defaultNodes: {
|
|
24
|
+
[key in FormNodeKeys]: FormNodeType extends { type: infer T }
|
|
25
|
+
? T extends key
|
|
26
|
+
? FormNodeType
|
|
27
|
+
: never
|
|
28
|
+
: never;
|
|
29
|
+
} = {
|
|
30
|
+
string: {
|
|
31
|
+
...baseDefaultNode,
|
|
32
|
+
type: "string",
|
|
33
|
+
title: "New String Field",
|
|
34
|
+
},
|
|
35
|
+
file: {
|
|
36
|
+
...baseDefaultNode,
|
|
37
|
+
type: "file",
|
|
38
|
+
title: "New File Field",
|
|
39
|
+
},
|
|
40
|
+
integer: {
|
|
41
|
+
...baseDefaultNode,
|
|
42
|
+
type: "integer",
|
|
43
|
+
title: "New Integer Field",
|
|
44
|
+
},
|
|
45
|
+
date: {
|
|
46
|
+
...baseDefaultNode,
|
|
47
|
+
type: "date",
|
|
48
|
+
title: "New Date Field",
|
|
49
|
+
},
|
|
50
|
+
array: {
|
|
51
|
+
...baseDefaultNode,
|
|
52
|
+
type: "array",
|
|
53
|
+
title: "New Array Field",
|
|
54
|
+
child: { type: "string" },
|
|
55
|
+
},
|
|
56
|
+
object: {
|
|
57
|
+
...baseDefaultNode,
|
|
58
|
+
type: "object",
|
|
59
|
+
title: "New Object",
|
|
60
|
+
children: [],
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export function generateNodeData(
|
|
65
|
+
type: FormNodeKeys,
|
|
66
|
+
children?: FormNodeType[]
|
|
67
|
+
) {
|
|
68
|
+
return {
|
|
69
|
+
...defaultNodes[type],
|
|
70
|
+
nodeId: uuidv4(),
|
|
71
|
+
children: type !== "object" ? undefined : children ?? [],
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* ---------------------- FORM JSON -> BUILDER JSON ----------------------
|
|
77
|
+
*/
|
|
78
|
+
|
|
79
|
+
export function serializeFormToBuilder(
|
|
80
|
+
rjsfForm: RJSFSchema,
|
|
81
|
+
rjsfUiSchema: UiSchema = {}
|
|
82
|
+
): {
|
|
83
|
+
form: FormType;
|
|
84
|
+
nodes: FormNodeType[];
|
|
85
|
+
} {
|
|
86
|
+
if (rjsfForm === undefined) return { form: {}, nodes: [] };
|
|
87
|
+
function generate(form: RJSFSchema, ui: UiSchema) {
|
|
88
|
+
const nodes: (FormNodeType | undefined)[] = Object.keys(
|
|
89
|
+
form.properties!
|
|
90
|
+
).map((key) => {
|
|
91
|
+
// The two parts of the current working node.
|
|
92
|
+
const formNode = form.properties![key] as JSONSchema7;
|
|
93
|
+
const uiNode = ui[key] ?? {};
|
|
94
|
+
|
|
95
|
+
const type = formNode.type;
|
|
96
|
+
const required = form.required?.includes(key);
|
|
97
|
+
|
|
98
|
+
const baseNode = {
|
|
99
|
+
title: formNode.title!,
|
|
100
|
+
nodeId: uuidv4(),
|
|
101
|
+
description: uiNode["ui:description"],
|
|
102
|
+
required,
|
|
103
|
+
propertyName: key,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
if (type === "string") {
|
|
107
|
+
if (formNode.format === "data-url") {
|
|
108
|
+
// ==================== FILE ====================
|
|
109
|
+
const node: FileNode = {
|
|
110
|
+
type: "file",
|
|
111
|
+
...baseNode,
|
|
112
|
+
};
|
|
113
|
+
if (uiNode["ui:options"] && uiNode["ui:options"].accept)
|
|
114
|
+
node.filetype = uiNode["ui:options"].accept;
|
|
115
|
+
return node;
|
|
116
|
+
}
|
|
117
|
+
if (formNode.format === "date-time") {
|
|
118
|
+
// ==================== DATE ====================
|
|
119
|
+
const node: DateNode = {
|
|
120
|
+
type: "date",
|
|
121
|
+
...baseNode,
|
|
122
|
+
};
|
|
123
|
+
return node;
|
|
124
|
+
}
|
|
125
|
+
// ==================== TEXT ====================
|
|
126
|
+
const node: TextNode = {
|
|
127
|
+
type: "string",
|
|
128
|
+
...baseNode,
|
|
129
|
+
};
|
|
130
|
+
if (formNode.format) node.format = formNode.format;
|
|
131
|
+
if (formNode.minLength) node.minLength = formNode.minLength;
|
|
132
|
+
if (formNode.maxLength) node.maxLength = formNode.maxLength;
|
|
133
|
+
if (formNode.default) node.default = formNode.default as string;
|
|
134
|
+
return node;
|
|
135
|
+
}
|
|
136
|
+
if (type === "integer") {
|
|
137
|
+
// ==================== INTEGER ====================
|
|
138
|
+
const node: IntegerNode = {
|
|
139
|
+
type: "integer",
|
|
140
|
+
...baseNode,
|
|
141
|
+
};
|
|
142
|
+
return node;
|
|
143
|
+
}
|
|
144
|
+
if (type === "array") {
|
|
145
|
+
const items = formNode.items as JSONSchema7;
|
|
146
|
+
if (items.type === "string" && items.format === "data-url") {
|
|
147
|
+
// ==================== MULTIPLE FILES ====================
|
|
148
|
+
const node: FileNode = {
|
|
149
|
+
type: "file",
|
|
150
|
+
multiple: true,
|
|
151
|
+
...baseNode,
|
|
152
|
+
};
|
|
153
|
+
if (uiNode["ui:options"] && uiNode["ui:options"].accept)
|
|
154
|
+
node.filetype = uiNode["ui:options"].accept;
|
|
155
|
+
return node;
|
|
156
|
+
}
|
|
157
|
+
if (items.type === "string" && items.format === "date-time") {
|
|
158
|
+
// ==================== DATE ARRAY ====================
|
|
159
|
+
const node: ArrayNode = {
|
|
160
|
+
type: "array",
|
|
161
|
+
...baseNode,
|
|
162
|
+
child: { type: "date" },
|
|
163
|
+
};
|
|
164
|
+
return node;
|
|
165
|
+
}
|
|
166
|
+
if (items.type === "string" || items.type === "integer") {
|
|
167
|
+
// ==================== ARRAY ====================
|
|
168
|
+
const node: ArrayNode = {
|
|
169
|
+
type: "array",
|
|
170
|
+
...baseNode,
|
|
171
|
+
child: { type: items.type },
|
|
172
|
+
};
|
|
173
|
+
return node;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (type === "object") {
|
|
177
|
+
// ==================== OBJECT ====================
|
|
178
|
+
const node: ObjectNode = {
|
|
179
|
+
type: "object",
|
|
180
|
+
children: generate(formNode, uiNode).nodes, // Reruns through object as if it was base layer. '.form' is not needed because title is assigned automatically for anything nested inside an object.
|
|
181
|
+
...baseNode,
|
|
182
|
+
};
|
|
183
|
+
return node;
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
return {
|
|
187
|
+
form: { title: form.title, description: form.description },
|
|
188
|
+
nodes: nodes.filter((n) => n !== undefined) as FormNodeType[],
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return generate(rjsfForm, rjsfUiSchema);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* ---------------------- BUILDER JSON -> FORM JSON ----------------------
|
|
197
|
+
*/
|
|
198
|
+
|
|
199
|
+
export function serializeBuilderToForm(
|
|
200
|
+
formConfig: FormType,
|
|
201
|
+
nodes: FormNodeType[]
|
|
202
|
+
): { form: RJSFSchema; ui: UiSchema } {
|
|
203
|
+
function generate(_n: FormNodeType[]) {
|
|
204
|
+
const ui: UiSchema = {};
|
|
205
|
+
|
|
206
|
+
const formObjs: {
|
|
207
|
+
required: string[];
|
|
208
|
+
properties: Record<string, JSONSchema7>;
|
|
209
|
+
} = {
|
|
210
|
+
required: [],
|
|
211
|
+
properties: {},
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
_n.forEach((node) => {
|
|
215
|
+
let uniqueNum = 0;
|
|
216
|
+
const basePropName = node.propertyName
|
|
217
|
+
? node.propertyName
|
|
218
|
+
: formatPropName(node.title!); // Can't use ?? because node.propertyName can be ""
|
|
219
|
+
let propName = basePropName;
|
|
220
|
+
while (formObjs.properties[propName]) {
|
|
221
|
+
uniqueNum++;
|
|
222
|
+
propName = `${basePropName}-${uniqueNum}`;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
let propUi: any = node.description
|
|
226
|
+
? {
|
|
227
|
+
"ui:description": node.description,
|
|
228
|
+
"ui:enableMarkdownInDescription": true,
|
|
229
|
+
}
|
|
230
|
+
: {};
|
|
231
|
+
|
|
232
|
+
const props: JSONSchema7 = {};
|
|
233
|
+
switch (node.type) {
|
|
234
|
+
case "string": {
|
|
235
|
+
// ==================== TEXT ====================
|
|
236
|
+
props.type = "string";
|
|
237
|
+
if (node.default) props.default = node.default;
|
|
238
|
+
if (node.format) props.format = node.format;
|
|
239
|
+
if (node.minLength)
|
|
240
|
+
props.minLength = Number.parseInt(`${node.minLength}`);
|
|
241
|
+
if (node.maxLength)
|
|
242
|
+
props.maxLength = Number.parseInt(`${node.maxLength}`);
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
case "file": {
|
|
246
|
+
// ==================== FILE ====================
|
|
247
|
+
if (node.filetype) {
|
|
248
|
+
let filetype = node.filetype.trim().toLowerCase();
|
|
249
|
+
if (filetype.charAt(0) !== ".") filetype = "." + filetype;
|
|
250
|
+
propUi["ui:options"] = { accept: filetype };
|
|
251
|
+
}
|
|
252
|
+
if (node.multiple) {
|
|
253
|
+
// ==================== MULTIPLE FILES ====================
|
|
254
|
+
props.type = "array";
|
|
255
|
+
props.items = {
|
|
256
|
+
type: "string",
|
|
257
|
+
format: "data-url",
|
|
258
|
+
};
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
// ==================== FILE CONT. ====================
|
|
262
|
+
props.type = "string";
|
|
263
|
+
props.format = "data-url";
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
case "integer": {
|
|
267
|
+
// ==================== INTEGER ====================
|
|
268
|
+
props.type = "integer";
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
case "object": {
|
|
272
|
+
// ==================== OBJECT ====================
|
|
273
|
+
props.type = "object";
|
|
274
|
+
const formData = generate(node.children);
|
|
275
|
+
props.properties = formData.form.properties;
|
|
276
|
+
props.required = formData.form.required;
|
|
277
|
+
propUi = { ...propUi, ...formData.ui };
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
case "array": {
|
|
281
|
+
// ==================== ARRAY ====================
|
|
282
|
+
props.type = "array";
|
|
283
|
+
if (node.child.type === "date") {
|
|
284
|
+
// ==================== DATE ARRAY ====================
|
|
285
|
+
props.items = {
|
|
286
|
+
type: "string",
|
|
287
|
+
format: "date-time",
|
|
288
|
+
};
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
if (node.child.type === "string") {
|
|
292
|
+
// ==================== TEXT ARRAY ====================
|
|
293
|
+
props.items = {
|
|
294
|
+
type: "string",
|
|
295
|
+
};
|
|
296
|
+
if (node.child.format) props.items.format = node.child.format;
|
|
297
|
+
if (node.child.default) props.items.default = node.child.default;
|
|
298
|
+
if (node.child.minLength)
|
|
299
|
+
props.items.minLength = Number.parseInt(
|
|
300
|
+
`${node.child.minLength}`
|
|
301
|
+
);
|
|
302
|
+
if (node.child.maxLength)
|
|
303
|
+
props.items.maxLength = Number.parseInt(
|
|
304
|
+
`${node.child.maxLength}`
|
|
305
|
+
);
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
if (node.child.type === "integer") {
|
|
309
|
+
// ==================== INTEGER ARRAY ====================
|
|
310
|
+
props.items = {
|
|
311
|
+
type: "integer",
|
|
312
|
+
};
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
if (node.child.type === "array") {
|
|
316
|
+
// ==================== NESTED ARRAY ====================
|
|
317
|
+
// props.items = {
|
|
318
|
+
// type: "array",
|
|
319
|
+
// };
|
|
320
|
+
|
|
321
|
+
// ? Potentially something that can be done in the future, may need to write a generateArray function for this.
|
|
322
|
+
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
case "date": {
|
|
328
|
+
// ==================== DATE ====================
|
|
329
|
+
props.type = "string";
|
|
330
|
+
props.format = "date-time";
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (Object.keys(props).length > 0) {
|
|
336
|
+
if (node.title) props.title = node.title;
|
|
337
|
+
if (node.required) formObjs.required.push(propName);
|
|
338
|
+
formObjs.properties[propName] = props;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (Object.keys(propUi).length !== 0) ui[propName] = propUi;
|
|
342
|
+
});
|
|
343
|
+
return { form: formObjs, ui: ui };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const formData = generate(nodes);
|
|
347
|
+
|
|
348
|
+
const form: RJSFSchema = {
|
|
349
|
+
title: formConfig.title,
|
|
350
|
+
description: formConfig.description,
|
|
351
|
+
type: "object",
|
|
352
|
+
...formData.form,
|
|
353
|
+
};
|
|
354
|
+
return { form, ui: formData.ui };
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* ---------------------- BASE VALIDATION ----------------------
|
|
359
|
+
*/
|
|
360
|
+
|
|
361
|
+
export function baseValidation(key: string, value: string, node: FormNodeType) {
|
|
362
|
+
if (key === "title" && !value && !node.propertyName) return false;
|
|
363
|
+
if (key === "propertyName" && !value && !node.title) return false;
|
|
364
|
+
return true;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* ---------------------- GENERATE UPDATE NODE BY KEY LOCAL ----------------------
|
|
369
|
+
*/
|
|
370
|
+
|
|
371
|
+
export function generateUpdateNodeByKeyLocal(
|
|
372
|
+
isValid: (key: string, value: any) => boolean,
|
|
373
|
+
setNode: React.Dispatch<React.SetStateAction<any>>
|
|
374
|
+
) {
|
|
375
|
+
return (
|
|
376
|
+
key: string,
|
|
377
|
+
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
|
378
|
+
type?: string
|
|
379
|
+
) => {
|
|
380
|
+
updateNodeByKey(key, e, isValid, setNode, type);
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* ---------------------- UPDATE NODE BY KEY ----------------------
|
|
386
|
+
*/
|
|
387
|
+
|
|
388
|
+
export function updateNodeByKey(
|
|
389
|
+
key: string,
|
|
390
|
+
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
|
391
|
+
isValid: (key: string, value: any) => boolean,
|
|
392
|
+
setNode: UseStateType<any>[1],
|
|
393
|
+
type?: string
|
|
394
|
+
) {
|
|
395
|
+
if (type === "check") {
|
|
396
|
+
// @ts-ignore
|
|
397
|
+
const val = e.target.checked;
|
|
398
|
+
if (!isValid(key, val)) return;
|
|
399
|
+
setNode((n: any) => ({ ...n, [key]: val }));
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
const val = e.target.value;
|
|
403
|
+
if (!isValid(key, val)) return;
|
|
404
|
+
setNode((n: any) => ({ ...n, [key]: val }));
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* ---------------------- FIELD TITLE GENERATOR ----------------------
|
|
409
|
+
*/
|
|
410
|
+
|
|
411
|
+
export const formatTitle = (field: string) =>
|
|
412
|
+
field.replace(/([A-Z])/g, " $1").replace(/^./, (str) => str.toUpperCase());
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* ---------------------- FORMAT PROP NAME ----------------------
|
|
416
|
+
*/
|
|
417
|
+
|
|
418
|
+
export const formatPropName = (propName: string) =>
|
|
419
|
+
propName
|
|
420
|
+
.replace(/[()]/g, "")
|
|
421
|
+
.replace(/([A-Z])/g, " $1")
|
|
422
|
+
.trim()
|
|
423
|
+
.replace(/ +|--+/g, "-")
|
|
424
|
+
.toLowerCase();
|