dce-reactkit 3.0.0-beta.4 → 3.0.0-beta.42

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 (60) hide show
  1. package/.eslintrc.js +95 -0
  2. package/README.md +1 -546
  3. package/dist/cjs/index.js +944 -85
  4. package/dist/cjs/index.js.map +1 -1
  5. package/dist/cjs/types/components/AppWrapper.d.ts +25 -2
  6. package/dist/cjs/types/components/ButtonInputGroup.d.ts +12 -0
  7. package/dist/cjs/types/components/CheckboxButton.d.ts +20 -0
  8. package/dist/cjs/types/components/RadioButton.d.ts +20 -0
  9. package/dist/cjs/types/components/SimpleDateChooser.d.ts +22 -0
  10. package/dist/cjs/types/components/TabBox.d.ts +1 -0
  11. package/dist/cjs/types/helpers/genRouteHandler.d.ts +30 -0
  12. package/dist/cjs/types/helpers/getOrdinal.d.ts +8 -0
  13. package/dist/cjs/types/helpers/getTimeInfoInET.d.ts +17 -0
  14. package/dist/cjs/types/helpers/handleError.d.ts +18 -0
  15. package/dist/cjs/types/helpers/handleSuccess.d.ts +8 -0
  16. package/dist/cjs/types/helpers/visitServerEndpoint.d.ts +23 -0
  17. package/dist/cjs/types/index.d.ts +13 -1
  18. package/dist/cjs/types/server/initServer.d.ts +21 -0
  19. package/dist/cjs/types/types/ParamType.d.ts +17 -0
  20. package/dist/cjs/types/types/ReactKitErrorCode.d.ts +3 -1
  21. package/dist/esm/index.js +933 -86
  22. package/dist/esm/index.js.map +1 -1
  23. package/dist/esm/types/components/AppWrapper.d.ts +25 -2
  24. package/dist/esm/types/components/ButtonInputGroup.d.ts +12 -0
  25. package/dist/esm/types/components/CheckboxButton.d.ts +20 -0
  26. package/dist/esm/types/components/RadioButton.d.ts +20 -0
  27. package/dist/esm/types/components/SimpleDateChooser.d.ts +22 -0
  28. package/dist/esm/types/components/TabBox.d.ts +1 -0
  29. package/dist/esm/types/helpers/genRouteHandler.d.ts +30 -0
  30. package/dist/esm/types/helpers/getOrdinal.d.ts +8 -0
  31. package/dist/esm/types/helpers/getTimeInfoInET.d.ts +17 -0
  32. package/dist/esm/types/helpers/handleError.d.ts +18 -0
  33. package/dist/esm/types/helpers/handleSuccess.d.ts +8 -0
  34. package/dist/esm/types/helpers/visitServerEndpoint.d.ts +23 -0
  35. package/dist/esm/types/index.d.ts +13 -1
  36. package/dist/esm/types/server/initServer.d.ts +21 -0
  37. package/dist/esm/types/types/ParamType.d.ts +17 -0
  38. package/dist/esm/types/types/ReactKitErrorCode.d.ts +3 -1
  39. package/dist/index.d.ts +229 -10
  40. package/package.json +13 -1
  41. package/rollup.config.js +0 -1
  42. package/src/components/AppWrapper.tsx +73 -15
  43. package/src/components/ButtonInputGroup.tsx +89 -0
  44. package/src/components/CheckboxButton.tsx +100 -0
  45. package/src/components/ErrorBox.tsx +4 -4
  46. package/src/components/LoadingSpinner.tsx +3 -3
  47. package/src/components/Modal.tsx +185 -71
  48. package/src/components/RadioButton.tsx +102 -0
  49. package/src/components/SimpleDateChooser.tsx +222 -0
  50. package/src/components/TabBox.tsx +65 -58
  51. package/src/helpers/genRouteHandler.ts +326 -0
  52. package/src/helpers/getOrdinal.tsx +13 -0
  53. package/src/helpers/getTimeInfoInET.tsx +56 -0
  54. package/src/helpers/handleError.ts +65 -0
  55. package/src/helpers/handleSuccess.ts +18 -0
  56. package/src/helpers/visitServerEndpoint.tsx +110 -0
  57. package/src/index.ts +27 -0
  58. package/src/server/initServer.ts +53 -0
  59. package/src/types/ParamType.ts +18 -0
  60. package/src/types/ReactKitErrorCode.tsx +3 -1
package/dist/cjs/index.js CHANGED
@@ -3,10 +3,9 @@
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
5
  var React = require('react');
6
- require('bootstrap/dist/css/bootstrap.min.css');
7
- require('bootstrap/dist/js/bootstrap.min');
8
6
  var reactFontawesome = require('@fortawesome/react-fontawesome');
9
7
  var freeSolidSvgIcons = require('@fortawesome/free-solid-svg-icons');
8
+ var freeRegularSvgIcons = require('@fortawesome/free-regular-svg-icons');
10
9
 
11
10
  function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
12
11
 
@@ -37,7 +36,7 @@ function __awaiter(thisArg, _arguments, P, generator) {
37
36
  });
38
37
  }
39
38
 
40
- // Highest error code = DRK2
39
+ // Highest error code = DRK8
41
40
  /**
42
41
  * List of error codes built into the react kit
43
42
  * @author Gabe Abrams
@@ -50,6 +49,8 @@ var ReactKitErrorCode;
50
49
  ReactKitErrorCode["MissingParameter"] = "DRK4";
51
50
  ReactKitErrorCode["InvalidParameter"] = "DRK5";
52
51
  ReactKitErrorCode["WrongCourse"] = "DRK6";
52
+ ReactKitErrorCode["NoCACCLSendRequestFunction"] = "DRK7";
53
+ ReactKitErrorCode["NoCACCLGetLaunchInfoFunction"] = "DRK8";
53
54
  })(ReactKitErrorCode || (ReactKitErrorCode = {}));
54
55
  var ReactKitErrorCode$1 = ReactKitErrorCode;
55
56
 
@@ -81,9 +82,9 @@ const ErrorBox = (props) => {
81
82
  ' ',
82
83
  React__default["default"].createElement("span", { style: {
83
84
  backgroundColor: 'white',
84
- borderRadius: '5px',
85
- paddingLeft: '3px',
86
- paddingRight: '3px',
85
+ borderRadius: '0.3rem',
86
+ paddingLeft: '0.2rem',
87
+ paddingRight: '0.2rem',
87
88
  color: '#DC4150',
88
89
  fontVariant: 'small-caps',
89
90
  fontSize: '80%',
@@ -95,7 +96,7 @@ const ErrorBox = (props) => {
95
96
  }
96
97
  // Main UI
97
98
  return (React__default["default"].createElement("div", { className: "alert alert-danger text-center", style: {
98
- maxWidth: '650px',
99
+ maxWidth: '40rem',
99
100
  margin: 'auto',
100
101
  } },
101
102
  React__default["default"].createElement("h4", { className: "mb-1" },
@@ -194,9 +195,7 @@ var ModalSize$1 = ModalSize;
194
195
  /* Constants */
195
196
  /*------------------------------------------------------------------------*/
196
197
  // Constants
197
- const MS_TO_ANIMATE = 400; // Time to animate in/out (defined by bootstrap)
198
- const MS_ANIMATE_IN_DELAY = 10;
199
- // Time to wait before animating in (must be >0 or animation won't trigger)
198
+ const MS_TO_ANIMATE = 200; // Animation duration
200
199
  // Modal type to list of buttons
201
200
  const modalTypeToModalButtonTypes = {
202
201
  [ModalType$1.Okay]: [
@@ -276,6 +275,114 @@ const ModalButtonTypeToLabelAndVariant = {
276
275
  },
277
276
  };
278
277
  /*------------------------------------------------------------------------*/
278
+ /* Style */
279
+ /*------------------------------------------------------------------------*/
280
+ const style$2 = `
281
+ .Modal-backdrop {
282
+ position: fixed;
283
+ top: 0;
284
+ left: 0;
285
+ width: 100vw;
286
+ height: 100vw;
287
+ background-color: rgba(0, 0, 0, 0.7);
288
+ }
289
+
290
+ .Modal-fading-in {
291
+ animation-name: Modal-fading-in;
292
+ animation-duration: ${Math.floor(MS_TO_ANIMATE * 2)}ms;
293
+ animation-iteration-count: 1;
294
+ animation-fill-mode: both;
295
+ animation-timing-function: ease-out;
296
+ }
297
+
298
+ @keyframes Modal-fading-in {
299
+ 0% {
300
+ opacity: 0;
301
+ }
302
+ 100% {
303
+ opacity: 1;
304
+ }
305
+ }
306
+
307
+ .Modal-fading-out {
308
+ animation-name: Modal-fading-out;
309
+ animation-duration: ${MS_TO_ANIMATE}ms;
310
+ animation-iteration-count: 1;
311
+ animation-fill-mode: both;
312
+ animation-timing-function: ease-in;
313
+ }
314
+
315
+ @keyframes Modal-fading-out {
316
+ 0% {
317
+ opacity: 1;
318
+ }
319
+ 100% {
320
+ opacity: 0;
321
+ }
322
+ }
323
+
324
+ .Modal-animating-in {
325
+ animation-name: Modal-animating-in;
326
+ animation-duration: ${MS_TO_ANIMATE}ms;
327
+ animation-iteration-count: 1;
328
+ animation-fill-mode: both;
329
+ animation-timing-function: ease-out;
330
+ }
331
+
332
+ @keyframes Modal-animating-in {
333
+ 0% {
334
+ transform: scale(1.05) translate(0, -1.5rem);
335
+ opacity: 0;
336
+ }
337
+ 100% {
338
+ transform: scale(1) translate(0, 0);
339
+ opacity: 1;
340
+ }
341
+ }
342
+
343
+ .Modal-animating-pop {
344
+ animation-name: Modal-animating-pop;
345
+ animation-duration: ${MS_TO_ANIMATE}ms;
346
+ animation-iteration-count: 1;
347
+ animation-fill-mode: both;
348
+ animation-timing-function: ease-in-out;
349
+ }
350
+
351
+ @keyframes Modal-animating-pop {
352
+ 0% {
353
+ transform: scale(1);
354
+ opacity: 1;
355
+ }
356
+ 50% {
357
+ transform: scale(1.05);
358
+ opacity: 0.9;
359
+ }
360
+ 100% {
361
+ transform: scale(1);
362
+ opacity: 1;
363
+ }
364
+ }
365
+
366
+ .Modal-animating-out {
367
+ animation-name: Modal-animating-out;
368
+ animation-duration: ${MS_TO_ANIMATE}ms;
369
+ animation-iteration-count: 1;
370
+ animation-fill-mode: both;
371
+ animation-timing-function: ease-in;
372
+ }
373
+
374
+ @keyframes Modal-animating-out {
375
+ 0% {
376
+ transform: scale(1) translate(0, 0);
377
+ opacity: 1;
378
+ }
379
+ 100% {
380
+ transform: scale(1.05) translate(0, -1.5rem);
381
+ opacity: 0;
382
+ }
383
+ }
384
+ `;
385
+ /*------------------------------------------------------------------------*/
279
386
  /* Component */
280
387
  /*------------------------------------------------------------------------*/
281
388
  const Modal = (props) => {
@@ -286,10 +393,15 @@ const Modal = (props) => {
286
393
  /* -------------- Props ------------- */
287
394
  const { type = ModalType$1.NoButtons, size = ModalSize$1.Large, title, children, onClose, dontAllowBackdropExit, onTopOfOtherModals, } = props;
288
395
  /* -------------- State ------------- */
396
+ // If true, the modal is completely gone
397
+ // (not just invisible, also removed from the dom)
398
+ const [gone, setGone] = React.useState(false);
289
399
  // If true, the modal is shown
290
400
  const [visible, setVisible] = React.useState(false);
291
- // True if currently animating in
292
- const [animatingIn, setAnimatingIn] = React.useState(false);
401
+ // True if animation is in use
402
+ const [animatingIn, setAnimatingIn] = React.useState(true);
403
+ const [animatingPop, setAnimatingPop] = React.useState(false);
404
+ const [animatingOut, setAnimatingOut] = React.useState(false);
293
405
  /*------------------------------------------------------------------------*/
294
406
  /* Lifecycle Functions */
295
407
  /*------------------------------------------------------------------------*/
@@ -299,12 +411,10 @@ const Modal = (props) => {
299
411
  */
300
412
  React.useEffect(() => {
301
413
  (() => __awaiter(void 0, void 0, void 0, function* () {
302
- // Start the component animating in
303
- yield waitMs(MS_ANIMATE_IN_DELAY);
304
- setAnimatingIn(true);
305
414
  // Wait and then set visible to true
306
415
  yield waitMs(MS_TO_ANIMATE);
307
416
  setVisible(true);
417
+ setAnimatingIn(false);
308
418
  }))();
309
419
  }, []);
310
420
  /*------------------------------------------------------------------------*/
@@ -331,13 +441,19 @@ const Modal = (props) => {
331
441
  }
332
442
  // Update the state
333
443
  setVisible(false);
444
+ setAnimatingOut(true);
334
445
  // Call the handler after the modal has animated out
335
446
  yield waitMs(MS_TO_ANIMATE);
336
447
  onClose(ModalButtonType);
448
+ setGone(true);
337
449
  });
338
450
  /*------------------------------------------------------------------------*/
339
451
  /* Render */
340
452
  /*------------------------------------------------------------------------*/
453
+ // Return nothing if gone
454
+ if (gone) {
455
+ return null;
456
+ }
341
457
  /*----------------------------------------*/
342
458
  /* Footer */
343
459
  /*----------------------------------------*/
@@ -359,7 +475,7 @@ const Modal = (props) => {
359
475
  // Check if this button is last
360
476
  const last = (i === ModalButtonTypes.length - 1);
361
477
  // Create the button
362
- return (React__default["default"].createElement("button", { type: "button", className: `Modal-${ModalButtonType}-button btn btn-${variant} ${last ? '' : 'mr-1'}`, onClick: () => {
478
+ return (React__default["default"].createElement("button", { key: ModalButtonType, type: "button", className: `Modal-${ModalButtonType}-button btn btn-${variant} ${last ? '' : 'me-1'}`, onClick: () => {
363
479
  handleClose(ModalButtonType);
364
480
  } }, label));
365
481
  });
@@ -367,21 +483,62 @@ const Modal = (props) => {
367
483
  const footer = ((buttons && buttons.length)
368
484
  ? (React__default["default"].createElement("div", null, buttons))
369
485
  : undefined);
486
+ // Choose an animation
487
+ let animationClass = '';
488
+ let backdropAnimationClass = '';
489
+ if (animatingIn) {
490
+ animationClass = 'Modal-animating-in';
491
+ backdropAnimationClass = 'Modal-fading-in';
492
+ }
493
+ else if (animatingOut) {
494
+ animationClass = 'Modal-animating-out';
495
+ backdropAnimationClass = 'Modal-fading-out';
496
+ }
497
+ else if (animatingPop) {
498
+ animationClass = 'Modal-animating-pop';
499
+ }
370
500
  // Render the modal
371
- return (React__default["default"].createElement("div", { className: `modal fade modal-${size}`, tabIndex: -1, style: {
501
+ return (React__default["default"].createElement("div", { className: `modal show modal-dialog-scrollable modal-dialog-centered modal-${size}`, tabIndex: -1, style: {
372
502
  zIndex: (onTopOfOtherModals
373
- ? 6000000000
503
+ ? 5000000001
374
504
  : 5000000000),
505
+ display: 'block',
506
+ margin: 'auto',
507
+ left: 0,
508
+ right: 0,
375
509
  } },
376
- React__default["default"].createElement("div", { className: "modal-dialog" },
510
+ React__default["default"].createElement("style", null, style$2),
511
+ React__default["default"].createElement("div", { className: `Modal-backdrop ${backdropAnimationClass}`, style: {
512
+ zIndex: 5000000003,
513
+ }, onClick: () => __awaiter(void 0, void 0, void 0, function* () {
514
+ // Skip if exit via backdrop not allowed
515
+ if (dontAllowBackdropExit || !onClose) {
516
+ // Show pop animation
517
+ if (!animatingPop) {
518
+ setAnimatingPop(true);
519
+ // Wait then stop pop animation
520
+ yield waitMs(MS_TO_ANIMATE);
521
+ setAnimatingPop(false);
522
+ }
523
+ return;
524
+ }
525
+ // Handle close
526
+ handleClose(ModalButtonType$1.Cancel);
527
+ }) }),
528
+ React__default["default"].createElement("div", { className: `modal-dialog ${animationClass}`, style: {
529
+ zIndex: 5000000002,
530
+ } },
377
531
  React__default["default"].createElement("div", { className: "modal-content" },
378
532
  React__default["default"].createElement("div", { className: "modal-header" },
379
- React__default["default"].createElement("h5", { className: "modal-title" }, title),
380
- onClose && (React__default["default"].createElement("button", { type: "button", className: "btn-close", "data-bs-dismiss": "modal", "aria-label": "Close", onClick: () => {
533
+ React__default["default"].createElement("h5", { className: "modal-title", style: {
534
+ fontWeight: 'bold',
535
+ } }, title),
536
+ onClose && (React__default["default"].createElement("button", { type: "button", className: "btn-close", "aria-label": "Close", onClick: () => {
537
+ // Handle close
381
538
  handleClose(ModalButtonType$1.Cancel);
382
539
  } }))),
383
540
  children && (React__default["default"].createElement("div", { className: "modal-body" }, children)),
384
- footer && (React__default["default"].createElement("div", { className: "modal-footer" }, footer))))));
541
+ footer && (React__default["default"].createElement("div", { className: "modal-footer pt-1 pb-1" }, footer))))));
385
542
  };
386
543
 
387
544
  /**
@@ -405,6 +562,24 @@ class ErrorWithCode extends Error {
405
562
  /* Static Helpers */
406
563
  /*------------------------------------------------------------------------*/
407
564
  /*----------------------------------------*/
565
+ /* Send Request */
566
+ /*----------------------------------------*/
567
+ // Store copy of caccl send request
568
+ let _cacclSendRequest;
569
+ /**
570
+ * Send a request using caccl's send request feature
571
+ * @author Gabe Abrams
572
+ * @param opts send request options
573
+ * @returns send request response
574
+ */
575
+ const cacclSendRequest = (opts) => __awaiter(void 0, void 0, void 0, function* () {
576
+ // Make sure send request has been passed in
577
+ if (!_cacclSendRequest) {
578
+ throw new ErrorWithCode('The request could not be sent because the AppWrapper component does not have a copy of sendRequest from CACCL.', ReactKitErrorCode$1.NoCACCLSendRequestFunction);
579
+ }
580
+ return _cacclSendRequest(opts);
581
+ });
582
+ /*----------------------------------------*/
408
583
  /* Alert */
409
584
  /*----------------------------------------*/
410
585
  // Stored copies of setters
@@ -416,7 +591,7 @@ let onAlertClosed;
416
591
  * @param title the title text to display at the top of the alert
417
592
  * @param text the text to display in the alert
418
593
  */
419
- const alert = (title, text) => __awaiter(void 0, void 0, void 0, function* () {
594
+ const alert$1 = (title, text) => __awaiter(void 0, void 0, void 0, function* () {
420
595
  // Fallback if alert not available
421
596
  if (!setAlertInfo) {
422
597
  window.alert(`${title}\n\n${text}`);
@@ -490,7 +665,7 @@ const showFatalError = (error, errorTitle = 'An Error Occurred') => {
490
665
  : String((_b = error.code) !== null && _b !== void 0 ? _b : ReactKitErrorCode$1.NoCode));
491
666
  // Handle case where app hasn't loaded
492
667
  if (!setFatalErrorMessage || !setFatalErrorCode) {
493
- alert(errorTitle, `${message} (code: ${code}). Please contact support.`);
668
+ alert$1(errorTitle, `${message} (code: ${code}). Please contact support.`);
494
669
  return undefined;
495
670
  }
496
671
  // Use setters
@@ -507,7 +682,9 @@ const AppWrapper = (props) => {
507
682
  /* Setup */
508
683
  /*------------------------------------------------------------------------*/
509
684
  /* -------------- Props ------------- */
510
- const { children, dark, sessionExpiredMessage = 'Your session has expired. Please go back to Canvas and start over.', } = props;
685
+ const { children, sendRequest, dark, sessionExpiredMessage = 'Your session has expired. Please go back to Canvas and start over.', } = props;
686
+ // Store copy of send request
687
+ _cacclSendRequest = sendRequest;
511
688
  /* -------------- State ------------- */
512
689
  // Fatal error
513
690
  const [fatalErrorMessage, setFatalErrorMessageInner,] = React.useState();
@@ -517,10 +694,10 @@ const AppWrapper = (props) => {
517
694
  const [fatalErrorTitle, setFatalErrorTitleInner,] = React.useState();
518
695
  setFatalErrorTitle = setFatalErrorTitleInner;
519
696
  // Alert
520
- const [alertInfo, setAlertInfoInner,] = React.useState();
697
+ const [alertInfo, setAlertInfoInner,] = React.useState(undefined);
521
698
  setAlertInfo = setAlertInfoInner;
522
699
  // Confirm
523
- const [confirmInfo, setConfirmInfoInner,] = React.useState();
700
+ const [confirmInfo, setConfirmInfoInner,] = React.useState(undefined);
524
701
  setConfirmInfo = setConfirmInfoInner;
525
702
  // Session expired
526
703
  const [sessionHasExpired, setSessionHasExpiredInner,] = React.useState(false);
@@ -534,8 +711,9 @@ const AppWrapper = (props) => {
534
711
  let modal;
535
712
  /* -------------- Alert ------------- */
536
713
  if (alertInfo) {
537
- modal = (React__default["default"].createElement(Modal, { title: alertInfo.title, type: ModalType$1.Okay, onClose: () => {
714
+ modal = (React__default["default"].createElement(Modal, { key: `alert-${alertInfo.title}-${alertInfo.text}`, title: alertInfo.title, type: ModalType$1.Okay, onClose: () => {
538
715
  // Alert closed
716
+ setAlertInfo(undefined);
539
717
  if (onAlertClosed) {
540
718
  onAlertClosed();
541
719
  }
@@ -543,11 +721,12 @@ const AppWrapper = (props) => {
543
721
  }
544
722
  /* ------------- Confirm ------------ */
545
723
  if (confirmInfo) {
546
- modal = (React__default["default"].createElement(Modal, { title: confirmInfo.title, type: ModalType$1.OkayCancel, onClose: (buttonType) => {
724
+ modal = (React__default["default"].createElement(Modal, { key: `confirm-${confirmInfo.title}-${confirmInfo.text}`, title: confirmInfo.title, type: ModalType$1.OkayCancel, onClose: (buttonType) => {
725
+ setConfirmInfo(undefined);
547
726
  if (onConfirmClosed) {
548
727
  onConfirmClosed(buttonType === ModalButtonType$1.Okay);
549
728
  }
550
- }, dontAllowBackdropExit: true }));
729
+ }, dontAllowBackdropExit: true }, confirmInfo.text));
551
730
  }
552
731
  /*----------------------------------------*/
553
732
  /* Views */
@@ -599,10 +778,10 @@ const style$1 = `
599
778
  .LoadingSpinner-blip-2,
600
779
  .LoadingSpinner-blip-3,
601
780
  .LoadingSpinner-blip-4 {
602
- font-size: 25px;
781
+ font-size: 1.8rem;
603
782
  opacity: 0.6;
604
- margin-top: 20px;
605
- margin-bottom: 20px;
783
+ margin-top: 1rem;
784
+ margin-bottom: 1rem;
606
785
  }
607
786
 
608
787
  /* First Blip */
@@ -669,63 +848,67 @@ const LoadingSpinner = () => {
669
848
  /* Style */
670
849
  /*------------------------------------------------------------------------*/
671
850
  const style = `
672
- /* Tab Box */
673
- .TabBox-box {
674
- /* Light Border */
675
- border: 2px solid #dedede;
676
-
677
- /* Rounded Corners (except top-left) */
678
- border-bottom-right-radius: 5px;
679
- border-bottom-left-radius: 5px;
680
- border-top-right-radius: 5px;
681
-
682
- /* Very Light Gray Border */
683
- background: #fdfdfd;
684
-
685
- /* Align Contents on Left */
686
- text-align: left;
687
- }
851
+ /* Tab Box */
852
+ .TabBox-box {
853
+ /* Light Border */
854
+ border: 0.15rem solid #dedede;
855
+
856
+ /* Rounded Corners (except top-left) */
857
+ border-bottom-right-radius: 0.3rem;
858
+ border-bottom-left-radius: 0.3rem;
859
+ border-top-right-radius: 0.3rem;
688
860
 
689
- /* Container for Title */
690
- .TabBox-title-container {
691
- /* Place on Left */
692
- position: relative;
693
- left: 0;
694
- text-align: left;
695
- }
861
+ /* Very Light Gray Border */
862
+ background: #fdfdfd;
696
863
 
697
- /* Tab-style Title */
698
- .TabBox-title {
699
- /* Place so it Barely Overlaps the Box Border */
700
- display: inline-block;
701
- position: relative;
702
- top: 2px; /* Gives Illusion that Border Doesn't Exist Below Tab */
864
+ /* Align Contents on Left */
865
+ text-align: left;
703
866
 
704
- /* Title-sized Font */
705
- font-size: 25px;
867
+ /* Add Text Padding */
868
+ padding-left: 0.3rem;
869
+ padding-right: 0.3rem;
870
+ }
706
871
 
707
- /* Add Border on Top and Sides */
708
- border-top: 2px solid #dedede;
709
- border-left: 2px solid #dedede;
710
- border-right: 2px solid #dedede;
872
+ /* Container for Title */
873
+ .TabBox-title-container {
874
+ /* Place on Left */
875
+ position: relative;
876
+ left: 0;
877
+ text-align: left;
878
+ }
711
879
 
712
- /* Round the Top Corners */
713
- border-top-left-radius: 5px;
714
- border-top-right-radius: 5px;
880
+ /* Tab-style Title */
881
+ .TabBox-title {
882
+ /* Place so it Barely Overlaps the Box Border */
883
+ display: inline-block;
884
+ position: relative;
885
+ top: 0.15rem; /* Gives Illusion that Border Doesn't Exist Below Tab */
715
886
 
716
- /* Add Text Padding */
717
- padding-left: 12px;
718
- padding-right: 12px;
887
+ /* Title-sized Font */
888
+ font-size: 1.5rem;
719
889
 
720
- /* Match Background Color of Box */
721
- background: #fdfdfd;
722
- }
890
+ /* Add Border on Top and Sides */
891
+ border-top: 0.15rem solid #dedede;
892
+ border-left: 0.15rem solid #dedede;
893
+ border-right: 0.15rem solid #dedede;
723
894
 
724
- /* Make the TabBox's Children Appear Above Title if Overlap Occurs */
725
- .TabBox-children {
726
- position: relative;
727
- z-index: 1;
728
- }
895
+ /* Round the Top Corners */
896
+ border-top-left-radius: 0.3rem;
897
+ border-top-right-radius: 0.3rem;
898
+
899
+ /* Add Text Padding */
900
+ padding-left: 0.5rem;
901
+ padding-right: 0.5rem;
902
+
903
+ /* Match Background Color of Box */
904
+ background: #fdfdfd;
905
+ }
906
+
907
+ /* Make the TabBox's Children Appear Above Title if Overlap Occurs */
908
+ .TabBox-children {
909
+ position: relative;
910
+ z-index: 1;
911
+ }
729
912
  `;
730
913
  /*------------------------------------------------------------------------*/
731
914
  /* Component */
@@ -735,7 +918,7 @@ const TabBox = (props) => {
735
918
  /* Setup */
736
919
  /*------------------------------------------------------------------------*/
737
920
  /* -------------- Props ------------- */
738
- const { title, children, } = props;
921
+ const { title, children, noBottomPadding, } = props;
739
922
  /*------------------------------------------------------------------------*/
740
923
  /* Render */
741
924
  /*------------------------------------------------------------------------*/
@@ -743,7 +926,7 @@ const TabBox = (props) => {
743
926
  /* Main UI */
744
927
  /*----------------------------------------*/
745
928
  // Full UI
746
- return (React__default["default"].createElement("div", null,
929
+ return (React__default["default"].createElement("div", { className: `TabBox-container ${noBottomPadding ? '' : 'mb-2'}` },
747
930
  React__default["default"].createElement("style", null, style),
748
931
  React__default["default"].createElement("div", { className: "TabBox-title-container" },
749
932
  React__default["default"].createElement("div", { className: "TabBox-title" }, title)),
@@ -751,6 +934,252 @@ const TabBox = (props) => {
751
934
  React__default["default"].createElement("div", { className: "TabBox-children" }, children))));
752
935
  };
753
936
 
937
+ /**
938
+ * A radio selection button
939
+ * @author Gabe Abrams
940
+ */
941
+ /*------------------------------------------------------------------------*/
942
+ /* Component */
943
+ /*------------------------------------------------------------------------*/
944
+ const RadioButton = (props) => {
945
+ /*------------------------------------------------------------------------*/
946
+ /* Setup */
947
+ /*------------------------------------------------------------------------*/
948
+ /* -------------- Props ------------- */
949
+ const { text, onSelected, ariaLabel, title, selected, id, noMarginOnRight, selectedVariant = Variant$1.Secondary, unselectedVariant = Variant$1.Light, small, } = props;
950
+ /*------------------------------------------------------------------------*/
951
+ /* Render */
952
+ /*------------------------------------------------------------------------*/
953
+ /*----------------------------------------*/
954
+ /* Main UI */
955
+ /*----------------------------------------*/
956
+ return (React__default["default"].createElement("button", { type: "button", id: id, title: title, className: `btn btn-${selected ? selectedVariant : unselectedVariant}${selected ? ' selected' : ''}${small ? ' btn-sm' : ''} m-0${noMarginOnRight ? '' : ' me-1'}`, "aria-label": `${ariaLabel}${selected ? ': currently selected' : ''}`, onClick: () => {
957
+ if (!selected) {
958
+ onSelected();
959
+ }
960
+ } },
961
+ React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: selected ? freeSolidSvgIcons.faDotCircle : freeRegularSvgIcons.faCircle, className: "me-1" }),
962
+ text));
963
+ };
964
+
965
+ /**
966
+ * A checkbox button
967
+ * @author Gabe Abrams
968
+ */
969
+ /*------------------------------------------------------------------------*/
970
+ /* Component */
971
+ /*------------------------------------------------------------------------*/
972
+ const CheckboxButton = (props) => {
973
+ /*------------------------------------------------------------------------*/
974
+ /* Setup */
975
+ /*------------------------------------------------------------------------*/
976
+ /* -------------- Props ------------- */
977
+ const { text, onChanged, ariaLabel, title, checked, id, noMarginOnRight, checkedVariant = Variant$1.Secondary, uncheckedVariant = Variant$1.Light, small, } = props;
978
+ /*------------------------------------------------------------------------*/
979
+ /* Render */
980
+ /*------------------------------------------------------------------------*/
981
+ /*----------------------------------------*/
982
+ /* Main UI */
983
+ /*----------------------------------------*/
984
+ return (React__default["default"].createElement("button", { type: "button", id: id, title: title, className: `btn btn-${checked ? checkedVariant : uncheckedVariant}${checked ? ' selected' : ''}${small ? ' btn-sm' : ''} m-0${noMarginOnRight ? '' : ' me-1'}`, "aria-label": `${ariaLabel}${checked ? ': currently checked' : ''}`, onClick: () => {
985
+ onChanged(!checked);
986
+ } },
987
+ React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: checked ? freeSolidSvgIcons.faCheckSquare : freeRegularSvgIcons.faSquare, className: "me-1" }),
988
+ text));
989
+ };
990
+
991
+ /**
992
+ * Input group with a title and space for buttons
993
+ * @author Gabe Abrams
994
+ */
995
+ /*------------------------------------------------------------------------*/
996
+ /* Component */
997
+ /*------------------------------------------------------------------------*/
998
+ const ButtonInputGroup = (props) => {
999
+ /*------------------------------------------------------------------------*/
1000
+ /* Setup */
1001
+ /*------------------------------------------------------------------------*/
1002
+ /* -------------- Props ------------- */
1003
+ // Destructure all props
1004
+ const { label, minLabelWidth, children, } = props;
1005
+ /*------------------------------------------------------------------------*/
1006
+ /* Render */
1007
+ /*------------------------------------------------------------------------*/
1008
+ /*----------------------------------------*/
1009
+ /* Main UI */
1010
+ /*----------------------------------------*/
1011
+ return (React__default["default"].createElement("div", { className: "input-group" },
1012
+ React__default["default"].createElement("div", { className: "input-group-prepend d-flex w-100" },
1013
+ React__default["default"].createElement("span", { className: "input-group-text", style: {
1014
+ minWidth: (minLabelWidth !== null && minLabelWidth !== void 0 ? minLabelWidth : undefined),
1015
+ borderTopRightRadius: 0,
1016
+ borderBottomRightRadius: 0,
1017
+ } }, label),
1018
+ React__default["default"].createElement("span", { className: "input-group-text flex-grow-1 rounded-right", style: {
1019
+ backgroundColor: 'white',
1020
+ borderTopLeftRadius: 0,
1021
+ borderBottomLeftRadius: 0,
1022
+ borderLeftWidth: 0,
1023
+ } }, children))));
1024
+ };
1025
+
1026
+ const ORDINALS = ['th', 'st', 'nd', 'rd'];
1027
+ /**
1028
+ * Get a number's ordinal
1029
+ * @author Gabe Abrams
1030
+ * @param num the number being analyzed
1031
+ * @returns ordinal
1032
+ */
1033
+ const getOrdinal = (num) => {
1034
+ var _a, _b;
1035
+ return ((_b = (_a = ORDINALS[(num - 20) % 10]) !== null && _a !== void 0 ? _a : ORDINALS[num]) !== null && _b !== void 0 ? _b : ORDINALS[0]);
1036
+ };
1037
+
1038
+ /**
1039
+ * Get current time info in US Boston Eastern Time, independent of machine
1040
+ * timezone
1041
+ * @author Gabe Abrams
1042
+ * @param {Date} [date=now] the date to get info on
1043
+ * @returns object with timestamp (ms since epoch) and numbers
1044
+ * corresponding to ET time values for year, month, day, hour, minute
1045
+ */
1046
+ const getTimeInfoInET = (date) => {
1047
+ // Create a time string
1048
+ const d = (date || new Date());
1049
+ const str = d.toLocaleString('en-US', // Using US encoding (it's the only one installed on containers)
1050
+ { timeZone: 'America/New_York' } // Force EST timezone
1051
+ );
1052
+ // Parse the string for the date/time info
1053
+ const [dateStr, timeStr] = str.split(', '); // Format: MM/DD/YYYY, HH:MM:SS AM
1054
+ const [monthStr, dayStr, yearStr] = dateStr.split('/'); // Format: MM/DD/YYYY
1055
+ const [hourStr, minStr, ending] = timeStr.split(':'); // Format: HH:MM:SS AM
1056
+ // Create all time numbers
1057
+ const timestamp = d.getTime();
1058
+ const year = Number.parseInt(yearStr, 10);
1059
+ const month = Number.parseInt(monthStr, 10);
1060
+ const day = Number.parseInt(dayStr, 10);
1061
+ const minute = Number.parseInt(minStr, 10);
1062
+ let hour = Number.parseInt(hourStr, 10);
1063
+ // Convert from am/pm to 24hr
1064
+ const isAM = ending.toLowerCase().includes('am');
1065
+ const isPM = !isAM;
1066
+ if (isPM && hour !== 12) {
1067
+ hour += 12;
1068
+ }
1069
+ else if (isAM && hour === 12) {
1070
+ hour = 0;
1071
+ }
1072
+ // Return
1073
+ return {
1074
+ timestamp,
1075
+ year,
1076
+ month,
1077
+ day,
1078
+ hour,
1079
+ minute,
1080
+ };
1081
+ };
1082
+
1083
+ /**
1084
+ * A very simple, lightweight date chooser
1085
+ * @author Gabe Abrams
1086
+ */
1087
+ /*------------------------------------------------------------------------*/
1088
+ /* Constants */
1089
+ /*------------------------------------------------------------------------*/
1090
+ // Constants
1091
+ const MONTH_NAMES = [
1092
+ 'January',
1093
+ 'February',
1094
+ 'March',
1095
+ 'April',
1096
+ 'May',
1097
+ 'June',
1098
+ 'July',
1099
+ 'August',
1100
+ 'September',
1101
+ 'October',
1102
+ 'November',
1103
+ 'December',
1104
+ ];
1105
+ /*------------------------------------------------------------------------*/
1106
+ /* Component */
1107
+ /*------------------------------------------------------------------------*/
1108
+ const SimpleDateChooser = (props) => {
1109
+ /*------------------------------------------------------------------------*/
1110
+ /* Setup */
1111
+ /*------------------------------------------------------------------------*/
1112
+ var _a;
1113
+ /* -------------- Props ------------- */
1114
+ const { ariaLabel, name, month, day, year, onChange, } = props;
1115
+ const numMonthsToShow = Math.min((_a = props.numMonthsToShow) !== null && _a !== void 0 ? _a : 6, 12);
1116
+ /*------------------------------------------------------------------------*/
1117
+ /* Render */
1118
+ /*------------------------------------------------------------------------*/
1119
+ /*----------------------------------------*/
1120
+ /* Main UI */
1121
+ /*----------------------------------------*/
1122
+ // Determine the set of choices allowed
1123
+ const today = getTimeInfoInET();
1124
+ const choices = [];
1125
+ for (let i = 0; i < numMonthsToShow; i++) {
1126
+ // Get month and year info
1127
+ const unmoddedMonth = (today.month + i);
1128
+ const month = (unmoddedMonth > 12
1129
+ ? unmoddedMonth - 12
1130
+ : unmoddedMonth);
1131
+ const monthName = MONTH_NAMES[month - 1];
1132
+ const year = (unmoddedMonth > 12
1133
+ ? today.year + 1
1134
+ : today.year);
1135
+ // Figure out which days are allowed
1136
+ const days = [];
1137
+ const numDaysInMonth = (new Date(year, month, 0)).getDate();
1138
+ const firstDay = (month === today.month
1139
+ ? today.day // Current month: start at current date
1140
+ : 1 // Future month: start at beginning of month
1141
+ );
1142
+ for (let day = firstDay; day <= numDaysInMonth; day++) {
1143
+ days.push(day);
1144
+ }
1145
+ choices.push({
1146
+ choiceName: `${monthName} ${year}`,
1147
+ month,
1148
+ year,
1149
+ days,
1150
+ });
1151
+ }
1152
+ // Create choice options
1153
+ const monthOptions = [];
1154
+ const dayOptions = [];
1155
+ choices.forEach((choice) => {
1156
+ // Create month option
1157
+ monthOptions.push(React__default["default"].createElement("option", { key: choice.month, value: choice.month, "aria-label": `choose ${choice.choiceName}`, onSelect: () => {
1158
+ onChange(choice.month, choice.days[0], choice.year);
1159
+ } }, choice.choiceName));
1160
+ if (month === choice.month) {
1161
+ // This is the currently selected month
1162
+ // Create day options
1163
+ choice.days.forEach((dayChoice) => {
1164
+ const ordinal = getOrdinal(dayChoice);
1165
+ dayOptions.push(React__default["default"].createElement("option", { key: dayChoice, value: dayChoice, "aria-label": `choose date ${dayChoice}` },
1166
+ dayChoice,
1167
+ ordinal));
1168
+ });
1169
+ }
1170
+ });
1171
+ return (React__default["default"].createElement("div", { className: "SimpleDateChooser d-inline-block", "aria-label": `date chooser with selected date: ${month} ${day}, ${year}` },
1172
+ React__default["default"].createElement("select", { "aria-label": `month for ${ariaLabel}`, className: "custom-select d-inline-block mr-1", style: { width: 'auto' }, id: `SimpleDateChooser-${name}-month`, value: month, onChange: (e) => {
1173
+ const choice = choices[e.target.selectedIndex];
1174
+ // Change day, month, and year
1175
+ onChange(choice.month, choice.days[0], choice.year);
1176
+ } }, monthOptions),
1177
+ React__default["default"].createElement("select", { "aria-label": `day for ${ariaLabel}`, className: "custom-select d-inline-block", style: { width: 'auto' }, id: `SimpleDateChooser-${name}-day`, value: day, onChange: (e) => {
1178
+ // Only change the day
1179
+ onChange(month, Number.parseInt(e.target.value, 10), year);
1180
+ } }, dayOptions)));
1181
+ };
1182
+
754
1183
  /**
755
1184
  * Shorten text so it fits into a certain number of chars
756
1185
  * @author Gabe Abrams
@@ -894,7 +1323,427 @@ const roundToNumDecimals = (num, numDecimals) => {
894
1323
  return (Math.round(num * rounder) / rounder);
895
1324
  };
896
1325
 
1326
+ // Keep track of whether or not session expiry has already been handled
1327
+ let sessionAlreadyExpired = false;
1328
+ /*------------------------------------------------------------------------*/
1329
+ /* Main */
1330
+ /*------------------------------------------------------------------------*/
1331
+ /**
1332
+ * Visit an endpoint on the server [for client only]
1333
+ * @author Gabe Abrams
1334
+ * @param opts object containing all arguments
1335
+ * @param opts.path - the path of the server endpoint
1336
+ * @param [opts.method=GET] - the method of the endpoint
1337
+ * @param [opts.params] - query/body parameters to include
1338
+ * @returns response from server
1339
+ */
1340
+ const visitServerEndpoint = (opts) => __awaiter(void 0, void 0, void 0, function* () {
1341
+ var _a;
1342
+ // Send the request
1343
+ const response = yield cacclSendRequest({
1344
+ path: opts.path,
1345
+ method: (_a = opts.method) !== null && _a !== void 0 ? _a : 'GET',
1346
+ params: opts.params,
1347
+ });
1348
+ // Check for failure
1349
+ if (!response || !response.body) {
1350
+ throw new ErrorWithCode('We didn\'t get a response from the server. Please check your internet connection.', ReactKitErrorCode$1.NoResponse);
1351
+ }
1352
+ if (!response.body.success) {
1353
+ // Session expired
1354
+ if (response.body.code === ReactKitErrorCode$1.SessionExpired) {
1355
+ // Skip notice if session was already expired
1356
+ if (sessionAlreadyExpired) {
1357
+ // Never return (browser is already reloading)
1358
+ yield new Promise(() => {
1359
+ // Promise that never returns
1360
+ });
1361
+ }
1362
+ sessionAlreadyExpired = true;
1363
+ // Show session expiration message
1364
+ {
1365
+ // Fallback to alert
1366
+ // eslint-disable-next-line no-alert
1367
+ alert('Your session has expired. Please start over.');
1368
+ }
1369
+ // Never return (don't continue execution)
1370
+ yield new Promise(() => {
1371
+ // Promise that never returns
1372
+ });
1373
+ }
1374
+ // Other errors
1375
+ throw new ErrorWithCode((response.body.message
1376
+ || 'An unknown error occurred. Please contact an admin.'), (response.body.code
1377
+ || ReactKitErrorCode$1.NoCode));
1378
+ }
1379
+ // Success! Extract the body
1380
+ const { body } = response.body;
1381
+ // Return
1382
+ return body;
1383
+ });
1384
+
1385
+ // Import custom error
1386
+ // Stored copy of caccl functions
1387
+ let _cacclGetLaunchInfo;
1388
+ /*------------------------------------------------------------------------*/
1389
+ /* Helpers */
1390
+ /*------------------------------------------------------------------------*/
1391
+ /**
1392
+ * Get launch info via CACCL
1393
+ * @author Gabe Abrams
1394
+ * @param req express request object
1395
+ * @returns object { launched, launchInfo }
1396
+ */
1397
+ const cacclGetLaunchInfo = (req) => {
1398
+ if (!_cacclGetLaunchInfo) {
1399
+ throw new ErrorWithCode('Could not get launch info because server was not initialized with dce-reactkit\'s initServer function', ReactKitErrorCode$1.NoCACCLGetLaunchInfoFunction);
1400
+ }
1401
+ return _cacclGetLaunchInfo(req);
1402
+ };
1403
+ /*------------------------------------------------------------------------*/
1404
+ /* Main */
1405
+ /*------------------------------------------------------------------------*/
1406
+ /**
1407
+ * Prepare dce-reactkit to run on the server
1408
+ * @author Gabe Abrams
1409
+ * @param opts object containing all arguments
1410
+ * @param opts.getLaunchInfo CACCL LTI's get launch info function
1411
+ */
1412
+ const initServer = (opts) => {
1413
+ _cacclGetLaunchInfo = opts.getLaunchInfo;
1414
+ };
1415
+
1416
+ /**
1417
+ * Server-side API param types
1418
+ * @author Gabe Abrams
1419
+ */
1420
+ var ParamType;
1421
+ (function (ParamType) {
1422
+ ParamType["Boolean"] = "boolean";
1423
+ ParamType["BooleanOptional"] = "boolean-optional";
1424
+ ParamType["Float"] = "float";
1425
+ ParamType["FloatOptional"] = "float-optional";
1426
+ ParamType["Int"] = "int";
1427
+ ParamType["IntOptional"] = "int-optional";
1428
+ ParamType["JSON"] = "json";
1429
+ ParamType["JSONOptional"] = "json-optional";
1430
+ ParamType["String"] = "string";
1431
+ ParamType["StringOptional"] = "string-optional";
1432
+ })(ParamType || (ParamType = {}));
1433
+ var ParamType$1 = ParamType;
1434
+
1435
+ // Import shared types
1436
+ /**
1437
+ * Handle an error and respond to the client
1438
+ * @author Gabe Abrams
1439
+ * @param res express response
1440
+ * @param error error info
1441
+ * @param opts.err the error to send to the client
1442
+ * or the error message
1443
+ * @param [opts.code] an error code (only used if err.code is not
1444
+ * included)
1445
+ * @param [opts.status=500] the https status code to use
1446
+ * defined)
1447
+ */
1448
+ const handleError = (res, error) => {
1449
+ // Get the error message
1450
+ let message;
1451
+ if (error && error.message) {
1452
+ message = (error.message || 'An unknown error occurred.');
1453
+ }
1454
+ else if (typeof error === 'string') {
1455
+ message = (error.trim().length > 0
1456
+ ? error
1457
+ : 'An unknown error occurred.');
1458
+ }
1459
+ else {
1460
+ message = 'An unknown error occurred.';
1461
+ }
1462
+ // Get the error code
1463
+ const code = (error.code || ReactKitErrorCode$1.NoCode);
1464
+ // Get the status code
1465
+ const status = (error.status || 500);
1466
+ // Respond to user
1467
+ res
1468
+ // Set the http status code
1469
+ .status(status)
1470
+ // Send a JSON response
1471
+ .json({
1472
+ // Error message
1473
+ message,
1474
+ // Error code
1475
+ code,
1476
+ // Success = false flag so client can detect server-side errors
1477
+ success: false,
1478
+ });
1479
+ return undefined;
1480
+ };
1481
+
1482
+ /**
1483
+ * Send successful API response
1484
+ * @author Gabe Abrams
1485
+ * @param res express response
1486
+ * @param body the body of the response to send to the client
1487
+ */
1488
+ const handleSuccess = (res, body) => {
1489
+ // Send a http 200 json response
1490
+ res.json({
1491
+ // Include the body as a parameter
1492
+ body,
1493
+ // Success = true flag so client can detect successful responses
1494
+ success: true,
1495
+ });
1496
+ return undefined;
1497
+ };
1498
+
1499
+ /**
1500
+ * Generate an express API route handler
1501
+ * @author Gabe Abrams
1502
+ * @param opts object containing all arguments
1503
+ * @param opts.paramTypes map containing the types for each parameter that is
1504
+ * included in the request (map: param name => type)
1505
+ * @param opts.handler function that processes the request
1506
+ * @returns express route handler that takes the following arguments:
1507
+ * params (map: param name => value), handleSuccess (function for handling
1508
+ * successful requests), handleError (function for handling failed requests),
1509
+ * req (express request object), res (express response object)
1510
+ */
1511
+ const genRouteHandler = (opts) => {
1512
+ // Return a route handler
1513
+ return (req, res) => __awaiter(void 0, void 0, void 0, function* () {
1514
+ var _a;
1515
+ // Output params
1516
+ const output = {};
1517
+ /*----------------------------------------*/
1518
+ /* Parse Params */
1519
+ /*----------------------------------------*/
1520
+ // Process items one by one
1521
+ const paramList = Object.entries((_a = opts.paramTypes) !== null && _a !== void 0 ? _a : {});
1522
+ for (let i = 0; i < paramList.length; i++) {
1523
+ const [name, type] = paramList[i];
1524
+ // Find the value as a string
1525
+ const value = (req.params[name]
1526
+ || req.query[name]
1527
+ || req.body[name]);
1528
+ // Parse
1529
+ if (type === ParamType$1.Boolean || type === ParamType$1.BooleanOptional) {
1530
+ // Boolean
1531
+ // Handle case where value doesn't exist
1532
+ if (value === undefined) {
1533
+ if (type === ParamType$1.BooleanOptional) {
1534
+ output[name] = undefined;
1535
+ }
1536
+ else {
1537
+ return handleError(res, {
1538
+ message: `Parameter ${name} is required, but it was not included.`,
1539
+ code: ReactKitErrorCode$1.MissingParameter,
1540
+ status: 422,
1541
+ });
1542
+ }
1543
+ }
1544
+ else {
1545
+ // Value exists
1546
+ // Simplify value
1547
+ const simpleVal = (String(value)
1548
+ .trim()
1549
+ .toLowerCase());
1550
+ // Parse
1551
+ output[name] = ([
1552
+ 'true',
1553
+ 'yes',
1554
+ 'y',
1555
+ '1',
1556
+ 't',
1557
+ ].indexOf(simpleVal) >= 0);
1558
+ }
1559
+ }
1560
+ else if (type === ParamType$1.Float || type === ParamType$1.FloatOptional) {
1561
+ // Float
1562
+ // Handle case where value doesn't exist
1563
+ if (value === undefined) {
1564
+ if (type === ParamType$1.FloatOptional) {
1565
+ output[name] = undefined;
1566
+ }
1567
+ else {
1568
+ return handleError(res, {
1569
+ message: `Parameter ${name} is required, but it was not included.`,
1570
+ code: ReactKitErrorCode$1.MissingParameter,
1571
+ status: 422,
1572
+ });
1573
+ }
1574
+ }
1575
+ else if (!Number.isNaN(Number.parseFloat(String(value)))) {
1576
+ // Value is a number
1577
+ output[name] = Number.parseFloat(String(value));
1578
+ }
1579
+ else {
1580
+ // Issue!
1581
+ return handleError(res, {
1582
+ message: `Request data was malformed: ${name} was not a valid float.`,
1583
+ code: ReactKitErrorCode$1.InvalidParameter,
1584
+ status: 422,
1585
+ });
1586
+ }
1587
+ }
1588
+ else if (type === ParamType$1.Int || type === ParamType$1.IntOptional) {
1589
+ // Int
1590
+ // Handle case where value doesn't exist
1591
+ if (value === undefined) {
1592
+ if (type === ParamType$1.IntOptional) {
1593
+ output[name] = undefined;
1594
+ }
1595
+ else {
1596
+ return handleError(res, {
1597
+ message: `Parameter ${name} is required, but it was not included.`,
1598
+ code: ReactKitErrorCode$1.MissingParameter,
1599
+ status: 422,
1600
+ });
1601
+ }
1602
+ }
1603
+ else if (!Number.isNaN(Number.parseInt(String(value), 10))) {
1604
+ // Value is a number
1605
+ output[name] = Number.parseInt(String(value), 10);
1606
+ }
1607
+ else {
1608
+ // Issue!
1609
+ return handleError(res, {
1610
+ message: `Request data was malformed: ${name} was not a valid int.`,
1611
+ code: ReactKitErrorCode$1.InvalidParameter,
1612
+ status: 422,
1613
+ });
1614
+ }
1615
+ }
1616
+ else if (type === ParamType$1.JSON || type === ParamType$1.JSONOptional) {
1617
+ // Stringified JSON
1618
+ // Handle case where value doesn't exist
1619
+ if (value === undefined) {
1620
+ if (type === ParamType$1.JSONOptional) {
1621
+ output[name] = undefined;
1622
+ }
1623
+ else {
1624
+ return handleError(res, {
1625
+ message: `Parameter ${name} is required, but it was not included.`,
1626
+ code: ReactKitErrorCode$1.MissingParameter,
1627
+ status: 422,
1628
+ });
1629
+ }
1630
+ }
1631
+ else {
1632
+ // Value exists
1633
+ // Parse
1634
+ try {
1635
+ output[name] = JSON.parse(String(value));
1636
+ }
1637
+ catch (err) {
1638
+ return handleError(res, {
1639
+ message: `Request data was malformed: ${name} was not a valid JSON payload.`,
1640
+ code: ReactKitErrorCode$1.InvalidParameter,
1641
+ status: 422,
1642
+ });
1643
+ }
1644
+ }
1645
+ }
1646
+ else if (type === ParamType$1.String || type === ParamType$1.StringOptional) {
1647
+ // String
1648
+ // Handle case where value doesn't exist
1649
+ if (value === undefined) {
1650
+ if (type === ParamType$1.StringOptional) {
1651
+ output[name] = undefined;
1652
+ }
1653
+ else {
1654
+ return handleError(res, {
1655
+ message: `Parameter ${name} is required, but it was not included.`,
1656
+ code: ReactKitErrorCode$1.MissingParameter,
1657
+ status: 422,
1658
+ });
1659
+ }
1660
+ }
1661
+ else {
1662
+ // Value exists
1663
+ // Leave as is
1664
+ output[name] = value;
1665
+ }
1666
+ }
1667
+ else {
1668
+ // No valid data type
1669
+ return handleError(res, {
1670
+ message: `An internal error occurred: we could not determine the type of ${name}.`,
1671
+ code: ReactKitErrorCode$1.InvalidParameter,
1672
+ status: 422,
1673
+ });
1674
+ }
1675
+ }
1676
+ /*----------------------------------------*/
1677
+ /* Launch Info */
1678
+ /*----------------------------------------*/
1679
+ // Get launch info
1680
+ const { launched, launchInfo } = cacclGetLaunchInfo(req);
1681
+ if (!launched || !launchInfo) {
1682
+ return handleError(res, {
1683
+ message: 'Your session has expired. Please refresh the page and try again.',
1684
+ code: ReactKitErrorCode$1.SessionExpired,
1685
+ status: 440,
1686
+ });
1687
+ }
1688
+ // Error if user info cannot be found
1689
+ if (!launchInfo.userId
1690
+ || !launchInfo.userFirstName
1691
+ || !launchInfo.userLastName
1692
+ || (launchInfo.notInCourse
1693
+ && !launchInfo.isAdmin)
1694
+ || (!launchInfo.isTTM
1695
+ && !launchInfo.isLearner
1696
+ && !launchInfo.isAdmin)) {
1697
+ return handleError(res, {
1698
+ message: 'Your session was invalid. Please refresh the page and try again.',
1699
+ code: ReactKitErrorCode$1.SessionExpired,
1700
+ status: 440,
1701
+ });
1702
+ }
1703
+ // Add launch info to output
1704
+ output.userId = launchInfo.userId;
1705
+ output.userFirstName = launchInfo.userFirstName;
1706
+ output.userLastName = launchInfo.userLastName;
1707
+ output.isLearner = !!launchInfo.isLearner;
1708
+ output.isTTM = !!launchInfo.isTTM;
1709
+ output.isAdmin = !!launchInfo.isAdmin;
1710
+ output.isWatchingInPrivate = !!(req.session.isWatchingInPrivate);
1711
+ /*----------------------------------------*/
1712
+ /* Require Course Consistency */
1713
+ /*----------------------------------------*/
1714
+ // Make sure the user actually launched from the appropriate course
1715
+ if (output.courseId
1716
+ && launchInfo.courseId
1717
+ && output.courseId !== launchInfo.courseId
1718
+ && !output.isTTM
1719
+ && !output.isAdmin) {
1720
+ // Course of interest is not the launch course
1721
+ return handleError(res, {
1722
+ message: 'You switched sessions by opening Immersive Classroom in another tab. Please refresh the page and try again.',
1723
+ code: ReactKitErrorCode$1.WrongCourse,
1724
+ status: 401,
1725
+ });
1726
+ }
1727
+ /*------------------------------------------------------------------------*/
1728
+ /* Call handler */
1729
+ /*------------------------------------------------------------------------*/
1730
+ opts.handler({
1731
+ params: output,
1732
+ handleSuccess: (body) => {
1733
+ return handleSuccess(res, body);
1734
+ },
1735
+ handleError: (error) => {
1736
+ return handleError(res, error);
1737
+ },
1738
+ req,
1739
+ res,
1740
+ });
1741
+ });
1742
+ };
1743
+
897
1744
  exports.AppWrapper = AppWrapper;
1745
+ exports.ButtonInputGroup = ButtonInputGroup;
1746
+ exports.CheckboxButton = CheckboxButton;
898
1747
  exports.ErrorBox = ErrorBox;
899
1748
  exports.ErrorWithCode = ErrorWithCode;
900
1749
  exports.LoadingSpinner = LoadingSpinner;
@@ -902,20 +1751,30 @@ exports.Modal = Modal;
902
1751
  exports.ModalButtonType = ModalButtonType$1;
903
1752
  exports.ModalSize = ModalSize$1;
904
1753
  exports.ModalType = ModalType$1;
1754
+ exports.ParamType = ParamType$1;
1755
+ exports.RadioButton = RadioButton;
905
1756
  exports.ReactKitErrorCode = ReactKitErrorCode$1;
1757
+ exports.SimpleDateChooser = SimpleDateChooser;
906
1758
  exports.TabBox = TabBox;
907
1759
  exports.Variant = Variant$1;
908
1760
  exports.abbreviate = abbreviate;
909
- exports.alert = alert;
1761
+ exports.alert = alert$1;
910
1762
  exports.avg = avg;
911
1763
  exports.ceilToNumDecimals = ceilToNumDecimals;
912
1764
  exports.confirm = confirm;
913
1765
  exports.floorToNumDecimals = floorToNumDecimals;
914
1766
  exports.forceNumIntoBounds = forceNumIntoBounds;
1767
+ exports.genRouteHandler = genRouteHandler;
1768
+ exports.getOrdinal = getOrdinal;
1769
+ exports.getTimeInfoInET = getTimeInfoInET;
1770
+ exports.handleError = handleError;
1771
+ exports.handleSuccess = handleSuccess;
1772
+ exports.initServer = initServer;
915
1773
  exports.padDecimalZeros = padDecimalZeros;
916
1774
  exports.padZerosLeft = padZerosLeft;
917
1775
  exports.roundToNumDecimals = roundToNumDecimals;
918
1776
  exports.showFatalError = showFatalError;
919
1777
  exports.sum = sum;
1778
+ exports.visitServerEndpoint = visitServerEndpoint;
920
1779
  exports.waitMs = waitMs;
921
1780
  //# sourceMappingURL=index.js.map