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/esm/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import React, { useState, useEffect } from 'react';
2
2
  import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
3
- 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';
4
5
 
5
6
  /******************************************************************************
6
7
  Copyright (c) Microsoft Corporation.
@@ -27,7 +28,7 @@ function __awaiter(thisArg, _arguments, P, generator) {
27
28
  });
28
29
  }
29
30
 
30
- // Highest error code = DRK2
31
+ // Highest error code = DRK8
31
32
  /**
32
33
  * List of error codes built into the react kit
33
34
  * @author Gabe Abrams
@@ -40,6 +41,8 @@ var ReactKitErrorCode;
40
41
  ReactKitErrorCode["MissingParameter"] = "DRK4";
41
42
  ReactKitErrorCode["InvalidParameter"] = "DRK5";
42
43
  ReactKitErrorCode["WrongCourse"] = "DRK6";
44
+ ReactKitErrorCode["NoCACCLSendRequestFunction"] = "DRK7";
45
+ ReactKitErrorCode["NoCACCLGetLaunchInfoFunction"] = "DRK8";
43
46
  })(ReactKitErrorCode || (ReactKitErrorCode = {}));
44
47
  var ReactKitErrorCode$1 = ReactKitErrorCode;
45
48
 
@@ -71,9 +74,9 @@ const ErrorBox = (props) => {
71
74
  ' ',
72
75
  React.createElement("span", { style: {
73
76
  backgroundColor: 'white',
74
- borderRadius: '5px',
75
- paddingLeft: '3px',
76
- paddingRight: '3px',
77
+ borderRadius: '0.3rem',
78
+ paddingLeft: '0.2rem',
79
+ paddingRight: '0.2rem',
77
80
  color: '#DC4150',
78
81
  fontVariant: 'small-caps',
79
82
  fontSize: '80%',
@@ -85,7 +88,7 @@ const ErrorBox = (props) => {
85
88
  }
86
89
  // Main UI
87
90
  return (React.createElement("div", { className: "alert alert-danger text-center", style: {
88
- maxWidth: '650px',
91
+ maxWidth: '40rem',
89
92
  margin: 'auto',
90
93
  } },
91
94
  React.createElement("h4", { className: "mb-1" },
@@ -184,9 +187,7 @@ var ModalSize$1 = ModalSize;
184
187
  /* Constants */
185
188
  /*------------------------------------------------------------------------*/
186
189
  // Constants
187
- const MS_TO_ANIMATE = 400; // Time to animate in/out (defined by bootstrap)
188
- const MS_ANIMATE_IN_DELAY = 10;
189
- // Time to wait before animating in (must be >0 or animation won't trigger)
190
+ const MS_TO_ANIMATE = 200; // Animation duration
190
191
  // Modal type to list of buttons
191
192
  const modalTypeToModalButtonTypes = {
192
193
  [ModalType$1.Okay]: [
@@ -266,6 +267,114 @@ const ModalButtonTypeToLabelAndVariant = {
266
267
  },
267
268
  };
268
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
+ /*------------------------------------------------------------------------*/
269
378
  /* Component */
270
379
  /*------------------------------------------------------------------------*/
271
380
  const Modal = (props) => {
@@ -276,10 +385,15 @@ const Modal = (props) => {
276
385
  /* -------------- Props ------------- */
277
386
  const { type = ModalType$1.NoButtons, size = ModalSize$1.Large, title, children, onClose, dontAllowBackdropExit, onTopOfOtherModals, } = props;
278
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);
279
391
  // If true, the modal is shown
280
392
  const [visible, setVisible] = useState(false);
281
- // True if currently animating in
282
- 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);
283
397
  /*------------------------------------------------------------------------*/
284
398
  /* Lifecycle Functions */
285
399
  /*------------------------------------------------------------------------*/
@@ -289,12 +403,10 @@ const Modal = (props) => {
289
403
  */
290
404
  useEffect(() => {
291
405
  (() => __awaiter(void 0, void 0, void 0, function* () {
292
- // Start the component animating in
293
- yield waitMs(MS_ANIMATE_IN_DELAY);
294
- setAnimatingIn(true);
295
406
  // Wait and then set visible to true
296
407
  yield waitMs(MS_TO_ANIMATE);
297
408
  setVisible(true);
409
+ setAnimatingIn(false);
298
410
  }))();
299
411
  }, []);
300
412
  /*------------------------------------------------------------------------*/
@@ -321,13 +433,19 @@ const Modal = (props) => {
321
433
  }
322
434
  // Update the state
323
435
  setVisible(false);
436
+ setAnimatingOut(true);
324
437
  // Call the handler after the modal has animated out
325
438
  yield waitMs(MS_TO_ANIMATE);
326
439
  onClose(ModalButtonType);
440
+ setGone(true);
327
441
  });
328
442
  /*------------------------------------------------------------------------*/
329
443
  /* Render */
330
444
  /*------------------------------------------------------------------------*/
445
+ // Return nothing if gone
446
+ if (gone) {
447
+ return null;
448
+ }
331
449
  /*----------------------------------------*/
332
450
  /* Footer */
333
451
  /*----------------------------------------*/
@@ -349,7 +467,7 @@ const Modal = (props) => {
349
467
  // Check if this button is last
350
468
  const last = (i === ModalButtonTypes.length - 1);
351
469
  // Create the button
352
- 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: () => {
353
471
  handleClose(ModalButtonType);
354
472
  } }, label));
355
473
  });
@@ -357,22 +475,62 @@ const Modal = (props) => {
357
475
  const footer = ((buttons && buttons.length)
358
476
  ? (React.createElement("div", null, buttons))
359
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
+ }
360
492
  // Render the modal
361
- return (React.createElement("div", { className: `modal fade show modal-dialog-scrollable modal-${size}`, tabIndex: -1, style: {
493
+ return (React.createElement("div", { className: `modal show modal-dialog-scrollable modal-dialog-centered modal-${size}`, tabIndex: -1, style: {
362
494
  zIndex: (onTopOfOtherModals
363
- ? 6000000000
495
+ ? 5000000001
364
496
  : 5000000000),
365
497
  display: 'block',
498
+ margin: 'auto',
499
+ left: 0,
500
+ right: 0,
366
501
  } },
367
- 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
+ } },
368
523
  React.createElement("div", { className: "modal-content" },
369
524
  React.createElement("div", { className: "modal-header" },
370
- React.createElement("h5", { className: "modal-title" }, title),
371
- 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
372
530
  handleClose(ModalButtonType$1.Cancel);
373
531
  } }))),
374
532
  children && (React.createElement("div", { className: "modal-body" }, children)),
375
- footer && (React.createElement("div", { className: "modal-footer" }, footer))))));
533
+ footer && (React.createElement("div", { className: "modal-footer pt-1 pb-1" }, footer))))));
376
534
  };
377
535
 
378
536
  /**
@@ -396,6 +554,24 @@ class ErrorWithCode extends Error {
396
554
  /* Static Helpers */
397
555
  /*------------------------------------------------------------------------*/
398
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
+ /*----------------------------------------*/
399
575
  /* Alert */
400
576
  /*----------------------------------------*/
401
577
  // Stored copies of setters
@@ -407,7 +583,7 @@ let onAlertClosed;
407
583
  * @param title the title text to display at the top of the alert
408
584
  * @param text the text to display in the alert
409
585
  */
410
- 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* () {
411
587
  // Fallback if alert not available
412
588
  if (!setAlertInfo) {
413
589
  window.alert(`${title}\n\n${text}`);
@@ -481,7 +657,7 @@ const showFatalError = (error, errorTitle = 'An Error Occurred') => {
481
657
  : String((_b = error.code) !== null && _b !== void 0 ? _b : ReactKitErrorCode$1.NoCode));
482
658
  // Handle case where app hasn't loaded
483
659
  if (!setFatalErrorMessage || !setFatalErrorCode) {
484
- alert(errorTitle, `${message} (code: ${code}). Please contact support.`);
660
+ alert$1(errorTitle, `${message} (code: ${code}). Please contact support.`);
485
661
  return undefined;
486
662
  }
487
663
  // Use setters
@@ -498,7 +674,9 @@ const AppWrapper = (props) => {
498
674
  /* Setup */
499
675
  /*------------------------------------------------------------------------*/
500
676
  /* -------------- Props ------------- */
501
- 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;
502
680
  /* -------------- State ------------- */
503
681
  // Fatal error
504
682
  const [fatalErrorMessage, setFatalErrorMessageInner,] = useState();
@@ -508,10 +686,10 @@ const AppWrapper = (props) => {
508
686
  const [fatalErrorTitle, setFatalErrorTitleInner,] = useState();
509
687
  setFatalErrorTitle = setFatalErrorTitleInner;
510
688
  // Alert
511
- const [alertInfo, setAlertInfoInner,] = useState();
689
+ const [alertInfo, setAlertInfoInner,] = useState(undefined);
512
690
  setAlertInfo = setAlertInfoInner;
513
691
  // Confirm
514
- const [confirmInfo, setConfirmInfoInner,] = useState();
692
+ const [confirmInfo, setConfirmInfoInner,] = useState(undefined);
515
693
  setConfirmInfo = setConfirmInfoInner;
516
694
  // Session expired
517
695
  const [sessionHasExpired, setSessionHasExpiredInner,] = useState(false);
@@ -525,8 +703,9 @@ const AppWrapper = (props) => {
525
703
  let modal;
526
704
  /* -------------- Alert ------------- */
527
705
  if (alertInfo) {
528
- 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: () => {
529
707
  // Alert closed
708
+ setAlertInfo(undefined);
530
709
  if (onAlertClosed) {
531
710
  onAlertClosed();
532
711
  }
@@ -534,11 +713,12 @@ const AppWrapper = (props) => {
534
713
  }
535
714
  /* ------------- Confirm ------------ */
536
715
  if (confirmInfo) {
537
- 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);
538
718
  if (onConfirmClosed) {
539
719
  onConfirmClosed(buttonType === ModalButtonType$1.Okay);
540
720
  }
541
- }, dontAllowBackdropExit: true }));
721
+ }, dontAllowBackdropExit: true }, confirmInfo.text));
542
722
  }
543
723
  /*----------------------------------------*/
544
724
  /* Views */
@@ -590,10 +770,10 @@ const style$1 = `
590
770
  .LoadingSpinner-blip-2,
591
771
  .LoadingSpinner-blip-3,
592
772
  .LoadingSpinner-blip-4 {
593
- font-size: 25px;
773
+ font-size: 1.8rem;
594
774
  opacity: 0.6;
595
- margin-top: 20px;
596
- margin-bottom: 20px;
775
+ margin-top: 1rem;
776
+ margin-bottom: 1rem;
597
777
  }
598
778
 
599
779
  /* First Blip */
@@ -660,63 +840,67 @@ const LoadingSpinner = () => {
660
840
  /* Style */
661
841
  /*------------------------------------------------------------------------*/
662
842
  const style = `
663
- /* Tab Box */
664
- .TabBox-box {
665
- /* Light Border */
666
- border: 2px solid #dedede;
667
-
668
- /* Rounded Corners (except top-left) */
669
- border-bottom-right-radius: 5px;
670
- border-bottom-left-radius: 5px;
671
- border-top-right-radius: 5px;
672
-
673
- /* Very Light Gray Border */
674
- background: #fdfdfd;
675
-
676
- /* Align Contents on Left */
677
- text-align: left;
678
- }
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;
679
852
 
680
- /* Container for Title */
681
- .TabBox-title-container {
682
- /* Place on Left */
683
- position: relative;
684
- left: 0;
685
- text-align: left;
686
- }
853
+ /* Very Light Gray Border */
854
+ background: #fdfdfd;
687
855
 
688
- /* Tab-style Title */
689
- .TabBox-title {
690
- /* Place so it Barely Overlaps the Box Border */
691
- display: inline-block;
692
- position: relative;
693
- top: 2px; /* Gives Illusion that Border Doesn't Exist Below Tab */
856
+ /* Align Contents on Left */
857
+ text-align: left;
694
858
 
695
- /* Title-sized Font */
696
- font-size: 25px;
859
+ /* Add Text Padding */
860
+ padding-left: 0.3rem;
861
+ padding-right: 0.3rem;
862
+ }
697
863
 
698
- /* Add Border on Top and Sides */
699
- border-top: 2px solid #dedede;
700
- border-left: 2px solid #dedede;
701
- 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
+ }
702
871
 
703
- /* Round the Top Corners */
704
- border-top-left-radius: 5px;
705
- 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 */
706
878
 
707
- /* Add Text Padding */
708
- padding-left: 12px;
709
- padding-right: 12px;
879
+ /* Title-sized Font */
880
+ font-size: 1.5rem;
710
881
 
711
- /* Match Background Color of Box */
712
- background: #fdfdfd;
713
- }
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;
714
886
 
715
- /* Make the TabBox's Children Appear Above Title if Overlap Occurs */
716
- .TabBox-children {
717
- position: relative;
718
- z-index: 1;
719
- }
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
+ }
720
904
  `;
721
905
  /*------------------------------------------------------------------------*/
722
906
  /* Component */
@@ -726,7 +910,7 @@ const TabBox = (props) => {
726
910
  /* Setup */
727
911
  /*------------------------------------------------------------------------*/
728
912
  /* -------------- Props ------------- */
729
- const { title, children, } = props;
913
+ const { title, children, noBottomPadding, } = props;
730
914
  /*------------------------------------------------------------------------*/
731
915
  /* Render */
732
916
  /*------------------------------------------------------------------------*/
@@ -734,7 +918,7 @@ const TabBox = (props) => {
734
918
  /* Main UI */
735
919
  /*----------------------------------------*/
736
920
  // Full UI
737
- return (React.createElement("div", null,
921
+ return (React.createElement("div", { className: `TabBox-container ${noBottomPadding ? '' : 'mb-2'}` },
738
922
  React.createElement("style", null, style),
739
923
  React.createElement("div", { className: "TabBox-title-container" },
740
924
  React.createElement("div", { className: "TabBox-title" }, title)),
@@ -742,6 +926,252 @@ const TabBox = (props) => {
742
926
  React.createElement("div", { className: "TabBox-children" }, children))));
743
927
  };
744
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
+
745
1175
  /**
746
1176
  * Shorten text so it fits into a certain number of chars
747
1177
  * @author Gabe Abrams
@@ -885,5 +1315,425 @@ const roundToNumDecimals = (num, numDecimals) => {
885
1315
  return (Math.round(num * rounder) / rounder);
886
1316
  };
887
1317
 
888
- 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
+ * next (express next function)
1503
+ */
1504
+ const genRouteHandler = (opts) => {
1505
+ // Return a route handler
1506
+ return (req, res, next) => __awaiter(void 0, void 0, void 0, function* () {
1507
+ var _a;
1508
+ // Output params
1509
+ const output = {};
1510
+ /*----------------------------------------*/
1511
+ /* Parse Params */
1512
+ /*----------------------------------------*/
1513
+ // Process items one by one
1514
+ const paramList = Object.entries((_a = opts.paramTypes) !== null && _a !== void 0 ? _a : {});
1515
+ for (let i = 0; i < paramList.length; i++) {
1516
+ const [name, type] = paramList[i];
1517
+ // Find the value as a string
1518
+ const value = (req.params[name]
1519
+ || req.query[name]
1520
+ || req.body[name]);
1521
+ // Parse
1522
+ if (type === ParamType$1.Boolean || type === ParamType$1.BooleanOptional) {
1523
+ // Boolean
1524
+ // Handle case where value doesn't exist
1525
+ if (value === undefined) {
1526
+ if (type === ParamType$1.BooleanOptional) {
1527
+ output[name] = undefined;
1528
+ }
1529
+ else {
1530
+ return handleError(res, {
1531
+ message: `Parameter ${name} is required, but it was not included.`,
1532
+ code: ReactKitErrorCode$1.MissingParameter,
1533
+ status: 422,
1534
+ });
1535
+ }
1536
+ }
1537
+ else {
1538
+ // Value exists
1539
+ // Simplify value
1540
+ const simpleVal = (String(value)
1541
+ .trim()
1542
+ .toLowerCase());
1543
+ // Parse
1544
+ output[name] = ([
1545
+ 'true',
1546
+ 'yes',
1547
+ 'y',
1548
+ '1',
1549
+ 't',
1550
+ ].indexOf(simpleVal) >= 0);
1551
+ }
1552
+ }
1553
+ else if (type === ParamType$1.Float || type === ParamType$1.FloatOptional) {
1554
+ // Float
1555
+ // Handle case where value doesn't exist
1556
+ if (value === undefined) {
1557
+ if (type === ParamType$1.FloatOptional) {
1558
+ output[name] = undefined;
1559
+ }
1560
+ else {
1561
+ return handleError(res, {
1562
+ message: `Parameter ${name} is required, but it was not included.`,
1563
+ code: ReactKitErrorCode$1.MissingParameter,
1564
+ status: 422,
1565
+ });
1566
+ }
1567
+ }
1568
+ else if (!Number.isNaN(Number.parseFloat(String(value)))) {
1569
+ // Value is a number
1570
+ output[name] = Number.parseFloat(String(value));
1571
+ }
1572
+ else {
1573
+ // Issue!
1574
+ return handleError(res, {
1575
+ message: `Request data was malformed: ${name} was not a valid float.`,
1576
+ code: ReactKitErrorCode$1.InvalidParameter,
1577
+ status: 422,
1578
+ });
1579
+ }
1580
+ }
1581
+ else if (type === ParamType$1.Int || type === ParamType$1.IntOptional) {
1582
+ // Int
1583
+ // Handle case where value doesn't exist
1584
+ if (value === undefined) {
1585
+ if (type === ParamType$1.IntOptional) {
1586
+ output[name] = undefined;
1587
+ }
1588
+ else {
1589
+ return handleError(res, {
1590
+ message: `Parameter ${name} is required, but it was not included.`,
1591
+ code: ReactKitErrorCode$1.MissingParameter,
1592
+ status: 422,
1593
+ });
1594
+ }
1595
+ }
1596
+ else if (!Number.isNaN(Number.parseInt(String(value), 10))) {
1597
+ // Value is a number
1598
+ output[name] = Number.parseInt(String(value), 10);
1599
+ }
1600
+ else {
1601
+ // Issue!
1602
+ return handleError(res, {
1603
+ message: `Request data was malformed: ${name} was not a valid int.`,
1604
+ code: ReactKitErrorCode$1.InvalidParameter,
1605
+ status: 422,
1606
+ });
1607
+ }
1608
+ }
1609
+ else if (type === ParamType$1.JSON || type === ParamType$1.JSONOptional) {
1610
+ // Stringified JSON
1611
+ // Handle case where value doesn't exist
1612
+ if (value === undefined) {
1613
+ if (type === ParamType$1.JSONOptional) {
1614
+ output[name] = undefined;
1615
+ }
1616
+ else {
1617
+ return handleError(res, {
1618
+ message: `Parameter ${name} is required, but it was not included.`,
1619
+ code: ReactKitErrorCode$1.MissingParameter,
1620
+ status: 422,
1621
+ });
1622
+ }
1623
+ }
1624
+ else {
1625
+ // Value exists
1626
+ // Parse
1627
+ try {
1628
+ output[name] = JSON.parse(String(value));
1629
+ }
1630
+ catch (err) {
1631
+ return handleError(res, {
1632
+ message: `Request data was malformed: ${name} was not a valid JSON payload.`,
1633
+ code: ReactKitErrorCode$1.InvalidParameter,
1634
+ status: 422,
1635
+ });
1636
+ }
1637
+ }
1638
+ }
1639
+ else if (type === ParamType$1.String || type === ParamType$1.StringOptional) {
1640
+ // String
1641
+ // Handle case where value doesn't exist
1642
+ if (value === undefined) {
1643
+ if (type === ParamType$1.StringOptional) {
1644
+ output[name] = undefined;
1645
+ }
1646
+ else {
1647
+ return handleError(res, {
1648
+ message: `Parameter ${name} is required, but it was not included.`,
1649
+ code: ReactKitErrorCode$1.MissingParameter,
1650
+ status: 422,
1651
+ });
1652
+ }
1653
+ }
1654
+ else {
1655
+ // Value exists
1656
+ // Leave as is
1657
+ output[name] = value;
1658
+ }
1659
+ }
1660
+ else {
1661
+ // No valid data type
1662
+ return handleError(res, {
1663
+ message: `An internal error occurred: we could not determine the type of ${name}.`,
1664
+ code: ReactKitErrorCode$1.InvalidParameter,
1665
+ status: 422,
1666
+ });
1667
+ }
1668
+ }
1669
+ /*----------------------------------------*/
1670
+ /* Launch Info */
1671
+ /*----------------------------------------*/
1672
+ // Get launch info
1673
+ const { launched, launchInfo } = cacclGetLaunchInfo(req);
1674
+ if (!launched || !launchInfo) {
1675
+ return handleError(res, {
1676
+ message: 'Your session has expired. Please refresh the page and try again.',
1677
+ code: ReactKitErrorCode$1.SessionExpired,
1678
+ status: 440,
1679
+ });
1680
+ }
1681
+ // Error if user info cannot be found
1682
+ if (!launchInfo.userId
1683
+ || !launchInfo.userFirstName
1684
+ || !launchInfo.userLastName
1685
+ || (launchInfo.notInCourse
1686
+ && !launchInfo.isAdmin)
1687
+ || (!launchInfo.isTTM
1688
+ && !launchInfo.isLearner
1689
+ && !launchInfo.isAdmin)) {
1690
+ return handleError(res, {
1691
+ message: 'Your session was invalid. Please refresh the page and try again.',
1692
+ code: ReactKitErrorCode$1.SessionExpired,
1693
+ status: 440,
1694
+ });
1695
+ }
1696
+ // Add launch info to output
1697
+ output.userId = launchInfo.userId;
1698
+ output.userFirstName = launchInfo.userFirstName;
1699
+ output.userLastName = launchInfo.userLastName;
1700
+ output.isLearner = !!launchInfo.isLearner;
1701
+ output.isTTM = !!launchInfo.isTTM;
1702
+ output.isAdmin = !!launchInfo.isAdmin;
1703
+ output.isWatchingInPrivate = !!(req.session.isWatchingInPrivate);
1704
+ /*----------------------------------------*/
1705
+ /* Require Course Consistency */
1706
+ /*----------------------------------------*/
1707
+ // Make sure the user actually launched from the appropriate course
1708
+ if (output.courseId
1709
+ && launchInfo.courseId
1710
+ && output.courseId !== launchInfo.courseId
1711
+ && !output.isTTM
1712
+ && !output.isAdmin) {
1713
+ // Course of interest is not the launch course
1714
+ return handleError(res, {
1715
+ message: 'You switched sessions by opening Immersive Classroom in another tab. Please refresh the page and try again.',
1716
+ code: ReactKitErrorCode$1.WrongCourse,
1717
+ status: 401,
1718
+ });
1719
+ }
1720
+ /*------------------------------------------------------------------------*/
1721
+ /* Call handler */
1722
+ /*------------------------------------------------------------------------*/
1723
+ opts.handler({
1724
+ params: output,
1725
+ handleSuccess: (body) => {
1726
+ return handleSuccess(res, body);
1727
+ },
1728
+ handleError: (error) => {
1729
+ return handleError(res, error);
1730
+ },
1731
+ req,
1732
+ res,
1733
+ next,
1734
+ });
1735
+ });
1736
+ };
1737
+
1738
+ 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 };
889
1739
  //# sourceMappingURL=index.js.map