dce-reactkit 3.0.0-beta.9 → 3.0.2

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 (59) hide show
  1. package/README.md +1 -546
  2. package/dist/cjs/index.js +945 -83
  3. package/dist/cjs/index.js.map +1 -1
  4. package/dist/cjs/types/components/AppWrapper.d.ts +25 -0
  5. package/dist/cjs/types/components/ButtonInputGroup.d.ts +12 -0
  6. package/dist/cjs/types/components/CheckboxButton.d.ts +20 -0
  7. package/dist/cjs/types/components/RadioButton.d.ts +20 -0
  8. package/dist/cjs/types/components/SimpleDateChooser.d.ts +22 -0
  9. package/dist/cjs/types/components/TabBox.d.ts +1 -0
  10. package/dist/cjs/types/helpers/genRouteHandler.d.ts +32 -0
  11. package/dist/cjs/types/helpers/getOrdinal.d.ts +8 -0
  12. package/dist/cjs/types/helpers/getTimeInfoInET.d.ts +17 -0
  13. package/dist/cjs/types/helpers/handleError.d.ts +18 -0
  14. package/dist/cjs/types/helpers/handleSuccess.d.ts +8 -0
  15. package/dist/cjs/types/helpers/visitServerEndpoint.d.ts +23 -0
  16. package/dist/cjs/types/index.d.ts +13 -1
  17. package/dist/cjs/types/server/initServer.d.ts +21 -0
  18. package/dist/cjs/types/types/ParamType.d.ts +17 -0
  19. package/dist/cjs/types/types/ReactKitErrorCode.d.ts +3 -1
  20. package/dist/esm/index.js +934 -84
  21. package/dist/esm/index.js.map +1 -1
  22. package/dist/esm/types/components/AppWrapper.d.ts +25 -0
  23. package/dist/esm/types/components/ButtonInputGroup.d.ts +12 -0
  24. package/dist/esm/types/components/CheckboxButton.d.ts +20 -0
  25. package/dist/esm/types/components/RadioButton.d.ts +20 -0
  26. package/dist/esm/types/components/SimpleDateChooser.d.ts +22 -0
  27. package/dist/esm/types/components/TabBox.d.ts +1 -0
  28. package/dist/esm/types/helpers/genRouteHandler.d.ts +32 -0
  29. package/dist/esm/types/helpers/getOrdinal.d.ts +8 -0
  30. package/dist/esm/types/helpers/getTimeInfoInET.d.ts +17 -0
  31. package/dist/esm/types/helpers/handleError.d.ts +18 -0
  32. package/dist/esm/types/helpers/handleSuccess.d.ts +8 -0
  33. package/dist/esm/types/helpers/visitServerEndpoint.d.ts +23 -0
  34. package/dist/esm/types/index.d.ts +13 -1
  35. package/dist/esm/types/server/initServer.d.ts +21 -0
  36. package/dist/esm/types/types/ParamType.d.ts +17 -0
  37. package/dist/esm/types/types/ReactKitErrorCode.d.ts +3 -1
  38. package/dist/index.d.ts +231 -10
  39. package/package.json +3 -1
  40. package/rollup.config.js +0 -1
  41. package/src/components/AppWrapper.tsx +73 -11
  42. package/src/components/ButtonInputGroup.tsx +89 -0
  43. package/src/components/CheckboxButton.tsx +100 -0
  44. package/src/components/ErrorBox.tsx +4 -4
  45. package/src/components/LoadingSpinner.tsx +3 -3
  46. package/src/components/Modal.tsx +184 -23
  47. package/src/components/RadioButton.tsx +102 -0
  48. package/src/components/SimpleDateChooser.tsx +222 -0
  49. package/src/components/TabBox.tsx +65 -58
  50. package/src/helpers/genRouteHandler.ts +329 -0
  51. package/src/helpers/getOrdinal.tsx +13 -0
  52. package/src/helpers/getTimeInfoInET.tsx +56 -0
  53. package/src/helpers/handleError.ts +65 -0
  54. package/src/helpers/handleSuccess.ts +18 -0
  55. package/src/helpers/visitServerEndpoint.tsx +110 -0
  56. package/src/index.ts +27 -0
  57. package/src/server/initServer.ts +53 -0
  58. package/src/types/ParamType.ts +18 -0
  59. package/src/types/ReactKitErrorCode.tsx +3 -1
package/dist/cjs/index.js CHANGED
@@ -5,6 +5,7 @@ Object.defineProperty(exports, '__esModule', { value: true });
5
5
  var React = require('react');
6
6
  var reactFontawesome = require('@fortawesome/react-fontawesome');
7
7
  var freeSolidSvgIcons = require('@fortawesome/free-solid-svg-icons');
8
+ var freeRegularSvgIcons = require('@fortawesome/free-regular-svg-icons');
8
9
 
9
10
  function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
10
11
 
@@ -35,7 +36,7 @@ function __awaiter(thisArg, _arguments, P, generator) {
35
36
  });
36
37
  }
37
38
 
38
- // Highest error code = DRK2
39
+ // Highest error code = DRK8
39
40
  /**
40
41
  * List of error codes built into the react kit
41
42
  * @author Gabe Abrams
@@ -48,6 +49,8 @@ var ReactKitErrorCode;
48
49
  ReactKitErrorCode["MissingParameter"] = "DRK4";
49
50
  ReactKitErrorCode["InvalidParameter"] = "DRK5";
50
51
  ReactKitErrorCode["WrongCourse"] = "DRK6";
52
+ ReactKitErrorCode["NoCACCLSendRequestFunction"] = "DRK7";
53
+ ReactKitErrorCode["NoCACCLGetLaunchInfoFunction"] = "DRK8";
51
54
  })(ReactKitErrorCode || (ReactKitErrorCode = {}));
52
55
  var ReactKitErrorCode$1 = ReactKitErrorCode;
53
56
 
@@ -79,9 +82,9 @@ const ErrorBox = (props) => {
79
82
  ' ',
80
83
  React__default["default"].createElement("span", { style: {
81
84
  backgroundColor: 'white',
82
- borderRadius: '5px',
83
- paddingLeft: '3px',
84
- paddingRight: '3px',
85
+ borderRadius: '0.3rem',
86
+ paddingLeft: '0.2rem',
87
+ paddingRight: '0.2rem',
85
88
  color: '#DC4150',
86
89
  fontVariant: 'small-caps',
87
90
  fontSize: '80%',
@@ -93,7 +96,7 @@ const ErrorBox = (props) => {
93
96
  }
94
97
  // Main UI
95
98
  return (React__default["default"].createElement("div", { className: "alert alert-danger text-center", style: {
96
- maxWidth: '650px',
99
+ maxWidth: '40rem',
97
100
  margin: 'auto',
98
101
  } },
99
102
  React__default["default"].createElement("h4", { className: "mb-1" },
@@ -192,9 +195,7 @@ var ModalSize$1 = ModalSize;
192
195
  /* Constants */
193
196
  /*------------------------------------------------------------------------*/
194
197
  // Constants
195
- const MS_TO_ANIMATE = 400; // Time to animate in/out (defined by bootstrap)
196
- const MS_ANIMATE_IN_DELAY = 10;
197
- // Time to wait before animating in (must be >0 or animation won't trigger)
198
+ const MS_TO_ANIMATE = 200; // Animation duration
198
199
  // Modal type to list of buttons
199
200
  const modalTypeToModalButtonTypes = {
200
201
  [ModalType$1.Okay]: [
@@ -274,6 +275,114 @@ const ModalButtonTypeToLabelAndVariant = {
274
275
  },
275
276
  };
276
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
+ /*------------------------------------------------------------------------*/
277
386
  /* Component */
278
387
  /*------------------------------------------------------------------------*/
279
388
  const Modal = (props) => {
@@ -284,10 +393,15 @@ const Modal = (props) => {
284
393
  /* -------------- Props ------------- */
285
394
  const { type = ModalType$1.NoButtons, size = ModalSize$1.Large, title, children, onClose, dontAllowBackdropExit, onTopOfOtherModals, } = props;
286
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);
287
399
  // If true, the modal is shown
288
400
  const [visible, setVisible] = React.useState(false);
289
- // True if currently animating in
290
- 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);
291
405
  /*------------------------------------------------------------------------*/
292
406
  /* Lifecycle Functions */
293
407
  /*------------------------------------------------------------------------*/
@@ -297,12 +411,10 @@ const Modal = (props) => {
297
411
  */
298
412
  React.useEffect(() => {
299
413
  (() => __awaiter(void 0, void 0, void 0, function* () {
300
- // Start the component animating in
301
- yield waitMs(MS_ANIMATE_IN_DELAY);
302
- setAnimatingIn(true);
303
414
  // Wait and then set visible to true
304
415
  yield waitMs(MS_TO_ANIMATE);
305
416
  setVisible(true);
417
+ setAnimatingIn(false);
306
418
  }))();
307
419
  }, []);
308
420
  /*------------------------------------------------------------------------*/
@@ -329,13 +441,19 @@ const Modal = (props) => {
329
441
  }
330
442
  // Update the state
331
443
  setVisible(false);
444
+ setAnimatingOut(true);
332
445
  // Call the handler after the modal has animated out
333
446
  yield waitMs(MS_TO_ANIMATE);
334
447
  onClose(ModalButtonType);
448
+ setGone(true);
335
449
  });
336
450
  /*------------------------------------------------------------------------*/
337
451
  /* Render */
338
452
  /*------------------------------------------------------------------------*/
453
+ // Return nothing if gone
454
+ if (gone) {
455
+ return null;
456
+ }
339
457
  /*----------------------------------------*/
340
458
  /* Footer */
341
459
  /*----------------------------------------*/
@@ -357,7 +475,7 @@ const Modal = (props) => {
357
475
  // Check if this button is last
358
476
  const last = (i === ModalButtonTypes.length - 1);
359
477
  // Create the button
360
- 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: () => {
361
479
  handleClose(ModalButtonType);
362
480
  } }, label));
363
481
  });
@@ -365,22 +483,62 @@ const Modal = (props) => {
365
483
  const footer = ((buttons && buttons.length)
366
484
  ? (React__default["default"].createElement("div", null, buttons))
367
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
+ }
368
500
  // Render the modal
369
- return (React__default["default"].createElement("div", { className: `modal fade show modal-dialog-scrollable 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: {
370
502
  zIndex: (onTopOfOtherModals
371
- ? 6000000000
503
+ ? 5000000001
372
504
  : 5000000000),
373
505
  display: 'block',
506
+ margin: 'auto',
507
+ left: 0,
508
+ right: 0,
374
509
  } },
375
- 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
+ } },
376
531
  React__default["default"].createElement("div", { className: "modal-content" },
377
532
  React__default["default"].createElement("div", { className: "modal-header" },
378
- React__default["default"].createElement("h5", { className: "modal-title" }, title),
379
- 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
380
538
  handleClose(ModalButtonType$1.Cancel);
381
539
  } }))),
382
540
  children && (React__default["default"].createElement("div", { className: "modal-body" }, children)),
383
- 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))))));
384
542
  };
385
543
 
386
544
  /**
@@ -404,6 +562,24 @@ class ErrorWithCode extends Error {
404
562
  /* Static Helpers */
405
563
  /*------------------------------------------------------------------------*/
406
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
+ /*----------------------------------------*/
407
583
  /* Alert */
408
584
  /*----------------------------------------*/
409
585
  // Stored copies of setters
@@ -415,7 +591,7 @@ let onAlertClosed;
415
591
  * @param title the title text to display at the top of the alert
416
592
  * @param text the text to display in the alert
417
593
  */
418
- 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* () {
419
595
  // Fallback if alert not available
420
596
  if (!setAlertInfo) {
421
597
  window.alert(`${title}\n\n${text}`);
@@ -489,7 +665,7 @@ const showFatalError = (error, errorTitle = 'An Error Occurred') => {
489
665
  : String((_b = error.code) !== null && _b !== void 0 ? _b : ReactKitErrorCode$1.NoCode));
490
666
  // Handle case where app hasn't loaded
491
667
  if (!setFatalErrorMessage || !setFatalErrorCode) {
492
- alert(errorTitle, `${message} (code: ${code}). Please contact support.`);
668
+ alert$1(errorTitle, `${message} (code: ${code}). Please contact support.`);
493
669
  return undefined;
494
670
  }
495
671
  // Use setters
@@ -506,7 +682,9 @@ const AppWrapper = (props) => {
506
682
  /* Setup */
507
683
  /*------------------------------------------------------------------------*/
508
684
  /* -------------- Props ------------- */
509
- 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;
510
688
  /* -------------- State ------------- */
511
689
  // Fatal error
512
690
  const [fatalErrorMessage, setFatalErrorMessageInner,] = React.useState();
@@ -516,10 +694,10 @@ const AppWrapper = (props) => {
516
694
  const [fatalErrorTitle, setFatalErrorTitleInner,] = React.useState();
517
695
  setFatalErrorTitle = setFatalErrorTitleInner;
518
696
  // Alert
519
- const [alertInfo, setAlertInfoInner,] = React.useState();
697
+ const [alertInfo, setAlertInfoInner,] = React.useState(undefined);
520
698
  setAlertInfo = setAlertInfoInner;
521
699
  // Confirm
522
- const [confirmInfo, setConfirmInfoInner,] = React.useState();
700
+ const [confirmInfo, setConfirmInfoInner,] = React.useState(undefined);
523
701
  setConfirmInfo = setConfirmInfoInner;
524
702
  // Session expired
525
703
  const [sessionHasExpired, setSessionHasExpiredInner,] = React.useState(false);
@@ -533,8 +711,9 @@ const AppWrapper = (props) => {
533
711
  let modal;
534
712
  /* -------------- Alert ------------- */
535
713
  if (alertInfo) {
536
- 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: () => {
537
715
  // Alert closed
716
+ setAlertInfo(undefined);
538
717
  if (onAlertClosed) {
539
718
  onAlertClosed();
540
719
  }
@@ -542,11 +721,12 @@ const AppWrapper = (props) => {
542
721
  }
543
722
  /* ------------- Confirm ------------ */
544
723
  if (confirmInfo) {
545
- 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);
546
726
  if (onConfirmClosed) {
547
727
  onConfirmClosed(buttonType === ModalButtonType$1.Okay);
548
728
  }
549
- }, dontAllowBackdropExit: true }));
729
+ }, dontAllowBackdropExit: true }, confirmInfo.text));
550
730
  }
551
731
  /*----------------------------------------*/
552
732
  /* Views */
@@ -598,10 +778,10 @@ const style$1 = `
598
778
  .LoadingSpinner-blip-2,
599
779
  .LoadingSpinner-blip-3,
600
780
  .LoadingSpinner-blip-4 {
601
- font-size: 25px;
781
+ font-size: 1.8rem;
602
782
  opacity: 0.6;
603
- margin-top: 20px;
604
- margin-bottom: 20px;
783
+ margin-top: 1rem;
784
+ margin-bottom: 1rem;
605
785
  }
606
786
 
607
787
  /* First Blip */
@@ -668,63 +848,67 @@ const LoadingSpinner = () => {
668
848
  /* Style */
669
849
  /*------------------------------------------------------------------------*/
670
850
  const style = `
671
- /* Tab Box */
672
- .TabBox-box {
673
- /* Light Border */
674
- border: 2px solid #dedede;
675
-
676
- /* Rounded Corners (except top-left) */
677
- border-bottom-right-radius: 5px;
678
- border-bottom-left-radius: 5px;
679
- border-top-right-radius: 5px;
680
-
681
- /* Very Light Gray Border */
682
- background: #fdfdfd;
683
-
684
- /* Align Contents on Left */
685
- text-align: left;
686
- }
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;
687
860
 
688
- /* Container for Title */
689
- .TabBox-title-container {
690
- /* Place on Left */
691
- position: relative;
692
- left: 0;
693
- text-align: left;
694
- }
861
+ /* Very Light Gray Border */
862
+ background: #fdfdfd;
695
863
 
696
- /* Tab-style Title */
697
- .TabBox-title {
698
- /* Place so it Barely Overlaps the Box Border */
699
- display: inline-block;
700
- position: relative;
701
- top: 2px; /* Gives Illusion that Border Doesn't Exist Below Tab */
864
+ /* Align Contents on Left */
865
+ text-align: left;
702
866
 
703
- /* Title-sized Font */
704
- font-size: 25px;
867
+ /* Add Text Padding */
868
+ padding-left: 0.3rem;
869
+ padding-right: 0.3rem;
870
+ }
705
871
 
706
- /* Add Border on Top and Sides */
707
- border-top: 2px solid #dedede;
708
- border-left: 2px solid #dedede;
709
- 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
+ }
710
879
 
711
- /* Round the Top Corners */
712
- border-top-left-radius: 5px;
713
- 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 */
714
886
 
715
- /* Add Text Padding */
716
- padding-left: 12px;
717
- padding-right: 12px;
887
+ /* Title-sized Font */
888
+ font-size: 1.5rem;
718
889
 
719
- /* Match Background Color of Box */
720
- background: #fdfdfd;
721
- }
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;
722
894
 
723
- /* Make the TabBox's Children Appear Above Title if Overlap Occurs */
724
- .TabBox-children {
725
- position: relative;
726
- z-index: 1;
727
- }
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
+ }
728
912
  `;
729
913
  /*------------------------------------------------------------------------*/
730
914
  /* Component */
@@ -734,7 +918,7 @@ const TabBox = (props) => {
734
918
  /* Setup */
735
919
  /*------------------------------------------------------------------------*/
736
920
  /* -------------- Props ------------- */
737
- const { title, children, } = props;
921
+ const { title, children, noBottomPadding, } = props;
738
922
  /*------------------------------------------------------------------------*/
739
923
  /* Render */
740
924
  /*------------------------------------------------------------------------*/
@@ -742,7 +926,7 @@ const TabBox = (props) => {
742
926
  /* Main UI */
743
927
  /*----------------------------------------*/
744
928
  // Full UI
745
- return (React__default["default"].createElement("div", null,
929
+ return (React__default["default"].createElement("div", { className: `TabBox-container ${noBottomPadding ? '' : 'mb-2'}` },
746
930
  React__default["default"].createElement("style", null, style),
747
931
  React__default["default"].createElement("div", { className: "TabBox-title-container" },
748
932
  React__default["default"].createElement("div", { className: "TabBox-title" }, title)),
@@ -750,6 +934,252 @@ const TabBox = (props) => {
750
934
  React__default["default"].createElement("div", { className: "TabBox-children" }, children))));
751
935
  };
752
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
+
753
1183
  /**
754
1184
  * Shorten text so it fits into a certain number of chars
755
1185
  * @author Gabe Abrams
@@ -893,7 +1323,429 @@ const roundToNumDecimals = (num, numDecimals) => {
893
1323
  return (Math.round(num * rounder) / rounder);
894
1324
  };
895
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
+ * next (express next function)
1511
+ */
1512
+ const genRouteHandler = (opts) => {
1513
+ // Return a route handler
1514
+ return (req, res, next) => __awaiter(void 0, void 0, void 0, function* () {
1515
+ var _a;
1516
+ // Output params
1517
+ const output = {};
1518
+ /*----------------------------------------*/
1519
+ /* Parse Params */
1520
+ /*----------------------------------------*/
1521
+ // Process items one by one
1522
+ const paramList = Object.entries((_a = opts.paramTypes) !== null && _a !== void 0 ? _a : {});
1523
+ for (let i = 0; i < paramList.length; i++) {
1524
+ const [name, type] = paramList[i];
1525
+ // Find the value as a string
1526
+ const value = (req.params[name]
1527
+ || req.query[name]
1528
+ || req.body[name]);
1529
+ // Parse
1530
+ if (type === ParamType$1.Boolean || type === ParamType$1.BooleanOptional) {
1531
+ // Boolean
1532
+ // Handle case where value doesn't exist
1533
+ if (value === undefined) {
1534
+ if (type === ParamType$1.BooleanOptional) {
1535
+ output[name] = undefined;
1536
+ }
1537
+ else {
1538
+ return handleError(res, {
1539
+ message: `Parameter ${name} is required, but it was not included.`,
1540
+ code: ReactKitErrorCode$1.MissingParameter,
1541
+ status: 422,
1542
+ });
1543
+ }
1544
+ }
1545
+ else {
1546
+ // Value exists
1547
+ // Simplify value
1548
+ const simpleVal = (String(value)
1549
+ .trim()
1550
+ .toLowerCase());
1551
+ // Parse
1552
+ output[name] = ([
1553
+ 'true',
1554
+ 'yes',
1555
+ 'y',
1556
+ '1',
1557
+ 't',
1558
+ ].indexOf(simpleVal) >= 0);
1559
+ }
1560
+ }
1561
+ else if (type === ParamType$1.Float || type === ParamType$1.FloatOptional) {
1562
+ // Float
1563
+ // Handle case where value doesn't exist
1564
+ if (value === undefined) {
1565
+ if (type === ParamType$1.FloatOptional) {
1566
+ output[name] = undefined;
1567
+ }
1568
+ else {
1569
+ return handleError(res, {
1570
+ message: `Parameter ${name} is required, but it was not included.`,
1571
+ code: ReactKitErrorCode$1.MissingParameter,
1572
+ status: 422,
1573
+ });
1574
+ }
1575
+ }
1576
+ else if (!Number.isNaN(Number.parseFloat(String(value)))) {
1577
+ // Value is a number
1578
+ output[name] = Number.parseFloat(String(value));
1579
+ }
1580
+ else {
1581
+ // Issue!
1582
+ return handleError(res, {
1583
+ message: `Request data was malformed: ${name} was not a valid float.`,
1584
+ code: ReactKitErrorCode$1.InvalidParameter,
1585
+ status: 422,
1586
+ });
1587
+ }
1588
+ }
1589
+ else if (type === ParamType$1.Int || type === ParamType$1.IntOptional) {
1590
+ // Int
1591
+ // Handle case where value doesn't exist
1592
+ if (value === undefined) {
1593
+ if (type === ParamType$1.IntOptional) {
1594
+ output[name] = undefined;
1595
+ }
1596
+ else {
1597
+ return handleError(res, {
1598
+ message: `Parameter ${name} is required, but it was not included.`,
1599
+ code: ReactKitErrorCode$1.MissingParameter,
1600
+ status: 422,
1601
+ });
1602
+ }
1603
+ }
1604
+ else if (!Number.isNaN(Number.parseInt(String(value), 10))) {
1605
+ // Value is a number
1606
+ output[name] = Number.parseInt(String(value), 10);
1607
+ }
1608
+ else {
1609
+ // Issue!
1610
+ return handleError(res, {
1611
+ message: `Request data was malformed: ${name} was not a valid int.`,
1612
+ code: ReactKitErrorCode$1.InvalidParameter,
1613
+ status: 422,
1614
+ });
1615
+ }
1616
+ }
1617
+ else if (type === ParamType$1.JSON || type === ParamType$1.JSONOptional) {
1618
+ // Stringified JSON
1619
+ // Handle case where value doesn't exist
1620
+ if (value === undefined) {
1621
+ if (type === ParamType$1.JSONOptional) {
1622
+ output[name] = undefined;
1623
+ }
1624
+ else {
1625
+ return handleError(res, {
1626
+ message: `Parameter ${name} is required, but it was not included.`,
1627
+ code: ReactKitErrorCode$1.MissingParameter,
1628
+ status: 422,
1629
+ });
1630
+ }
1631
+ }
1632
+ else {
1633
+ // Value exists
1634
+ // Parse
1635
+ try {
1636
+ output[name] = JSON.parse(String(value));
1637
+ }
1638
+ catch (err) {
1639
+ return handleError(res, {
1640
+ message: `Request data was malformed: ${name} was not a valid JSON payload.`,
1641
+ code: ReactKitErrorCode$1.InvalidParameter,
1642
+ status: 422,
1643
+ });
1644
+ }
1645
+ }
1646
+ }
1647
+ else if (type === ParamType$1.String || type === ParamType$1.StringOptional) {
1648
+ // String
1649
+ // Handle case where value doesn't exist
1650
+ if (value === undefined) {
1651
+ if (type === ParamType$1.StringOptional) {
1652
+ output[name] = undefined;
1653
+ }
1654
+ else {
1655
+ return handleError(res, {
1656
+ message: `Parameter ${name} is required, but it was not included.`,
1657
+ code: ReactKitErrorCode$1.MissingParameter,
1658
+ status: 422,
1659
+ });
1660
+ }
1661
+ }
1662
+ else {
1663
+ // Value exists
1664
+ // Leave as is
1665
+ output[name] = value;
1666
+ }
1667
+ }
1668
+ else {
1669
+ // No valid data type
1670
+ return handleError(res, {
1671
+ message: `An internal error occurred: we could not determine the type of ${name}.`,
1672
+ code: ReactKitErrorCode$1.InvalidParameter,
1673
+ status: 422,
1674
+ });
1675
+ }
1676
+ }
1677
+ /*----------------------------------------*/
1678
+ /* Launch Info */
1679
+ /*----------------------------------------*/
1680
+ // Get launch info
1681
+ const { launched, launchInfo } = cacclGetLaunchInfo(req);
1682
+ if (!launched || !launchInfo) {
1683
+ return handleError(res, {
1684
+ message: 'Your session has expired. Please refresh the page and try again.',
1685
+ code: ReactKitErrorCode$1.SessionExpired,
1686
+ status: 440,
1687
+ });
1688
+ }
1689
+ // Error if user info cannot be found
1690
+ if (!launchInfo.userId
1691
+ || !launchInfo.userFirstName
1692
+ || !launchInfo.userLastName
1693
+ || (launchInfo.notInCourse
1694
+ && !launchInfo.isAdmin)
1695
+ || (!launchInfo.isTTM
1696
+ && !launchInfo.isLearner
1697
+ && !launchInfo.isAdmin)) {
1698
+ return handleError(res, {
1699
+ message: 'Your session was invalid. Please refresh the page and try again.',
1700
+ code: ReactKitErrorCode$1.SessionExpired,
1701
+ status: 440,
1702
+ });
1703
+ }
1704
+ // Add launch info to output
1705
+ output.userId = launchInfo.userId;
1706
+ output.userFirstName = launchInfo.userFirstName;
1707
+ output.userLastName = launchInfo.userLastName;
1708
+ output.isLearner = !!launchInfo.isLearner;
1709
+ output.isTTM = !!launchInfo.isTTM;
1710
+ output.isAdmin = !!launchInfo.isAdmin;
1711
+ output.isWatchingInPrivate = !!(req.session.isWatchingInPrivate);
1712
+ /*----------------------------------------*/
1713
+ /* Require Course Consistency */
1714
+ /*----------------------------------------*/
1715
+ // Make sure the user actually launched from the appropriate course
1716
+ if (output.courseId
1717
+ && launchInfo.courseId
1718
+ && output.courseId !== launchInfo.courseId
1719
+ && !output.isTTM
1720
+ && !output.isAdmin) {
1721
+ // Course of interest is not the launch course
1722
+ return handleError(res, {
1723
+ message: 'You switched sessions by opening Immersive Classroom in another tab. Please refresh the page and try again.',
1724
+ code: ReactKitErrorCode$1.WrongCourse,
1725
+ status: 401,
1726
+ });
1727
+ }
1728
+ /*------------------------------------------------------------------------*/
1729
+ /* Call handler */
1730
+ /*------------------------------------------------------------------------*/
1731
+ opts.handler({
1732
+ params: output,
1733
+ handleSuccess: (body) => {
1734
+ return handleSuccess(res, body);
1735
+ },
1736
+ handleError: (error) => {
1737
+ return handleError(res, error);
1738
+ },
1739
+ req,
1740
+ res,
1741
+ next,
1742
+ });
1743
+ });
1744
+ };
1745
+
896
1746
  exports.AppWrapper = AppWrapper;
1747
+ exports.ButtonInputGroup = ButtonInputGroup;
1748
+ exports.CheckboxButton = CheckboxButton;
897
1749
  exports.ErrorBox = ErrorBox;
898
1750
  exports.ErrorWithCode = ErrorWithCode;
899
1751
  exports.LoadingSpinner = LoadingSpinner;
@@ -901,20 +1753,30 @@ exports.Modal = Modal;
901
1753
  exports.ModalButtonType = ModalButtonType$1;
902
1754
  exports.ModalSize = ModalSize$1;
903
1755
  exports.ModalType = ModalType$1;
1756
+ exports.ParamType = ParamType$1;
1757
+ exports.RadioButton = RadioButton;
904
1758
  exports.ReactKitErrorCode = ReactKitErrorCode$1;
1759
+ exports.SimpleDateChooser = SimpleDateChooser;
905
1760
  exports.TabBox = TabBox;
906
1761
  exports.Variant = Variant$1;
907
1762
  exports.abbreviate = abbreviate;
908
- exports.alert = alert;
1763
+ exports.alert = alert$1;
909
1764
  exports.avg = avg;
910
1765
  exports.ceilToNumDecimals = ceilToNumDecimals;
911
1766
  exports.confirm = confirm;
912
1767
  exports.floorToNumDecimals = floorToNumDecimals;
913
1768
  exports.forceNumIntoBounds = forceNumIntoBounds;
1769
+ exports.genRouteHandler = genRouteHandler;
1770
+ exports.getOrdinal = getOrdinal;
1771
+ exports.getTimeInfoInET = getTimeInfoInET;
1772
+ exports.handleError = handleError;
1773
+ exports.handleSuccess = handleSuccess;
1774
+ exports.initServer = initServer;
914
1775
  exports.padDecimalZeros = padDecimalZeros;
915
1776
  exports.padZerosLeft = padZerosLeft;
916
1777
  exports.roundToNumDecimals = roundToNumDecimals;
917
1778
  exports.showFatalError = showFatalError;
918
1779
  exports.sum = sum;
1780
+ exports.visitServerEndpoint = visitServerEndpoint;
919
1781
  exports.waitMs = waitMs;
920
1782
  //# sourceMappingURL=index.js.map