dce-reactkit 3.3.5 → 3.4.0-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 (34) hide show
  1. package/dist/cjs/index.js +37524 -380
  2. package/dist/cjs/index.js.map +1 -1
  3. package/dist/cjs/types/components/DBEntryManagerPanel/AddOrEditDBEntry/CreatableMultiselect.d.ts +10 -0
  4. package/dist/cjs/types/components/DBEntryManagerPanel/AddOrEditDBEntry/index.d.ts +33 -0
  5. package/dist/cjs/types/components/DBEntryManagerPanel/helpers/generateEndpointPath.d.ts +8 -0
  6. package/dist/cjs/types/components/DBEntryManagerPanel/index.d.ts +25 -0
  7. package/dist/cjs/types/components/DBEntryManagerPanel/types/DBEntry.d.ts +8 -0
  8. package/dist/cjs/types/components/DBEntryManagerPanel/types/DBEntryField.d.ts +49 -0
  9. package/dist/cjs/types/components/DBEntryManagerPanel/types/DBEntryFieldType.d.ts +12 -0
  10. package/dist/cjs/types/helpers/addDBEditorEndpoints.d.ts +40 -0
  11. package/dist/cjs/types/index.d.ts +6 -1
  12. package/dist/esm/index.js +37698 -575
  13. package/dist/esm/index.js.map +1 -1
  14. package/dist/esm/types/components/DBEntryManagerPanel/AddOrEditDBEntry/CreatableMultiselect.d.ts +10 -0
  15. package/dist/esm/types/components/DBEntryManagerPanel/AddOrEditDBEntry/index.d.ts +33 -0
  16. package/dist/esm/types/components/DBEntryManagerPanel/helpers/generateEndpointPath.d.ts +8 -0
  17. package/dist/esm/types/components/DBEntryManagerPanel/index.d.ts +25 -0
  18. package/dist/esm/types/components/DBEntryManagerPanel/types/DBEntry.d.ts +8 -0
  19. package/dist/esm/types/components/DBEntryManagerPanel/types/DBEntryField.d.ts +49 -0
  20. package/dist/esm/types/components/DBEntryManagerPanel/types/DBEntryFieldType.d.ts +12 -0
  21. package/dist/esm/types/helpers/addDBEditorEndpoints.d.ts +40 -0
  22. package/dist/esm/types/index.d.ts +6 -1
  23. package/dist/index.d.ts +166 -35
  24. package/package.json +8 -3
  25. package/src/components/DBEntryManagerPanel/AddOrEditDBEntry/CreatableMultiselect.tsx +290 -0
  26. package/src/components/DBEntryManagerPanel/AddOrEditDBEntry/index.tsx +699 -0
  27. package/src/components/DBEntryManagerPanel/helpers/generateEndpointPath.ts +15 -0
  28. package/src/components/DBEntryManagerPanel/index.tsx +511 -0
  29. package/src/components/DBEntryManagerPanel/types/DBEntry.ts +7 -0
  30. package/src/components/DBEntryManagerPanel/types/DBEntryField.ts +94 -0
  31. package/src/components/DBEntryManagerPanel/types/DBEntryFieldType.ts +18 -0
  32. package/src/helpers/addDBEditorEndpoints.ts +121 -0
  33. package/src/helpers/getLocalTimeInfo.tsx +4 -1
  34. package/src/index.ts +11 -1
@@ -0,0 +1,511 @@
1
+ /**
2
+ * DB Entry Manager Panel
3
+ * @author Yuen Ler Chow
4
+ */
5
+
6
+ // Import React
7
+ import React, { useReducer, useEffect } from 'react';
8
+
9
+ // Import FontAwesome
10
+ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
11
+ import { faPlus, faTrash, faCog } from '@fortawesome/free-solid-svg-icons';
12
+
13
+ // import dce-reactkit components
14
+ import TabBox from '../TabBox';
15
+ import visitServerEndpoint from '../../helpers/visitServerEndpoint';
16
+ import { showFatalError, confirm } from '../AppWrapper';
17
+ import LoadingSpinner from '../LoadingSpinner';
18
+
19
+ // import types
20
+ import DBEntry from './types/DBEntry';
21
+ import DBEntryField from './types/DBEntryField';
22
+
23
+ // Import other components
24
+ import AddOrEditDBEntry from './AddOrEditDBEntry';
25
+ import generateEndpointPath from './helpers/generateEndpointPath';
26
+
27
+ /*------------------------------------------------------------------------*/
28
+ /* -------------------------------- Types ------------------------------- */
29
+ /*------------------------------------------------------------------------*/
30
+
31
+ // Props definition
32
+ type Props = {
33
+ // List of db entry fields
34
+ entryFields: DBEntryField[],
35
+ // a prop that is unique to each item
36
+ idPropName: string,
37
+ // the prop that you want to show up as the title of each item
38
+ titlePropName: string,
39
+ // The prop that you want in parentheses after the title
40
+ descriptionPropName: string,
41
+ // the title of the tabBox
42
+ itemListTitle: string,
43
+ // the phrase you want when you say "add a new [itemName]"
44
+ itemName: string,
45
+ // Function to validate the db entry before sending to the server
46
+ validateEntry?: (dbEntry: DBEntry) => Promise<void>,
47
+ // Function to modify the db entry before sending to the server
48
+ modifyEntry?: (dbEntry: DBEntry) => Promise<DBEntry>,
49
+ // True if editing is disabled
50
+ disableEdit?: boolean,
51
+ // Name of the collection in the database
52
+ collectionName: string,
53
+ // True if only admins can access this page
54
+ adminsOnly?: boolean,
55
+ // the query to filter the db entries
56
+ filterQuery?: { [k: string]: any },
57
+ };
58
+
59
+ /*------------------------------------------------------------------------*/
60
+ /* -------------------------------- State ------------------------------- */
61
+ /*------------------------------------------------------------------------*/
62
+
63
+ /* -------- State Definition -------- */
64
+
65
+ type State = {
66
+ // List of db items
67
+ dbEntries: DBEntry[],
68
+ // True if adding a new db item
69
+ adding: boolean,
70
+ // Db item to edit
71
+ dbEntryToEdit?: DBEntry,
72
+ // True if loading
73
+ loading: boolean,
74
+ };
75
+
76
+ /* ------------- Actions ------------ */
77
+
78
+ // Types of actions
79
+ enum ActionType {
80
+ // Show adder
81
+ ShowAdder = 'ShowAdder',
82
+ // Show editor
83
+ ShowEditor = 'ShowEditor',
84
+ // Finish adding
85
+ FinishAdd = 'FinishAdd',
86
+ // Finish loading
87
+ FinishLoading = 'FinishLoading',
88
+ // Start deletion process
89
+ StartDelete = 'StartDelete',
90
+ // Finish deletion process
91
+ FinishDelete = 'FinishDelete',
92
+ }
93
+
94
+ // Action definitions
95
+ type Action = (
96
+ | {
97
+ // Action type
98
+ type: ActionType.FinishLoading,
99
+ // db entry list
100
+ dbEntries: DBEntry[],
101
+ }
102
+ | {
103
+ // Action type
104
+ type: ActionType.ShowEditor,
105
+ // db item to edit
106
+ dbEntry: DBEntry,
107
+ }
108
+ | {
109
+ // Action type
110
+ type: (
111
+ | ActionType.ShowAdder
112
+ ),
113
+ }
114
+ | {
115
+ // Action type
116
+ type: ActionType.FinishAdd,
117
+ // DB entry that was added
118
+ dbEntry?: DBEntry,
119
+ idPropName: string,
120
+ }
121
+ | {
122
+ // Action type
123
+ type: ActionType.StartDelete,
124
+ }
125
+ | {
126
+ // Action type
127
+ type: ActionType.FinishDelete,
128
+ // db entry that was deleted
129
+ dbEntry: DBEntry,
130
+ // unique id prop name
131
+ idPropName: string,
132
+ }
133
+ );
134
+
135
+ /**
136
+ * Reducer that executes actions
137
+ * @author Yuen Ler Chow
138
+ * @param state current state
139
+ * @param action action to execute
140
+ */
141
+ const reducer = (state: State, action: Action): State => {
142
+ switch (action.type) {
143
+ case ActionType.FinishLoading: {
144
+ return {
145
+ ...state,
146
+ loading: false,
147
+ dbEntries: action.dbEntries,
148
+ };
149
+ }
150
+ case ActionType.ShowAdder: {
151
+ return {
152
+ ...state,
153
+ adding: true,
154
+ dbEntryToEdit: undefined,
155
+ };
156
+ }
157
+ case ActionType.ShowEditor: {
158
+ return {
159
+ ...state,
160
+ adding: false,
161
+ dbEntryToEdit: action.dbEntry,
162
+ };
163
+ }
164
+ case ActionType.FinishAdd: {
165
+ // Handle cancel
166
+ const finishedEntry = action.dbEntry;
167
+ if (!finishedEntry) {
168
+ return {
169
+ ...state,
170
+ adding: false,
171
+ dbEntryToEdit: undefined,
172
+ };
173
+ }
174
+
175
+ // Create an updated list of DB entries
176
+ let updatedDbEntries: DBEntry[];
177
+ if (state.adding) {
178
+ updatedDbEntries = [...state.dbEntries, finishedEntry];
179
+ } else {
180
+ updatedDbEntries = state.dbEntries.map((existingDbEntry) => {
181
+ if (state.dbEntryToEdit && state.dbEntryToEdit[action.idPropName] === existingDbEntry[action.idPropName]) {
182
+ // This is the entry being edited! Replace
183
+ return finishedEntry;
184
+ }
185
+ // This is not the entry being edited
186
+ return existingDbEntry;
187
+ });
188
+ }
189
+
190
+ // Update the state
191
+ return {
192
+ ...state,
193
+ adding: false,
194
+ dbEntryToEdit: undefined,
195
+ dbEntries: updatedDbEntries,
196
+ };
197
+ }
198
+ case ActionType.StartDelete: {
199
+ return {
200
+ ...state,
201
+ loading: true,
202
+ };
203
+ }
204
+ case ActionType.FinishDelete: {
205
+ return {
206
+ ...state,
207
+ loading: false,
208
+ // Remove the deleted entry from the list
209
+ dbEntries: state.dbEntries.filter((entry: DBEntry) => {
210
+ return (
211
+ entry[action.idPropName] !== action.dbEntry[action.idPropName]
212
+ );
213
+ }),
214
+ };
215
+ }
216
+ default: {
217
+ return state;
218
+ }
219
+ }
220
+ };
221
+
222
+ /*------------------------------------------------------------------------*/
223
+ /* ------------------------------ Component ----------------------------- */
224
+ /*------------------------------------------------------------------------*/
225
+
226
+ const DBEntryManagerPanel: React.FC<Props> = (props) => {
227
+
228
+ // Destructure all props
229
+ const {
230
+ entryFields,
231
+ titlePropName,
232
+ descriptionPropName,
233
+ idPropName,
234
+ itemListTitle,
235
+ itemName,
236
+ validateEntry,
237
+ modifyEntry,
238
+ disableEdit,
239
+ collectionName,
240
+ adminsOnly,
241
+ filterQuery
242
+ } = props;
243
+
244
+ /* -------------- State ------------- */
245
+
246
+ // Initial state
247
+ const initialState: State = {
248
+ dbEntries: [],
249
+ adding: false,
250
+ loading: true,
251
+ };
252
+
253
+ // Initialize state
254
+ const [state, dispatch] = useReducer(reducer, initialState);
255
+
256
+ // Destructure common state
257
+ const {
258
+ adding,
259
+ dbEntryToEdit,
260
+ dbEntries,
261
+ loading,
262
+ } = state;
263
+
264
+ /*------------------------------------------------------------------------*/
265
+ /* ------------------------- Component Functions ------------------------ */
266
+ /*------------------------------------------------------------------------*/
267
+
268
+ // Generate the endpoint path
269
+ const endpoint = generateEndpointPath(collectionName, adminsOnly);
270
+
271
+ /**
272
+ * Delete a database entry
273
+ * @author Yuen Ler Chow
274
+ * @param entry the database entry to delete
275
+ */
276
+ const deleteEntry = async (entry: DBEntry) => {
277
+ // Confirm
278
+ const confirmed = await confirm(
279
+ 'Remove?',
280
+ `Are you sure you want to remove this ${itemName}?`,
281
+ {
282
+ confirmButtonText: 'Remove Item',
283
+ },
284
+ );
285
+
286
+ // Skip if cancelled
287
+ if (!confirmed) {
288
+ return;
289
+ }
290
+
291
+ // Remove the entry
292
+ try {
293
+ // Start loader
294
+ dispatch({
295
+ type: ActionType.StartDelete,
296
+ });
297
+
298
+ // Perform deletion
299
+ await visitServerEndpoint({
300
+ path: `${endpoint}/${entry[idPropName]}`,
301
+ method: 'DELETE',
302
+ });
303
+
304
+ // Finish loader
305
+ dispatch({
306
+ type: ActionType.FinishDelete,
307
+ dbEntry: entry,
308
+ idPropName,
309
+ });
310
+ } catch (err) {
311
+ return showFatalError(err);
312
+ }
313
+ };
314
+
315
+ /*------------------------------------------------------------------------*/
316
+ /* ------------------------- Lifecycle Functions ------------------------ */
317
+ /*------------------------------------------------------------------------*/
318
+
319
+ /**
320
+ * Mount
321
+ * @author Yuen Ler Chow
322
+ */
323
+ useEffect(
324
+ () => {
325
+ (async () => {
326
+ // Load list of database entries
327
+ try {
328
+ const data = await visitServerEndpoint({
329
+ path: endpoint,
330
+ method: 'GET',
331
+ params: {
332
+ filterQuery: JSON.stringify(filterQuery),
333
+ },
334
+ });
335
+
336
+ // Save loaded data
337
+ dispatch({
338
+ type: ActionType.FinishLoading,
339
+ dbEntries: data,
340
+ });
341
+ } catch (err) {
342
+ return showFatalError(err);
343
+ }
344
+ })();
345
+ },
346
+ [],
347
+ );
348
+
349
+ /*------------------------------------------------------------------------*/
350
+ /* ------------------------------- Render ------------------------------- */
351
+ /*------------------------------------------------------------------------*/
352
+
353
+ /*----------------------------------------*/
354
+ /* ---------------- Views --------------- */
355
+ /*----------------------------------------*/
356
+
357
+ let body: React.ReactNode;
358
+
359
+ /* ------------- Loading ------------ */
360
+
361
+ if (loading) {
362
+ body = (
363
+ <LoadingSpinner />
364
+ );
365
+ }
366
+
367
+ /* ------------- List of entries ------------ */
368
+
369
+ if (!loading && !adding && !dbEntryToEdit) {
370
+ // Create body
371
+ body = (
372
+ <div>
373
+ <TabBox
374
+ title={itemListTitle}
375
+ >
376
+ {/* List of DB entries */}
377
+ {dbEntries.map((entry) => {
378
+ return (
379
+ <div
380
+ key={entry[idPropName]}
381
+ className="alert alert-secondary p-2 mb-2 d-flex align-items-center justify-content-center mb-1"
382
+ >
383
+ {/* Title */}
384
+ <div className="flex-grow-1">
385
+ <h4 className="m-0">
386
+ <span className="fw-bold">
387
+ {entry[titlePropName]}
388
+ </span>
389
+ <span className="small">
390
+ {' '}
391
+ (
392
+ {entry[descriptionPropName]}
393
+ )
394
+ </span>
395
+ </h4>
396
+ </div>
397
+
398
+ {/* Buttons */}
399
+ <div className="d-flex align-items-center">
400
+ {/* Remove Button */}
401
+ <button
402
+ type="button"
403
+ id={`DBEntryManagerPanel-remove-entry-with-id-${entry[idPropName]}`}
404
+ className="btn btn-secondary me-1"
405
+ aria-label={`remove database entry: ${entry[titlePropName]}`}
406
+ onClick={() => {
407
+ deleteEntry(entry);
408
+ }}
409
+ >
410
+ <FontAwesomeIcon
411
+ icon={faTrash}
412
+ />
413
+ <span className="d-none d-md-inline ms-1">
414
+ Remove
415
+ </span>
416
+ </button>
417
+
418
+ {/* Edit Button */}
419
+ {!disableEdit && (
420
+ <button
421
+ type="button"
422
+ id={`DBEntryManagerPanel-edit-with-id-${entry[idPropName]}`}
423
+ className="btn btn-primary"
424
+ aria-label={`edit db entry: ${entry[titlePropName]}`}
425
+ onClick={() => {
426
+ dispatch({
427
+ type: ActionType.ShowEditor,
428
+ dbEntry: entry,
429
+ });
430
+ }}
431
+ >
432
+ <FontAwesomeIcon
433
+ icon={faCog}
434
+ />
435
+ <span className="d-none d-md-inline ms-1">
436
+ Edit
437
+ </span>
438
+ </button>
439
+ )}
440
+ </div>
441
+ </div>
442
+ );
443
+ })}
444
+
445
+ {/* Add DB entry Button */}
446
+ <div className="d-grid">
447
+ <button
448
+ type="button"
449
+ id="DBEntryManagerPanel-add-entry"
450
+ className="btn btn-lg btn-primary"
451
+ aria-label={`add a new ${itemName} entry to the list of entries`}
452
+ onClick={() => {
453
+ dispatch({
454
+ type: ActionType.ShowAdder,
455
+ });
456
+ }}
457
+ >
458
+ <FontAwesomeIcon
459
+ icon={faPlus}
460
+ className="me-2"
461
+ />
462
+ Add
463
+ {' '}
464
+ {itemName}
465
+ </button>
466
+ </div>
467
+ </TabBox>
468
+ </div>
469
+ );
470
+ }
471
+
472
+ /* --------- Create or edit entry -------- */
473
+
474
+ if (!loading && (adding || dbEntryToEdit)) {
475
+ body = (
476
+ <AddOrEditDBEntry
477
+ saveEndpointPath={endpoint}
478
+ validateEntry={validateEntry}
479
+ modifyEntry={modifyEntry}
480
+ entryFields={entryFields}
481
+ dbEntryToEdit={dbEntryToEdit}
482
+ idPropName={idPropName}
483
+ entries={dbEntries}
484
+ onFinished={(entry?: DBEntry) => {
485
+ dispatch({
486
+ type: ActionType.FinishAdd,
487
+ dbEntry: entry,
488
+ idPropName,
489
+ });
490
+ }}
491
+ />
492
+ );
493
+ }
494
+
495
+ /*----------------------------------------*/
496
+ /* --------------- Main UI -------------- */
497
+ /*----------------------------------------*/
498
+
499
+ return (
500
+ <div>
501
+ {body}
502
+ </div>
503
+ );
504
+ };
505
+
506
+ /*------------------------------------------------------------------------*/
507
+ /* ------------------------------- Wrap Up ------------------------------ */
508
+ /*------------------------------------------------------------------------*/
509
+
510
+ // Export component
511
+ export default DBEntryManagerPanel;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Generic type for an object
3
+ * @author Yuen Ler Chow
4
+ */
5
+ type DBEntry = { [k: string]: any };
6
+
7
+ export default DBEntry;
@@ -0,0 +1,94 @@
1
+ import DBEntryFieldType from './DBEntryFieldType';
2
+
3
+ /**
4
+ * A database entry input field
5
+ * @author Yuen Ler Chow
6
+ */
7
+ type DBEntryField = (
8
+ {
9
+ // The label of the field
10
+ label: string,
11
+ // The key/prop this corresponds to in the DBEntry
12
+ objectKey: string,
13
+ // The placeholder text
14
+ placeholder: string,
15
+ // If true, only allow the user to edit when creating (not when editing)
16
+ lockAfterCreation?: boolean,
17
+ // If true, the field is required
18
+ required?: boolean,
19
+ } & (
20
+ // A string input field
21
+ | {
22
+ // The type of the field
23
+ type: DBEntryFieldType.String,
24
+ // The required minimum number of characters
25
+ minNumChars?: number,
26
+ // The required maximum number of characters
27
+ maxNumChars?: number,
28
+ // The default value for the field
29
+ defaultValue?: string,
30
+ // If defined, the choices the user can choose from
31
+ choices?: {
32
+ // The title of the choice (human-readable)
33
+ title: string,
34
+ // The value of the choice (what is stored in the DB)
35
+ value: string,
36
+ }[],
37
+ }
38
+ // A number input field
39
+ | {
40
+ // The type of the field
41
+ type: DBEntryFieldType.Number,
42
+ // The required minimum number
43
+ minNumber?: number,
44
+ // The required maximum number
45
+ maxNumber?: number,
46
+ // The default value for the field
47
+ defaultValue?: number,
48
+ }
49
+ // Checkbox input field
50
+ | {
51
+ // The type of the field
52
+ type: DBEntryFieldType.StringArray,
53
+ // The required minimum number of elements
54
+ minNumElements?: number,
55
+ // The required maximum number of elements
56
+ maxNumElements?: number,
57
+ // The default value for the field
58
+ defaultValue?: string[],
59
+ // If defined, the choices the user can choose from
60
+ choices?: {
61
+ // The title of the choice (human-readable)
62
+ title: string,
63
+ // The value of the choice (what is stored in the DB)
64
+ value: string,
65
+ }[],
66
+ }
67
+ // A number list input field
68
+ | {
69
+ // The type of the field
70
+ type: DBEntryFieldType.NumberArray,
71
+ // The required minimum number of elements
72
+ minNumElements?: number,
73
+ // The required maximum number of elements
74
+ maxNumElements?: number,
75
+ // The required minimum number
76
+ minNumber?: number,
77
+ // The required maximum number
78
+ maxNumber?: number,
79
+ // The default value for the field
80
+ defaultValue?: number[],
81
+ }
82
+ // Object input field
83
+ | {
84
+ // The type of the field
85
+ type: DBEntryFieldType.Object,
86
+ // The required minimum number of elements
87
+ defaultValue?: { [k: string]: any },
88
+ // The required minimum number of elements
89
+ subfields: DBEntryField[],
90
+ }
91
+ )
92
+ );
93
+
94
+ export default DBEntryField;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Options for field types
3
+ * @author Yuen Ler Chow
4
+ */
5
+ enum DBEntryFieldType {
6
+ // A string input field
7
+ String = 'String',
8
+ // A number input field
9
+ Number = 'Number',
10
+ // input field with subfields that are also DBEntryFields
11
+ Object = 'Object',
12
+ // list of strings input field
13
+ StringArray = 'StringArray',
14
+ // list of numbers input field
15
+ NumberArray = 'NumberArray',
16
+ }
17
+
18
+ export default DBEntryFieldType;