dce-reactkit 3.2.1 → 3.2.2-beta.10

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 (62) hide show
  1. package/.vscode/settings.json +3 -0
  2. package/dist/cjs/index.js +2195 -442
  3. package/dist/cjs/index.js.map +1 -1
  4. package/dist/cjs/types/components/ButtonInputGroup.d.ts +1 -0
  5. package/dist/cjs/types/components/CSVDownloadButton.d.ts +19 -0
  6. package/dist/cjs/types/components/IntelliTable.d.ts +17 -0
  7. package/dist/cjs/types/components/ItemPicker/index.d.ts +1 -0
  8. package/dist/cjs/types/components/LogReviewer.d.ts +13 -0
  9. package/dist/cjs/types/components/SimpleDateChooser.d.ts +1 -0
  10. package/dist/cjs/types/constants/LOG_REVIEW_ROUTE_PATH_PREFIX.d.ts +6 -0
  11. package/dist/cjs/types/constants/LOG_REVIEW_STATUS_ROUTE.d.ts +7 -0
  12. package/dist/cjs/types/helpers/canReviewLogs.d.ts +7 -0
  13. package/dist/cjs/types/helpers/genCSV.d.ts +14 -0
  14. package/dist/cjs/types/helpers/getMonthName.d.ts +14 -0
  15. package/dist/cjs/types/index.d.ts +9 -1
  16. package/dist/cjs/types/server/initServer.d.ts +8 -0
  17. package/dist/cjs/types/types/IntelliTableColumn.d.ts +12 -0
  18. package/dist/cjs/types/types/LogBuiltInMetadata.d.ts +1 -0
  19. package/dist/cjs/types/types/LogMetadataType.d.ts +19 -0
  20. package/dist/cjs/types/types/ReactKitErrorCode.d.ts +2 -1
  21. package/dist/esm/index.js +2191 -444
  22. package/dist/esm/index.js.map +1 -1
  23. package/dist/esm/types/components/ButtonInputGroup.d.ts +1 -0
  24. package/dist/esm/types/components/CSVDownloadButton.d.ts +19 -0
  25. package/dist/esm/types/components/IntelliTable.d.ts +17 -0
  26. package/dist/esm/types/components/ItemPicker/index.d.ts +1 -0
  27. package/dist/esm/types/components/LogReviewer.d.ts +13 -0
  28. package/dist/esm/types/components/SimpleDateChooser.d.ts +1 -0
  29. package/dist/esm/types/constants/LOG_REVIEW_ROUTE_PATH_PREFIX.d.ts +6 -0
  30. package/dist/esm/types/constants/LOG_REVIEW_STATUS_ROUTE.d.ts +7 -0
  31. package/dist/esm/types/helpers/canReviewLogs.d.ts +7 -0
  32. package/dist/esm/types/helpers/genCSV.d.ts +14 -0
  33. package/dist/esm/types/helpers/getMonthName.d.ts +14 -0
  34. package/dist/esm/types/index.d.ts +9 -1
  35. package/dist/esm/types/server/initServer.d.ts +8 -0
  36. package/dist/esm/types/types/IntelliTableColumn.d.ts +12 -0
  37. package/dist/esm/types/types/LogBuiltInMetadata.d.ts +1 -0
  38. package/dist/esm/types/types/LogMetadataType.d.ts +19 -0
  39. package/dist/esm/types/types/ReactKitErrorCode.d.ts +2 -1
  40. package/dist/index.d.ts +172 -47
  41. package/package.json +1 -1
  42. package/sandbox.js +78 -17
  43. package/src/components/AppWrapper.tsx +15 -0
  44. package/src/components/ButtonInputGroup.tsx +4 -1
  45. package/src/components/CSVDownloadButton.tsx +102 -0
  46. package/src/components/IntelliTable.tsx +610 -0
  47. package/src/components/ItemPicker/index.tsx +4 -0
  48. package/src/components/LogReviewer.tsx +2091 -0
  49. package/src/components/SimpleDateChooser.tsx +26 -27
  50. package/src/constants/LOG_REVIEW_ROUTE_PATH_PREFIX.ts +9 -0
  51. package/src/constants/LOG_REVIEW_STATUS_ROUTE.ts +10 -0
  52. package/src/helpers/canReviewLogs.ts +33 -0
  53. package/src/helpers/genCSV.ts +59 -0
  54. package/src/helpers/getHumanReadableDate.ts +6 -17
  55. package/src/helpers/getMonthName.ts +29 -0
  56. package/src/helpers/logClientEvent.tsx +5 -0
  57. package/src/index.ts +16 -0
  58. package/src/server/initServer.ts +125 -3
  59. package/src/types/IntelliTableColumn.ts +24 -0
  60. package/src/types/LogBuiltInMetadata.ts +1 -0
  61. package/src/types/LogMetadataType.ts +23 -0
  62. package/src/types/ReactKitErrorCode.tsx +2 -1
@@ -0,0 +1,610 @@
1
+ /**
2
+ * Intelligent table
3
+ * @author Gabe Abrams
4
+ */
5
+
6
+ // Import React
7
+ import React, { useReducer } from 'react';
8
+
9
+ // Import FontAwesome
10
+ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
11
+ import {
12
+ faSort,
13
+ faSortUp,
14
+ faSortDown,
15
+ faCheckCircle,
16
+ faXmarkCircle,
17
+ faMinus,
18
+ } from '@fortawesome/free-solid-svg-icons';
19
+
20
+ // Import shared types
21
+ import ParamType from '../types/ParamType';
22
+ import IntelliTableColumn from '../types/IntelliTableColumn';
23
+
24
+ // Import shared helpers
25
+ import roundToNumDecimals from '../helpers/roundToNumDecimals';
26
+ import genCSV from '../helpers/genCSV';
27
+
28
+ // Import shared components
29
+ import Modal from './Modal';
30
+ import ModalType from '../types/ModalType';
31
+ import CheckboxButton from './CheckboxButton';
32
+ import CSVDownloadButton from './CSVDownloadButton';
33
+
34
+ /*------------------------------------------------------------------------*/
35
+ /* Types */
36
+ /*------------------------------------------------------------------------*/
37
+
38
+ // Props definition
39
+ type Props = {
40
+ // Title of the table
41
+ title: string,
42
+ // Unique hyphenated id of the table (e.g. table-name)
43
+ id: string,
44
+ // Ordered list of data objects
45
+ data: {
46
+ // Required id column
47
+ id: string | number,
48
+ // Rest of data
49
+ [k: string]: any
50
+ }[],
51
+ // Column information
52
+ columns: IntelliTableColumn[],
53
+ };
54
+
55
+ // Sort types
56
+ enum SortType {
57
+ // Ascending
58
+ Ascending = 'ascending',
59
+ // Descending
60
+ Descending = 'descending',
61
+ }
62
+
63
+ /*------------------------------------------------------------------------*/
64
+ /* State */
65
+ /*------------------------------------------------------------------------*/
66
+
67
+ /* -------- State Definition -------- */
68
+
69
+ type State = {
70
+ // Current sort column (or undefined for data order sort)
71
+ sortColumnParam: (undefined | string),
72
+ // Sort type
73
+ sortType: SortType,
74
+ // Map of which columns are visible
75
+ columnVisibilityMap: { // param => true if visible
76
+ [k: string]: boolean,
77
+ },
78
+ // If true, the column visibility customization modal is visible
79
+ columnVisibilityCustomizationModalVisible: boolean,
80
+ };
81
+
82
+ /* ------------- Actions ------------ */
83
+
84
+ // Types of actions
85
+ enum ActionType {
86
+ // Toggle sort column param
87
+ ToggleSortColumn = 'toggle-sort-column',
88
+ // Toggle the visibility of a column
89
+ ToggleColumnVisibility = 'toggle-column-visibility',
90
+ // Toggle the column visibility customization modal
91
+ ToggleColVisCusModalVisibility = 'toggle-col-vis-cus-modal-visibility',
92
+ }
93
+
94
+ // Action definitions
95
+ type Action = (
96
+ | {
97
+ // Action type
98
+ type: ActionType.ToggleSortColumn,
99
+ // Param for the column to toggle
100
+ param: string,
101
+ }
102
+ | {
103
+ // Action type
104
+ type: ActionType.ToggleColumnVisibility,
105
+ // Param for the column to toggle
106
+ param: string,
107
+ }
108
+ | {
109
+ // Action type
110
+ type: (
111
+ | ActionType.ToggleColVisCusModalVisibility
112
+ ),
113
+ }
114
+ );
115
+
116
+ /**
117
+ * Reducer that executes actions
118
+ * @author Gabe Abrams
119
+ * @param state current state
120
+ * @param action action to execute
121
+ */
122
+ const reducer = (state: State, action: Action): State => {
123
+ switch (action.type) {
124
+ case ActionType.ToggleSortColumn: {
125
+ if (action.param !== state.sortColumnParam) {
126
+ // Different column param
127
+ return {
128
+ ...state,
129
+ sortColumnParam: action.param,
130
+ sortType: SortType.Ascending,
131
+ };
132
+ }
133
+ if (state.sortType === SortType.Ascending) {
134
+ // Switch to descending
135
+ return {
136
+ ...state,
137
+ sortType: SortType.Descending,
138
+ };
139
+ }
140
+ // Stop sorting by column
141
+ return {
142
+ ...state,
143
+ sortColumnParam: undefined,
144
+ sortType: SortType.Ascending,
145
+ };
146
+ }
147
+ case ActionType.ToggleColumnVisibility: {
148
+ const { columnVisibilityMap } = state;
149
+ columnVisibilityMap[action.param] = !columnVisibilityMap[action.param];
150
+ return {
151
+ ...state,
152
+ columnVisibilityMap,
153
+ };
154
+ }
155
+ case ActionType.ToggleColVisCusModalVisibility: {
156
+ return {
157
+ ...state,
158
+ columnVisibilityCustomizationModalVisible: !state.columnVisibilityCustomizationModalVisible,
159
+ };
160
+ }
161
+ default: {
162
+ return state;
163
+ }
164
+ }
165
+ };
166
+
167
+ /*------------------------------------------------------------------------*/
168
+ /* Component */
169
+ /*------------------------------------------------------------------------*/
170
+
171
+ const IntelliTable: React.FC<Props> = (props) => {
172
+ /*------------------------------------------------------------------------*/
173
+ /* Setup */
174
+ /*------------------------------------------------------------------------*/
175
+
176
+ /* -------------- Props ------------- */
177
+
178
+ // Destructure all props
179
+ const {
180
+ title,
181
+ id,
182
+ data,
183
+ columns,
184
+ } = props;
185
+
186
+ /* -------------- State ------------- */
187
+
188
+ // Initial state
189
+ const initColumnVisibilityMap: {
190
+ [k: string]: boolean
191
+ } = {};
192
+ columns.forEach((column) => {
193
+ initColumnVisibilityMap[column.param] = !column.startsHidden;
194
+ });
195
+ const initialState: State = {
196
+ sortColumnParam: undefined,
197
+ sortType: SortType.Descending,
198
+ columnVisibilityMap: initColumnVisibilityMap,
199
+ columnVisibilityCustomizationModalVisible: false,
200
+ };
201
+
202
+ // Initialize state
203
+ const [state, dispatch] = useReducer(reducer, initialState);
204
+
205
+ // Destructure common state
206
+ const {
207
+ sortColumnParam,
208
+ sortType,
209
+ columnVisibilityMap,
210
+ columnVisibilityCustomizationModalVisible,
211
+ } = state;
212
+
213
+ /*------------------------------------------------------------------------*/
214
+ /* Render */
215
+ /*------------------------------------------------------------------------*/
216
+
217
+ /*----------------------------------------*/
218
+ /* Modal */
219
+ /*----------------------------------------*/
220
+
221
+ // Modal that may be defined
222
+ let modal: React.ReactNode;
223
+
224
+ /* ------- Col Vis Customization Modal ------ */
225
+
226
+ if (columnVisibilityCustomizationModalVisible) {
227
+ // Create modal
228
+ modal = (
229
+ <Modal
230
+ type={ModalType.Okay}
231
+ title="Choose columns to show:"
232
+ onClose={() => {
233
+ dispatch({
234
+ type: ActionType.ToggleColVisCusModalVisibility,
235
+ });
236
+ }}
237
+ >
238
+ {
239
+ columns.map((column) => {
240
+ return (
241
+ <CheckboxButton
242
+ key={column.param}
243
+ id={`IntelliTable-${id}-toggle-visibility-${column.param}`}
244
+ text={column.title}
245
+ onChanged={() => {
246
+ dispatch({
247
+ type: ActionType.ToggleColumnVisibility,
248
+ param: column.param,
249
+ });
250
+ }}
251
+ checked={columnVisibilityMap[column.param]}
252
+ ariaLabel={`show "${column.title}" column`}
253
+ />
254
+ );
255
+ })
256
+ }
257
+ </Modal>
258
+ );
259
+ }
260
+
261
+ /*----------------------------------------*/
262
+ /* Main UI */
263
+ /*----------------------------------------*/
264
+
265
+ // Table header
266
+ const headerCells = (
267
+ columns
268
+ .filter((column) => {
269
+ return columnVisibilityMap[column.param];
270
+ })
271
+ .map((column) => {
272
+ // Custom info based on current sort type
273
+ let sortButtonAriaLabel;
274
+ let sortIcon = faSort;
275
+ if (!sortColumnParam) {
276
+ // Not being sorted yet
277
+ sortButtonAriaLabel = `sort ascending by ${column.title}`;
278
+ sortIcon = faSort;
279
+ } else if (column.param === sortColumnParam) {
280
+ // Already sorted by this column
281
+ if (sortType === SortType.Ascending) {
282
+ // Sorted ascending
283
+ sortButtonAriaLabel = `sort descending by ${column.title}`;
284
+ sortIcon = faSortDown;
285
+ } else {
286
+ // Sorted descending
287
+ sortButtonAriaLabel = `stop sorting by ${column.title}`;
288
+ sortIcon = faSortUp;
289
+ }
290
+ } else {
291
+ // Sorted by a different column
292
+ sortButtonAriaLabel = `sort ascending by ${column.title}`;
293
+ sortIcon = faSort;
294
+ }
295
+
296
+ // Create the cell UI
297
+ return (
298
+ <th
299
+ key={column.param}
300
+ scope="col"
301
+ id={`IntelliTable-${id}-header-${column.param}`}
302
+ >
303
+ <div className="d-flex align-items-center justify-content-center flex-row h-100">
304
+ {/* Title */}
305
+ <span className="text-nowrap">
306
+ {column.title}
307
+ </span>
308
+ {/* Buttons */}
309
+ <div>
310
+ <button
311
+ type="button"
312
+ className="btn btn-light btn-sm ms-1"
313
+ aria-label={sortButtonAriaLabel}
314
+ onClick={() => {
315
+ dispatch({
316
+ type: ActionType.ToggleSortColumn,
317
+ param: column.param,
318
+ });
319
+ }}
320
+ >
321
+ <FontAwesomeIcon
322
+ icon={sortIcon}
323
+ />
324
+ </button>
325
+ </div>
326
+ </div>
327
+ </th>
328
+ );
329
+ })
330
+ );
331
+ const tableHeader = (
332
+ <thead>
333
+ <tr>
334
+ {headerCells}
335
+ </tr>
336
+ </thead>
337
+ );
338
+
339
+ // Sort data
340
+ let sortedData = [...data];
341
+ const paramType = columns.find((column) => {
342
+ return (column.param === sortColumnParam);
343
+ })?.type;
344
+ const descending = (sortType === SortType.Descending);
345
+ if (sortColumnParam) {
346
+ sortedData.sort((a, b) => {
347
+ const aVal = a[sortColumnParam];
348
+ const bVal = b[sortColumnParam];
349
+
350
+ // Sort differently based on the data type
351
+ // > Boolean
352
+ if (paramType === ParamType.Boolean) {
353
+ if (aVal && !bVal) {
354
+ return (descending ? -1 : 1);
355
+ }
356
+ if (!aVal && bVal) {
357
+ return (descending ? 1 : -1);
358
+ }
359
+ return 0;
360
+ }
361
+ // > Number
362
+ if (
363
+ paramType === ParamType.Int
364
+ || paramType === ParamType.Float
365
+ ) {
366
+ return (
367
+ descending
368
+ ? (bVal - aVal)
369
+ : (aVal - bVal)
370
+ );
371
+ }
372
+ // > String
373
+ if (paramType === ParamType.String) {
374
+ if (aVal < bVal) {
375
+ return (descending ? -1 : 1);
376
+ }
377
+ if (aVal > bVal) {
378
+ return (descending ? 1 : -1);
379
+ }
380
+ return 0;
381
+ }
382
+ // > JSON
383
+ if (paramType === ParamType.JSON) {
384
+ const aSize = (
385
+ Array.isArray(aVal)
386
+ ? aVal.length
387
+ : Object.keys(aVal).length
388
+ );
389
+ const bSize = (
390
+ Array.isArray(bVal)
391
+ ? bVal.length
392
+ : Object.keys(bVal).length
393
+ );
394
+ return (
395
+ descending
396
+ ? (bSize - aSize)
397
+ : (aSize - bSize)
398
+ );
399
+ }
400
+
401
+ // No sort
402
+ return 0;
403
+ });
404
+ }
405
+
406
+ // Table body
407
+ const rows = sortedData.map((datum) => {
408
+ // Build cells
409
+ const cells = (
410
+ columns
411
+ .filter((column) => {
412
+ return columnVisibilityMap[column.param];
413
+ })
414
+ .map((column) => {
415
+ // Get value
416
+ let value: any = datum;
417
+ const paramParts = column.param.split('.');
418
+ paramParts.forEach((paramPart) => {
419
+ value = (value ?? {})[paramPart];
420
+ });
421
+ let fullValue: React.ReactNode;
422
+ let visibleValue: React.ReactNode;
423
+ let title: string = '';
424
+ if (column.type === ParamType.Boolean) {
425
+ fullValue = !!(value);
426
+ const noValue = (
427
+ value === undefined
428
+ || value === null
429
+ );
430
+ visibleValue = (
431
+ noValue
432
+ ? (
433
+ <FontAwesomeIcon
434
+ icon={faCheckCircle}
435
+ />
436
+ )
437
+ : (
438
+ <FontAwesomeIcon
439
+ icon={faXmarkCircle}
440
+ />
441
+ )
442
+ );
443
+ title = (fullValue ? 'True' : 'False');
444
+ } else if (column.type === ParamType.Int) {
445
+ fullValue = Number.parseInt(value, 10);
446
+ const noValue = Number.isNaN(fullValue);
447
+ visibleValue = (
448
+ noValue
449
+ ? (
450
+ <FontAwesomeIcon
451
+ icon={faMinus}
452
+ />
453
+ )
454
+ : fullValue
455
+ );
456
+ title = String(fullValue);
457
+ } else if (column.type === ParamType.Float) {
458
+ fullValue = Number.parseFloat(value);
459
+ const noValue = Number.isNaN(fullValue);
460
+ visibleValue = (
461
+ noValue
462
+ ? (
463
+ <FontAwesomeIcon
464
+ icon={faMinus}
465
+ />
466
+ )
467
+ : roundToNumDecimals(fullValue as number, 2)
468
+ );
469
+ title = String(fullValue);
470
+ } else if (column.type === ParamType.String) {
471
+ fullValue = String(value).trim();
472
+ const noValue = (value.trim().length) === 0;
473
+ visibleValue = (
474
+ noValue
475
+ ? (
476
+ <FontAwesomeIcon
477
+ icon={faMinus}
478
+ />
479
+ )
480
+ : fullValue
481
+ );
482
+ title = `"${value}"`;
483
+ } else if (column.type === ParamType.JSON) {
484
+ fullValue = JSON.stringify(value);
485
+ const noValue = (
486
+ Array.isArray(value)
487
+ ? (!value || value.length === 0)
488
+ : Object.keys(value ?? {}).length === 0
489
+ );
490
+ visibleValue = (
491
+ noValue
492
+ ? (
493
+ <FontAwesomeIcon
494
+ icon={faMinus}
495
+ />
496
+ )
497
+ : fullValue
498
+ );
499
+ }
500
+
501
+ // Create UI
502
+ return (
503
+ <td
504
+ key={`${datum.id}-${column.param}`}
505
+ title={title}
506
+ >
507
+ {visibleValue}
508
+ </td>
509
+ );
510
+ })
511
+ );
512
+
513
+ // Add cells to a row
514
+ return (
515
+ <tr key={datum.id}>
516
+ {cells}
517
+ </tr>
518
+ );
519
+ });
520
+ const tableBody = (
521
+ <tbody>
522
+ {rows}
523
+ </tbody>
524
+ );
525
+
526
+ // Build table
527
+ const table = (
528
+ <table className="table table-dark table-striped">
529
+ {tableHeader}
530
+ {tableBody}
531
+ </table>
532
+ );
533
+
534
+ // Count the number of hidden columns
535
+ const numHiddenCols = (
536
+ Object.values(columnVisibilityMap)
537
+ .filter((isVisible) => {
538
+ return !isVisible;
539
+ })
540
+ .length
541
+ );
542
+
543
+ // Build CSV
544
+ const csv = genCSV(
545
+ data,
546
+ columns,
547
+ );
548
+
549
+ // Build main UI
550
+ return (
551
+ <div className={`IntelliTable-container-${id}`}>
552
+ {/* Header */}
553
+ <div className="d-flex align-items-center justify-content-center">
554
+ {/* Title */}
555
+ <h3 className="m-0">
556
+ {title}
557
+ </h3>
558
+ {/* Buttons */}
559
+ <div className="flex-grow-1 text-end">
560
+ {/* Download Button */}
561
+ <CSVDownloadButton
562
+ aria-label={`download data as csv for ${title}`}
563
+ id={`IntelliTable-${id}-download-as-csv`}
564
+ filename={`${title}.csv`}
565
+ csv={csv}
566
+ />
567
+ {/* Show/Hide Columns */}
568
+ <button
569
+ type="button"
570
+ className="btn btn-secondary ms-2"
571
+ aria-label={`show panel for customizing which columns show in table ${title}`}
572
+ id={`IntelliTable-${id}-show-column-customization-modal`}
573
+ onClick={() => {
574
+ dispatch({
575
+ type: ActionType.ToggleColVisCusModalVisibility,
576
+ });
577
+ }}
578
+ >
579
+ Show/Hide Cols
580
+ {numHiddenCols > 0 && (
581
+ <>
582
+ {' '}
583
+ (
584
+ {numHiddenCols}
585
+ {' '}
586
+ hidden)
587
+ </>
588
+ )}
589
+ </button>
590
+ </div>
591
+ </div>
592
+ {/* Table */}
593
+ <div
594
+ className={`IntelliTable-table-${id}`}
595
+ style={{
596
+ overflowX: 'auto',
597
+ }}
598
+ >
599
+ {table}
600
+ </div>
601
+ </div>
602
+ );
603
+ };
604
+
605
+ /*------------------------------------------------------------------------*/
606
+ /* Wrap Up */
607
+ /*------------------------------------------------------------------------*/
608
+
609
+ // Export component
610
+ export default IntelliTable;
@@ -31,6 +31,8 @@ type Props = {
31
31
  * values updated
32
32
  */
33
33
  onChanged: (updatedItems: PickableItem[]) => void,
34
+ // If true, don't add margin to bottom of item picker
35
+ noBottomMargin?: boolean,
34
36
  };
35
37
 
36
38
  /*------------------------------------------------------------------------*/
@@ -49,6 +51,7 @@ const ItemPicker: React.FC<Props> = (props) => {
49
51
  title,
50
52
  items,
51
53
  onChanged,
54
+ noBottomMargin,
52
55
  } = props;
53
56
 
54
57
  /*------------------------------------------------------------------------*/
@@ -66,6 +69,7 @@ const ItemPicker: React.FC<Props> = (props) => {
66
69
  return (
67
70
  <TabBox
68
71
  title={title}
72
+ noBottomMargin={noBottomMargin}
69
73
  >
70
74
  <div style={{ overflowX: 'auto' }}>
71
75
  <NestableItemList