dce-reactkit 3.12.3 → 4.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/.vscode/settings.json +1 -3
  2. package/README.md +20 -0
  3. package/dist/cjs/index.js +156 -1792
  4. package/dist/cjs/index.js.map +1 -1
  5. package/dist/cjs/types/components/ItemPicker/index.d.ts +0 -1
  6. package/dist/cjs/types/components/Modal/ModalProps.d.ts +0 -2
  7. package/dist/cjs/types/components/TabBox.d.ts +0 -1
  8. package/dist/cjs/types/index.d.ts +8 -12
  9. package/dist/cjs/types/types/ParamType.d.ts +10 -10
  10. package/dist/cjs/types/types/ReactKitErrorCode.d.ts +1 -15
  11. package/dist/esm/index.js +157 -1785
  12. package/dist/esm/index.js.map +1 -1
  13. package/dist/esm/types/components/ItemPicker/index.d.ts +0 -1
  14. package/dist/esm/types/components/Modal/ModalProps.d.ts +0 -2
  15. package/dist/esm/types/components/TabBox.d.ts +0 -1
  16. package/dist/esm/types/index.d.ts +8 -12
  17. package/dist/esm/types/types/ParamType.d.ts +10 -10
  18. package/dist/esm/types/types/ReactKitErrorCode.d.ts +1 -15
  19. package/dist/index.d.ts +122 -351
  20. package/genEncodedSecret.ts +84 -0
  21. package/package.json +7 -5
  22. package/rollup.config.js +2 -0
  23. package/src/components/AppWrapper.tsx +3 -1
  24. package/src/components/CheckboxButton.tsx +1 -1
  25. package/src/components/Dropdown.tsx +1 -1
  26. package/src/components/IntelliTable.tsx +1 -1
  27. package/src/components/ItemPicker/NestableItemList.tsx +0 -1
  28. package/src/components/ItemPicker/index.tsx +0 -96
  29. package/src/components/Modal/ModalProps.ts +0 -4
  30. package/src/components/Modal/index.tsx +21 -60
  31. package/src/components/RadioButton.tsx +3 -1
  32. package/src/components/TabBox.tsx +9 -27
  33. package/src/{client → helpers}/initClient.tsx +5 -1
  34. package/src/helpers/logClientEvent.tsx +3 -1
  35. package/src/helpers/visitServerEndpoint.tsx +1 -1
  36. package/src/index.ts +12 -22
  37. package/src/types/ParamType.ts +10 -10
  38. package/src/types/ReactKitErrorCode.tsx +1 -17
  39. package/dist/cjs/types/helpers/addDBEditorEndpoints.d.ts +0 -41
  40. package/dist/cjs/types/helpers/genRouteHandler.d.ts +0 -76
  41. package/dist/cjs/types/helpers/getWordCount.d.ts +0 -10
  42. package/dist/cjs/types/helpers/handleError.d.ts +0 -18
  43. package/dist/cjs/types/helpers/handleSuccess.d.ts +0 -8
  44. package/dist/cjs/types/helpers/parseUserAgent.d.ts +0 -17
  45. package/dist/cjs/types/helpers/visitEndpointOnAnotherServer/index.d.ts +0 -24
  46. package/dist/cjs/types/helpers/visitEndpointOnAnotherServer/sendServerToServerRequest.d.ts +0 -33
  47. package/dist/cjs/types/server/initLogCollection.d.ts +0 -8
  48. package/dist/cjs/types/server/initServer.d.ts +0 -42
  49. package/dist/esm/types/helpers/addDBEditorEndpoints.d.ts +0 -41
  50. package/dist/esm/types/helpers/genRouteHandler.d.ts +0 -76
  51. package/dist/esm/types/helpers/getWordCount.d.ts +0 -10
  52. package/dist/esm/types/helpers/handleError.d.ts +0 -18
  53. package/dist/esm/types/helpers/handleSuccess.d.ts +0 -8
  54. package/dist/esm/types/helpers/parseUserAgent.d.ts +0 -17
  55. package/dist/esm/types/helpers/visitEndpointOnAnotherServer/index.d.ts +0 -24
  56. package/dist/esm/types/helpers/visitEndpointOnAnotherServer/sendServerToServerRequest.d.ts +0 -33
  57. package/dist/esm/types/server/initLogCollection.d.ts +0 -8
  58. package/dist/esm/types/server/initServer.d.ts +0 -42
  59. package/src/components/ItemPicker/index.test.tsx +0 -783
  60. package/src/helpers/addDBEditorEndpoints.ts +0 -128
  61. package/src/helpers/genRouteHandler.ts +0 -948
  62. package/src/helpers/getWordCount.ts +0 -26
  63. package/src/helpers/handleError.ts +0 -65
  64. package/src/helpers/handleSuccess.ts +0 -18
  65. package/src/helpers/parseUserAgent.ts +0 -108
  66. package/src/helpers/visitEndpointOnAnotherServer/index.ts +0 -93
  67. package/src/helpers/visitEndpointOnAnotherServer/sendServerToServerRequest.ts +0 -164
  68. package/src/server/initLogCollection.ts +0 -27
  69. package/src/server/initServer.ts +0 -287
  70. /package/dist/cjs/types/{client → helpers}/initClient.d.ts +0 -0
  71. /package/dist/esm/types/{client → helpers}/initClient.d.ts +0 -0
package/dist/cjs/index.js CHANGED
@@ -7,7 +7,6 @@ var freeSolidSvgIcons = require('@fortawesome/free-solid-svg-icons');
7
7
  var reactFontawesome = require('@fortawesome/react-fontawesome');
8
8
  var ReactDOM = require('react-dom');
9
9
  var freeRegularSvgIcons = require('@fortawesome/free-regular-svg-icons');
10
- var qs = require('qs');
11
10
 
12
11
  function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
13
12
 
@@ -32,7 +31,6 @@ function _interopNamespace(e) {
32
31
  var React__default = /*#__PURE__*/_interopDefaultLegacy(React);
33
32
  var React__namespace = /*#__PURE__*/_interopNamespace(React);
34
33
  var ReactDOM__default = /*#__PURE__*/_interopDefaultLegacy(ReactDOM);
35
- var qs__default = /*#__PURE__*/_interopDefaultLegacy(qs);
36
34
 
37
35
  /******************************************************************************
38
36
  Copyright (c) Microsoft Corporation.
@@ -59,7 +57,7 @@ function __awaiter(thisArg, _arguments, P, generator) {
59
57
  });
60
58
  }
61
59
 
62
- // Highest error code = DRK18
60
+ // Highest error code = DRK34
63
61
  /**
64
62
  * List of error codes built into the react kit
65
63
  * @author Gabe Abrams
@@ -69,22 +67,7 @@ var ReactKitErrorCode;
69
67
  ReactKitErrorCode["NoResponse"] = "DRK1";
70
68
  ReactKitErrorCode["NoCode"] = "DRK2";
71
69
  ReactKitErrorCode["SessionExpired"] = "DRK3";
72
- ReactKitErrorCode["MissingParameter"] = "DRK4";
73
- ReactKitErrorCode["InvalidParameter"] = "DRK5";
74
- ReactKitErrorCode["HostNotAllowed"] = "DRK17";
75
- ReactKitErrorCode["HostBanned"] = "DRK18";
76
- ReactKitErrorCode["WrongCourse"] = "DRK6";
77
70
  ReactKitErrorCode["NoCACCLSendRequestFunction"] = "DRK7";
78
- ReactKitErrorCode["NoCACCLGetLaunchInfoFunction"] = "DRK8";
79
- ReactKitErrorCode["NotTTM"] = "DRK9";
80
- ReactKitErrorCode["NotAdmin"] = "DRK10";
81
- ReactKitErrorCode["NotAllowedToReviewLogs"] = "DRK11";
82
- ReactKitErrorCode["ThemeCheckedBeforeReactKitReady"] = "DRK12";
83
- ReactKitErrorCode["SessionExpiredMessageGottenBeforeReactKitReady"] = "DRK13";
84
- // Server-to-server requests
85
- ReactKitErrorCode["NotConnected"] = "DRK14";
86
- ReactKitErrorCode["SelfSigned"] = "DRK15";
87
- ReactKitErrorCode["ResponseParseError"] = "DRK16";
88
71
  })(ReactKitErrorCode || (ReactKitErrorCode = {}));
89
72
  var ReactKitErrorCode$1 = ReactKitErrorCode;
90
73
 
@@ -216,98 +199,6 @@ const LogBuiltInMetadata = {
216
199
  },
217
200
  };
218
201
 
219
- /**
220
- * Loading spinner/indicator
221
- * @author Gabe Abrams
222
- */
223
- /*------------------------------------------------------------------------*/
224
- /* -------------------------------- Style ------------------------------- */
225
- /*------------------------------------------------------------------------*/
226
- const style$a = `
227
- /* Container fades in */
228
- .LoadingSpinner-container {
229
- animation-name: LoadingSpinner-container-fade-in;
230
- animation-duration: 0.3s;
231
- animation-delay: 0.4s;
232
- animation-fill-mode: both;
233
- animation-timing-function: ease-out;
234
- }
235
- @keyframes LoadingSpinner-container-fade-in {
236
- 0% {
237
- opacity: 0;
238
- }
239
- 100% {
240
- opacity: 1;
241
- }
242
- }
243
-
244
- /* Blips */
245
- .LoadingSpinner-blip-1,
246
- .LoadingSpinner-blip-2,
247
- .LoadingSpinner-blip-3,
248
- .LoadingSpinner-blip-4 {
249
- font-size: 1.8rem;
250
- opacity: 0.6;
251
- margin-top: 1rem;
252
- margin-bottom: 1rem;
253
- }
254
-
255
- /* First Blip */
256
- .LoadingSpinner-blip-1 {
257
- animation: LoadingSpinner-pop-animation 2s infinite;
258
- }
259
-
260
- /* Second Blip */
261
- .LoadingSpinner-blip-2 {
262
- animation: LoadingSpinner-pop-animation 2s infinite;
263
- -webkit-animation-delay: 0.1s;
264
- animation-delay: 0.1s;
265
- }
266
-
267
- /* Third Blip */
268
- .LoadingSpinner-blip-3 {
269
- animation: LoadingSpinner-pop-animation 2s infinite;
270
- animation-delay: 0.2s;
271
- }
272
-
273
- /* Fourth Blip */
274
- .LoadingSpinner-blip-4 {
275
- animation: LoadingSpinner-pop-animation 2s infinite;
276
- animation-delay: 0.3s;
277
- }
278
-
279
- /* Pop Animation for Each Blip */
280
- @keyframes LoadingSpinner-pop-animation {
281
- 0% {
282
- transform: scale(1.0);
283
- }
284
- 10% {
285
- transform: scale(1.5);
286
- }
287
- 30% {
288
- transform: scale(1.0);
289
- }
290
- 100% {
291
- transform: scale(1.0);
292
- }
293
- }
294
- `;
295
- /*------------------------------------------------------------------------*/
296
- /* ------------------------------ Component ----------------------------- */
297
- /*------------------------------------------------------------------------*/
298
- const LoadingSpinner = () => {
299
- /*------------------------------------------------------------------------*/
300
- /* ------------------------------- Render ------------------------------- */
301
- /*------------------------------------------------------------------------*/
302
- // Add all four blips to a container
303
- return (React__default["default"].createElement("div", { className: "text-center LoadingSpinner LoadingSpinner-container" },
304
- React__default["default"].createElement("style", null, style$a),
305
- React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faCircle, className: "LoadingSpinner-blip-1 me-1" }),
306
- React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faCircle, className: "LoadingSpinner-blip-2 me-1" }),
307
- React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faCircle, className: "LoadingSpinner-blip-3 me-1" }),
308
- React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faCircle, className: "LoadingSpinner-blip-4" })));
309
- };
310
-
311
202
  /**
312
203
  * Wait for a certain number of ms
313
204
  * @author Gabe Abrams
@@ -358,6 +249,15 @@ let onInitialized;
358
249
  const initialized = new Promise((resolve) => {
359
250
  onInitialized = resolve;
360
251
  });
252
+ /* ------------ No Server ----------- */
253
+ let noServer = false;
254
+ /**
255
+ * Check if there is no server for this app
256
+ * @author Gabe Abrams
257
+ */
258
+ const appHasNoServer = () => {
259
+ return noServer;
260
+ };
361
261
  /* ---------- Send Request ---------- */
362
262
  let storedSendRequest;
363
263
  /**
@@ -419,11 +319,13 @@ const initClient = (opts) => {
419
319
  storedSendRequest = () => __awaiter(void 0, void 0, void 0, function* () {
420
320
  throw new Error('Cannot send requests because there is no server');
421
321
  });
322
+ noServer = true;
422
323
  }
423
324
  else {
424
325
  // Store values
425
326
  storedSendRequest = opts.sendRequest;
426
327
  sessionExpiredMessage = opts.sessionExpiredMessage;
328
+ noServer = false;
427
329
  }
428
330
  // Handle universal parts
429
331
  darkModeOn = !!opts.darkModeOn;
@@ -440,7 +342,6 @@ const BASE_Z_INDEX = 1000000000;
440
342
  const BASE_Z_INDEX_ON_TOP = 2000000000;
441
343
  // Constants
442
344
  const MS_TO_ANIMATE = 200; // Animation duration
443
- const MS_TO_WAIT_BEFORE_SHOWING_LOADING_INDICATOR = 1000;
444
345
  // Modal type to list of buttons
445
346
  const modalTypeToModalButtonTypes = {
446
347
  [ModalType$1.Okay]: [
@@ -554,7 +455,7 @@ const getNextUniqueId = () => {
554
455
  /*------------------------------------------------------------------------*/
555
456
  /* -------------------------------- Style ------------------------------- */
556
457
  /*------------------------------------------------------------------------*/
557
- const style$9 = `
458
+ const style$a = `
558
459
  .Modal-backdrop {
559
460
  position: fixed;
560
461
  top: 0;
@@ -626,14 +527,13 @@ const Modal = (props) => {
626
527
  /*------------------------------------------------------------------------*/
627
528
  var _a;
628
529
  /* -------------- Props ------------- */
629
- const { type = ModalType$1.NoButtons, size = ModalSize$1.Large, title, largeTitle, children, onClose, dontAllowBackdropExit, dontShowXButton, onTopOfOtherModals, isLoading, isLoadingCancelable, } = props;
530
+ const { type = ModalType$1.NoButtons, size = ModalSize$1.Large, title, largeTitle, children, onClose, dontAllowBackdropExit, dontShowXButton, onTopOfOtherModals, } = props;
630
531
  // Determine if no header either
631
532
  const noHeader = (!title && type === ModalType$1.NoButtons);
632
533
  /* -------------- State ------------- */
633
534
  // True if animation is in use
634
535
  const [animatingIn, setAnimatingIn] = React.useState(true);
635
536
  const [animatingPop, setAnimatingPop] = React.useState(false);
636
- const [showModal, setShowModal] = React.useState(false);
637
537
  /* -------------- Refs -------------- */
638
538
  // Keep track of whether modal is still mounted
639
539
  const mounted = React.useRef(false);
@@ -660,16 +560,6 @@ const Modal = (props) => {
660
560
  if (mounted.current) {
661
561
  setAnimatingIn(false);
662
562
  }
663
- if (isLoading) {
664
- // wait before showing modal
665
- yield waitMs(MS_TO_WAIT_BEFORE_SHOWING_LOADING_INDICATOR);
666
- if (mounted.current) {
667
- setShowModal(true);
668
- }
669
- }
670
- else {
671
- setShowModal(true);
672
- }
673
563
  }))();
674
564
  return () => {
675
565
  mounted.current = false;
@@ -736,15 +626,6 @@ const Modal = (props) => {
736
626
  else if (animatingPop) {
737
627
  animationClass = 'Modal-animating-pop';
738
628
  }
739
- // default to show close button when not loading and not show close button when loading
740
- // unless downShowXButton or isLoadingCancelable
741
- const showCloseButton = (
742
- // The modal must have an onClose function
743
- onClose
744
- // ...and the close button is allowed to be visible
745
- && !dontShowXButton
746
- // ...and should not be shown if the modal is loading and not cancelable
747
- && !(isLoading && !isLoadingCancelable));
748
629
  // Render the modal
749
630
  const contentToRender = (React__default["default"].createElement("div", { className: "modal show", tabIndex: -1, style: {
750
631
  zIndex: baseZIndex,
@@ -753,7 +634,7 @@ const Modal = (props) => {
753
634
  left: 0,
754
635
  right: 0,
755
636
  } },
756
- React__default["default"].createElement("style", null, style$9),
637
+ React__default["default"].createElement("style", null, style$a),
757
638
  React__default["default"].createElement("div", { className: `Modal-backdrop ${backdropAnimationClass}`, style: {
758
639
  zIndex: baseZIndex + 1,
759
640
  }, onClick: () => __awaiter(void 0, void 0, void 0, function* () {
@@ -771,7 +652,7 @@ const Modal = (props) => {
771
652
  // Handle close
772
653
  handleClose(ModalButtonType$1.Cancel);
773
654
  }) }),
774
- showModal && (React__default["default"].createElement("div", { className: `modal-dialog modal-${size} ${animationClass} modal-dialog-scrollable modal-dialog-centered`, style: {
655
+ React__default["default"].createElement("div", { className: `modal-dialog modal-${size} ${animationClass} modal-dialog-scrollable modal-dialog-centered`, style: {
775
656
  zIndex: baseZIndex + 2,
776
657
  // Override sizes for even larger for XL
777
658
  width: (size === ModalSize$1.ExtraLarge
@@ -803,7 +684,7 @@ const Modal = (props) => {
803
684
  ? '1.6rem'
804
685
  : undefined),
805
686
  } }, title),
806
- showCloseButton && (React__default["default"].createElement("button", { type: "button", className: "Modal-x-button btn-close", "aria-label": "Close", style: {
687
+ (onClose && !dontShowXButton) && (React__default["default"].createElement("button", { type: "button", className: "Modal-x-button btn-close", "aria-label": "Close", style: {
807
688
  backgroundColor: (isDarkModeOn()
808
689
  ? 'white'
809
690
  : undefined),
@@ -811,18 +692,14 @@ const Modal = (props) => {
811
692
  // Handle close
812
693
  handleClose(ModalButtonType$1.Cancel);
813
694
  } })))),
814
- React__default["default"].createElement("div", { className: "modal-body", style: {
695
+ children && (React__default["default"].createElement("div", { className: "modal-body", style: {
815
696
  color: (isDarkModeOn()
816
697
  ? 'white'
817
698
  : undefined),
818
699
  backgroundColor: (isDarkModeOn()
819
700
  ? '#444'
820
701
  : undefined),
821
- } }, isLoading
822
- ? (React__default["default"].createElement(React__default["default"].Fragment, null,
823
- React__default["default"].createElement(LoadingSpinner, null),
824
- React__default["default"].createElement("span", { className: "sr-only" }, "Content loading")))
825
- : children),
702
+ } }, children)),
826
703
  footer && (React__default["default"].createElement("div", { className: "modal-footer pt-1 pb-1", style: {
827
704
  color: (isDarkModeOn()
828
705
  ? 'white'
@@ -833,7 +710,7 @@ const Modal = (props) => {
833
710
  borderTop: (isDarkModeOn()
834
711
  ? '0.1rem solid gray'
835
712
  : undefined),
836
- } }, footer)))))));
713
+ } }, footer))))));
837
714
  // Determine which portal to use
838
715
  const portalNumber = id.current % NUM_MODAL_PORTALS;
839
716
  // Render in a portal
@@ -1004,6 +881,10 @@ const setClientEventMetadataPopulator = (newMetadataPopulator) => {
1004
881
  */
1005
882
  const logClientEvent = (opts) => __awaiter(void 0, void 0, void 0, function* () {
1006
883
  var _a, _b, _c, _d, _e, _f, _g;
884
+ // Don't write to the server if there is none
885
+ if (appHasNoServer()) {
886
+ return;
887
+ }
1007
888
  // Populate metadata
1008
889
  let metadata = ((_a = opts.metadata) !== null && _a !== void 0 ? _a : {});
1009
890
  if (metadataPopulator) {
@@ -1400,7 +1281,7 @@ const showSessionExpiredMessage = () => __awaiter(void 0, void 0, void 0, functi
1400
1281
  /*------------------------------------------------------------------------*/
1401
1282
  /* -------------------------------- Style ------------------------------- */
1402
1283
  /*------------------------------------------------------------------------*/
1403
- const style$8 = `
1284
+ const style$9 = `
1404
1285
  .AppWrapper-leave-to-url-notice {
1405
1286
  opacity: 0;
1406
1287
 
@@ -1614,13 +1495,105 @@ const AppWrapper = (props) => {
1614
1495
  /* --------------- Main UI -------------- */
1615
1496
  /*----------------------------------------*/
1616
1497
  return (React__default["default"].createElement(React__default["default"].Fragment, null,
1617
- React__default["default"].createElement("style", null, style$8),
1498
+ React__default["default"].createElement("style", null, style$9),
1618
1499
  React__default["default"].createElement("style", null, tooltipStyle),
1619
1500
  modal,
1620
1501
  customModalPortals,
1621
1502
  body));
1622
1503
  };
1623
1504
 
1505
+ /**
1506
+ * Loading spinner/indicator
1507
+ * @author Gabe Abrams
1508
+ */
1509
+ /*------------------------------------------------------------------------*/
1510
+ /* -------------------------------- Style ------------------------------- */
1511
+ /*------------------------------------------------------------------------*/
1512
+ const style$8 = `
1513
+ /* Container fades in */
1514
+ .LoadingSpinner-container {
1515
+ animation-name: LoadingSpinner-container-fade-in;
1516
+ animation-duration: 0.3s;
1517
+ animation-delay: 0.4s;
1518
+ animation-fill-mode: both;
1519
+ animation-timing-function: ease-out;
1520
+ }
1521
+ @keyframes LoadingSpinner-container-fade-in {
1522
+ 0% {
1523
+ opacity: 0;
1524
+ }
1525
+ 100% {
1526
+ opacity: 1;
1527
+ }
1528
+ }
1529
+
1530
+ /* Blips */
1531
+ .LoadingSpinner-blip-1,
1532
+ .LoadingSpinner-blip-2,
1533
+ .LoadingSpinner-blip-3,
1534
+ .LoadingSpinner-blip-4 {
1535
+ font-size: 1.8rem;
1536
+ opacity: 0.6;
1537
+ margin-top: 1rem;
1538
+ margin-bottom: 1rem;
1539
+ }
1540
+
1541
+ /* First Blip */
1542
+ .LoadingSpinner-blip-1 {
1543
+ animation: LoadingSpinner-pop-animation 2s infinite;
1544
+ }
1545
+
1546
+ /* Second Blip */
1547
+ .LoadingSpinner-blip-2 {
1548
+ animation: LoadingSpinner-pop-animation 2s infinite;
1549
+ -webkit-animation-delay: 0.1s;
1550
+ animation-delay: 0.1s;
1551
+ }
1552
+
1553
+ /* Third Blip */
1554
+ .LoadingSpinner-blip-3 {
1555
+ animation: LoadingSpinner-pop-animation 2s infinite;
1556
+ animation-delay: 0.2s;
1557
+ }
1558
+
1559
+ /* Fourth Blip */
1560
+ .LoadingSpinner-blip-4 {
1561
+ animation: LoadingSpinner-pop-animation 2s infinite;
1562
+ animation-delay: 0.3s;
1563
+ }
1564
+
1565
+ /* Pop Animation for Each Blip */
1566
+ @keyframes LoadingSpinner-pop-animation {
1567
+ 0% {
1568
+ transform: scale(1.0);
1569
+ }
1570
+ 10% {
1571
+ transform: scale(1.5);
1572
+ }
1573
+ 30% {
1574
+ transform: scale(1.0);
1575
+ }
1576
+ 100% {
1577
+ transform: scale(1.0);
1578
+ }
1579
+ }
1580
+ `;
1581
+ /*------------------------------------------------------------------------*/
1582
+ /* ------------------------------ Component ----------------------------- */
1583
+ /*------------------------------------------------------------------------*/
1584
+ const LoadingSpinner = () => {
1585
+ /*------------------------------------------------------------------------*/
1586
+ /* ------------------------------- Render ------------------------------- */
1587
+ /*------------------------------------------------------------------------*/
1588
+ // Add all four blips to a container
1589
+ return (React__default["default"].createElement("div", { className: "text-center LoadingSpinner LoadingSpinner-container" },
1590
+ React__default["default"].createElement("style", null, style$8),
1591
+ React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faCircle, className: "LoadingSpinner-blip-1 me-1" }),
1592
+ React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faCircle, className: "LoadingSpinner-blip-2 me-1" }),
1593
+ React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faCircle, className: "LoadingSpinner-blip-3 me-1" }),
1594
+ React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faCircle, className: "LoadingSpinner-blip-4" })));
1595
+ };
1596
+
1624
1597
  /**
1625
1598
  * A box with a tab on the top that holds buttons and other content
1626
1599
  * @author Gabe Abrams
@@ -1652,9 +1625,7 @@ const style$7 = `
1652
1625
 
1653
1626
  /* Container for Title */
1654
1627
  .TabBox-title-container {
1655
- display: flex;
1656
- justify-content: space-between;
1657
- align-items: center;
1628
+ /* Place on Left */
1658
1629
  position: relative;
1659
1630
  left: 0;
1660
1631
  text-align: left;
@@ -1687,19 +1658,6 @@ const style$7 = `
1687
1658
  background: #fdfdfd;
1688
1659
  }
1689
1660
 
1690
- .TabBox-title-right-container {
1691
- display: flex;
1692
- flex-direction: row;
1693
- align-items: bottom;
1694
- height: 2.4rem;
1695
- overflow: visible;
1696
- }
1697
-
1698
- .TabBox-title-right-contents {
1699
- margin-right: 0.5rem;
1700
- margin-bottom: 0.2rem;
1701
- }
1702
-
1703
1661
  /* Make the TabBox's Children Appear Above Title if Overlap Occurs */
1704
1662
  .TabBox-children {
1705
1663
  position: relative;
@@ -1713,16 +1671,19 @@ const TabBox = (props) => {
1713
1671
  /*------------------------------------------------------------------------*/
1714
1672
  /* -------------------------------- Setup ------------------------------- */
1715
1673
  /*------------------------------------------------------------------------*/
1716
- const { title, children, topRightChildren, noBottomPadding, noBottomMargin, } = props;
1674
+ /* -------------- Props ------------- */
1675
+ const { title, children, noBottomPadding, noBottomMargin, } = props;
1717
1676
  /*------------------------------------------------------------------------*/
1718
1677
  /* ------------------------------- Render ------------------------------- */
1719
1678
  /*------------------------------------------------------------------------*/
1679
+ /*----------------------------------------*/
1680
+ /* --------------- Main UI -------------- */
1681
+ /*----------------------------------------*/
1682
+ // Full UI
1720
1683
  return (React__default["default"].createElement("div", { className: `TabBox-container ${noBottomMargin ? '' : 'mb-2'}` },
1721
1684
  React__default["default"].createElement("style", null, style$7),
1722
1685
  React__default["default"].createElement("div", { className: "TabBox-title-container" },
1723
- React__default["default"].createElement("div", { className: "TabBox-title" }, title),
1724
- topRightChildren && (React__default["default"].createElement("div", { className: "TabBox-title-right-container" },
1725
- React__default["default"].createElement("div", { className: "TabBox-title-right-contents" }, topRightChildren)))),
1686
+ React__default["default"].createElement("div", { className: "TabBox-title" }, title)),
1726
1687
  React__default["default"].createElement("div", { className: `TabBox-box ps-2 pt-2 pe-2 ${noBottomPadding ? '' : 'pb-2'}` },
1727
1688
  React__default["default"].createElement("div", { className: "TabBox-children" }, children))));
1728
1689
  };
@@ -2709,7 +2670,7 @@ const NestableItemList = (props) => {
2709
2670
  React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: childExpanded[item.id] ? freeSolidSvgIcons.faChevronDown : freeSolidSvgIcons.faChevronRight })))),
2710
2671
  React__default["default"].createElement(CheckboxButton, { className: `NestableItemList-CheckboxButton-${item.id}`, text: item.name, checked: item.isGroup ? allChecked(item.children) : item.checked, dashed: item.isGroup ? !noneChecked(item.children) : false, onChanged: (checked) => {
2711
2672
  onChanged(changeChecked(item.id, checked, items));
2712
- }, ariaLabel: `Select ${item.name}`, checkedVariant: Variant$1.Light, uncheckedVariant: Variant$1.Light }),
2673
+ }, ariaLabel: `Select ${item.name}`, checkedVariant: Variant$1.Light }),
2713
2674
  (item.isGroup && childExpanded[item.id]) && (React__default["default"].createElement("div", { className: "NestableItemList-children-container", style: {
2714
2675
  paddingLeft: '2.2rem',
2715
2676
  } },
@@ -2732,62 +2693,17 @@ const ItemPicker = (props) => {
2732
2693
  /*------------------------------------------------------------------------*/
2733
2694
  /* -------------- Props ------------- */
2734
2695
  // Destructure all props
2735
- const { title, items, onChanged, noBottomMargin, hideSelectAllOrNoneButtons, } = props;
2696
+ const { title, items, onChanged, noBottomMargin, } = props;
2736
2697
  /*------------------------------------------------------------------------*/
2737
2698
  /* ------------------------- Component Functions ------------------------ */
2738
2699
  /*------------------------------------------------------------------------*/
2739
- /**
2740
- * Updates the checked state of an item (including all children)
2741
- * @author Yuen Ler Chow
2742
- * @param item the item to update
2743
- * @param checked the new checked state
2744
- * @returns the updated item
2745
- */
2746
- const updateItemChecked = (item, checked) => {
2747
- if (item.isGroup) {
2748
- return Object.assign(Object.assign({}, item), { children: item.children.map((child) => {
2749
- return updateItemChecked(child, checked);
2750
- }) });
2751
- }
2752
- return Object.assign(Object.assign({}, item), { checked });
2753
- };
2754
- /**
2755
- * Selects all items in the list
2756
- * @author Yuen Ler Chow
2757
- */
2758
- const handleSelectAll = () => {
2759
- const updatedItems = items.map((item) => {
2760
- return updateItemChecked(item, true);
2761
- });
2762
- onChanged(updatedItems);
2763
- };
2764
- /**
2765
- * Deselects all items in the list
2766
- * @author Yuen Ler Chow
2767
- */
2768
- const handleDeselectAll = () => {
2769
- const updatedItems = items.map((item) => {
2770
- return updateItemChecked(item, false);
2771
- });
2772
- onChanged(updatedItems);
2773
- };
2774
2700
  /*------------------------------------------------------------------------*/
2775
2701
  /* ------------------------------- Render ------------------------------- */
2776
2702
  /*------------------------------------------------------------------------*/
2777
2703
  /*----------------------------------------*/
2778
2704
  /* --------------- Main UI -------------- */
2779
2705
  /*----------------------------------------*/
2780
- // Select all/none
2781
- const selectAllOrNone = (!hideSelectAllOrNoneButtons
2782
- ? (React__default["default"].createElement("div", { className: "d-flex h-100 align-items-end flex-row" },
2783
- React__default["default"].createElement("div", { className: "d-flex justify-content-end" },
2784
- React__default["default"].createElement("div", { className: "me-2", style: { fontSize: '1.2rem' } }, "Select"),
2785
- React__default["default"].createElement("div", { className: "btn-group", role: "group" },
2786
- React__default["default"].createElement("button", { type: "button", style: { borderRight: '0.1rem solid white' }, "aria-label": "Select all contexts", className: "btn btn-secondary py-0", onClick: handleSelectAll }, "All"),
2787
- React__default["default"].createElement("button", { type: "button", "aria-label": "Deselect all contexts", className: "btn btn-secondary py-0", onClick: handleDeselectAll }, "None")))))
2788
- : undefined);
2789
- // Main UI
2790
- return (React__default["default"].createElement(TabBox, { title: title, noBottomMargin: noBottomMargin, topRightChildren: selectAllOrNone },
2706
+ return (React__default["default"].createElement(TabBox, { title: title, noBottomMargin: noBottomMargin },
2791
2707
  React__default["default"].createElement("div", { style: { overflowX: 'auto' } },
2792
2708
  React__default["default"].createElement(NestableItemList, { items: items, onChanged: onChanged }))));
2793
2709
  };
@@ -2902,16 +2818,16 @@ var LogAction$1 = LogAction;
2902
2818
  */
2903
2819
  var ParamType;
2904
2820
  (function (ParamType) {
2905
- ParamType["Boolean"] = "boolean";
2906
- ParamType["BooleanOptional"] = "boolean-optional";
2907
- ParamType["Float"] = "float";
2908
- ParamType["FloatOptional"] = "float-optional";
2909
- ParamType["Int"] = "int";
2910
- ParamType["IntOptional"] = "int-optional";
2911
- ParamType["JSON"] = "json";
2912
- ParamType["JSONOptional"] = "json-optional";
2913
- ParamType["String"] = "string";
2914
- ParamType["StringOptional"] = "string-optional";
2821
+ ParamType["Boolean"] = "Boolean";
2822
+ ParamType["BooleanOptional"] = "BooleanOptional";
2823
+ ParamType["Float"] = "Float";
2824
+ ParamType["FloatOptional"] = "FloatOptional";
2825
+ ParamType["Int"] = "Int";
2826
+ ParamType["IntOptional"] = "IntOptional";
2827
+ ParamType["JSON"] = "JSON";
2828
+ ParamType["JSONOptional"] = "JSONOptional";
2829
+ ParamType["String"] = "String";
2830
+ ParamType["StringOptional"] = "StringOptional";
2915
2831
  })(ParamType || (ParamType = {}));
2916
2832
  var ParamType$1 = ParamType;
2917
2833
 
@@ -3159,7 +3075,7 @@ const IntelliTable = (props) => {
3159
3075
  param: column.param,
3160
3076
  visible: checked,
3161
3077
  });
3162
- }, checked: columnVisibilityMap[column.param], ariaLabel: `show "${column.title}" column in the ${title} table`, checkedVariant: Variant$1.Light, uncheckedVariant: Variant$1.Light }));
3078
+ }, checked: columnVisibilityMap[column.param], ariaLabel: `show "${column.title}" column in the ${title} table`, checkedVariant: Variant$1.Light, uncheckedVariant: Variant$1.Secondary }));
3163
3079
  }),
3164
3080
  React__default["default"].createElement("div", { className: "mt-3" }, "Or you can:"),
3165
3081
  React__default["default"].createElement("button", { type: "button", id: `IntelliTable-${id}-select-all-columns`, className: "btn btn-secondary me-2", "aria-label": `show all columns in the ${title} table`, onClick: () => {
@@ -14121,1295 +14037,23 @@ const padZerosLeft = (num, numDigits) => {
14121
14037
  };
14122
14038
 
14123
14039
  /**
14124
- * Route for checking the status of the current user's
14125
- * access to log review
14040
+ * Stub a server endpoint response
14126
14041
  * @author Gabe Abrams
14042
+ * @param opts object containing all arguments
14043
+ * @param [opts.method=GET] http method to stub
14044
+ * @param opts.path full pathname to stub
14045
+ * @param opts.body body of response if stubbing a successful response
14046
+ * @param opts.errorMessage message of error if stubbing a failed response
14047
+ * @param [opts.errorCode] error code if stubbing a failed response
14127
14048
  */
14128
- const LOG_REVIEW_STATUS_ROUTE = `${ROUTE_PATH_PREFIX}/logs/access_allowed`;
14049
+ const stubServerEndpoint = _setStubResponse;
14129
14050
 
14130
- // Stored copy of caccl functions
14131
- let _cacclGetLaunchInfo;
14132
- // Stored copy of dce-mango log collection
14133
- let _logCollection;
14134
- /*------------------------------------------------------------------------*/
14135
- /* Helpers */
14136
- /*------------------------------------------------------------------------*/
14137
- /**
14138
- * Get launch info via CACCL
14139
- * @author Gabe Abrams
14140
- * @param req express request object
14141
- * @returns object { launched, launchInfo }
14142
- */
14143
- const cacclGetLaunchInfo = (req) => {
14144
- if (!_cacclGetLaunchInfo) {
14145
- throw new ErrorWithCode('Could not get launch info because server was not initialized with dce-reactkit\'s initServer function', ReactKitErrorCode$1.NoCACCLGetLaunchInfoFunction);
14146
- }
14147
- return _cacclGetLaunchInfo(req);
14148
- };
14149
14051
  /**
14150
- * Get log collection
14052
+ * Start a dynamic wait, call the function once the operation has completed and
14053
+ * the dynamic wait will continue waiting for the rest of the minimum time
14151
14054
  * @author Gabe Abrams
14152
- * @returns log collection if one was included during launch or null if we don't
14153
- * have a log collection (yet)
14154
- */
14155
- const internalGetLogCollection = () => {
14156
- return _logCollection !== null && _logCollection !== void 0 ? _logCollection : null;
14157
- };
14158
- /*------------------------------------------------------------------------*/
14159
- /* Main */
14160
- /*------------------------------------------------------------------------*/
14161
- /**
14162
- * Prepare dce-reactkit to run on the server
14163
- * @author Gabe Abrams
14164
- * @param opts object containing all arguments
14165
- * @param opts.app express app from inside of the postprocessor function that
14166
- * we will add routes to
14167
- * @param opts.getLaunchInfo CACCL LTI's get launch info function
14168
- * @param [opts.logCollection] mongo collection from dce-mango to use for
14169
- * storing logs. If none is included, logs are written to the console
14170
- * @param [opts.logReviewAdmins=all] info on which admins can review
14171
- * logs from the client. If not included, all Canvas admins are allowed to
14172
- * review logs. If null, no Canvas admins are allowed to review logs.
14173
- * If an array of Canvas userIds (numbers), only Canvas admins with those
14174
- * userIds are allowed to review logs. If a dce-mango collection, only
14175
- * Canvas admins with entries in that collection ({ userId, ...}) are allowed
14176
- * to review logs
14177
- */
14178
- const initServer = (opts) => {
14179
- _cacclGetLaunchInfo = opts.getLaunchInfo;
14180
- _logCollection = opts.logCollection;
14181
- /*----------------------------------------*/
14182
- /* Logging */
14183
- /*----------------------------------------*/
14184
- /**
14185
- * Log an event
14186
- * @author Gabe Abrams
14187
- * @param {string} context Context of the event (each app determines how to
14188
- * organize its contexts)
14189
- * @param {string} subcontext Subcontext of the event (each app determines
14190
- * how to organize its subcontexts)
14191
- * @param {string} tags stringified list of tags that apply to this action
14192
- * (each app determines tag usage)
14193
- * @param {string} metadata stringified object containing optional custom metadata
14194
- * @param {string} level log level
14195
- * @param {string} [errorMessage] error message if type is an error
14196
- * @param {string} [errorCode] error code if type is an error
14197
- * @param {string} [errorStack] error stack if type is an error
14198
- * @param {string} [target] Target of the action (each app determines the list
14199
- * of targets) These are usually buttons, panels, elements, etc.
14200
- * @param {LogAction} [action] the type of action performed on the target
14201
- * @returns {Log}
14202
- */
14203
- opts.app.post(LOG_ROUTE_PATH, genRouteHandler({
14204
- paramTypes: {
14205
- context: ParamType$1.String,
14206
- subcontext: ParamType$1.String,
14207
- tags: ParamType$1.JSON,
14208
- level: ParamType$1.String,
14209
- metadata: ParamType$1.JSON,
14210
- errorMessage: ParamType$1.StringOptional,
14211
- errorCode: ParamType$1.StringOptional,
14212
- errorStack: ParamType$1.StringOptional,
14213
- target: ParamType$1.StringOptional,
14214
- action: ParamType$1.StringOptional,
14215
- },
14216
- handler: ({ params, logServerEvent }) => {
14217
- // Create log info
14218
- const logInfo = ((params.errorMessage || params.errorCode || params.errorStack)
14219
- // Error
14220
- ? {
14221
- context: params.context,
14222
- subcontext: params.subcontext,
14223
- tags: params.tags,
14224
- level: params.level,
14225
- metadata: params.metadata,
14226
- error: {
14227
- message: params.errorMessage,
14228
- code: params.errorCode,
14229
- stack: params.errorStack,
14230
- },
14231
- }
14232
- // Action
14233
- : {
14234
- context: params.context,
14235
- subcontext: params.subcontext,
14236
- tags: params.tags,
14237
- level: params.level,
14238
- metadata: params.metadata,
14239
- target: params.target,
14240
- action: params.action,
14241
- });
14242
- // Add hidden boolean to change source to "client"
14243
- const logInfoForcedFromClient = Object.assign(Object.assign({}, logInfo), { overrideAsClientEvent: true });
14244
- // Write the log
14245
- const log = logServerEvent(logInfoForcedFromClient);
14246
- // Return
14247
- return log;
14248
- },
14249
- }));
14250
- /*----------------------------------------*/
14251
- /* Log Reviewer */
14252
- /*----------------------------------------*/
14253
- /**
14254
- * Check if a given user is allowed to review logs
14255
- * @author Gabe Abrams
14256
- * @param userId the id of the user
14257
- * @param isAdmin if true, the user is an admin
14258
- * @returns true if the user can review logs
14259
- */
14260
- const canReviewLogs = (userId, isAdmin) => __awaiter(void 0, void 0, void 0, function* () {
14261
- // Immediately deny access if user is not an admin
14262
- if (!isAdmin) {
14263
- return false;
14264
- }
14265
- // If all admins are allowed, we're done
14266
- if (!opts.logReviewAdmins) {
14267
- return true;
14268
- }
14269
- // Do a dynamic check
14270
- try {
14271
- // Array of userIds
14272
- if (Array.isArray(opts.logReviewAdmins)) {
14273
- return opts.logReviewAdmins.some((allowedId) => {
14274
- return (userId === allowedId);
14275
- });
14276
- }
14277
- // Must be a collection
14278
- const matches = yield opts.logReviewAdmins.find({ userId });
14279
- // Make sure at least one entry matches
14280
- return matches.length > 0;
14281
- }
14282
- catch (err) {
14283
- // If an error occurred, simply return false
14284
- return false;
14285
- }
14286
- });
14287
- /**
14288
- * Check if the current user has access to logs
14289
- * @author Gabe Abrams
14290
- * @returns {boolean} true if user has access
14291
- */
14292
- opts.app.get(LOG_REVIEW_STATUS_ROUTE, genRouteHandler({
14293
- handler: ({ params }) => __awaiter(void 0, void 0, void 0, function* () {
14294
- const { userId, isAdmin } = params;
14295
- const canReview = yield canReviewLogs(userId, isAdmin);
14296
- return canReview;
14297
- }),
14298
- }));
14299
- /**
14300
- * Get all logs for a certain month
14301
- * @author Gabe Abrams
14302
- * @param {number} year the year to query (e.g. 2022)
14303
- * @param {number} month the month to query (e.g. 1 = January)
14304
- * @returns {Log[]} list of logs from the given month
14305
- */
14306
- opts.app.get(`${LOG_REVIEW_ROUTE_PATH_PREFIX}/years/:year/months/:month`, genRouteHandler({
14307
- paramTypes: {
14308
- year: ParamType$1.Int,
14309
- month: ParamType$1.Int,
14310
- pageNumber: ParamType$1.Int,
14311
- },
14312
- handler: ({ params }) => __awaiter(void 0, void 0, void 0, function* () {
14313
- // Get user info
14314
- const { year, month, pageNumber, userId, isAdmin, } = params;
14315
- // Validate user
14316
- const canReview = yield canReviewLogs(userId, isAdmin);
14317
- if (!canReview) {
14318
- throw new ErrorWithCode('You cannot access this resource because you do not have the appropriate permissions.', ReactKitErrorCode$1.NotAllowedToReviewLogs);
14319
- }
14320
- // Query for logs
14321
- const response = yield _logCollection.findPaged({
14322
- query: {
14323
- year,
14324
- month,
14325
- },
14326
- perPage: 1000,
14327
- pageNumber,
14328
- });
14329
- // Return response
14330
- return response;
14331
- }),
14332
- }));
14333
- };
14334
-
14335
- // Import shared types
14336
- /**
14337
- * Handle an error and respond to the client
14338
- * @author Gabe Abrams
14339
- * @param res express response
14340
- * @param error error info
14341
- * @param opts.err the error to send to the client
14342
- * or the error message
14343
- * @param [opts.code] an error code (only used if err.code is not
14344
- * included)
14345
- * @param [opts.status=500] the https status code to use
14346
- * defined)
14347
- */
14348
- const handleError = (res, error) => {
14349
- // Get the error message
14350
- let message;
14351
- if (error && error.message) {
14352
- message = (error.message || 'An unknown error occurred.');
14353
- }
14354
- else if (typeof error === 'string') {
14355
- message = (error.trim().length > 0
14356
- ? error
14357
- : 'An unknown error occurred.');
14358
- }
14359
- else {
14360
- message = 'An unknown error occurred.';
14361
- }
14362
- // Get the error code
14363
- const code = (error.code || ReactKitErrorCode$1.NoCode);
14364
- // Get the status code
14365
- const status = (error.status || 500);
14366
- // Respond to user
14367
- res
14368
- // Set the http status code
14369
- .status(status)
14370
- // Send a JSON response
14371
- .json({
14372
- // Error message
14373
- message,
14374
- // Error code
14375
- code,
14376
- // Success = false flag so client can detect server-side errors
14377
- success: false,
14378
- });
14379
- return undefined;
14380
- };
14381
-
14382
- /**
14383
- * Send successful API response
14384
- * @author Gabe Abrams
14385
- * @param res express response
14386
- * @param body the body of the response to send to the client
14387
- */
14388
- const handleSuccess = (res, body) => {
14389
- // Send a http 200 json response
14390
- res.json({
14391
- // Include the body as a parameter
14392
- body,
14393
- // Success = true flag so client can detect successful responses
14394
- success: true,
14395
- });
14396
- return undefined;
14397
- };
14398
-
14399
- // Import shared types
14400
- /**
14401
- * Generate a static error page
14402
- * @author Gabe Abrams
14403
- * @param opts object containing all arguments
14404
- * @param [opts.title=An Error Occurred] title of the error box
14405
- * @param [opts.description=An unknown server error occurred. Please contact support.]
14406
- * a human-readable description of the error
14407
- * @param [opts.code=ReactKitErrorCode.NoCode] error code to show
14408
- * @param [opts.pageTitle=opts.title] title of the page/tab if it differs from
14409
- * the title of the error
14410
- * @returns html of the page
14411
- */
14412
- const genErrorPage = (opts = {}) => {
14413
- var _a, _b, _c, _d;
14414
- const title = ((_a = opts.title) !== null && _a !== void 0 ? _a : 'An Error Occurred');
14415
- const pageTitle = ((_b = opts.pageTitle) !== null && _b !== void 0 ? _b : title);
14416
- const description = ((_c = opts.description) !== null && _c !== void 0 ? _c : 'An unknown server error occurred. Please contact support.');
14417
- const code = ((_d = opts.code) !== null && _d !== void 0 ? _d : ReactKitErrorCode$1.NoCode);
14418
- return `
14419
- <head>
14420
- <!-- Metadata -->
14421
- <meta
14422
- name="viewport"
14423
- content="width=device-width, height=device-height, initial-scale=1.0, minimum-scale=1.0"
14424
- >
14425
-
14426
- <!-- Title -->
14427
- <title>${pageTitle}</title>
14428
-
14429
- <!-- Bootstrap -->
14430
- <link
14431
- rel="stylesheet"
14432
- href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.2.1/css/bootstrap.min.css"
14433
- integrity="sha512-siwe/oXMhSjGCwLn+scraPOWrJxHlUgMBMZXdPe2Tnk3I0x3ESCoLz7WZ5NTH6SZrywMY+PB1cjyqJ5jAluCOg=="
14434
- crossorigin="anonymous"
14435
- referrerpolicy="no-referrer"
14436
- />
14437
- <script
14438
- src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.2.1/js/bootstrap.min.js"
14439
- integrity="sha512-vyRAVI0IEm6LI/fVSv/Wq/d0KUfrg3hJq2Qz5FlfER69sf3ZHlOrsLriNm49FxnpUGmhx+TaJKwJ+ByTLKT+Yg=="
14440
- crossorigin="anonymous"
14441
- referrerpolicy="no-referrer"
14442
- ></script>
14443
-
14444
- <!-- FontAwesome -->
14445
- <link
14446
- rel="stylesheet"
14447
- href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.0/css/all.min.css"
14448
- integrity="sha512-xh6O/CkQoPOWDdYTDqeRdPCVd1SpvCA9XXcUnZS2FmJNp1coAFzvtCN9BmamE+4aHK8yyUHUSCcJHgXloTyT2A=="
14449
- crossorigin="anonymous"
14450
- referrerpolicy="no-referrer"
14451
- />
14452
-
14453
- <!-- Style -->
14454
- <style>
14455
- .DCEReactKit-pop-in {
14456
- animation-name: DCEReactKit-pop-in;
14457
- animation-duration: 0.5s;
14458
- animation-iteration-count: 1;
14459
- animation-timing-function: ease-out;
14460
- animation-fill-mode: both;
14461
-
14462
- transform-origin: center;
14463
- }
14464
-
14465
- @keyframes DCEReactKit-pop-in {
14466
- 0% {
14467
- opacity: 0;
14468
- transform: scale(0.9);
14469
- }
14470
- 100% {
14471
- opacity: 1;
14472
- transform: scale(1);
14473
- }
14474
- }
14475
-
14476
- .DCEReactKit-slide-in {
14477
- animation-name: DCEReactKit-slide-in;
14478
- animation-duration: 1s;
14479
- animation-iteration-count: 1;
14480
- animation-timing-function: ease-out;
14481
- animation-fill-mode: both;
14482
- animation-delay: 0.2s;
14483
- }
14484
-
14485
- @keyframes DCEReactKit-slide-in {
14486
- 0% {
14487
- opacity: 0;
14488
- transform: translate(0, 0.3em);
14489
- }
14490
- 100% {
14491
- opacity: 1;
14492
- transform: translate(0, 0);
14493
- }
14494
- }
14495
- </style>
14496
- </head>
14497
-
14498
- <!-- Body -->
14499
- <body class="bg-dark text-center pt-3 ps-3 pe-3">
14500
- <!-- Alert -->
14501
- <div
14502
- class="DCEReactKit-pop-in alert alert-warning d-inline-block"
14503
- style="width: 50em; max-width: 100%"
14504
- >
14505
- <!-- Title -->
14506
- <h2>
14507
- <i class="me-1 fa-solid fa-triangle-exclamation"></i>
14508
- ${title}
14509
- </h2>
14510
- <!-- Description -->
14511
- <div>
14512
- ${description}
14513
- </div>
14514
- </div>
14515
-
14516
- <!-- Error Code -->
14517
- <div class="DCEReactKit-slide-in text-light">
14518
- <strong>
14519
- Error Code:
14520
- </strong>
14521
- ${code}
14522
- </div>
14523
- </body>
14524
- `;
14525
- };
14526
-
14527
- /**
14528
- * Generate a static info page
14529
- * @author Gabe Abrams
14530
- * @param opts object containing all arguments
14531
- * @param opts.title title of the info box
14532
- * @param opts.body a human-readable text body for the info alert
14533
- * @returns the HTML for the info page
14534
- */
14535
- const genInfoPage = (opts) => {
14536
- const { title, body, } = opts;
14537
- return `
14538
- <head>
14539
- <!-- Metadata -->
14540
- <meta
14541
- name="viewport"
14542
- content="width=device-width, height=device-height, initial-scale=1.0, minimum-scale=1.0"
14543
- >
14544
-
14545
- <!-- Title -->
14546
- <title>${title}</title>
14547
-
14548
- <!-- Bootstrap -->
14549
- <link
14550
- rel="stylesheet"
14551
- href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.2.1/css/bootstrap.min.css"
14552
- integrity="sha512-siwe/oXMhSjGCwLn+scraPOWrJxHlUgMBMZXdPe2Tnk3I0x3ESCoLz7WZ5NTH6SZrywMY+PB1cjyqJ5jAluCOg=="
14553
- crossorigin="anonymous"
14554
- referrerpolicy="no-referrer"
14555
- />
14556
- <script
14557
- src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.2.1/js/bootstrap.min.js"
14558
- integrity="sha512-vyRAVI0IEm6LI/fVSv/Wq/d0KUfrg3hJq2Qz5FlfER69sf3ZHlOrsLriNm49FxnpUGmhx+TaJKwJ+ByTLKT+Yg=="
14559
- crossorigin="anonymous"
14560
- referrerpolicy="no-referrer"
14561
- ></script>
14562
-
14563
- <!-- FontAwesome -->
14564
- <link
14565
- rel="stylesheet"
14566
- href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.0/css/all.min.css"
14567
- integrity="sha512-xh6O/CkQoPOWDdYTDqeRdPCVd1SpvCA9XXcUnZS2FmJNp1coAFzvtCN9BmamE+4aHK8yyUHUSCcJHgXloTyT2A=="
14568
- crossorigin="anonymous"
14569
- referrerpolicy="no-referrer"
14570
- />
14571
-
14572
- <!-- Style -->
14573
- <style>
14574
- .DCEReactKit-pop-in {
14575
- animation-name: DCEReactKit-pop-in;
14576
- animation-duration: 0.5s;
14577
- animation-iteration-count: 1;
14578
- animation-timing-function: ease-out;
14579
- animation-fill-mode: both;
14580
-
14581
- transform-origin: center;
14582
- }
14583
-
14584
- @keyframes DCEReactKit-pop-in {
14585
- 0% {
14586
- opacity: 0;
14587
- transform: scale(0.9);
14588
- }
14589
- 100% {
14590
- opacity: 1;
14591
- transform: scale(1);
14592
- }
14593
- }
14594
- </style>
14595
- </head>
14596
-
14597
- <!-- Body -->
14598
- <body class="bg-dark text-center pt-3 ps-3 pe-3">
14599
- <!-- Alert -->
14600
- <div
14601
- class="DCEReactKit-pop-in alert alert-info d-inline-block"
14602
- style="width: 50em; max-width: 100%"
14603
- >
14604
- <!-- Title -->
14605
- <h2>
14606
- <i class="me-1 fa-solid fa-circle-info"></i>
14607
- ${title}
14608
- </h2>
14609
- <!-- Body -->
14610
- <div>
14611
- ${body}
14612
- </div>
14613
- </div>
14614
- </body>
14615
- `;
14616
- };
14617
-
14618
- /**
14619
- * Perform a rudimentary parsing of the user's browser agent string
14620
- * @author Gabe Abrams
14621
- * @param userAgent the user's browser agent
14622
- * @returns user info
14623
- */
14624
- const parseUserAgent = (userAgent) => {
14625
- /* ------------- Browser ------------ */
14626
- let browser = {
14627
- name: 'Unknown',
14628
- version: 'Unknown',
14629
- };
14630
- // Parse user agent
14631
- let verOffset;
14632
- let nameOffset;
14633
- if ((verOffset = userAgent.indexOf('Opera')) !== -1) {
14634
- // In Opera, the true version is after 'Opera' or after 'Version'
14635
- browser = {
14636
- name: 'Opera',
14637
- version: userAgent.substring(verOffset + 6),
14638
- };
14639
- if ((verOffset = userAgent.indexOf('Version')) !== -1) {
14640
- browser.version = userAgent.substring(verOffset + 8);
14641
- }
14642
- }
14643
- else if ((verOffset = userAgent.indexOf('MSIE')) !== -1) {
14644
- // In MSIE, the true version is after 'MSIE' in userAgent
14645
- browser = {
14646
- name: 'Internet Explorer',
14647
- version: userAgent.substring(verOffset + 5),
14648
- };
14649
- }
14650
- else if ((verOffset = userAgent.indexOf('Chrome')) !== -1) {
14651
- // In Chrome, the true version is after 'Chrome'
14652
- browser = {
14653
- name: 'Chrome',
14654
- version: userAgent.substring(verOffset + 7),
14655
- };
14656
- }
14657
- else if ((verOffset = userAgent.indexOf('Safari')) !== -1) {
14658
- // In Safari, the true version is after 'Safari' or after 'Version'
14659
- browser = {
14660
- name: 'Safari',
14661
- version: userAgent.substring(verOffset + 7),
14662
- };
14663
- if ((verOffset = userAgent.indexOf('Version')) !== -1) {
14664
- browser.version = userAgent.substring(verOffset + 8);
14665
- }
14666
- }
14667
- else if ((verOffset = userAgent.indexOf('Firefox')) != -1) {
14668
- // In Firefox, the true version is after 'Firefox'
14669
- browser = {
14670
- name: 'Firefox',
14671
- version: userAgent.substring(verOffset + 8),
14672
- };
14673
- }
14674
- else if ((nameOffset = userAgent.lastIndexOf(' ') + 1)
14675
- < (verOffset = userAgent.lastIndexOf('/'))) {
14676
- browser = {
14677
- name: userAgent.substring(nameOffset, verOffset),
14678
- version: userAgent.substring(verOffset + 1),
14679
- };
14680
- }
14681
- // Postprocess version
14682
- // trim the fullVersion string at semicolon/space if present
14683
- let ix;
14684
- if ((ix = browser.version.indexOf(';')) !== -1) {
14685
- browser.version = browser.version.substring(0, ix);
14686
- }
14687
- if ((ix = browser.version.indexOf(' ')) !== -1) {
14688
- browser.version = browser.version.substring(0, ix);
14689
- }
14690
- /* ------------- Device ------------- */
14691
- // Detect os
14692
- let os = 'Unknown';
14693
- if (userAgent.includes('Linux')) {
14694
- os = 'Linux';
14695
- }
14696
- else if (userAgent.includes('like Mac')) {
14697
- os = 'iOS';
14698
- }
14699
- else if (userAgent.includes('Mac')) {
14700
- os = 'Mac';
14701
- }
14702
- else if (userAgent.includes('Android')) {
14703
- os = 'Android';
14704
- }
14705
- else if (userAgent.includes('Win')) {
14706
- os = 'Win';
14707
- }
14708
- // Check if mobile
14709
- const isMobile = !!userAgent.match(/(iPhone|iPod|iPad|Android|BlackBerry)/);
14710
- // Device
14711
- const device = {
14712
- isMobile,
14713
- os,
14714
- };
14715
- /* ------------- Finish ------------- */
14716
- // Return info
14717
- return {
14718
- browser,
14719
- device,
14720
- };
14721
- };
14722
-
14723
- /**
14724
- * Generate an express API route handler
14725
- * @author Gabe Abrams
14726
- * @param opts object containing all arguments
14727
- * @param opts.paramTypes map containing the types for each parameter that is
14728
- * included in the request (map: param name => type)
14729
- * @param opts.handler function that processes the request
14730
- * @param [opts.skipSessionCheck] if true, skip the session check (allow users
14731
- * to not be logged in and launched via LTI)
14732
- * @param [opts.allowedHosts] if included, only allow requests from these hosts
14733
- * (start a hostname with a "*" to only check the end of the hostname)
14734
- * you can include just one string instead of an array
14735
- * @param [opts.bannedHosts] if included, do not allow requests from these hosts
14736
- * (start a hostname with a "*" to only check the end of the hostname)
14737
- * you can include just one string instead of an array
14738
- * @param [opts.unhandledErrorMessagePrefix] if included, when an error that
14739
- * is not of type ErrorWithCode is thrown, the client will receive an error
14740
- * where the error message is prefixed with this string. For example,
14741
- * if unhandledErrorMessagePrefix is
14742
- * 'While saving progress, we encountered an error:'
14743
- * and the error is 'progressInfo is not an object',
14744
- * the client will receive an error with the message
14745
- * 'While saving progress, we encountered an error: progressInfo is not an object'
14746
- * @returns express route handler that takes the following arguments:
14747
- * params (map: param name => value),
14748
- * req (express request object),
14749
- * next (express next function),
14750
- * send (a function that sends a string to the client),
14751
- * redirect (takes a url and redirects the user to that url),
14752
- * renderErrorPage (shows a static error page to the user),
14753
- * renderInfoPage (shows a static info page to the user),
14754
- * renderCustomHTML (renders custom html and sends it to the user),
14755
- * and returns the value to send to the client as a JSON API response, or
14756
- * calls next() or redirect(...) or send(...) or renderErrorPage(...).
14757
- * Note: params also has userId, userFirstName,
14758
- * userLastName, userEmail, userAvatarURL, isLearner, isTTM, isAdmin,
14759
- * and any other variables that
14760
- * are directly added to the session, if the user does have a session.
14761
- */
14762
- const genRouteHandler = (opts) => {
14763
- // Return a route handler
14764
- return (req, res, next) => __awaiter(void 0, void 0, void 0, function* () {
14765
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
14766
- // Output params
14767
- const output = {};
14768
- /*----------------------------------------*/
14769
- /* ----------- Hostname Check ----------- */
14770
- /*----------------------------------------*/
14771
- // Get hostnames
14772
- const originURL = String(req.get('origin')
14773
- || req.headers.origin
14774
- || req.headers.referer);
14775
- const originHostname = (originURL
14776
- // Remove protocol
14777
- .replace(/(^\w+:|^)\/\//, '')
14778
- // Remove port
14779
- .replace(/:\d+$/, ''));
14780
- const serverHostname = String(req.hostname);
14781
- // Check allowed
14782
- if (opts.allowedHosts) {
14783
- // Only accept requests from allowed hosts
14784
- const allowedArray = (Array.isArray(opts.allowedHosts)
14785
- ? opts.allowedHosts
14786
- : [opts.allowedHosts]);
14787
- // Check if server is localhost
14788
- if (serverHostname === 'localhost') {
14789
- // Allow localhost
14790
- allowedArray.push('localhost');
14791
- }
14792
- // Check if current host is allowed
14793
- const allowed = allowedArray.some((allowedHost) => {
14794
- if (allowedHost.startsWith('*')) {
14795
- // Check end of hostname
14796
- return originHostname.endsWith(allowedHost.substring(1));
14797
- }
14798
- // Check full hostname
14799
- return originHostname.toLowerCase() === allowedHost.toLowerCase();
14800
- });
14801
- // If not allowed, return error
14802
- if (!allowed) {
14803
- return handleError(res, {
14804
- message: 'You are not allowed to access this endpoint.',
14805
- code: ReactKitErrorCode$1.HostNotAllowed,
14806
- status: 403,
14807
- });
14808
- }
14809
- }
14810
- // Check banned
14811
- if (opts.bannedHosts) {
14812
- // Do not allow requests from banned hosts
14813
- const bannedArray = (Array.isArray(opts.bannedHosts)
14814
- ? opts.bannedHosts
14815
- : [opts.bannedHosts]);
14816
- // Check if current host is banned
14817
- const banned = bannedArray.some((bannedHost) => {
14818
- if (bannedHost.startsWith('*')) {
14819
- // Check end of hostname
14820
- return originHostname.endsWith(bannedHost.substring(1));
14821
- }
14822
- // Check full hostname
14823
- return originHostname.toLowerCase() === bannedHost.toLowerCase();
14824
- });
14825
- // If banned, return error
14826
- if (banned) {
14827
- return handleError(res, {
14828
- message: 'You are not allowed to access this endpoint.',
14829
- code: ReactKitErrorCode$1.HostBanned,
14830
- status: 403,
14831
- });
14832
- }
14833
- }
14834
- /*----------------------------------------*/
14835
- /* ------------ Parse Params ------------ */
14836
- /*----------------------------------------*/
14837
- // Process items one by one
14838
- const paramList = Object.entries((_a = opts.paramTypes) !== null && _a !== void 0 ? _a : {});
14839
- for (let i = 0; i < paramList.length; i++) {
14840
- const [name, type] = paramList[i];
14841
- // Find the value as a string
14842
- const value = (req.params[name]
14843
- || req.query[name]
14844
- || req.body[name]);
14845
- // Parse
14846
- if (type === ParamType$1.Boolean || type === ParamType$1.BooleanOptional) {
14847
- // Boolean
14848
- // Handle case where value doesn't exist
14849
- if (value === undefined) {
14850
- if (type === ParamType$1.BooleanOptional) {
14851
- output[name] = undefined;
14852
- }
14853
- else {
14854
- return handleError(res, {
14855
- message: `Parameter ${name} is required, but it was not included.`,
14856
- code: ReactKitErrorCode$1.MissingParameter,
14857
- status: 422,
14858
- });
14859
- }
14860
- }
14861
- else {
14862
- // Value exists
14863
- // Simplify value
14864
- const simpleVal = (String(value)
14865
- .trim()
14866
- .toLowerCase());
14867
- // Parse
14868
- output[name] = ([
14869
- 'true',
14870
- 'yes',
14871
- 'y',
14872
- '1',
14873
- 't',
14874
- ].indexOf(simpleVal) >= 0);
14875
- }
14876
- }
14877
- else if (type === ParamType$1.Float || type === ParamType$1.FloatOptional) {
14878
- // Float
14879
- // Handle case where value doesn't exist
14880
- if (value === undefined) {
14881
- if (type === ParamType$1.FloatOptional) {
14882
- output[name] = undefined;
14883
- }
14884
- else {
14885
- return handleError(res, {
14886
- message: `Parameter ${name} is required, but it was not included.`,
14887
- code: ReactKitErrorCode$1.MissingParameter,
14888
- status: 422,
14889
- });
14890
- }
14891
- }
14892
- else if (!Number.isNaN(Number.parseFloat(String(value)))) {
14893
- // Value is a number
14894
- output[name] = Number.parseFloat(String(value));
14895
- }
14896
- else {
14897
- // Issue!
14898
- return handleError(res, {
14899
- message: `Request data was malformed: ${name} was not a valid float.`,
14900
- code: ReactKitErrorCode$1.InvalidParameter,
14901
- status: 422,
14902
- });
14903
- }
14904
- }
14905
- else if (type === ParamType$1.Int || type === ParamType$1.IntOptional) {
14906
- // Int
14907
- // Handle case where value doesn't exist
14908
- if (value === undefined) {
14909
- if (type === ParamType$1.IntOptional) {
14910
- output[name] = undefined;
14911
- }
14912
- else {
14913
- return handleError(res, {
14914
- message: `Parameter ${name} is required, but it was not included.`,
14915
- code: ReactKitErrorCode$1.MissingParameter,
14916
- status: 422,
14917
- });
14918
- }
14919
- }
14920
- else if (!Number.isNaN(Number.parseInt(String(value), 10))) {
14921
- // Value is a number
14922
- output[name] = Number.parseInt(String(value), 10);
14923
- }
14924
- else {
14925
- // Issue!
14926
- return handleError(res, {
14927
- message: `Request data was malformed: ${name} was not a valid int.`,
14928
- code: ReactKitErrorCode$1.InvalidParameter,
14929
- status: 422,
14930
- });
14931
- }
14932
- }
14933
- else if (type === ParamType$1.JSON || type === ParamType$1.JSONOptional) {
14934
- // Stringified JSON
14935
- // Handle case where value doesn't exist
14936
- if (value === undefined) {
14937
- if (type === ParamType$1.JSONOptional) {
14938
- output[name] = undefined;
14939
- }
14940
- else {
14941
- return handleError(res, {
14942
- message: `Parameter ${name} is required, but it was not included.`,
14943
- code: ReactKitErrorCode$1.MissingParameter,
14944
- status: 422,
14945
- });
14946
- }
14947
- }
14948
- else {
14949
- // Value exists
14950
- // Parse
14951
- try {
14952
- output[name] = JSON.parse(String(value));
14953
- }
14954
- catch (err) {
14955
- return handleError(res, {
14956
- message: `Request data was malformed: ${name} was not a valid JSON payload.`,
14957
- code: ReactKitErrorCode$1.InvalidParameter,
14958
- status: 422,
14959
- });
14960
- }
14961
- }
14962
- }
14963
- else if (type === ParamType$1.String || type === ParamType$1.StringOptional) {
14964
- // String
14965
- // Handle case where value doesn't exist
14966
- if (value === undefined) {
14967
- if (type === ParamType$1.StringOptional) {
14968
- output[name] = undefined;
14969
- }
14970
- else {
14971
- return handleError(res, {
14972
- message: `Parameter ${name} is required, but it was not included.`,
14973
- code: ReactKitErrorCode$1.MissingParameter,
14974
- status: 422,
14975
- });
14976
- }
14977
- }
14978
- else {
14979
- // Value exists
14980
- // Leave as is
14981
- output[name] = value;
14982
- }
14983
- }
14984
- else {
14985
- // No valid data type
14986
- return handleError(res, {
14987
- message: `An internal error occurred: we could not determine the type of ${name}.`,
14988
- code: ReactKitErrorCode$1.InvalidParameter,
14989
- status: 422,
14990
- });
14991
- }
14992
- }
14993
- /*----------------------------------------*/
14994
- /* ------------- Launch Info ------------ */
14995
- /*----------------------------------------*/
14996
- // Get launch info
14997
- const { launched, launchInfo } = cacclGetLaunchInfo(req);
14998
- if (
14999
- // Not launched
15000
- (!launched || !launchInfo)
15001
- // Not skipping the session check
15002
- && !opts.skipSessionCheck) {
15003
- return handleError(res, {
15004
- message: 'Your session has expired. Please refresh the page and try again.',
15005
- code: ReactKitErrorCode$1.SessionExpired,
15006
- status: 401,
15007
- });
15008
- }
15009
- // Error if user info cannot be found
15010
- if (
15011
- // User information is incomplete
15012
- (!launchInfo
15013
- || !launchInfo.userId
15014
- || !launchInfo.userFirstName
15015
- || !launchInfo.userLastName
15016
- || (launchInfo.notInCourse
15017
- && !launchInfo.isAdmin)
15018
- || (!launchInfo.isTTM
15019
- && !launchInfo.isLearner
15020
- && !launchInfo.isAdmin))
15021
- // Not skipping the session check
15022
- && !opts.skipSessionCheck) {
15023
- return handleError(res, {
15024
- message: 'Your session was invalid. Please refresh the page and try again.',
15025
- code: ReactKitErrorCode$1.SessionExpired,
15026
- status: 401,
15027
- });
15028
- }
15029
- // Add launch info to output
15030
- output.userId = (launchInfo
15031
- ? launchInfo.userId
15032
- : ((_b = output.userId) !== null && _b !== void 0 ? _b : undefined));
15033
- output.userFirstName = (launchInfo
15034
- ? launchInfo.userFirstName
15035
- : ((_c = output.userFirstName) !== null && _c !== void 0 ? _c : undefined));
15036
- output.userLastName = (launchInfo
15037
- ? launchInfo.userLastName
15038
- : ((_d = output.userLastName) !== null && _d !== void 0 ? _d : undefined));
15039
- output.userEmail = (launchInfo
15040
- ? launchInfo.userEmail
15041
- : ((_e = output.userEmail) !== null && _e !== void 0 ? _e : undefined));
15042
- output.userAvatarURL = (launchInfo
15043
- ? ((_f = launchInfo.userImage) !== null && _f !== void 0 ? _f : 'http://www.gravatar.com/avatar/?d=identicon')
15044
- : ((_g = output.userAvatarURL) !== null && _g !== void 0 ? _g : undefined));
15045
- output.isLearner = (launchInfo
15046
- ? !!launchInfo.isLearner
15047
- : ((_h = output.isLearner) !== null && _h !== void 0 ? _h : undefined));
15048
- output.isTTM = (launchInfo
15049
- ? !!launchInfo.isTTM
15050
- : ((_j = output.isTTM) !== null && _j !== void 0 ? _j : undefined));
15051
- output.isAdmin = (launchInfo
15052
- ? !!launchInfo.isAdmin
15053
- : ((_k = output.isAdmin) !== null && _k !== void 0 ? _k : undefined));
15054
- output.courseId = (launchInfo
15055
- ? ((_l = output.courseId) !== null && _l !== void 0 ? _l : launchInfo.courseId)
15056
- : ((_m = output.courseId) !== null && _m !== void 0 ? _m : undefined));
15057
- output.courseName = (launchInfo
15058
- ? launchInfo.contextLabel
15059
- : ((_o = output.courseName) !== null && _o !== void 0 ? _o : undefined));
15060
- // Add other session variables
15061
- Object.keys(req.session).forEach((propName) => {
15062
- // Skip if prop already in output
15063
- if (output[propName] !== undefined) {
15064
- return;
15065
- }
15066
- // Add to output
15067
- const value = req.session[propName];
15068
- if (typeof value === 'string'
15069
- || typeof value === 'boolean'
15070
- || typeof value === 'number') {
15071
- output[propName] = value;
15072
- }
15073
- });
15074
- /*----------------------------------------*/
15075
- /* ----- Require Course Consistency ----- */
15076
- /*----------------------------------------*/
15077
- // Make sure the user actually launched from the appropriate course
15078
- if (output.courseId
15079
- && launchInfo
15080
- && launchInfo.courseId
15081
- && output.courseId !== launchInfo.courseId
15082
- && !output.isTTM
15083
- && !output.isAdmin) {
15084
- // Course of interest is not the launch course
15085
- return handleError(res, {
15086
- message: 'You switched sessions by opening this app in another tab. Please refresh the page and try again.',
15087
- code: ReactKitErrorCode$1.WrongCourse,
15088
- status: 401,
15089
- });
15090
- }
15091
- /*----------------------------------------*/
15092
- /* Require Proper Permissions */
15093
- /*----------------------------------------*/
15094
- // Add TTM endpoint security
15095
- if (
15096
- // This is a TTM endpoint
15097
- req.path.startsWith('/api/ttm')
15098
- // User is not a TTM
15099
- && (
15100
- // User is not a TTM
15101
- !output.isTTM
15102
- // User is not an admin
15103
- && !output.isAdmin)) {
15104
- // User does not have access
15105
- return handleError(res, {
15106
- message: 'This action is only allowed if you are a teaching team member for the course. Please go back to Canvas, log in as a teaching team member, and try again.',
15107
- code: ReactKitErrorCode$1.NotTTM,
15108
- status: 401,
15109
- });
15110
- }
15111
- // Add Admin endpoint security
15112
- if (
15113
- // This is an admin endpoint
15114
- req.path.startsWith('/api/admin')
15115
- // User is not an admin
15116
- && !output.isAdmin) {
15117
- // User does not have access
15118
- return handleError(res, {
15119
- message: 'This action is only allowed if you are a Canvas admin. Please go back to Canvas, log in as an admin, and try again.',
15120
- code: ReactKitErrorCode$1.NotAdmin,
15121
- status: 401,
15122
- });
15123
- }
15124
- /*----------------------------------------*/
15125
- /* ------------- Log Handler ------------ */
15126
- /*----------------------------------------*/
15127
- // Create a log handler function
15128
- /**
15129
- * Log an event on the server
15130
- * @author Gabe Abrams
15131
- */
15132
- const logServerEvent = (logOpts) => __awaiter(void 0, void 0, void 0, function* () {
15133
- var _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1;
15134
- // NOTE: internally, we slip through an opts.overrideAsClientEvent boolean
15135
- // that indicates that this is actually a client event, but we don't
15136
- // include that in the LogFunction type because this is internal and
15137
- // hidden from users
15138
- try {
15139
- // Parse user agent
15140
- const { browser, device, } = parseUserAgent(req.headers['user-agent']);
15141
- // Get time info in ET
15142
- const { timestamp, year, month, day, hour, minute, } = getTimeInfoInET();
15143
- // Main log info
15144
- const mainLogInfo = {
15145
- id: `${launchInfo ? launchInfo.userId : 'unknown'}-${Date.now()}-${Math.floor(Math.random() * 100000)}-${Math.floor(Math.random() * 100000)}`,
15146
- userFirstName: (launchInfo ? launchInfo.userFirstName : 'unknown'),
15147
- userLastName: (launchInfo ? launchInfo.userLastName : 'unknown'),
15148
- userEmail: (launchInfo ? launchInfo.userEmail : 'unknown'),
15149
- userId: (launchInfo ? launchInfo.userId : 'unknown'),
15150
- isLearner: (launchInfo && !!launchInfo.isLearner),
15151
- isAdmin: (launchInfo && !!launchInfo.isAdmin),
15152
- isTTM: (launchInfo && !!launchInfo.isTTM),
15153
- courseId: (launchInfo ? launchInfo.courseId : 'unknown'),
15154
- courseName: (launchInfo ? launchInfo.contextLabel : 'unknown'),
15155
- browser,
15156
- device,
15157
- year,
15158
- month,
15159
- day,
15160
- hour,
15161
- minute,
15162
- timestamp,
15163
- context: (typeof logOpts.context === 'string'
15164
- ? logOpts.context
15165
- : ((_q = ((_p = logOpts.context) !== null && _p !== void 0 ? _p : {})._) !== null && _q !== void 0 ? _q : LogBuiltInMetadata.Context.Uncategorized)),
15166
- subcontext: ((_r = logOpts.subcontext) !== null && _r !== void 0 ? _r : LogBuiltInMetadata.Context.Uncategorized),
15167
- tags: ((_s = logOpts.tags) !== null && _s !== void 0 ? _s : []),
15168
- level: ((_t = logOpts.level) !== null && _t !== void 0 ? _t : LogLevel$1.Info),
15169
- metadata: ((_u = logOpts.metadata) !== null && _u !== void 0 ? _u : {}),
15170
- };
15171
- // Type-specific info
15172
- const typeSpecificInfo = (('error' in opts && opts.error)
15173
- ? {
15174
- type: LogType$1.Error,
15175
- errorMessage: (_v = logOpts.error.message) !== null && _v !== void 0 ? _v : 'Unknown message',
15176
- errorCode: (_w = logOpts.error.code) !== null && _w !== void 0 ? _w : ReactKitErrorCode$1.NoCode,
15177
- errorStack: (_x = logOpts.error.stack) !== null && _x !== void 0 ? _x : 'No stack',
15178
- }
15179
- : {
15180
- type: LogType$1.Action,
15181
- target: ((_y = logOpts.target) !== null && _y !== void 0 ? _y : LogBuiltInMetadata.Target.NoTarget),
15182
- action: ((_z = logOpts.action) !== null && _z !== void 0 ? _z : LogAction$1.Unknown),
15183
- });
15184
- // Source-specific info
15185
- const sourceSpecificInfo = (logOpts.overrideAsClientEvent
15186
- ? {
15187
- source: LogSource$1.Client,
15188
- }
15189
- : {
15190
- source: LogSource$1.Server,
15191
- routePath: req.path,
15192
- routeTemplate: req.route.path,
15193
- });
15194
- // Build log event
15195
- const log = Object.assign(Object.assign(Object.assign({}, mainLogInfo), typeSpecificInfo), sourceSpecificInfo);
15196
- // Either print to console or save to db
15197
- const logCollection = internalGetLogCollection();
15198
- if (logCollection) {
15199
- // Store to the log collection
15200
- yield logCollection.insert(log);
15201
- }
15202
- else if (log.type === LogType$1.Error) {
15203
- // Print to console
15204
- // eslint-disable-next-line no-console
15205
- console.error('dce-reactkit error log:', log);
15206
- }
15207
- else {
15208
- // eslint-disable-next-line no-console
15209
- console.log('dce-reactkit action log:', log);
15210
- }
15211
- // Return log entry
15212
- return log;
15213
- }
15214
- catch (err) {
15215
- // Print because we cannot store the error
15216
- // eslint-disable-next-line no-console
15217
- console.error('Could not log the following:', logOpts, 'due to this error:', ((_0 = err) !== null && _0 !== void 0 ? _0 : {}).message, ((_1 = err) !== null && _1 !== void 0 ? _1 : {}).stack);
15218
- // Create a dummy log to return
15219
- const dummyMainInfo = {
15220
- id: '-1',
15221
- userFirstName: 'Unknown',
15222
- userLastName: 'Unknown',
15223
- userEmail: 'unknown@harvard.edu',
15224
- userId: 1,
15225
- isLearner: false,
15226
- isAdmin: false,
15227
- isTTM: false,
15228
- courseId: 1,
15229
- courseName: 'Unknown',
15230
- browser: {
15231
- name: 'Unknown',
15232
- version: 'Unknown',
15233
- },
15234
- device: {
15235
- isMobile: false,
15236
- os: 'Unknown',
15237
- },
15238
- year: 1,
15239
- month: 1,
15240
- day: 1,
15241
- hour: 1,
15242
- minute: 1,
15243
- timestamp: Date.now(),
15244
- tags: [],
15245
- level: LogLevel$1.Warn,
15246
- metadata: {},
15247
- context: LogBuiltInMetadata.Context.Uncategorized,
15248
- subcontext: LogBuiltInMetadata.Context.Uncategorized,
15249
- };
15250
- const dummyTypeSpecificInfo = {
15251
- type: LogType$1.Error,
15252
- errorMessage: 'Unknown',
15253
- errorCode: 'Unknown',
15254
- errorStack: 'No Stack',
15255
- };
15256
- const dummySourceSpecificInfo = {
15257
- source: LogSource$1.Server,
15258
- routePath: req.path,
15259
- routeTemplate: req.route.path,
15260
- };
15261
- const log = Object.assign(Object.assign(Object.assign({}, dummyMainInfo), dummyTypeSpecificInfo), dummySourceSpecificInfo);
15262
- return log;
15263
- }
15264
- });
15265
- /*------------------------------------------------------------------------*/
15266
- /* Call handler */
15267
- /*------------------------------------------------------------------------*/
15268
- // Keep track of whether a response was already sent
15269
- let responseSent = false;
15270
- /**
15271
- * Redirect the user to another path or url
15272
- * @author Gabe Abrams
15273
- * @param pathOrURL the path or url to redirect to
15274
- */
15275
- const redirect = (pathOrURL) => {
15276
- responseSent = true;
15277
- res.redirect(pathOrURL);
15278
- };
15279
- /**
15280
- * Send text to the client (with an optional status code)
15281
- * @author Gabe Abrams
15282
- * @param text the text to send to the client
15283
- * @parm [status=200] the http status code to send
15284
- */
15285
- const send = (text, status = 200) => {
15286
- responseSent = true;
15287
- res.status(status).send(text);
15288
- };
15289
- /**
15290
- * Render an error page
15291
- * @author Gabe Abrams
15292
- * @param renderOpts object containing all arguments
15293
- * @param [renderOpts.title=An Error Occurred] title of the error box
15294
- * @param [renderOpts.description=An unknown server error occurred. Please contact support.]
15295
- * a human-readable description of the error
15296
- * @param [renderOpts.code=ReactKitErrorCode.NoCode] error code to show
15297
- * @param [renderOpts.pageTitle=renderOpts.title] title of the page/tab if it differs from
15298
- * the title of the error
15299
- * @param [renderOpts.status=500] http status code
15300
- */
15301
- const renderErrorPage = (renderOpts = {}) => {
15302
- var _a, _b, _c;
15303
- const html = genErrorPage(renderOpts);
15304
- send(html, (_a = renderOpts.status) !== null && _a !== void 0 ? _a : 500);
15305
- // Log server-side error if not a session expired error or 404
15306
- if (renderOpts.status && renderOpts.status === 404) {
15307
- return;
15308
- }
15309
- if ((_b = renderOpts.title) === null || _b === void 0 ? void 0 : _b.toLowerCase().includes('session expired')) {
15310
- return;
15311
- }
15312
- logServerEvent({
15313
- context: LogBuiltInMetadata.Context.ServerRenderedErrorPage,
15314
- error: {
15315
- message: `${renderOpts.title}: ${renderOpts.description}`,
15316
- code: renderOpts.code,
15317
- },
15318
- metadata: {
15319
- title: renderOpts.title,
15320
- description: renderOpts.description,
15321
- code: renderOpts.code,
15322
- pageTitle: renderOpts.pageTitle,
15323
- status: (_c = renderOpts.status) !== null && _c !== void 0 ? _c : 500,
15324
- },
15325
- });
15326
- };
15327
- /**
15328
- * Render an info page
15329
- * @author Gabe Abrams
15330
- * @param renderOpts object containing all arguments
15331
- * @param renderOpts.title title of the info box
15332
- * @param renderOpts.body a human-readable text body for the info alert
15333
- */
15334
- const renderInfoPage = (renderOpts) => {
15335
- const html = genInfoPage(renderOpts);
15336
- send(html, 200);
15337
- };
15338
- /**
15339
- * Render custom HTML
15340
- * @author Gabe Abrams
15341
- * @param htmlOpts object containing all arguments
15342
- * @param htmlOpts.html the HTML to send to the client
15343
- * @param [ejsOpts.status=200] the http status code to send
15344
- */
15345
- const renderCustomHTML = (htmlOpts) => {
15346
- var _a;
15347
- send(htmlOpts.html, (_a = htmlOpts.status) !== null && _a !== void 0 ? _a : 200);
15348
- };
15349
- // Call the handler
15350
- try {
15351
- const results = yield opts.handler({
15352
- params: output,
15353
- req,
15354
- send,
15355
- next: () => {
15356
- responseSent = true;
15357
- next();
15358
- },
15359
- redirect,
15360
- renderErrorPage,
15361
- renderInfoPage,
15362
- renderCustomHTML,
15363
- logServerEvent,
15364
- });
15365
- // Send results to client (only if next wasn't called)
15366
- if (!responseSent) {
15367
- return handleSuccess(res, results !== null && results !== void 0 ? results : undefined);
15368
- }
15369
- }
15370
- catch (err) {
15371
- // Prefix error message if needed
15372
- if (opts.unhandledErrorMessagePrefix
15373
- && err instanceof Error
15374
- && err.message
15375
- && err.name !== 'ErrorWithCode') {
15376
- err.message = `${opts.unhandledErrorMessagePrefix.trim()} ${err.message.trim()}`;
15377
- }
15378
- // Send error to client (only if next wasn't called)
15379
- if (!responseSent) {
15380
- handleError(res, err);
15381
- // Log server-side error
15382
- logServerEvent({
15383
- context: LogBuiltInMetadata.Context.ServerEndpointError,
15384
- error: err,
15385
- });
15386
- return;
15387
- }
15388
- // Log error that was not responded with
15389
- // eslint-disable-next-line no-console
15390
- console.log('Error occurred but could not be sent to client because a response was already sent:', err);
15391
- }
15392
- });
15393
- };
15394
-
15395
- /**
15396
- * Stub a server endpoint response
15397
- * @author Gabe Abrams
15398
- * @param opts object containing all arguments
15399
- * @param [opts.method=GET] http method to stub
15400
- * @param opts.path full pathname to stub
15401
- * @param opts.body body of response if stubbing a successful response
15402
- * @param opts.errorMessage message of error if stubbing a failed response
15403
- * @param [opts.errorCode] error code if stubbing a failed response
15404
- */
15405
- const stubServerEndpoint = _setStubResponse;
15406
-
15407
- /**
15408
- * Start a dynamic wait, call the function once the operation has completed and
15409
- * the dynamic wait will continue waiting for the rest of the minimum time
15410
- * @author Gabe Abrams
15411
- * @param minWaitMs the minimum number of ms to wait
15412
- * @returns async function to call to finish the wait
14055
+ * @param minWaitMs the minimum number of ms to wait
14056
+ * @returns async function to call to finish the wait
15413
14057
  */
15414
14058
  const startMinWait = (minWaitMs) => {
15415
14059
  const startTimestamp = Date.now();
@@ -15545,27 +14189,11 @@ const parallelLimit = (taskFunctions, limit) => __awaiter(void 0, void 0, void 0
15545
14189
  });
15546
14190
 
15547
14191
  /**
15548
- * Initialize a log collection given the dce-mango Collection class
14192
+ * Route for checking the status of the current user's
14193
+ * access to log review
15549
14194
  * @author Gabe Abrams
15550
- * @param Collection the Collection class from dce-mango
15551
- * @returns initialized logCollection
15552
14195
  */
15553
- const initLogCollection = (Collection) => {
15554
- return new Collection('Log', {
15555
- uniqueIndexKey: 'id',
15556
- indexKeys: [
15557
- 'courseId',
15558
- 'context',
15559
- 'subcontext',
15560
- 'tags',
15561
- 'year',
15562
- 'month',
15563
- 'day',
15564
- 'hour',
15565
- 'type',
15566
- ],
15567
- });
15568
- };
14196
+ const LOG_REVIEW_STATUS_ROUTE = `${ROUTE_PATH_PREFIX}/logs/access_allowed`;
15569
14197
 
15570
14198
  /*------------------------------------------------------------------------*/
15571
14199
  /* -------------------------------- Cache ------------------------------- */
@@ -15707,66 +14335,6 @@ const getLocalTimeInfo = (dateOrTimestamp) => {
15707
14335
  };
15708
14336
  };
15709
14337
 
15710
- /**
15711
- * Add all routes for the DBEditor
15712
- * @author Yuen Ler Chow
15713
- * @param opts object containing all arguments
15714
- * @param opts.app express app to add routes too
15715
- * @param opts.collectionName the name of the collection
15716
- * @param opts.adminsOnly true if the endpoint is for admins only
15717
- * @param opts.collection dce-mango db collection
15718
- */
15719
- const addDBEditorEndpoints = (opts) => {
15720
- const { app, collectionName, adminsOnly, collection, } = opts;
15721
- // Generate the endpoint path
15722
- const endpointPath = generateEndpointPath(collectionName, adminsOnly);
15723
- /**
15724
- * List all items in the collection
15725
- * @author Yuen Ler Chow
15726
- * @returns {any[]} the list of items in the collection
15727
- */
15728
- app.get(endpointPath, genRouteHandler({
15729
- paramTypes: {
15730
- filterQuery: ParamType$1.JSONOptional,
15731
- },
15732
- handler: ({ params }) => __awaiter(void 0, void 0, void 0, function* () {
15733
- var _a;
15734
- const filterQuery = (_a = params.filterQuery) !== null && _a !== void 0 ? _a : {};
15735
- const categories = yield collection.find(filterQuery);
15736
- return categories;
15737
- }),
15738
- }));
15739
- /**
15740
- * Create a new item in the collection
15741
- * @author Yuen Ler Chow
15742
- * @param {any} item the item to create
15743
- */
15744
- app.post(endpointPath, genRouteHandler({
15745
- paramTypes: {
15746
- item: ParamType$1.JSON,
15747
- },
15748
- handler: ({ params, }) => __awaiter(void 0, void 0, void 0, function* () {
15749
- // Destructure params
15750
- const { item, } = params;
15751
- yield collection.insert(item);
15752
- }),
15753
- }));
15754
- /**
15755
- * Remove an item from the collection by id
15756
- * @author Yuen Ler Chow
15757
- */
15758
- app.delete(`${endpointPath}/:id`, genRouteHandler({
15759
- paramTypes: {
15760
- id: ParamType$1.String,
15761
- },
15762
- handler: ({ params, }) => __awaiter(void 0, void 0, void 0, function* () {
15763
- // Destructure params
15764
- const { id, } = params;
15765
- yield collection.delete({ id });
15766
- }),
15767
- }));
15768
- };
15769
-
15770
14338
  /**
15771
14339
  * Given an array of strings, create a single comma-separated string that includes
15772
14340
  * 'and' as well as an oxford comma.
@@ -16308,203 +14876,6 @@ const shuffleArray = (arr) => {
16308
14876
  return newArr;
16309
14877
  };
16310
14878
 
16311
- /**
16312
- * Sends and retries an http request
16313
- * @author Gabriel Abrams
16314
- * @param opts object containing all arguments
16315
- * @param opts.path path to send request to
16316
- * @param [opts.host] host to send request to
16317
- * @param [opts.method=GET] http method to use
16318
- * @param [opts.params] body/data to include in the request
16319
- * @param [opts.headers] headers to include in the request
16320
- * @param [opts.sendCrossDomainCredentials=true if in development mode] if true,
16321
- * send cross-domain credentials even if not in dev mode
16322
- * @param [opts.responseType=JSON] expected response type
16323
- * @returns { body, status, headers } on success
16324
- */
16325
- const sendServerToServerRequest = (opts) => __awaiter(void 0, void 0, void 0, function* () {
16326
- var _a;
16327
- // Process method
16328
- const method = (opts.method || 'GET');
16329
- // Encode objects within params
16330
- let params;
16331
- if (opts.params) {
16332
- params = {};
16333
- Object.entries(opts.params).forEach(([key, val]) => {
16334
- if (typeof val === 'object' && !Array.isArray(val)) {
16335
- params[key] = JSON.stringify(val);
16336
- }
16337
- else {
16338
- params[key] = val;
16339
- }
16340
- });
16341
- }
16342
- // Stringify parameters
16343
- const stringifiedParams = qs__default["default"].stringify(params || {}, {
16344
- encodeValuesOnly: true,
16345
- arrayFormat: 'brackets',
16346
- });
16347
- // Create url (include query if GET)
16348
- const query = (method === 'GET' ? `?${stringifiedParams}` : '');
16349
- let url;
16350
- if (!opts.host) {
16351
- // No host included at all. Just send to a path
16352
- url = `${opts.path}${query}`;
16353
- }
16354
- else {
16355
- url = `https://${opts.host}${opts.path}${query}`;
16356
- }
16357
- // Update headers
16358
- const headers = opts.headers || {};
16359
- let data = null;
16360
- if (!headers['Content-Type']) {
16361
- // Form encoded
16362
- headers['Content-Type'] = 'application/x-www-form-urlencoded';
16363
- // Add data if applicable
16364
- data = (method !== 'GET' ? stringifiedParams : null);
16365
- }
16366
- else {
16367
- // JSON encode
16368
- data = params;
16369
- }
16370
- // Encode data
16371
- let encodedData;
16372
- if (data) {
16373
- if (headers['Content-Type'] === 'application/x-www-form-urlencoded') {
16374
- encodedData = new URLSearchParams(params);
16375
- }
16376
- else {
16377
- encodedData = JSON.stringify(data);
16378
- }
16379
- }
16380
- // Send request
16381
- try {
16382
- const response = yield fetch(url, {
16383
- method,
16384
- mode: 'cors',
16385
- headers: headers !== null && headers !== void 0 ? headers : {},
16386
- body: ((method !== 'GET' && encodedData)
16387
- ? encodedData
16388
- : undefined),
16389
- redirect: 'follow',
16390
- });
16391
- // Get headers map
16392
- const responseHeaders = {};
16393
- response.headers.forEach((value, key) => {
16394
- responseHeaders[key] = value;
16395
- });
16396
- // Process response based on responseType
16397
- try {
16398
- // Parse response
16399
- let responseBody;
16400
- if (opts.responseType
16401
- && opts.responseType === 'Text') {
16402
- // Response type is text
16403
- responseBody = yield response.text();
16404
- }
16405
- else {
16406
- // Response type is JSON
16407
- responseBody = yield response.json();
16408
- }
16409
- // Return response
16410
- return {
16411
- body: responseBody,
16412
- status: response.status,
16413
- headers: responseHeaders,
16414
- };
16415
- }
16416
- catch (err) {
16417
- throw new ErrorWithCode(`Failed to parse response as ${opts.responseType}: ${err === null || err === void 0 ? void 0 : err.message}`, ReactKitErrorCode$1.ResponseParseError);
16418
- }
16419
- }
16420
- catch (err) {
16421
- // Self-signed certificate error:
16422
- if ((_a = err === null || err === void 0 ? void 0 : err.message) === null || _a === void 0 ? void 0 : _a.includes('self signed certificate')) {
16423
- throw new ErrorWithCode('We refused to send a request because the receiver has self-signed certificates.', ReactKitErrorCode$1.SelfSigned);
16424
- }
16425
- // No tries left
16426
- throw new ErrorWithCode(`We encountered an error when trying to send a network request. If this issue persists, contact an admin. Error: ${err === null || err === void 0 ? void 0 : err.message}`, ReactKitErrorCode$1.NotConnected);
16427
- }
16428
- });
16429
-
16430
- /**
16431
- * Send a server-to-server request from this sever to another server that uses
16432
- * dce-reactkit [for server only]
16433
- * @author Gabe Abrams
16434
- * @param opts object containing all arguments
16435
- * @param opts.host - the host of the other server
16436
- * @param opts.path - the path of the other server's endpoint
16437
- * @param [opts.method=GET] - the method of the endpoint
16438
- * @param [opts.params] - query/body parameters to include
16439
- * @param [opts.headers] - headers to include
16440
- * @returns response from server
16441
- */
16442
- const visitEndpointOnAnotherServer = (opts) => __awaiter(void 0, void 0, void 0, function* () {
16443
- var _a;
16444
- // Remove properties with undefined values
16445
- let params;
16446
- if (opts.params) {
16447
- params = Object.fromEntries(Object
16448
- .entries(opts.params)
16449
- .filter(([, value]) => {
16450
- return value !== undefined;
16451
- }));
16452
- }
16453
- // Automatically JSONify arrays and objects
16454
- if (params) {
16455
- params = Object.fromEntries(Object
16456
- .entries(params)
16457
- .map(([key, value]) => {
16458
- if (Array.isArray(value) || typeof value === 'object') {
16459
- return [key, JSON.stringify(value)];
16460
- }
16461
- return [key, value];
16462
- }));
16463
- }
16464
- // Send the request
16465
- const response = yield sendServerToServerRequest({
16466
- host: opts.host,
16467
- path: opts.path,
16468
- method: (_a = opts.method) !== null && _a !== void 0 ? _a : 'GET',
16469
- params,
16470
- });
16471
- // Check for failure
16472
- if (!response || !response.body) {
16473
- throw new ErrorWithCode('We didn\'t get a response from the server. Please check your internet connection.', ReactKitErrorCode$1.NoResponse);
16474
- }
16475
- if (!response.body.success) {
16476
- // Other errors
16477
- throw new ErrorWithCode((response.body.message
16478
- || 'An unknown error occurred. Please contact an admin.'), (response.body.code
16479
- || ReactKitErrorCode$1.NoCode));
16480
- }
16481
- // Success! Extract the body
16482
- const { body } = response.body;
16483
- // Return
16484
- return body;
16485
- });
16486
-
16487
- const punctuationRegex = /[!@#$%^&*(),.?\/;:'"\-\[\]]/g;
16488
- /**
16489
- * Get number of words in string
16490
- * @author Gardenia Liu
16491
- * @author Allison Zhang
16492
- * @author Gabe Abrams
16493
- * @param text the string to check
16494
- * @returns number of words in the string
16495
- */
16496
- const getWordCount = (text) => {
16497
- const trimmedTextWithoutPunctuation = (text
16498
- // Remove leading and trailing whitespace
16499
- .trim()
16500
- // Remove punctuation
16501
- .replace(punctuationRegex, ''));
16502
- if (trimmedTextWithoutPunctuation.length === 0) {
16503
- return 0;
16504
- }
16505
- return trimmedTextWithoutPunctuation.split(/\s+/g).length;
16506
- };
16507
-
16508
14879
  /**
16509
14880
  * Days of the week
16510
14881
  * @author Gabe Abrams
@@ -16543,6 +14914,7 @@ exports.ItemPicker = ItemPicker;
16543
14914
  exports.LoadingSpinner = LoadingSpinner;
16544
14915
  exports.LogAction = LogAction$1;
16545
14916
  exports.LogBuiltInMetadata = LogBuiltInMetadata;
14917
+ exports.LogLevel = LogLevel$1;
16546
14918
  exports.LogReviewer = LogReviewer;
16547
14919
  exports.LogSource = LogSource$1;
16548
14920
  exports.LogType = LogType$1;
@@ -16564,7 +14936,6 @@ exports.ToggleSwitch = ToggleSwitch;
16564
14936
  exports.Tooltip = Tooltip;
16565
14937
  exports.Variant = Variant$1;
16566
14938
  exports.abbreviate = abbreviate;
16567
- exports.addDBEditorEndpoints = addDBEditorEndpoints;
16568
14939
  exports.addFatalErrorHandler = addFatalErrorHandler;
16569
14940
  exports.alert = alert;
16570
14941
  exports.avg = avg;
@@ -16582,20 +14953,14 @@ exports.forEachAsync = forEachAsync;
16582
14953
  exports.forceNumIntoBounds = forceNumIntoBounds;
16583
14954
  exports.genCSV = genCSV;
16584
14955
  exports.genCommaList = genCommaList;
16585
- exports.genRouteHandler = genRouteHandler;
16586
14956
  exports.getHumanReadableDate = getHumanReadableDate;
16587
14957
  exports.getLocalTimeInfo = getLocalTimeInfo;
16588
14958
  exports.getMonthName = getMonthName;
16589
14959
  exports.getOrdinal = getOrdinal;
16590
14960
  exports.getPartOfDay = getPartOfDay;
16591
14961
  exports.getTimeInfoInET = getTimeInfoInET;
16592
- exports.getWordCount = getWordCount;
16593
- exports.handleError = handleError;
16594
- exports.handleSuccess = handleSuccess;
16595
14962
  exports.idify = idify;
16596
14963
  exports.initClient = initClient;
16597
- exports.initLogCollection = initLogCollection;
16598
- exports.initServer = initServer;
16599
14964
  exports.isMobileOrTablet = isMobileOrTablet;
16600
14965
  exports.leaveToURL = leaveToURL;
16601
14966
  exports.logClientEvent = logClientEvent;
@@ -16620,7 +14985,6 @@ exports.useForceRender = useForceRender;
16620
14985
  exports.validateEmail = validateEmail;
16621
14986
  exports.validatePhoneNumber = validatePhoneNumber;
16622
14987
  exports.validateString = validateString;
16623
- exports.visitEndpointOnAnotherServer = visitEndpointOnAnotherServer;
16624
14988
  exports.visitServerEndpoint = visitServerEndpoint;
16625
14989
  exports.waitMs = waitMs;
16626
14990
  //# sourceMappingURL=index.js.map