@springmicro/forms 0.1.3 → 0.2.0-alpha.1

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.
@@ -0,0 +1,135 @@
1
+ import { UseStateType } from "./utils.type";
2
+
3
+ export type FullFormType = {
4
+ form: FormType;
5
+ nodes: FormNodeType[];
6
+ };
7
+
8
+ export type FormType = {
9
+ title?: string;
10
+ description?: string;
11
+ };
12
+
13
+ /**
14
+ * Extracts each key from @type {FormNodeTypes} exctracting the string from @param {T["type"]}
15
+ * which is then mapped to @type {FormNodeKeys}
16
+ */
17
+ type ExtractTypes<T extends { type: string }> = T["type"];
18
+ export type FormNodeKeys = ExtractTypes<FormNodeType>;
19
+
20
+ export type SearchNodeTypeByKey = {
21
+ [key in FormNodeKeys]: FormNodeType extends { type: infer T }
22
+ ? T extends key
23
+ ? FormNodeType
24
+ : never
25
+ : never;
26
+ };
27
+
28
+ export interface BaseNode {
29
+ nodeId: string;
30
+ propertyName?: string;
31
+ title?: string;
32
+ description?: string;
33
+ required?: boolean;
34
+ }
35
+
36
+ /**
37
+ * =================== NODES ===================
38
+ */
39
+
40
+ export type FormNodeType =
41
+ | FileNode
42
+ | TextNode
43
+ | IntegerNode
44
+ | DateNode
45
+ | ArrayNode
46
+ | ObjectNode;
47
+
48
+ export type TextNode = BaseNode & TextNodeEmpty;
49
+
50
+ export type FileNode = BaseNode & FileNodeEmpty;
51
+
52
+ export type IntegerNode = BaseNode & IntegerNodeEmpty;
53
+
54
+ export type DateNode = BaseNode & DateNodeEmpty;
55
+
56
+ export type ArrayNode = BaseNode & ArrayNodeEmpty;
57
+
58
+ export type ObjectNode = BaseNode & ObjectNodeEmpty;
59
+
60
+ /**
61
+ * =================== NODES ===================
62
+ */
63
+
64
+ export type EmptyFormNodeType =
65
+ | FileNodeEmpty
66
+ | TextNodeEmpty
67
+ | IntegerNodeEmpty
68
+ | DateNodeEmpty
69
+ | ArrayNodeEmpty
70
+ | ObjectNodeEmpty;
71
+
72
+ export interface TextNodeEmpty {
73
+ type: "string";
74
+ default?: string;
75
+ minLength?: number;
76
+ maxLength?: number;
77
+ format?: string;
78
+ }
79
+
80
+ export interface FileNodeEmpty {
81
+ type: "file";
82
+ multiple?: boolean;
83
+ filetype?: string;
84
+ }
85
+
86
+ export interface IntegerNodeEmpty {
87
+ type: "integer";
88
+ }
89
+
90
+ export interface DateNodeEmpty {
91
+ type: "date";
92
+ }
93
+
94
+ export interface ArrayNodeEmpty {
95
+ type: "array";
96
+ child: EmptyFormNodeType;
97
+ }
98
+
99
+ export interface ObjectNodeEmpty {
100
+ type: "object";
101
+ children: FormNodeType[]; // a list of nodes
102
+ }
103
+
104
+ /**
105
+ * ================== UTILS ==================
106
+ */
107
+
108
+ export type CountdownType =
109
+ | undefined
110
+ | number
111
+ | "Saved."
112
+ | "Saving..."
113
+ | "Save failed.";
114
+
115
+ export type EditingStateType = false | string;
116
+
117
+ // The format for creating a field or checkmark inside a node.
118
+ export type FieldArrayType<T> = Array<{
119
+ prop: keyof T;
120
+ tooltip?: string;
121
+ title?: string;
122
+ props?: any;
123
+ }>;
124
+
125
+ /**
126
+ * ================== SHARED PROPS ==================
127
+ */
128
+
129
+ export type ChildNodeProps = {
130
+ nodeState: UseStateType<FormNodeType>;
131
+ updateNode: (nodeData: FormNodeType) => void;
132
+ defaultFields: React.JSX.Element[];
133
+ defaultChecks: React.JSX.Element[];
134
+ updatePath: () => void;
135
+ };
@@ -0,0 +1 @@
1
+ export type UseStateType<T> = [T, React.Dispatch<React.SetStateAction<T>>];
@@ -0,0 +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();