dce-reactkit 3.4.0 → 3.5.1-beta.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.
Files changed (129) hide show
  1. package/.eslintrc.js +2 -0
  2. package/.vscode/settings.json +1 -0
  3. package/dist/cjs/index.js +36372 -191
  4. package/dist/cjs/index.js.map +1 -1
  5. package/dist/cjs/types/components/DBEntryManagerPanel/AddOrEditDBEntry/CreatableMultiselect.d.ts +10 -0
  6. package/dist/cjs/types/components/DBEntryManagerPanel/AddOrEditDBEntry/index.d.ts +34 -0
  7. package/dist/cjs/types/components/DBEntryManagerPanel/helpers/generateEndpointPath.d.ts +9 -0
  8. package/dist/cjs/types/components/DBEntryManagerPanel/index.d.ts +25 -0
  9. package/dist/cjs/types/components/DBEntryManagerPanel/types/DBEntry.d.ts +8 -0
  10. package/dist/cjs/types/components/DBEntryManagerPanel/types/DBEntryField.d.ts +49 -0
  11. package/dist/cjs/types/components/DBEntryManagerPanel/types/DBEntryFieldType.d.ts +12 -0
  12. package/dist/cjs/types/helpers/addDBEditorEndpoints.d.ts +40 -0
  13. package/dist/cjs/types/helpers/canReviewLogs.d.ts +0 -5
  14. package/dist/cjs/types/index.d.ts +6 -1
  15. package/dist/esm/index.js +36587 -427
  16. package/dist/esm/index.js.map +1 -1
  17. package/dist/esm/types/components/DBEntryManagerPanel/AddOrEditDBEntry/CreatableMultiselect.d.ts +10 -0
  18. package/dist/esm/types/components/DBEntryManagerPanel/AddOrEditDBEntry/index.d.ts +34 -0
  19. package/dist/esm/types/components/DBEntryManagerPanel/helpers/generateEndpointPath.d.ts +9 -0
  20. package/dist/esm/types/components/DBEntryManagerPanel/index.d.ts +25 -0
  21. package/dist/esm/types/components/DBEntryManagerPanel/types/DBEntry.d.ts +8 -0
  22. package/dist/esm/types/components/DBEntryManagerPanel/types/DBEntryField.d.ts +49 -0
  23. package/dist/esm/types/components/DBEntryManagerPanel/types/DBEntryFieldType.d.ts +12 -0
  24. package/dist/esm/types/helpers/addDBEditorEndpoints.d.ts +40 -0
  25. package/dist/esm/types/helpers/canReviewLogs.d.ts +0 -5
  26. package/dist/esm/types/index.d.ts +6 -1
  27. package/dist/index.d.ts +166 -35
  28. package/package.json +16 -12
  29. package/src/client/initClient.tsx +23 -31
  30. package/src/components/AppWrapper.tsx +1 -1
  31. package/src/components/DBEntryManagerPanel/AddOrEditDBEntry/CreatableMultiselect.tsx +303 -0
  32. package/src/components/DBEntryManagerPanel/AddOrEditDBEntry/index.tsx +737 -0
  33. package/src/components/DBEntryManagerPanel/helpers/generateEndpointPath.ts +16 -0
  34. package/src/components/DBEntryManagerPanel/index.tsx +522 -0
  35. package/src/components/DBEntryManagerPanel/types/DBEntry.ts +7 -0
  36. package/src/components/DBEntryManagerPanel/types/DBEntryField.ts +94 -0
  37. package/src/components/DBEntryManagerPanel/types/DBEntryFieldType.ts +18 -0
  38. package/src/components/ItemPicker/index.test.tsx +10 -10
  39. package/src/helpers/addDBEditorEndpoints.ts +128 -0
  40. package/src/helpers/canReviewLogs.ts +0 -20
  41. package/src/helpers/genCSV.ts +2 -5
  42. package/src/helpers/{getOrdinal.ts → getOrdinal.tsx} +0 -1
  43. package/src/index.ts +11 -9
  44. package/tsconfig.json +2 -6
  45. package/dist/cjs/types/helpers/abbreviate.test.d.ts +0 -5
  46. package/dist/cjs/types/helpers/avg.test.d.ts +0 -5
  47. package/dist/cjs/types/helpers/canReviewLogs.test.d.ts +0 -5
  48. package/dist/cjs/types/helpers/ceilToNumDecimals.test.d.ts +0 -5
  49. package/dist/cjs/types/helpers/compareArraysByProp.test.d.ts +0 -5
  50. package/dist/cjs/types/helpers/extractProp.test.d.ts +0 -5
  51. package/dist/cjs/types/helpers/floorToNumDecimals.test.d.ts +0 -5
  52. package/dist/cjs/types/helpers/forceNumIntoBounds.test.d.ts +0 -5
  53. package/dist/cjs/types/helpers/genCSV.test.d.ts +0 -5
  54. package/dist/cjs/types/helpers/genCommaList.d.ts +0 -12
  55. package/dist/cjs/types/helpers/genCommaList.test.d.ts +0 -1
  56. package/dist/cjs/types/helpers/getHumanReadableDate.test.d.ts +0 -5
  57. package/dist/cjs/types/helpers/getLocalTimeInfo.test.d.ts +0 -6
  58. package/dist/cjs/types/helpers/getMonthName.test.d.ts +0 -5
  59. package/dist/cjs/types/helpers/getOrdinal.test.d.ts +0 -5
  60. package/dist/cjs/types/helpers/getPartOfDay.test.d.ts +0 -5
  61. package/dist/cjs/types/helpers/getTimeInfoInET.test.d.ts +0 -5
  62. package/dist/cjs/types/helpers/validators/shared/constants/ERROR_MESSAGES.d.ts +0 -10
  63. package/dist/cjs/types/helpers/validators/shared/helpers/validateRegex.d.ts +0 -19
  64. package/dist/cjs/types/helpers/validators/shared/types/ValidationResult.d.ts +0 -12
  65. package/dist/cjs/types/helpers/validators/validateEmail.d.ts +0 -11
  66. package/dist/cjs/types/helpers/validators/validateEmail.test.d.ts +0 -1
  67. package/dist/cjs/types/helpers/validators/validatePhoneNumber.d.ts +0 -11
  68. package/dist/cjs/types/helpers/validators/validatePhoneNumber.test.d.ts +0 -1
  69. package/dist/cjs/types/helpers/validators/validateString.d.ts +0 -22
  70. package/dist/cjs/types/helpers/validators/validateString.test.d.ts +0 -1
  71. package/dist/esm/types/helpers/abbreviate.test.d.ts +0 -5
  72. package/dist/esm/types/helpers/avg.test.d.ts +0 -5
  73. package/dist/esm/types/helpers/canReviewLogs.test.d.ts +0 -5
  74. package/dist/esm/types/helpers/ceilToNumDecimals.test.d.ts +0 -5
  75. package/dist/esm/types/helpers/compareArraysByProp.test.d.ts +0 -5
  76. package/dist/esm/types/helpers/extractProp.test.d.ts +0 -5
  77. package/dist/esm/types/helpers/floorToNumDecimals.test.d.ts +0 -5
  78. package/dist/esm/types/helpers/forceNumIntoBounds.test.d.ts +0 -5
  79. package/dist/esm/types/helpers/genCSV.test.d.ts +0 -5
  80. package/dist/esm/types/helpers/genCommaList.d.ts +0 -12
  81. package/dist/esm/types/helpers/genCommaList.test.d.ts +0 -1
  82. package/dist/esm/types/helpers/getHumanReadableDate.test.d.ts +0 -5
  83. package/dist/esm/types/helpers/getLocalTimeInfo.test.d.ts +0 -6
  84. package/dist/esm/types/helpers/getMonthName.test.d.ts +0 -5
  85. package/dist/esm/types/helpers/getOrdinal.test.d.ts +0 -5
  86. package/dist/esm/types/helpers/getPartOfDay.test.d.ts +0 -5
  87. package/dist/esm/types/helpers/getTimeInfoInET.test.d.ts +0 -5
  88. package/dist/esm/types/helpers/validators/shared/constants/ERROR_MESSAGES.d.ts +0 -10
  89. package/dist/esm/types/helpers/validators/shared/helpers/validateRegex.d.ts +0 -19
  90. package/dist/esm/types/helpers/validators/shared/types/ValidationResult.d.ts +0 -12
  91. package/dist/esm/types/helpers/validators/validateEmail.d.ts +0 -11
  92. package/dist/esm/types/helpers/validators/validateEmail.test.d.ts +0 -1
  93. package/dist/esm/types/helpers/validators/validatePhoneNumber.d.ts +0 -11
  94. package/dist/esm/types/helpers/validators/validatePhoneNumber.test.d.ts +0 -1
  95. package/dist/esm/types/helpers/validators/validateString.d.ts +0 -22
  96. package/dist/esm/types/helpers/validators/validateString.test.d.ts +0 -1
  97. package/src/helpers/abbreviate.test.ts +0 -26
  98. package/src/helpers/avg.test.ts +0 -24
  99. package/src/helpers/canReviewLogs.test.ts +0 -67
  100. package/src/helpers/ceilToNumDecimals.test.ts +0 -39
  101. package/src/helpers/compareArraysByProp.test.ts +0 -102
  102. package/src/helpers/extractProp.test.ts +0 -34
  103. package/src/helpers/floorToNumDecimals.test.ts +0 -39
  104. package/src/helpers/forceNumIntoBounds.test.ts +0 -34
  105. package/src/helpers/genCSV.test.ts +0 -61
  106. package/src/helpers/genCommaList.test.ts +0 -57
  107. package/src/helpers/genCommaList.ts +0 -21
  108. package/src/helpers/getHumanReadableDate.test.ts +0 -29
  109. package/src/helpers/getLocalTimeInfo.test.ts +0 -48
  110. package/src/helpers/getMonthName.test.ts +0 -79
  111. package/src/helpers/getOrdinal.test.ts +0 -84
  112. package/src/helpers/getPartOfDay.test.ts +0 -56
  113. package/src/helpers/getTimeInfoInET.test.ts +0 -72
  114. package/src/helpers/validators/shared/constants/ERROR_MESSAGES.ts +0 -18
  115. package/src/helpers/validators/shared/helpers/validateRegex.ts +0 -47
  116. package/src/helpers/validators/shared/types/ValidationResult.ts +0 -20
  117. package/src/helpers/validators/validateEmail.test.ts +0 -110
  118. package/src/helpers/validators/validateEmail.ts +0 -47
  119. package/src/helpers/validators/validatePhoneNumber.test.ts +0 -94
  120. package/src/helpers/validators/validatePhoneNumber.ts +0 -49
  121. package/src/helpers/validators/validateString.test.ts +0 -175
  122. package/src/helpers/validators/validateString.ts +0 -171
  123. /package/src/helpers/{abbreviate.ts → abbreviate.tsx} +0 -0
  124. /package/src/helpers/{avg.ts → avg.tsx} +0 -0
  125. /package/src/helpers/{ceilToNumDecimals.ts → ceilToNumDecimals.tsx} +0 -0
  126. /package/src/helpers/{floorToNumDecimals.ts → floorToNumDecimals.tsx} +0 -0
  127. /package/src/helpers/{forceNumIntoBounds.ts → forceNumIntoBounds.tsx} +0 -0
  128. /package/src/helpers/{getLocalTimeInfo.ts → getLocalTimeInfo.tsx} +0 -0
  129. /package/src/helpers/{getTimeInfoInET.ts → getTimeInfoInET.tsx} +0 -0
@@ -0,0 +1,737 @@
1
+ /**
2
+ * Panel for adding a DBEntry to the database
3
+ * @author Yuen Ler Chow
4
+ * @author Gabe Abrams
5
+ */
6
+
7
+ // Import React
8
+ import React, { useReducer } from 'react';
9
+
10
+ // Import FontAwesome
11
+ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
12
+ import { faSave } from '@fortawesome/free-solid-svg-icons';
13
+
14
+ // Import shared components
15
+ import CheckboxButton from '../../CheckboxButton';
16
+ import LoadingSpinner from '../../LoadingSpinner';
17
+ import RadioButton from '../../RadioButton';
18
+ import ButtonInputGroup from '../../ButtonInputGroup';
19
+
20
+ // Import shared helpers
21
+ import { alert, showFatalError } from '../../AppWrapper';
22
+ import visitServerEndpoint from '../../../helpers/visitServerEndpoint';
23
+
24
+ // Import other types
25
+ import DBEntry from '../types/DBEntry';
26
+ import DBEntryField from '../types/DBEntryField';
27
+ import DBEntryFieldType from '../types/DBEntryFieldType';
28
+
29
+ // Import other components
30
+ import CreatableMultiselect from './CreatableMultiselect';
31
+
32
+ /*------------------------------------------------------------------------*/
33
+ /* -------------------------------- Types ------------------------------- */
34
+ /*------------------------------------------------------------------------*/
35
+
36
+ // Props definition
37
+ type Props = {
38
+ /**
39
+ * Handler for when the user finishes adding the DBEntry
40
+ * (if no DBEntry is returned, process was cancelled)
41
+ * @param DBEntry the DBEntry that was just created
42
+ */
43
+ onFinished: (dbEntry?: DBEntry) => void,
44
+ /**
45
+ * Function to validate the DBEntry before saving
46
+ * @param dbEntry the DBEntry to validate
47
+ */
48
+ validateEntry?: (dbEntry: DBEntry) => Promise<void>,
49
+ /**
50
+ * Function to modify the DBEntry before saving
51
+ * @param dbEntry the DBEntry to modify
52
+ * @returns the modified DBEntry
53
+ */
54
+ modifyEntry?: (dbEntry: DBEntry) => Promise<DBEntry> | DBEntry,
55
+ // The fields needed to create a new DBEntry
56
+ entryFields: DBEntryField[],
57
+ // The DBEntry to edit (if any)
58
+ dbEntryToEdit?: DBEntry,
59
+ // The unique object key of the DBEntry
60
+ idPropName: string,
61
+ // item name
62
+ itemName: string,
63
+ // Server endpoint path to save the DBEntry
64
+ saveEndpointPath: string,
65
+ // All entries in the database
66
+ entries: DBEntry[],
67
+ };
68
+
69
+ /*------------------------------------------------------------------------*/
70
+ /* -------------------------------- Style ------------------------------- */
71
+ /*------------------------------------------------------------------------*/
72
+
73
+ const style = `
74
+ .AddOrEditDBEntry-input-label {
75
+ min-width: 7rem;
76
+ }
77
+ `;
78
+
79
+ /*------------------------------------------------------------------------*/
80
+ /* -------------------------------- State ------------------------------- */
81
+ /*------------------------------------------------------------------------*/
82
+
83
+ /* -------- State Definition -------- */
84
+
85
+ type State = {
86
+ // The DBEntry's current state
87
+ entry: DBEntry,
88
+ // True if currently saving
89
+ saving: boolean,
90
+ };
91
+
92
+ /* ------------- Actions ------------ */
93
+
94
+ // Types of actions
95
+ enum ActionType {
96
+ // Update the DBEntry
97
+ UpdateDBEntry = 'UpdateDBEntry',
98
+ // Start the save spinner
99
+ StartSave = 'StartSave',
100
+ }
101
+
102
+ // Action definitions
103
+ type Action = (
104
+ | {
105
+ // Action type
106
+ type: ActionType.UpdateDBEntry,
107
+ // New state of the DBEntry
108
+ dbEntry: DBEntry,
109
+ }
110
+ | {
111
+ // Action type
112
+ type: (
113
+ | ActionType.StartSave
114
+ ),
115
+ }
116
+ );
117
+
118
+ /**
119
+ * Reducer that executes actions
120
+ * @author Yuen Ler Chow
121
+ * @param state current state
122
+ * @param action action to execute
123
+ * @returns updated state
124
+ */
125
+ const reducer = (state: State, action: Action): State => {
126
+ switch (action.type) {
127
+ case ActionType.UpdateDBEntry: {
128
+ return {
129
+ ...state,
130
+ entry: action.dbEntry,
131
+ };
132
+ }
133
+ case ActionType.StartSave: {
134
+ return {
135
+ ...state,
136
+ saving: true,
137
+ };
138
+ }
139
+ default: {
140
+ return state;
141
+ }
142
+ }
143
+ };
144
+
145
+ /*------------------------------------------------------------------------*/
146
+ /* ------------------------------ Component ----------------------------- */
147
+ /*------------------------------------------------------------------------*/
148
+
149
+ const AddOrEditDBEntry: React.FC<Props> = (props) => {
150
+ /*------------------------------------------------------------------------*/
151
+ /* -------------------------------- Setup ------------------------------- */
152
+ /*------------------------------------------------------------------------*/
153
+
154
+ /* -------------- Props ------------- */
155
+
156
+ // Destructure all props
157
+ const {
158
+ onFinished,
159
+ entryFields,
160
+ dbEntryToEdit,
161
+ validateEntry,
162
+ modifyEntry,
163
+ idPropName,
164
+ saveEndpointPath,
165
+ entries,
166
+ itemName,
167
+ } = props;
168
+
169
+ /* -------------- State ------------- */
170
+
171
+ // Initial state
172
+ const initialState: State = {
173
+ entry: dbEntryToEdit || {},
174
+ saving: false,
175
+ };
176
+
177
+ // Initialize state
178
+ const [state, dispatch] = useReducer(reducer, initialState);
179
+
180
+ // Destructure common state
181
+ const {
182
+ entry,
183
+ saving,
184
+ } = state;
185
+
186
+ /*------------------------------------------------------------------------*/
187
+ /* ------------------------- Component Functions ------------------------ */
188
+ /*------------------------------------------------------------------------*/
189
+
190
+ /**
191
+ * Save changes to the DBEntry and then finish
192
+ * @author Yuen Ler Chow
193
+ */
194
+ const save = async () => {
195
+ // Start the save loading indicator
196
+ dispatch({ type: ActionType.StartSave });
197
+
198
+ // add all default values to the entry
199
+ entryFields.forEach((field) => {
200
+ if (field.defaultValue && !entry[field.objectKey]) {
201
+ entry[field.objectKey] = field.defaultValue;
202
+ }
203
+ });
204
+
205
+ // Modify entry if needed
206
+ const modifiedEntry = (
207
+ modifyEntry
208
+ ? await modifyEntry(entry)
209
+ : entry
210
+ );
211
+
212
+ // add id key to entry so that when we delete the item, the key will always be "id"
213
+ modifiedEntry.id = modifiedEntry[idPropName];
214
+
215
+ // Send to server
216
+ try {
217
+ await visitServerEndpoint({
218
+ path: saveEndpointPath,
219
+ method: 'POST',
220
+ params: {
221
+ item: JSON.stringify(modifiedEntry),
222
+ },
223
+ });
224
+
225
+ // Finish
226
+ onFinished(entry);
227
+ } catch (err) {
228
+ return showFatalError(err);
229
+ }
230
+ };
231
+
232
+ /**
233
+ * Cancel and return without saving
234
+ * @author Gabe Abrams
235
+ */
236
+ const cancel = () => {
237
+ onFinished(undefined);
238
+ };
239
+
240
+ /**
241
+ * Create validation error message for the DBEntry
242
+ * @author Yuen Ler Chow
243
+ * @param fields the fields to validate
244
+ * @returns the validation error message, or an empty string if no error
245
+ */
246
+ const validate = (fields: DBEntryField[]) => {
247
+ let validationError = '';
248
+ for (let i = 0; i < fields.length; i += 1) {
249
+ const field = fields[i];
250
+ const value = entry[field.objectKey];
251
+
252
+ // Check if required field is empty. Field is automatically required if
253
+ // it is the idPropName
254
+ if ((field.required || (field.objectKey === idPropName)) && !value) {
255
+ validationError = `Please fill in the ${field.label} field`;
256
+ return validationError;
257
+ }
258
+
259
+ // Check if unique field is unique
260
+ if (field.objectKey === idPropName && !dbEntryToEdit) {
261
+ const notUnique = entries.find((e) => {
262
+ return e[idPropName] === value;
263
+ });
264
+ if (notUnique) {
265
+ validationError = `An item with the ${field.label} ${value} already exists. ${field.label} must be unique.`;
266
+ return validationError;
267
+ }
268
+ }
269
+
270
+ // If they have entered a value for the field, check if it is valid
271
+ if (value || field.type === DBEntryFieldType.Object) {
272
+ // String validation
273
+ if (field.type === DBEntryFieldType.String) {
274
+ if (
275
+ // Minimum length requirement is defined
276
+ field.minNumChars
277
+ // Value is too short
278
+ && value.length < field.minNumChars
279
+ ) {
280
+ validationError = `${field.label} must be at least ${field.minNumChars} character${field.minNumChars === 1 ? '' : 's'} long`;
281
+ } else if (
282
+ // Maximum length requirement is defined
283
+ field.maxNumChars
284
+ // Value is too long
285
+ && value.length > field.maxNumChars
286
+ ) {
287
+ validationError = `${field.label} must be at most ${field.maxNumChars} character${field.maxNumChars === 1 ? '' : 's'} long`;
288
+ }
289
+ // Number validation
290
+ } else if (field.type === DBEntryFieldType.Number) {
291
+ if (
292
+ // Minimum value requirement is defined
293
+ field.minNumber
294
+ // Value is too small
295
+ && value < field.minNumber
296
+ ) {
297
+ validationError = `${field.label} must be at least ${field.minNumber}`;
298
+ } else if (
299
+ // Maximum value requirement is defined
300
+ field.maxNumber
301
+ // Value is too large
302
+ && value > field.maxNumber
303
+ ) {
304
+ validationError = `${field.label} must be at most ${field.maxNumber}`;
305
+ }
306
+ } else if (
307
+ field.type === DBEntryFieldType.StringArray
308
+ || field.type === DBEntryFieldType.NumberArray
309
+ ) {
310
+ // String and Number Array validation
311
+ if (
312
+ // Minimum number of elements requirement is defined
313
+ field.minNumElements
314
+ // Value has too few elements
315
+ && value.length < field.minNumElements
316
+ ) {
317
+ validationError = `${field.label} must have at least ${field.minNumElements} value${field.minNumElements === 1 ? '' : 's'}`;
318
+ } else if (
319
+ // Maximum number of elements requirement is defined
320
+ field.maxNumElements
321
+ // Value has too many elements
322
+ && value.length > field.maxNumElements
323
+ ) {
324
+ validationError = `${field.label} must have at most ${field.maxNumElements} value${field.maxNumElements === 1 ? '' : 's'}`;
325
+ } else if (field.type === DBEntryFieldType.NumberArray) {
326
+ // Number Array validation
327
+ for (let j = 0; j < value.length; j += 1) {
328
+ if (
329
+ // Minimum value requirement is defined
330
+ field.minNumber
331
+ // Value is too small
332
+ && value[j] < field.minNumber
333
+ ) {
334
+ validationError = `${field.label} values must be at least ${field.minNumber}`;
335
+ return validationError;
336
+ } if (
337
+ // Maximum value requirement is defined
338
+ field.maxNumber
339
+ // Value is too large
340
+ && value[j] > field.maxNumber
341
+ ) {
342
+ validationError = `${field.label} values must be at most ${field.maxNumber}`;
343
+ return validationError;
344
+ }
345
+ }
346
+ }
347
+ } else if (field.type === DBEntryFieldType.Object) {
348
+ validationError = validate(field.subfields);
349
+ }
350
+ }
351
+ if (validationError) {
352
+ return validationError;
353
+ }
354
+ }
355
+ return validationError;
356
+ };
357
+
358
+ /*------------------------------------------------------------------------*/
359
+ /* ------------------------------- Render ------------------------------- */
360
+ /*------------------------------------------------------------------------*/
361
+
362
+ /*----------------------------------------*/
363
+ /* ---------------- Views --------------- */
364
+ /*----------------------------------------*/
365
+
366
+ // Body
367
+ let body: React.ReactNode;
368
+
369
+ /* ------------- Loading ------------ */
370
+
371
+ if (saving) {
372
+ body = (
373
+ <LoadingSpinner />
374
+ );
375
+ }
376
+
377
+ /* -------------- Form -------------- */
378
+
379
+ // If not saving, validate what the user has entered
380
+ if (!saving) {
381
+ // Validation error if there is one
382
+ const validationError = validate(entryFields);
383
+
384
+ /**
385
+ * Render a single entry field
386
+ * @author Yuen Ler Chow
387
+ * @param field the entry field to render
388
+ * @param disabled true if the field should be disabled
389
+ * @returns rendered entry field
390
+ */
391
+ const renderEntryField = (field: DBEntryField, disabled: boolean) => {
392
+ if (field.type === DBEntryFieldType.String) {
393
+ if (field.choices) {
394
+ return (
395
+ <div
396
+ key={field.objectKey}
397
+ className="mb-2"
398
+ >
399
+ <div
400
+ className="input-group"
401
+ style={{
402
+ pointerEvents: (disabled ? 'none' : 'auto'),
403
+ }}
404
+ >
405
+ <ButtonInputGroup
406
+ label={field.label}
407
+ >
408
+ {
409
+ field.choices.map((choice) => {
410
+ return (
411
+ <RadioButton
412
+ key={choice.value}
413
+ text={choice.title}
414
+ selected={entry[field.objectKey] === choice.value}
415
+ onSelected={() => {
416
+ entry[field.objectKey] = choice.value;
417
+ dispatch({
418
+ type: ActionType.UpdateDBEntry,
419
+ dbEntry: entry,
420
+ });
421
+ }}
422
+ ariaLabel={choice.title}
423
+ />
424
+ );
425
+ })
426
+ }
427
+ </ButtonInputGroup>
428
+ </div>
429
+ </div>
430
+ );
431
+ }
432
+
433
+ return (
434
+ <div
435
+ className="mb-2"
436
+ key={field.objectKey}
437
+ >
438
+ <div className="input-group">
439
+ <span
440
+ className="AddOrEditDBEntry-input-label input-group-text"
441
+ >
442
+ {field.label}
443
+ </span>
444
+ <input
445
+ disabled={disabled}
446
+ type="text"
447
+ className="form-control"
448
+ placeholder={field.placeholder}
449
+ aria-describedby="AddOrEditDBEntry-form-name-label"
450
+ value={entry[field.objectKey] || ''}
451
+ onChange={(e) => {
452
+ entry[field.objectKey] = (
453
+ e.target.value
454
+ );
455
+ dispatch({
456
+ type: ActionType.UpdateDBEntry,
457
+ dbEntry: entry,
458
+ });
459
+ }}
460
+ />
461
+ </div>
462
+ </div>
463
+ );
464
+ }
465
+
466
+ if (field.type === DBEntryFieldType.Number) {
467
+ return (
468
+ <div
469
+ key={field.objectKey}
470
+ className="mb-2"
471
+ >
472
+ <div className="input-group">
473
+ <span
474
+ className="AddOrEditDBEntry-input-label input-group-text"
475
+ >
476
+ {field.label}
477
+ </span>
478
+ <input
479
+ type="text"
480
+ className="form-control"
481
+ placeholder={field.placeholder}
482
+ aria-describedby="AddOrEditDBEntry-form-name-label"
483
+ value={entry[field.objectKey] || ''}
484
+ disabled={disabled}
485
+ onChange={(e) => {
486
+ entry[field.objectKey] = (
487
+ e.target.value
488
+ .replace(/[^0-9]/g, '')
489
+ );
490
+ dispatch({
491
+ type: ActionType.UpdateDBEntry,
492
+ dbEntry: entry,
493
+ });
494
+ }}
495
+ />
496
+ </div>
497
+ </div>
498
+ );
499
+ }
500
+
501
+ if (field.type === DBEntryFieldType.StringArray) {
502
+ if (field.choices) {
503
+ return (
504
+ <div
505
+ key={field.objectKey}
506
+ className="mb-2"
507
+ >
508
+ <div
509
+ className="input-group"
510
+ style={{
511
+ pointerEvents: (disabled ? 'none' : 'auto'),
512
+ }}
513
+ >
514
+ <ButtonInputGroup
515
+ label={field.label}
516
+ >
517
+ {
518
+ field.choices.map((choice) => {
519
+ return (
520
+ <CheckboxButton
521
+ key={choice.value}
522
+ text={choice.title}
523
+ checked={entry[field.objectKey] && entry[field.objectKey].includes(choice.value)}
524
+ onChanged={(checked) => {
525
+ if (checked) {
526
+ // Initialize array if it doesn't exist
527
+ if (!entry[field.objectKey]) {
528
+ entry[field.objectKey] = [];
529
+ }
530
+
531
+ // Add value to array
532
+ entry[field.objectKey].push(choice.value);
533
+ } else {
534
+ // Remove value from array
535
+ entry[field.objectKey] = (
536
+ entry[field.objectKey]
537
+ .filter((val: string) => {
538
+ return val !== choice.value;
539
+ })
540
+ );
541
+ }
542
+
543
+ // Save
544
+ dispatch({
545
+ type: ActionType.UpdateDBEntry,
546
+ dbEntry: entry,
547
+ });
548
+ }}
549
+ ariaLabel={choice.title}
550
+ />
551
+ );
552
+ })
553
+ }
554
+ </ButtonInputGroup>
555
+ </div>
556
+ </div>
557
+ );
558
+ }
559
+
560
+ // Flexible (no specific choices for this field)
561
+ return (
562
+ <div
563
+ key={field.objectKey}
564
+ className="mb-2"
565
+ >
566
+ <div className="input-group">
567
+ <span
568
+ className="AddOrEditDBEntry-input-label input-group-text"
569
+ >
570
+ {field.label}
571
+ </span>
572
+ <div className="flex-grow-1">
573
+ <CreatableMultiselect
574
+ disabled={disabled}
575
+ type={DBEntryFieldType.StringArray}
576
+ values={entry[field.objectKey] || []}
577
+ onChange={(values) => {
578
+ // Update entry and save
579
+ entry[field.objectKey] = values;
580
+ dispatch({
581
+ type: ActionType.UpdateDBEntry,
582
+ dbEntry: entry,
583
+ });
584
+ }}
585
+ />
586
+ </div>
587
+ </div>
588
+ </div>
589
+ );
590
+ }
591
+
592
+ if (field.type === DBEntryFieldType.NumberArray) {
593
+ return (
594
+ <div
595
+ key={field.objectKey}
596
+ className="mb-2"
597
+ >
598
+ <div className="input-group">
599
+ <span
600
+ className="AddOrEditDBEntry-input-label input-group-text"
601
+ >
602
+ {field.label}
603
+ </span>
604
+ <div className="flex-grow-1">
605
+ <CreatableMultiselect
606
+ disabled={disabled}
607
+ type={DBEntryFieldType.NumberArray}
608
+ values={entry[field.objectKey] || []}
609
+ onChange={(values) => {
610
+ entry[field.objectKey] = values;
611
+ dispatch({
612
+ type: ActionType.UpdateDBEntry,
613
+ dbEntry: entry,
614
+ });
615
+ }}
616
+ />
617
+ </div>
618
+ </div>
619
+ </div>
620
+ );
621
+ }
622
+
623
+ if (field.type === DBEntryFieldType.Object) {
624
+ return (
625
+ <div
626
+ key={field.objectKey}
627
+ className="mb-2"
628
+ >
629
+ <div className="input-group">
630
+ {/* Label */}
631
+ <span
632
+ className="AddOrEditDBEntry-input-label input-group-text"
633
+ >
634
+ {field.label}
635
+ </span>
636
+ {/* Add each subfield */}
637
+ <div className="flex-grow-1 ps-2 pe-2 pt-2 pb-0 form-control">
638
+ {
639
+ field.subfields.map((subfield: DBEntryField) => {
640
+ return <div>{renderEntryField(subfield, disabled)}</div>;
641
+ })
642
+ }
643
+ </div>
644
+ </div>
645
+ </div>
646
+ );
647
+ }
648
+
649
+ // This should never happen
650
+ return null;
651
+ };
652
+
653
+ // UI
654
+ body = (
655
+ <div>
656
+ <h3 className="text-center">
657
+ {dbEntryToEdit ? `Edit ${itemName}` : `Create ${itemName}`}
658
+ </h3>
659
+ {/* Entry fields */}
660
+ {
661
+ entryFields.map((field: DBEntryField) => {
662
+ const disabled = (idPropName === field.objectKey && dbEntryToEdit !== undefined)
663
+ || (field.lockAfterCreation !== undefined && dbEntryToEdit !== undefined);
664
+ return renderEntryField(field, disabled);
665
+ })
666
+ }
667
+
668
+ {/* Buttons */}
669
+ <div className="text-center mt-2">
670
+ <button
671
+ type="button"
672
+ id="AddOrEditDBEntry-save-changes-button"
673
+ className="btn btn-primary btn-lg me-1"
674
+ aria-label="Save changes"
675
+ onClick={async () => {
676
+ // Show validation errors
677
+ if (validationError && validationError.length > 0) {
678
+ return alert(
679
+ 'Please fix the following error',
680
+ validationError,
681
+ );
682
+ }
683
+
684
+ // Run the included validator if it exists
685
+ if (validateEntry) {
686
+ try {
687
+ await validateEntry(entry);
688
+ } catch (error) {
689
+ return alert(
690
+ 'Please fix the following error',
691
+ String(error),
692
+ );
693
+ }
694
+ }
695
+ save();
696
+ }}
697
+ >
698
+ <FontAwesomeIcon
699
+ icon={faSave}
700
+ className="me-1"
701
+ />
702
+ Save
703
+ </button>
704
+ <button
705
+ type="button"
706
+ id="AddOrEditDBEntry-cancel-button"
707
+ className="btn btn-secondary btn-lg"
708
+ aria-label="save changes"
709
+ onClick={cancel}
710
+ >
711
+ Cancel
712
+ </button>
713
+ </div>
714
+ </div>
715
+ );
716
+ }
717
+
718
+ /*----------------------------------------*/
719
+ /* --------------- Main UI -------------- */
720
+ /*----------------------------------------*/
721
+
722
+ return (
723
+ <div className="alert alert-light text-black">
724
+ <style>
725
+ {style}
726
+ </style>
727
+ {body}
728
+ </div>
729
+ );
730
+ };
731
+
732
+ /*------------------------------------------------------------------------*/
733
+ /* ------------------------------- Wrap Up ------------------------------ */
734
+ /*------------------------------------------------------------------------*/
735
+
736
+ // Export component
737
+ export default AddOrEditDBEntry;