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