dce-reactkit 3.12.1-beta-cross-server.8 → 4.0.0-beta-date-and-time.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 (92) hide show
  1. package/.vscode/settings.json +3 -1
  2. package/README.md +0 -20
  3. package/dist/cjs/index.js +7838 -15905
  4. package/dist/cjs/index.js.map +1 -1
  5. package/dist/cjs/types/components/ItemPicker/index.d.ts +1 -0
  6. package/dist/cjs/types/components/LoadingSpinner.d.ts +2 -2
  7. package/dist/cjs/types/components/Modal/ModalProps.d.ts +2 -0
  8. package/dist/cjs/types/components/SimpleDateChooser.d.ts +3 -1
  9. package/dist/cjs/types/components/SimpleTimeChooser.d.ts +20 -0
  10. package/dist/cjs/types/components/TabBox.d.ts +1 -0
  11. package/dist/cjs/types/components/Tooltip.d.ts +1 -1
  12. package/dist/cjs/types/helpers/getWordCount.d.ts +10 -0
  13. package/dist/cjs/types/index.d.ts +13 -13
  14. package/dist/cjs/types/types/ParamType.d.ts +10 -10
  15. package/dist/cjs/types/types/ReactKitErrorCode.d.ts +2 -28
  16. package/dist/esm/index.js +6661 -14723
  17. package/dist/esm/index.js.map +1 -1
  18. package/dist/esm/types/components/ItemPicker/index.d.ts +1 -0
  19. package/dist/esm/types/components/LoadingSpinner.d.ts +2 -2
  20. package/dist/esm/types/components/Modal/ModalProps.d.ts +2 -0
  21. package/dist/esm/types/components/SimpleDateChooser.d.ts +3 -1
  22. package/dist/esm/types/components/SimpleTimeChooser.d.ts +20 -0
  23. package/dist/esm/types/components/TabBox.d.ts +1 -0
  24. package/dist/esm/types/components/Tooltip.d.ts +1 -1
  25. package/dist/esm/types/helpers/getWordCount.d.ts +10 -0
  26. package/dist/esm/types/index.d.ts +13 -13
  27. package/dist/esm/types/types/ParamType.d.ts +10 -10
  28. package/dist/esm/types/types/ReactKitErrorCode.d.ts +2 -28
  29. package/dist/index.d.ts +189 -379
  30. package/package.json +5 -11
  31. package/src/components/AppWrapper.tsx +3 -1
  32. package/src/components/CheckboxButton.tsx +1 -1
  33. package/src/components/Dropdown.tsx +1 -1
  34. package/src/components/IntelliTable.tsx +1 -1
  35. package/src/components/ItemPicker/NestableItemList.tsx +1 -0
  36. package/src/components/ItemPicker/index.tsx +96 -0
  37. package/src/components/LogReviewer.tsx +2 -2
  38. package/src/components/Modal/ModalProps.ts +4 -0
  39. package/src/components/Modal/index.tsx +60 -21
  40. package/src/components/RadioButton.tsx +3 -1
  41. package/src/components/SimpleDateChooser.tsx +62 -26
  42. package/src/components/SimpleTimeChooser.tsx +195 -0
  43. package/src/components/TabBox.tsx +27 -9
  44. package/src/components/Tooltip.tsx +1 -1
  45. package/src/helpers/getWordCount.ts +26 -0
  46. package/src/{client → helpers}/initClient.tsx +5 -1
  47. package/src/helpers/logClientEvent.tsx +3 -1
  48. package/src/helpers/visitServerEndpoint.tsx +1 -1
  49. package/src/index.ts +22 -24
  50. package/src/types/ParamType.ts +10 -10
  51. package/src/types/ReactKitErrorCode.tsx +3 -31
  52. package/dist/cjs/types/components/ItemPicker/index.test.d.ts +0 -1
  53. package/dist/cjs/types/helpers/addDBEditorEndpoints.d.ts +0 -41
  54. package/dist/cjs/types/helpers/dataSigner.d.ts +0 -40
  55. package/dist/cjs/types/helpers/genRouteHandler.d.ts +0 -76
  56. package/dist/cjs/types/helpers/handleError.d.ts +0 -18
  57. package/dist/cjs/types/helpers/handleSuccess.d.ts +0 -8
  58. package/dist/cjs/types/helpers/parseUserAgent.d.ts +0 -17
  59. package/dist/cjs/types/helpers/visitEndpointOnAnotherServer/index.d.ts +0 -20
  60. package/dist/cjs/types/helpers/visitEndpointOnAnotherServer/sendServerToServerRequest.d.ts +0 -27
  61. package/dist/cjs/types/server/initCrossServerCredentialCollection.d.ts +0 -8
  62. package/dist/cjs/types/server/initLogCollection.d.ts +0 -8
  63. package/dist/cjs/types/server/initServer.d.ts +0 -53
  64. package/dist/cjs/types/types/CrossServerCredential.d.ts +0 -11
  65. package/dist/esm/types/components/ItemPicker/index.test.d.ts +0 -1
  66. package/dist/esm/types/helpers/addDBEditorEndpoints.d.ts +0 -41
  67. package/dist/esm/types/helpers/dataSigner.d.ts +0 -40
  68. package/dist/esm/types/helpers/genRouteHandler.d.ts +0 -76
  69. package/dist/esm/types/helpers/handleError.d.ts +0 -18
  70. package/dist/esm/types/helpers/handleSuccess.d.ts +0 -8
  71. package/dist/esm/types/helpers/parseUserAgent.d.ts +0 -17
  72. package/dist/esm/types/helpers/visitEndpointOnAnotherServer/index.d.ts +0 -20
  73. package/dist/esm/types/helpers/visitEndpointOnAnotherServer/sendServerToServerRequest.d.ts +0 -27
  74. package/dist/esm/types/server/initCrossServerCredentialCollection.d.ts +0 -8
  75. package/dist/esm/types/server/initLogCollection.d.ts +0 -8
  76. package/dist/esm/types/server/initServer.d.ts +0 -53
  77. package/dist/esm/types/types/CrossServerCredential.d.ts +0 -11
  78. package/src/components/ItemPicker/index.test.tsx +0 -783
  79. package/src/helpers/addDBEditorEndpoints.ts +0 -128
  80. package/src/helpers/dataSigner.ts +0 -342
  81. package/src/helpers/genRouteHandler.ts +0 -911
  82. package/src/helpers/handleError.ts +0 -65
  83. package/src/helpers/handleSuccess.ts +0 -18
  84. package/src/helpers/parseUserAgent.ts +0 -108
  85. package/src/helpers/visitEndpointOnAnotherServer/index.ts +0 -152
  86. package/src/helpers/visitEndpointOnAnotherServer/sendServerToServerRequest.ts +0 -162
  87. package/src/server/initCrossServerCredentialCollection.ts +0 -16
  88. package/src/server/initLogCollection.ts +0 -27
  89. package/src/server/initServer.ts +0 -305
  90. package/src/types/CrossServerCredential.ts +0 -16
  91. /package/dist/cjs/types/{client → helpers}/initClient.d.ts +0 -0
  92. /package/dist/esm/types/{client → helpers}/initClient.d.ts +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dce-reactkit",
3
- "version": "3.12.1-beta-cross-server.8",
3
+ "version": "4.0.0-beta-date-and-time.1",
4
4
  "description": "Shared components for Harvard DCE apps",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -25,7 +25,6 @@
25
25
  },
26
26
  "homepage": "https://github.com/harvard-edtech/dce-reactkit#readme",
27
27
  "dependencies": {
28
- "qs": "^6.x.x",
29
28
  "react-select": "^5.7.3"
30
29
  },
31
30
  "peerDependencies": {
@@ -34,22 +33,19 @@
34
33
  "@fortawesome/free-solid-svg-icons": "^x.x.x",
35
34
  "@fortawesome/react-fontawesome": "^x.x.x",
36
35
  "bootstrap": "^5.x.x",
37
- "express": "^x.x.x",
38
- "react": "^x.x.x",
39
- "react-dom": "^x.x.x"
36
+ "react": "^x.x.x"
40
37
  },
41
38
  "devDependencies": {
42
39
  "@rollup/plugin-commonjs": "^22.0.0",
43
40
  "@rollup/plugin-json": "^4.1.0",
44
41
  "@rollup/plugin-node-resolve": "^13.2.1",
45
- "@rollup/plugin-typescript": "^8.3.2",
42
+ "@rollup/plugin-typescript": "^10.0.1",
46
43
  "@types/bootstrap": "^5.2.8",
47
44
  "@types/express": "^4.17.17",
48
45
  "@types/jest": "^28.1.7",
49
- "@types/jwt-simple": "^0.5.36",
50
46
  "@types/node": "^20.2.5",
51
- "@types/oauth-signature": "^1.5.2",
52
- "@types/react": "^17.0.7",
47
+ "@types/react": "^19.0.10",
48
+ "@types/react-dom": "^19.0.4",
53
49
  "@typescript-eslint/eslint-plugin": "^5.59.9",
54
50
  "@typescript-eslint/parser": "^5.59.9",
55
51
  "eslint": "^8.42.0",
@@ -61,8 +57,6 @@
61
57
  "eslint-plugin-react-hooks": "^4.6.0",
62
58
  "jest": "^28.1.3",
63
59
  "jest-environment-jsdom": "^28.1.3",
64
- "oauth-signature": "^1.5.0",
65
- "raixa": "^1.0.21",
66
60
  "rollup": "^2.70.2",
67
61
  "rollup-plugin-dts": "^4.2.1",
68
62
  "rollup-plugin-sourcemaps": "^0.6.3",
@@ -32,10 +32,12 @@ import ErrorWithCode from '../errors/ErrorWithCode';
32
32
  // TODO: fix dependency cycle
33
33
  // eslint-disable-next-line import/no-cycle
34
34
  import logClientEvent from '../helpers/logClientEvent';
35
+ // TODO: fix dependency cycle
36
+ // eslint-disable-next-line import/no-cycle
35
37
  import {
36
38
  getSessionExpiredMessage,
37
39
  isDarkModeOn,
38
- } from '../client/initClient';
40
+ } from '../helpers/initClient';
39
41
  import waitMs from '../helpers/waitMs';
40
42
 
41
43
  // Import constants
@@ -15,7 +15,7 @@ import { faSquare, faSquareMinus } from '@fortawesome/free-regular-svg-icons';
15
15
  import Variant from '../types/Variant';
16
16
 
17
17
  // Import shared helpers
18
- import { isDarkModeOn } from '../client/initClient';
18
+ import { isDarkModeOn } from '../helpers/initClient';
19
19
 
20
20
  /*------------------------------------------------------------------------*/
21
21
  /* -------------------------------- Types ------------------------------- */
@@ -16,7 +16,7 @@ import Variant from '../types/Variant';
16
16
  import DropdownItemType from '../types/DropdownItemType';
17
17
 
18
18
  // Check dark mode
19
- import { isDarkModeOn } from '../client/initClient';
19
+ import { isDarkModeOn } from '../helpers/initClient';
20
20
 
21
21
  /*------------------------------------------------------------------------*/
22
22
  /* -------------------------------- Types ------------------------------- */
@@ -315,7 +315,7 @@ const IntelliTable: React.FC<Props> = (props) => {
315
315
  checked={columnVisibilityMap[column.param]}
316
316
  ariaLabel={`show "${column.title}" column in the ${title} table`}
317
317
  checkedVariant={Variant.Light}
318
- uncheckedVariant={Variant.Secondary}
318
+ uncheckedVariant={Variant.Light}
319
319
  />
320
320
  );
321
321
  })
@@ -272,6 +272,7 @@ const NestableItemList: React.FC<Props> = (props) => {
272
272
  }}
273
273
  ariaLabel={`Select ${item.name}`}
274
274
  checkedVariant={Variant.Light}
275
+ uncheckedVariant={Variant.Light}
275
276
  />
276
277
 
277
278
  {/* Children */}
@@ -33,6 +33,8 @@ type Props = {
33
33
  onChanged: (updatedItems: PickableItem[]) => void,
34
34
  // If true, don't add margin to bottom of item picker
35
35
  noBottomMargin?: boolean,
36
+ // If true, hide select all or none buttons
37
+ hideSelectAllOrNoneButtons?: boolean,
36
38
  };
37
39
 
38
40
  /*------------------------------------------------------------------------*/
@@ -52,12 +54,61 @@ const ItemPicker: React.FC<Props> = (props) => {
52
54
  items,
53
55
  onChanged,
54
56
  noBottomMargin,
57
+ hideSelectAllOrNoneButtons,
55
58
  } = props;
56
59
 
57
60
  /*------------------------------------------------------------------------*/
58
61
  /* ------------------------- Component Functions ------------------------ */
59
62
  /*------------------------------------------------------------------------*/
60
63
 
64
+ /**
65
+ * Updates the checked state of an item (including all children)
66
+ * @author Yuen Ler Chow
67
+ * @param item the item to update
68
+ * @param checked the new checked state
69
+ * @returns the updated item
70
+ */
71
+ const updateItemChecked = (
72
+ item: PickableItem,
73
+ checked: boolean,
74
+ ): PickableItem => {
75
+ if (item.isGroup) {
76
+ return {
77
+ ...item,
78
+ children: item.children.map((child) => {
79
+ return updateItemChecked(child, checked);
80
+ }),
81
+ };
82
+ }
83
+ return { ...item, checked };
84
+ };
85
+
86
+ /**
87
+ * Selects all items in the list
88
+ * @author Yuen Ler Chow
89
+ */
90
+ const handleSelectAll = () => {
91
+ const updatedItems = items.map(
92
+ (item) => {
93
+ return updateItemChecked(item, true);
94
+ },
95
+ );
96
+ onChanged(updatedItems);
97
+ };
98
+
99
+ /**
100
+ * Deselects all items in the list
101
+ * @author Yuen Ler Chow
102
+ */
103
+ const handleDeselectAll = () => {
104
+ const updatedItems = items.map(
105
+ (item) => {
106
+ return updateItemChecked(item, false);
107
+ },
108
+ );
109
+ onChanged(updatedItems);
110
+ };
111
+
61
112
  /*------------------------------------------------------------------------*/
62
113
  /* ------------------------------- Render ------------------------------- */
63
114
  /*------------------------------------------------------------------------*/
@@ -66,10 +117,55 @@ const ItemPicker: React.FC<Props> = (props) => {
66
117
  /* --------------- Main UI -------------- */
67
118
  /*----------------------------------------*/
68
119
 
120
+ // Select all/none
121
+ const selectAllOrNone = (
122
+ !hideSelectAllOrNoneButtons
123
+ ? (
124
+ <div
125
+ className="d-flex h-100 align-items-end flex-row"
126
+ >
127
+ <div className="d-flex justify-content-end">
128
+ {/* Label */}
129
+ <div
130
+ className="me-2"
131
+ style={{ fontSize: '1.2rem' }}
132
+ >
133
+ Select
134
+ </div>
135
+ {/* Buttons */}
136
+ <div className="btn-group" role="group">
137
+ {/* All */}
138
+ <button
139
+ type="button"
140
+ style={{ borderRight: '0.1rem solid white' }}
141
+ aria-label="Select all contexts"
142
+ className="btn btn-secondary py-0"
143
+ onClick={handleSelectAll}
144
+ >
145
+ All
146
+ </button>
147
+ {/* None */}
148
+ <button
149
+ type="button"
150
+ aria-label="Deselect all contexts"
151
+ className="btn btn-secondary py-0"
152
+ onClick={handleDeselectAll}
153
+ >
154
+ None
155
+ </button>
156
+ </div>
157
+ </div>
158
+ </div>
159
+ )
160
+ : undefined
161
+ );
162
+
163
+ // Main UI
69
164
  return (
70
165
  <TabBox
71
166
  title={title}
72
167
  noBottomMargin={noBottomMargin}
168
+ topRightChildren={selectAllOrNone}
73
169
  >
74
170
  <div style={{ overflowX: 'auto' }}>
75
171
  <NestableItemList
@@ -1182,7 +1182,7 @@ const LogReviewer: React.FC<Props> = (props) => {
1182
1182
  year={dateFilterState.startDate.year}
1183
1183
  month={dateFilterState.startDate.month}
1184
1184
  day={dateFilterState.startDate.day}
1185
- chooseFromPast
1185
+ dontAllowFuture
1186
1186
  numMonthsToShow={36}
1187
1187
  onChange={(month, day, year) => {
1188
1188
  dateFilterState.startDate = { month, day, year };
@@ -1198,7 +1198,7 @@ const LogReviewer: React.FC<Props> = (props) => {
1198
1198
  year={dateFilterState.endDate.year}
1199
1199
  month={dateFilterState.endDate.month}
1200
1200
  day={dateFilterState.endDate.day}
1201
- chooseFromPast
1201
+ dontAllowFuture
1202
1202
  numMonthsToShow={12}
1203
1203
  onChange={(month, day, year) => {
1204
1204
  if (
@@ -69,6 +69,10 @@ type ModalProps = {
69
69
  confirmVariant?: Variant,
70
70
  // True if modal should be on top of other modals
71
71
  onTopOfOtherModals?: boolean,
72
+ // If true, the modal is loading
73
+ isLoading?: boolean,
74
+ // If true, the loading is cancelable
75
+ isLoadingCancelable?: boolean,
72
76
  };
73
77
 
74
78
  export default ModalProps;
@@ -14,6 +14,9 @@ import React, { useState, useEffect, useRef } from 'react';
14
14
  import ReactDOM from 'react-dom';
15
15
 
16
16
  // Import other components
17
+ import LoadingSpinner from '../LoadingSpinner';
18
+
19
+ // Import helpers
17
20
  import waitMs from '../../helpers/waitMs';
18
21
 
19
22
  // Import types
@@ -29,7 +32,7 @@ import NUM_MODAL_PORTALS from '../../constants/NUM_MODAL_PORTALS';
29
32
  // Import shared helpers
30
33
  // TODO: fix dependency cycle
31
34
  // eslint-disable-next-line import/no-cycle
32
- import { isDarkModeOn } from '../../client/initClient';
35
+ import { isDarkModeOn } from '../../helpers/initClient';
33
36
 
34
37
  /*------------------------------------------------------------------------*/
35
38
  /* ------------------------------ Constants ----------------------------- */
@@ -41,6 +44,7 @@ const BASE_Z_INDEX_ON_TOP = 2000000000;
41
44
 
42
45
  // Constants
43
46
  const MS_TO_ANIMATE = 200; // Animation duration
47
+ const MS_TO_WAIT_BEFORE_SHOWING_LOADING_INDICATOR = 1000;
44
48
 
45
49
  // Modal type to list of buttons
46
50
  const modalTypeToModalButtonTypes: {
@@ -256,6 +260,8 @@ const Modal: React.FC<ModalProps> = (props) => {
256
260
  dontAllowBackdropExit,
257
261
  dontShowXButton,
258
262
  onTopOfOtherModals,
263
+ isLoading,
264
+ isLoadingCancelable,
259
265
  } = props;
260
266
 
261
267
  // Determine if no header either
@@ -266,6 +272,7 @@ const Modal: React.FC<ModalProps> = (props) => {
266
272
  // True if animation is in use
267
273
  const [animatingIn, setAnimatingIn] = useState(true);
268
274
  const [animatingPop, setAnimatingPop] = useState(false);
275
+ const [showModal, setShowModal] = useState(false);
269
276
 
270
277
  /* -------------- Refs -------------- */
271
278
 
@@ -298,6 +305,15 @@ const Modal: React.FC<ModalProps> = (props) => {
298
305
  if (mounted.current) {
299
306
  setAnimatingIn(false);
300
307
  }
308
+ if (isLoading) {
309
+ // wait before showing modal
310
+ await waitMs(MS_TO_WAIT_BEFORE_SHOWING_LOADING_INDICATOR);
311
+ if (mounted.current) {
312
+ setShowModal(true);
313
+ }
314
+ } else {
315
+ setShowModal(true);
316
+ }
301
317
  })();
302
318
 
303
319
  return () => {
@@ -400,6 +416,17 @@ const Modal: React.FC<ModalProps> = (props) => {
400
416
  animationClass = 'Modal-animating-pop';
401
417
  }
402
418
 
419
+ // default to show close button when not loading and not show close button when loading
420
+ // unless downShowXButton or isLoadingCancelable
421
+ const showCloseButton = (
422
+ // The modal must have an onClose function
423
+ onClose
424
+ // ...and the close button is allowed to be visible
425
+ && !dontShowXButton
426
+ // ...and should not be shown if the modal is loading and not cancelable
427
+ && !(isLoading && !isLoadingCancelable)
428
+ );
429
+
403
430
  // Render the modal
404
431
  const contentToRender = (
405
432
  <div
@@ -436,6 +463,8 @@ const Modal: React.FC<ModalProps> = (props) => {
436
463
  handleClose(ModalButtonType.Cancel);
437
464
  }}
438
465
  />
466
+
467
+ {showModal && (
439
468
  <div
440
469
  className={`modal-dialog modal-${size} ${animationClass} modal-dialog-scrollable modal-dialog-centered`}
441
470
  style={{
@@ -498,7 +527,7 @@ const Modal: React.FC<ModalProps> = (props) => {
498
527
  {title}
499
528
  </h5>
500
529
 
501
- {(onClose && !dontShowXButton) && (
530
+ {showCloseButton && (
502
531
  <button
503
532
  type="button"
504
533
  className="Modal-x-button btn-close"
@@ -518,25 +547,34 @@ const Modal: React.FC<ModalProps> = (props) => {
518
547
  )}
519
548
  </div>
520
549
  )}
521
- {children && (
522
- <div
523
- className="modal-body"
524
- style={{
525
- color: (
526
- isDarkModeOn()
527
- ? 'white'
528
- : undefined
529
- ),
530
- backgroundColor: (
531
- isDarkModeOn()
532
- ? '#444'
533
- : undefined
534
- ),
535
- }}
536
- >
537
- {children}
538
- </div>
539
- )}
550
+
551
+ <div
552
+ className="modal-body"
553
+ style={{
554
+ color: (
555
+ isDarkModeOn()
556
+ ? 'white'
557
+ : undefined
558
+ ),
559
+ backgroundColor: (
560
+ isDarkModeOn()
561
+ ? '#444'
562
+ : undefined
563
+ ),
564
+ }}
565
+ >
566
+ {
567
+ isLoading
568
+ ? (
569
+ <>
570
+ <LoadingSpinner />
571
+ <span className="sr-only">Content loading</span>
572
+ </>
573
+ )
574
+ : children
575
+ }
576
+ </div>
577
+
540
578
  {footer && (
541
579
  <div
542
580
  className="modal-footer pt-1 pb-1"
@@ -563,6 +601,7 @@ const Modal: React.FC<ModalProps> = (props) => {
563
601
  )}
564
602
  </div>
565
603
  </div>
604
+ )}
566
605
  </div>
567
606
  );
568
607
 
@@ -13,7 +13,9 @@ import { faCircle } from '@fortawesome/free-regular-svg-icons';
13
13
 
14
14
  // Import shared types
15
15
  import Variant from '../types/Variant';
16
- import { isDarkModeOn } from '../client/initClient';
16
+
17
+ // Import shared helpers
18
+ import { isDarkModeOn } from '../helpers/initClient';
17
19
 
18
20
  /*------------------------------------------------------------------------*/
19
21
  /* -------------------------------- Types ------------------------------- */
@@ -1,16 +1,23 @@
1
1
  /**
2
2
  * A very simple, lightweight date chooser
3
3
  * @author Gabe Abrams
4
+ * @author Gardeniu Liu
4
5
  */
5
6
 
6
7
  // Import React
7
8
  import React from 'react';
8
- import getMonthName from '../helpers/getMonthName';
9
9
 
10
10
  // Import helpers
11
11
  import getOrdinal from '../helpers/getOrdinal';
12
+ import getMonthName from '../helpers/getMonthName';
12
13
  import getTimeInfoInET from '../helpers/getTimeInfoInET';
13
14
 
15
+ // Import classes
16
+ import ErrorWithCode from '../errors/ErrorWithCode';
17
+
18
+ // Import types
19
+ import ReactKitErrorCode from '../types/ReactKitErrorCode';
20
+
14
21
  /*------------------------------------------------------------------------*/
15
22
  /* -------------------------------- Types ------------------------------- */
16
23
  /*------------------------------------------------------------------------*/
@@ -33,12 +40,14 @@ type Props = {
33
40
  * @param year new full year number
34
41
  */
35
42
  onChange: (month: number, day: number, year: number) => void,
36
- // Number of months to allow the user to choose from
37
- // (max is 12, default is 6)
43
+ // Number of months in either the past or future to allow the user to choose from
44
+ // If we aren't allowing the past or the future, we will throw an error
45
+ // (max is 12, default is 6 in the past and 6 in the future)
38
46
  numMonthsToShow?: number,
39
- // If true, instead of showing numMonthsToShow months into the future,
40
- // show numMonthsToShow months into the past
41
- chooseFromPast?: boolean,
47
+ // If true, the user isn't allowed to select dates in the past
48
+ dontAllowPast?: boolean,
49
+ // If true, the user isn't allowed to select dates in the future
50
+ dontAllowFuture?: boolean,
42
51
  };
43
52
 
44
53
  /*------------------------------------------------------------------------*/
@@ -56,8 +65,9 @@ const SimpleDateChooser: React.FC<Props> = (props) => {
56
65
  ariaLabel,
57
66
  name,
58
67
  onChange,
59
- chooseFromPast,
60
68
  numMonthsToShow = 6,
69
+ dontAllowFuture,
70
+ dontAllowPast,
61
71
  } = props;
62
72
 
63
73
  /*------------------------------------------------------------------------*/
@@ -76,16 +86,41 @@ const SimpleDateChooser: React.FC<Props> = (props) => {
76
86
  days: number[],
77
87
  year: number,
78
88
  }[] = [];
89
+
79
90
  let startYear = today.year;
80
91
  let startMonth = today.month;
81
- if (chooseFromPast) {
92
+
93
+ // Don't allow past or future dates
94
+ if (dontAllowPast && dontAllowFuture) {
95
+ throw new ErrorWithCode(
96
+ 'No past or future dates allowed',
97
+ ReactKitErrorCode.SimpleDateChooserInvalidDateRange,
98
+ );
99
+ }
100
+
101
+ // Require numMonthsToShow to be positive
102
+ if (numMonthsToShow <= 0) {
103
+ throw new ErrorWithCode(
104
+ 'numMonthsToShow must be positive',
105
+ ReactKitErrorCode.SimpleDateChooserInvalidNumMonths,
106
+ );
107
+ }
108
+
109
+ // Recalculate startMonth and startYear when allowing past dates
110
+ if (!dontAllowPast) {
82
111
  startMonth -= Math.max(0, numMonthsToShow - 1);
83
112
  while (startMonth <= 0) {
84
113
  startMonth += 12;
85
114
  startYear -= 1;
86
115
  }
87
116
  }
88
- for (let i = 0; i < numMonthsToShow; i++) {
117
+ // Calculate total number of months to show
118
+ let totalMonthsToShow = numMonthsToShow;
119
+ if (!dontAllowPast && !dontAllowFuture) {
120
+ totalMonthsToShow = totalMonthsToShow * 2 - 1;
121
+ }
122
+
123
+ for (let i = 0; i < totalMonthsToShow; i++) {
89
124
  // Get month and year info
90
125
  const unmoddedMonth = (startMonth + i);
91
126
  let month = unmoddedMonth;
@@ -105,24 +140,25 @@ const SimpleDateChooser: React.FC<Props> = (props) => {
105
140
  // Figure out which days are allowed
106
141
  const days = [];
107
142
  const numDaysInMonth = (new Date(year, month, 0)).getDate();
108
- if (chooseFromPast) {
109
- // Past selection
110
- const numDaysToAdd = (
111
- (month === today.month)
112
- ? today.day // Current month, only add up to today
113
- : numDaysInMonth // Past month, add all days
114
- );
115
- for (let day = 1; day <= numDaysToAdd; day++) {
116
- days.push(day);
143
+
144
+ // Current month
145
+ if (month === today.month && year === today.year) {
146
+ // Past selection: add all previous days of the month
147
+ if (!dontAllowPast) {
148
+ for (let day = 1; day < today.day; day++) {
149
+ days.push(day);
150
+ }
117
151
  }
118
- } else {
152
+ days.push(today.day); // Add current day
119
153
  // Future selection: add all remaining days of the month
120
- const firstDay = (
121
- month === today.month
122
- ? today.day // Current month: start at current date
123
- : 1 // Future month: start at beginning of month
124
- );
125
- for (let day = firstDay; day <= numDaysInMonth; day++) {
154
+ if (!dontAllowFuture) {
155
+ for (let day = today.day + 1; day <= numDaysInMonth; day++) {
156
+ days.push(day);
157
+ }
158
+ }
159
+ } else { // Past or future month
160
+ // Include all days in the month
161
+ for (let day = 1; day <= numDaysInMonth; day++) {
126
162
  days.push(day);
127
163
  }
128
164
  }
@@ -158,8 +194,8 @@ const SimpleDateChooser: React.FC<Props> = (props) => {
158
194
  </option>,
159
195
  );
160
196
 
197
+ // This is the currently selected month
161
198
  if (month === choice.month) {
162
- // This is the currently selected month
163
199
  // Create day options
164
200
  choice.days.forEach((dayChoice) => {
165
201
  const ordinal = getOrdinal(dayChoice);