dce-reactkit 3.2.2-beta.4 → 3.2.2-beta.41

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 (32) hide show
  1. package/dist/cjs/index.js +404 -201
  2. package/dist/cjs/index.js.map +1 -1
  3. package/dist/cjs/types/components/ButtonInputGroup.d.ts +1 -0
  4. package/dist/cjs/types/components/Drawer.d.ts +1 -0
  5. package/dist/cjs/types/components/ItemPicker/NestableItemList.d.ts +1 -0
  6. package/dist/cjs/types/components/ItemPicker/index.d.ts +1 -0
  7. package/dist/cjs/types/types/LogAction.d.ts +16 -16
  8. package/dist/cjs/types/types/LogBuiltInMetadata.d.ts +1 -1
  9. package/dist/esm/index.js +405 -202
  10. package/dist/esm/index.js.map +1 -1
  11. package/dist/esm/types/components/ButtonInputGroup.d.ts +1 -0
  12. package/dist/esm/types/components/Drawer.d.ts +1 -0
  13. package/dist/esm/types/components/ItemPicker/NestableItemList.d.ts +1 -0
  14. package/dist/esm/types/components/ItemPicker/index.d.ts +1 -0
  15. package/dist/esm/types/types/LogAction.d.ts +16 -16
  16. package/dist/esm/types/types/LogBuiltInMetadata.d.ts +1 -1
  17. package/dist/index.d.ts +20 -17
  18. package/package.json +1 -1
  19. package/src/components/ButtonInputGroup.tsx +5 -2
  20. package/src/components/CSVDownloadButton.tsx +1 -1
  21. package/src/components/Drawer.tsx +9 -1
  22. package/src/components/IntelliTable.tsx +95 -23
  23. package/src/components/ItemPicker/NestableItemList.tsx +32 -14
  24. package/src/components/ItemPicker/index.tsx +4 -0
  25. package/src/components/LogReviewer.tsx +301 -147
  26. package/src/components/SimpleDateChooser.tsx +20 -7
  27. package/src/helpers/genCSV.ts +21 -4
  28. package/src/helpers/genRouteHandler.ts +2 -2
  29. package/src/helpers/logClientEvent.tsx +6 -1
  30. package/src/server/initServer.ts +11 -4
  31. package/src/types/LogAction.ts +16 -16
  32. package/src/types/LogBuiltInMetadata.ts +5 -5
@@ -7,6 +7,7 @@ declare type Props = {
7
7
  label: string;
8
8
  minLabelWidth?: string;
9
9
  children: React.ReactNode;
10
+ className?: string;
10
11
  };
11
12
  declare const ButtonInputGroup: React.FC<Props>;
12
13
  export default ButtonInputGroup;
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import React from 'react';
6
6
  declare type Props = {
7
+ customBackgroundColor?: string;
7
8
  children: React.ReactNode;
8
9
  };
9
10
  declare const Drawer: React.FC<Props>;
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Reusable nested item picker
3
3
  * @author Yuen Ler Chow
4
+ * @author Gabe Abrams
4
5
  */
5
6
  import React from 'react';
6
7
  import PickableItem from './types/PickableItem';
@@ -13,6 +13,7 @@ declare type Props = {
13
13
  * values updated
14
14
  */
15
15
  onChanged: (updatedItems: PickableItem[]) => void;
16
+ noBottomMargin?: boolean;
16
17
  };
17
18
  declare const ItemPicker: React.FC<Props>;
18
19
  export default ItemPicker;
@@ -3,21 +3,21 @@
3
3
  * @author Gabe Abrams
4
4
  */
5
5
  declare enum LogAction {
6
- Open = "open",
7
- Close = "close",
8
- Cancel = "cancel",
9
- Expand = "expand",
10
- Collapse = "collapse",
11
- View = "view",
12
- Interrupt = "interrupt",
13
- Create = "create",
14
- Edit = "edit",
15
- Delete = "delete",
16
- Add = "add",
17
- Remove = "remove",
18
- Activate = "activate",
19
- Deactivate = "deactivate",
20
- Peek = "peek",
21
- Unknown = "unknown"
6
+ Open = "Open",
7
+ Close = "Close",
8
+ Cancel = "Cancel",
9
+ Expand = "Expand",
10
+ Collapse = "Collapse",
11
+ View = "View",
12
+ Interrupt = "Interrupt",
13
+ Create = "Create",
14
+ Edit = "Edit",
15
+ Delete = "Delete",
16
+ Add = "Add",
17
+ Remove = "Remove",
18
+ Activate = "Activate",
19
+ Deactivate = "Deactivate",
20
+ Peek = "Peek",
21
+ Unknown = "Unknown"
22
22
  }
23
23
  export default LogAction;
@@ -10,7 +10,7 @@ declare const LogBuiltInMetadata: {
10
10
  ClientFatalError: string;
11
11
  };
12
12
  Target: {
13
- NoSpecificTarget: string;
13
+ NoTarget: string;
14
14
  };
15
15
  };
16
16
  export default LogBuiltInMetadata;
package/dist/index.d.ts CHANGED
@@ -242,6 +242,7 @@ declare type Props$a = {
242
242
  label: string;
243
243
  minLabelWidth?: string;
244
244
  children: React.ReactNode;
245
+ className?: string;
245
246
  };
246
247
  declare const ButtonInputGroup: React.FC<Props$a>;
247
248
 
@@ -274,6 +275,7 @@ declare const SimpleDateChooser: React.FC<Props$9>;
274
275
  */
275
276
 
276
277
  declare type Props$8 = {
278
+ customBackgroundColor?: string;
277
279
  children: React.ReactNode;
278
280
  };
279
281
  declare const Drawer: React.FC<Props$8>;
@@ -363,6 +365,7 @@ declare type Props$3 = {
363
365
  * values updated
364
366
  */
365
367
  onChanged: (updatedItems: PickableItem[]) => void;
368
+ noBottomMargin?: boolean;
366
369
  };
367
370
  declare const ItemPicker: React.FC<Props$3>;
368
371
 
@@ -667,22 +670,22 @@ declare type LogSourceSpecificInfo = ({
667
670
  * @author Gabe Abrams
668
671
  */
669
672
  declare enum LogAction {
670
- Open = "open",
671
- Close = "close",
672
- Cancel = "cancel",
673
- Expand = "expand",
674
- Collapse = "collapse",
675
- View = "view",
676
- Interrupt = "interrupt",
677
- Create = "create",
678
- Edit = "edit",
679
- Delete = "delete",
680
- Add = "add",
681
- Remove = "remove",
682
- Activate = "activate",
683
- Deactivate = "deactivate",
684
- Peek = "peek",
685
- Unknown = "unknown"
673
+ Open = "Open",
674
+ Close = "Close",
675
+ Cancel = "Cancel",
676
+ Expand = "Expand",
677
+ Collapse = "Collapse",
678
+ View = "View",
679
+ Interrupt = "Interrupt",
680
+ Create = "Create",
681
+ Edit = "Edit",
682
+ Delete = "Delete",
683
+ Add = "Add",
684
+ Remove = "Remove",
685
+ Activate = "Activate",
686
+ Deactivate = "Deactivate",
687
+ Peek = "Peek",
688
+ Unknown = "Unknown"
686
689
  }
687
690
 
688
691
  /**
@@ -1026,7 +1029,7 @@ declare const LogBuiltInMetadata: {
1026
1029
  ClientFatalError: string;
1027
1030
  };
1028
1031
  Target: {
1029
- NoSpecificTarget: string;
1032
+ NoTarget: string;
1030
1033
  };
1031
1034
  };
1032
1035
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dce-reactkit",
3
- "version": "3.2.2-beta.4",
3
+ "version": "3.2.2-beta.41",
4
4
  "description": "Shared components for Harvard DCE apps",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -18,6 +18,8 @@ type Props = {
18
18
  minLabelWidth?: string,
19
19
  // Buttons
20
20
  children: React.ReactNode,
21
+ // Additional class names to add to the outer container
22
+ className?: string,
21
23
  };
22
24
 
23
25
  /*------------------------------------------------------------------------*/
@@ -36,6 +38,7 @@ const ButtonInputGroup: React.FC<Props> = (props) => {
36
38
  label,
37
39
  minLabelWidth,
38
40
  children,
41
+ className,
39
42
  } = props;
40
43
 
41
44
  /*------------------------------------------------------------------------*/
@@ -47,7 +50,7 @@ const ButtonInputGroup: React.FC<Props> = (props) => {
47
50
  /*----------------------------------------*/
48
51
 
49
52
  return (
50
- <div className="input-group">
53
+ <div className={`input-group ${className ?? ''}`}>
51
54
  <div className="input-group-prepend d-flex w-100">
52
55
  {/* Label */}
53
56
  <span
@@ -66,7 +69,7 @@ const ButtonInputGroup: React.FC<Props> = (props) => {
66
69
 
67
70
  {/* Contents */}
68
71
  <span
69
- className="input-group-text flex-grow-1 rounded-right"
72
+ className="input-group-text flex-grow-1 rounded-right d-flex flex-wrap"
70
73
  style={{
71
74
  backgroundColor: 'white',
72
75
  borderTopLeftRadius: 0,
@@ -84,8 +84,8 @@ const CSVDownloadButton: React.FC<Props> = (props) => {
84
84
  <>
85
85
  <FontAwesomeIcon
86
86
  icon={faCloudDownloadAlt}
87
- className="mr-2"
88
87
  />
88
+ {' '}
89
89
  Download CSV
90
90
  </>
91
91
  )}
@@ -12,6 +12,8 @@ import React from 'react';
12
12
 
13
13
  // Props definition
14
14
  type Props = {
15
+ // Custom background color
16
+ customBackgroundColor?: string,
15
17
  // Children to display
16
18
  children: React.ReactNode,
17
19
  };
@@ -49,6 +51,7 @@ const Drawer: React.FC<Props> = (props) => {
49
51
 
50
52
  // Destructure all props
51
53
  const {
54
+ customBackgroundColor,
52
55
  children,
53
56
  } = props;
54
57
 
@@ -61,7 +64,12 @@ const Drawer: React.FC<Props> = (props) => {
61
64
  /*----------------------------------------*/
62
65
 
63
66
  return (
64
- <div className="Drawer-container">
67
+ <div
68
+ className="Drawer-container"
69
+ style={{
70
+ backgroundColor: (customBackgroundColor ?? undefined),
71
+ }}
72
+ >
65
73
  <style>{style}</style>
66
74
  {children}
67
75
  </div>
@@ -30,6 +30,7 @@ import Modal from './Modal';
30
30
  import ModalType from '../types/ModalType';
31
31
  import CheckboxButton from './CheckboxButton';
32
32
  import CSVDownloadButton from './CSVDownloadButton';
33
+ import Variant from '../types/Variant';
33
34
 
34
35
  /*------------------------------------------------------------------------*/
35
36
  /* Types */
@@ -86,7 +87,7 @@ enum ActionType {
86
87
  // Toggle sort column param
87
88
  ToggleSortColumn = 'toggle-sort-column',
88
89
  // Toggle the visibility of a column
89
- ToggleColumnVisibility = 'toggle-column-visibility',
90
+ UpdateColumnVisibility = 'update-column-visibility',
90
91
  // Toggle the column visibility customization modal
91
92
  ToggleColVisCusModalVisibility = 'toggle-col-vis-cus-modal-visibility',
92
93
  }
@@ -101,9 +102,11 @@ type Action = (
101
102
  }
102
103
  | {
103
104
  // Action type
104
- type: ActionType.ToggleColumnVisibility,
105
- // Param for the column to toggle
105
+ type: ActionType.UpdateColumnVisibility,
106
+ // Param for the column to update
106
107
  param: string,
108
+ // If true, make the column visible
109
+ visible: boolean,
107
110
  }
108
111
  | {
109
112
  // Action type
@@ -144,9 +147,9 @@ const reducer = (state: State, action: Action): State => {
144
147
  sortType: SortType.Ascending,
145
148
  };
146
149
  }
147
- case ActionType.ToggleColumnVisibility: {
150
+ case ActionType.UpdateColumnVisibility: {
148
151
  const { columnVisibilityMap } = state;
149
- columnVisibilityMap[action.param] = !columnVisibilityMap[action.param];
152
+ columnVisibilityMap[action.param] = action.visible;
150
153
  return {
151
154
  ...state,
152
155
  columnVisibilityMap,
@@ -179,10 +182,16 @@ const IntelliTable: React.FC<Props> = (props) => {
179
182
  const {
180
183
  title,
181
184
  id,
182
- data,
183
185
  columns,
184
186
  } = props;
185
187
 
188
+ // Get data, show empty row if none
189
+ const data = (
190
+ (props.data && props.data.length > 0)
191
+ ? props.data
192
+ : [{ id: 'empty-row' }]
193
+ );
194
+
186
195
  /* -------------- State ------------- */
187
196
 
188
197
  // Initial state
@@ -234,6 +243,7 @@ const IntelliTable: React.FC<Props> = (props) => {
234
243
  type: ActionType.ToggleColVisCusModalVisibility,
235
244
  });
236
245
  }}
246
+ okayVariant={Variant.Light}
237
247
  >
238
248
  {
239
249
  columns.map((column) => {
@@ -241,15 +251,19 @@ const IntelliTable: React.FC<Props> = (props) => {
241
251
  <CheckboxButton
242
252
  key={column.param}
243
253
  id={`IntelliTable-${id}-toggle-visibility-${column.param}`}
254
+ className="mb-2"
244
255
  text={column.title}
245
- onChanged={() => {
256
+ onChanged={(checked) => {
246
257
  dispatch({
247
- type: ActionType.ToggleColumnVisibility,
258
+ type: ActionType.UpdateColumnVisibility,
248
259
  param: column.param,
260
+ visible: checked,
249
261
  });
250
262
  }}
251
263
  checked={columnVisibilityMap[column.param]}
252
264
  ariaLabel={`show "${column.title}" column`}
265
+ checkedVariant={Variant.Light}
266
+ uncheckedVariant={Variant.Secondary}
253
267
  />
254
268
  );
255
269
  })
@@ -272,12 +286,14 @@ const IntelliTable: React.FC<Props> = (props) => {
272
286
  // Custom info based on current sort type
273
287
  let sortButtonAriaLabel;
274
288
  let sortIcon = faSort;
289
+ let sortingByThisColumn = false;
275
290
  if (!sortColumnParam) {
276
291
  // Not being sorted yet
277
292
  sortButtonAriaLabel = `sort ascending by ${column.title}`;
278
293
  sortIcon = faSort;
279
294
  } else if (column.param === sortColumnParam) {
280
295
  // Already sorted by this column
296
+ sortingByThisColumn = true;
281
297
  if (sortType === SortType.Ascending) {
282
298
  // Sorted ascending
283
299
  sortButtonAriaLabel = `sort descending by ${column.title}`;
@@ -299,17 +315,23 @@ const IntelliTable: React.FC<Props> = (props) => {
299
315
  key={column.param}
300
316
  scope="col"
301
317
  id={`IntelliTable-${id}-header-${column.param}`}
318
+ className="text-start"
319
+ style={{
320
+ borderRight: '0.05rem solid #555',
321
+ borderLeft: '0.05rem solid #555',
322
+ }}
302
323
  >
303
- <div className="d-flex align-items-center justify-content-center flex-row h-100">
324
+ <div className="d-flex align-items-center justify-content-start flex-row h-100">
304
325
  {/* Title */}
305
- <h4 className="m-0">
326
+ <span className="text-nowrap">
306
327
  {column.title}
307
- </h4>
328
+ </span>
308
329
  {/* Buttons */}
309
330
  <div>
310
331
  <button
311
332
  type="button"
312
- className="btn btn-light"
333
+ id={`IntelliTable-${id}-sort-by-${column.param}-button`}
334
+ className={`btn btn-${sortingByThisColumn ? 'light' : 'secondary'} btn-sm ms-1 ps-1 pe-1 pt-0 pb-0`}
313
335
  aria-label={sortButtonAriaLabel}
314
336
  onClick={() => {
315
337
  dispatch({
@@ -347,6 +369,33 @@ const IntelliTable: React.FC<Props> = (props) => {
347
369
  const aVal = a[sortColumnParam];
348
370
  const bVal = b[sortColumnParam];
349
371
 
372
+ // Tiebreaker sort by timestamp, most recent first (used if tied)
373
+ const tiebreaker = (
374
+ (b.timestamp ?? 0)
375
+ - (a.timestamp ?? 0)
376
+ );
377
+
378
+ // Auto-sort undefined and null to end of list
379
+ if (
380
+ (aVal === undefined || aVal === null)
381
+ || (bVal === undefined || bVal === null)
382
+ ) {
383
+ // At least one was undefined
384
+ if (
385
+ (aVal === undefined || aVal === null)
386
+ && (bVal === undefined || bVal === null)
387
+ ) {
388
+ // Both undefined
389
+ return tiebreaker;
390
+ }
391
+ if (aVal === undefined || aVal === null) {
392
+ // First was undefined
393
+ return 1;
394
+ }
395
+ // Second was undefined
396
+ return -1;
397
+ }
398
+
350
399
  // Sort differently based on the data type
351
400
  // > Boolean
352
401
  if (paramType === ParamType.Boolean) {
@@ -356,7 +405,7 @@ const IntelliTable: React.FC<Props> = (props) => {
356
405
  if (!aVal && bVal) {
357
406
  return (descending ? 1 : -1);
358
407
  }
359
- return 0;
408
+ return tiebreaker;
360
409
  }
361
410
  // > Number
362
411
  if (
@@ -372,12 +421,12 @@ const IntelliTable: React.FC<Props> = (props) => {
372
421
  // > String
373
422
  if (paramType === ParamType.String) {
374
423
  if (aVal < bVal) {
375
- return (descending ? -1 : 1);
424
+ return (descending ? 1 : -1);
376
425
  }
377
426
  if (aVal > bVal) {
378
- return (descending ? 1 : -1);
427
+ return (descending ? -1 : 1);
379
428
  }
380
- return 0;
429
+ return tiebreaker;
381
430
  }
382
431
  // > JSON
383
432
  if (paramType === ParamType.JSON) {
@@ -399,7 +448,7 @@ const IntelliTable: React.FC<Props> = (props) => {
399
448
  }
400
449
 
401
450
  // No sort
402
- return 0;
451
+ return tiebreaker;
403
452
  });
404
453
  }
405
454
 
@@ -431,16 +480,19 @@ const IntelliTable: React.FC<Props> = (props) => {
431
480
  noValue
432
481
  ? (
433
482
  <FontAwesomeIcon
434
- icon={faCheckCircle}
483
+ icon={faMinus}
435
484
  />
436
485
  )
437
486
  : (
438
487
  <FontAwesomeIcon
439
- icon={faXmarkCircle}
488
+ icon={fullValue ? faCheckCircle : faXmarkCircle}
440
489
  />
441
490
  )
442
491
  );
443
492
  title = (fullValue ? 'True' : 'False');
493
+ if (noValue) {
494
+ title = 'Empty Cell';
495
+ }
444
496
  } else if (column.type === ParamType.Int) {
445
497
  fullValue = Number.parseInt(value, 10);
446
498
  const noValue = Number.isNaN(fullValue);
@@ -454,6 +506,9 @@ const IntelliTable: React.FC<Props> = (props) => {
454
506
  : fullValue
455
507
  );
456
508
  title = String(fullValue);
509
+ if (noValue) {
510
+ title = 'Empty Cell';
511
+ }
457
512
  } else if (column.type === ParamType.Float) {
458
513
  fullValue = Number.parseFloat(value);
459
514
  const noValue = Number.isNaN(fullValue);
@@ -467,9 +522,16 @@ const IntelliTable: React.FC<Props> = (props) => {
467
522
  : roundToNumDecimals(fullValue as number, 2)
468
523
  );
469
524
  title = String(fullValue);
525
+ if (noValue) {
526
+ title = 'Empty Cell';
527
+ }
470
528
  } else if (column.type === ParamType.String) {
471
529
  fullValue = String(value).trim();
472
- const noValue = (value.trim().length) === 0;
530
+ const noValue = (
531
+ value === undefined
532
+ || value === null
533
+ || String(fullValue).trim().length === 0
534
+ );
473
535
  visibleValue = (
474
536
  noValue
475
537
  ? (
@@ -480,6 +542,9 @@ const IntelliTable: React.FC<Props> = (props) => {
480
542
  : fullValue
481
543
  );
482
544
  title = `"${value}"`;
545
+ if (noValue) {
546
+ title = 'Empty Cell';
547
+ }
483
548
  } else if (column.type === ParamType.JSON) {
484
549
  fullValue = JSON.stringify(value);
485
550
  const noValue = (
@@ -496,6 +561,7 @@ const IntelliTable: React.FC<Props> = (props) => {
496
561
  )
497
562
  : fullValue
498
563
  );
564
+ title="JSON Object"
499
565
  }
500
566
 
501
567
  // Create UI
@@ -503,6 +569,10 @@ const IntelliTable: React.FC<Props> = (props) => {
503
569
  <td
504
570
  key={`${datum.id}-${column.param}`}
505
571
  title={title}
572
+ style={{
573
+ borderRight: '0.05rem solid #555',
574
+ borderLeft: '0.05rem solid #555',
575
+ }}
506
576
  >
507
577
  {visibleValue}
508
578
  </td>
@@ -549,8 +619,10 @@ const IntelliTable: React.FC<Props> = (props) => {
549
619
  // Build main UI
550
620
  return (
551
621
  <div className={`IntelliTable-container-${id}`}>
622
+ {/* Modal */}
623
+ {modal}
552
624
  {/* Header */}
553
- <div className="d-flex align-items-center justify-content-center">
625
+ <div className="d-flex align-items-center justify-content-start">
554
626
  {/* Title */}
555
627
  <h3 className="m-0">
556
628
  {title}
@@ -567,7 +639,7 @@ const IntelliTable: React.FC<Props> = (props) => {
567
639
  {/* Show/Hide Columns */}
568
640
  <button
569
641
  type="button"
570
- className="btn btn-secondary"
642
+ className="btn btn-secondary ms-2"
571
643
  aria-label={`show panel for customizing which columns show in table ${title}`}
572
644
  id={`IntelliTable-${id}-show-column-customization-modal`}
573
645
  onClick={() => {
@@ -591,7 +663,7 @@ const IntelliTable: React.FC<Props> = (props) => {
591
663
  </div>
592
664
  {/* Table */}
593
665
  <div
594
- className={`IntelliTable-table-${id}`}
666
+ className={`IntelliTable-table-${id} mt-2`}
595
667
  style={{
596
668
  overflowX: 'auto',
597
669
  }}
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Reusable nested item picker
3
3
  * @author Yuen Ler Chow
4
+ * @author Gabe Abrams
4
5
  */
5
6
 
6
7
  // Import React
@@ -43,36 +44,46 @@ type Props = {
43
44
  /* -------- State Definition -------- */
44
45
 
45
46
  type State = {
46
- // True if children are being shown
47
- isShowingItems: boolean,
47
+ // Map of children that are expanded
48
+ childExpanded: {
49
+ [k: string]: boolean
50
+ },
48
51
  };
49
52
 
50
53
  /* ------------- Actions ------------ */
51
54
 
52
55
  // Types of actions
53
56
  enum ActionType {
54
- // Toggle whether the children are being shown
55
- ToggleItems = 'toggle-items',
57
+ // Toggle whether a child are being shown
58
+ ToggleChild = 'toggle-child',
56
59
  }
57
60
 
58
61
  // Action definitions
59
62
  type Action = (
60
63
  | {
61
64
  // Action type
62
- type: ActionType.ToggleItems,
65
+ type: ActionType.ToggleChild,
66
+ // Id of the child to show
67
+ id: (string | number),
63
68
  }
64
69
  );
65
70
 
66
71
  /**
67
72
  * Reducer that executes actions
68
- * @author Yuen Ler Chow
73
+ * @author Gabe Abrams
69
74
  * @param state current state
70
75
  * @param action action to execute
71
76
  */
72
77
  const reducer = (state: State, action: Action): State => {
73
78
  switch (action.type) {
74
- case ActionType.ToggleItems: {
75
- return { isShowingItems: !state.isShowingItems };
79
+ case ActionType.ToggleChild: {
80
+ return {
81
+ ...state,
82
+ childExpanded: {
83
+ ...state.childExpanded,
84
+ [String(action.id)]: !state.childExpanded[String(action.id)],
85
+ },
86
+ };
76
87
  }
77
88
  default: {
78
89
  return state;
@@ -99,9 +110,15 @@ const NestableItemList: React.FC<Props> = (props) => {
99
110
 
100
111
  /* -------------- State ------------- */
101
112
 
113
+ // Create initial map of child expanded booleans
114
+ const initChildExpanded: { [k: string]: boolean } = {};
115
+ items.forEach((item) => {
116
+ initChildExpanded[String(item.id)] = false;
117
+ });
118
+
102
119
  // Initial state
103
120
  const initialState: State = {
104
- isShowingItems: false,
121
+ childExpanded: initChildExpanded,
105
122
  };
106
123
 
107
124
  // Initialize state
@@ -109,7 +126,7 @@ const NestableItemList: React.FC<Props> = (props) => {
109
126
 
110
127
  // Destructure common state
111
128
  const {
112
- isShowingItems,
129
+ childExpanded,
113
130
  } = state;
114
131
 
115
132
  /*------------------------------------------------------------------------*/
@@ -231,13 +248,14 @@ const NestableItemList: React.FC<Props> = (props) => {
231
248
  type="button"
232
249
  onClick={() => {
233
250
  dispatch({
234
- type: ActionType.ToggleItems,
251
+ type: ActionType.ToggleChild,
252
+ id: item.id,
235
253
  });
236
254
  }}
237
- aria-label={`${isShowingItems ? 'Hide' : 'Show'} items in ${item.name}`}
255
+ aria-label={`${childExpanded[item.id] ? 'Hide' : 'Show'} items in ${item.name}`}
238
256
  >
239
257
  <FontAwesomeIcon
240
- icon={isShowingItems ? faChevronDown : faChevronRight}
258
+ icon={childExpanded[item.id] ? faChevronDown : faChevronRight}
241
259
  />
242
260
  </button>
243
261
  )}
@@ -257,7 +275,7 @@ const NestableItemList: React.FC<Props> = (props) => {
257
275
  />
258
276
 
259
277
  {/* Children */}
260
- {item.isGroup && isShowingItems && (
278
+ {(item.isGroup && childExpanded[item.id]) && (
261
279
  <div
262
280
  className="NestableItemList-children-container"
263
281
  style={{
@@ -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