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/esm/index.js CHANGED
@@ -1,10 +1,9 @@
1
1
  import * as React from 'react';
2
2
  import React__default, { useState, useRef, useEffect, useReducer, forwardRef, useContext, useLayoutEffect, createContext, useMemo, useCallback, Component, Fragment } from 'react';
3
- import { faExclamationTriangle, faCircle, faHourglassEnd, faDotCircle, faCheckSquare, faHourglass, faClipboard, faChevronDown, faChevronRight, faCloudDownloadAlt, faMinus, faCheckCircle, faXmarkCircle, faSort, faSortDown, faSortUp, faCalendar, faTag, faHammer, faList, faTimes, faSave, faTrash, faCog, faPlus } from '@fortawesome/free-solid-svg-icons';
3
+ import { faExclamationTriangle, faHourglassEnd, faCircle, faDotCircle, faCheckSquare, faHourglass, faClipboard, faChevronDown, faChevronRight, faCloudDownloadAlt, faMinus, faCheckCircle, faXmarkCircle, faSort, faSortDown, faSortUp, faCalendar, faTag, faHammer, faList, faTimes, faSave, faTrash, faCog, faPlus } from '@fortawesome/free-solid-svg-icons';
4
4
  import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
5
5
  import ReactDOM, { createPortal } from 'react-dom';
6
6
  import { faCircle as faCircle$1, faSquareMinus, faSquare } from '@fortawesome/free-regular-svg-icons';
7
- import qs from 'qs';
8
7
 
9
8
  /******************************************************************************
10
9
  Copyright (c) Microsoft Corporation.
@@ -31,7 +30,7 @@ function __awaiter(thisArg, _arguments, P, generator) {
31
30
  });
32
31
  }
33
32
 
34
- // Highest error code = DRK18
33
+ // Highest error code = DRK34
35
34
  /**
36
35
  * List of error codes built into the react kit
37
36
  * @author Gabe Abrams
@@ -41,22 +40,7 @@ var ReactKitErrorCode;
41
40
  ReactKitErrorCode["NoResponse"] = "DRK1";
42
41
  ReactKitErrorCode["NoCode"] = "DRK2";
43
42
  ReactKitErrorCode["SessionExpired"] = "DRK3";
44
- ReactKitErrorCode["MissingParameter"] = "DRK4";
45
- ReactKitErrorCode["InvalidParameter"] = "DRK5";
46
- ReactKitErrorCode["HostNotAllowed"] = "DRK17";
47
- ReactKitErrorCode["HostBanned"] = "DRK18";
48
- ReactKitErrorCode["WrongCourse"] = "DRK6";
49
43
  ReactKitErrorCode["NoCACCLSendRequestFunction"] = "DRK7";
50
- ReactKitErrorCode["NoCACCLGetLaunchInfoFunction"] = "DRK8";
51
- ReactKitErrorCode["NotTTM"] = "DRK9";
52
- ReactKitErrorCode["NotAdmin"] = "DRK10";
53
- ReactKitErrorCode["NotAllowedToReviewLogs"] = "DRK11";
54
- ReactKitErrorCode["ThemeCheckedBeforeReactKitReady"] = "DRK12";
55
- ReactKitErrorCode["SessionExpiredMessageGottenBeforeReactKitReady"] = "DRK13";
56
- // Server-to-server requests
57
- ReactKitErrorCode["NotConnected"] = "DRK14";
58
- ReactKitErrorCode["SelfSigned"] = "DRK15";
59
- ReactKitErrorCode["ResponseParseError"] = "DRK16";
60
44
  })(ReactKitErrorCode || (ReactKitErrorCode = {}));
61
45
  var ReactKitErrorCode$1 = ReactKitErrorCode;
62
46
 
@@ -188,98 +172,6 @@ const LogBuiltInMetadata = {
188
172
  },
189
173
  };
190
174
 
191
- /**
192
- * Loading spinner/indicator
193
- * @author Gabe Abrams
194
- */
195
- /*------------------------------------------------------------------------*/
196
- /* -------------------------------- Style ------------------------------- */
197
- /*------------------------------------------------------------------------*/
198
- const style$a = `
199
- /* Container fades in */
200
- .LoadingSpinner-container {
201
- animation-name: LoadingSpinner-container-fade-in;
202
- animation-duration: 0.3s;
203
- animation-delay: 0.4s;
204
- animation-fill-mode: both;
205
- animation-timing-function: ease-out;
206
- }
207
- @keyframes LoadingSpinner-container-fade-in {
208
- 0% {
209
- opacity: 0;
210
- }
211
- 100% {
212
- opacity: 1;
213
- }
214
- }
215
-
216
- /* Blips */
217
- .LoadingSpinner-blip-1,
218
- .LoadingSpinner-blip-2,
219
- .LoadingSpinner-blip-3,
220
- .LoadingSpinner-blip-4 {
221
- font-size: 1.8rem;
222
- opacity: 0.6;
223
- margin-top: 1rem;
224
- margin-bottom: 1rem;
225
- }
226
-
227
- /* First Blip */
228
- .LoadingSpinner-blip-1 {
229
- animation: LoadingSpinner-pop-animation 2s infinite;
230
- }
231
-
232
- /* Second Blip */
233
- .LoadingSpinner-blip-2 {
234
- animation: LoadingSpinner-pop-animation 2s infinite;
235
- -webkit-animation-delay: 0.1s;
236
- animation-delay: 0.1s;
237
- }
238
-
239
- /* Third Blip */
240
- .LoadingSpinner-blip-3 {
241
- animation: LoadingSpinner-pop-animation 2s infinite;
242
- animation-delay: 0.2s;
243
- }
244
-
245
- /* Fourth Blip */
246
- .LoadingSpinner-blip-4 {
247
- animation: LoadingSpinner-pop-animation 2s infinite;
248
- animation-delay: 0.3s;
249
- }
250
-
251
- /* Pop Animation for Each Blip */
252
- @keyframes LoadingSpinner-pop-animation {
253
- 0% {
254
- transform: scale(1.0);
255
- }
256
- 10% {
257
- transform: scale(1.5);
258
- }
259
- 30% {
260
- transform: scale(1.0);
261
- }
262
- 100% {
263
- transform: scale(1.0);
264
- }
265
- }
266
- `;
267
- /*------------------------------------------------------------------------*/
268
- /* ------------------------------ Component ----------------------------- */
269
- /*------------------------------------------------------------------------*/
270
- const LoadingSpinner = () => {
271
- /*------------------------------------------------------------------------*/
272
- /* ------------------------------- Render ------------------------------- */
273
- /*------------------------------------------------------------------------*/
274
- // Add all four blips to a container
275
- return (React__default.createElement("div", { className: "text-center LoadingSpinner LoadingSpinner-container" },
276
- React__default.createElement("style", null, style$a),
277
- React__default.createElement(FontAwesomeIcon, { icon: faCircle, className: "LoadingSpinner-blip-1 me-1" }),
278
- React__default.createElement(FontAwesomeIcon, { icon: faCircle, className: "LoadingSpinner-blip-2 me-1" }),
279
- React__default.createElement(FontAwesomeIcon, { icon: faCircle, className: "LoadingSpinner-blip-3 me-1" }),
280
- React__default.createElement(FontAwesomeIcon, { icon: faCircle, className: "LoadingSpinner-blip-4" })));
281
- };
282
-
283
175
  /**
284
176
  * Wait for a certain number of ms
285
177
  * @author Gabe Abrams
@@ -330,6 +222,15 @@ let onInitialized;
330
222
  const initialized = new Promise((resolve) => {
331
223
  onInitialized = resolve;
332
224
  });
225
+ /* ------------ No Server ----------- */
226
+ let noServer = false;
227
+ /**
228
+ * Check if there is no server for this app
229
+ * @author Gabe Abrams
230
+ */
231
+ const appHasNoServer = () => {
232
+ return noServer;
233
+ };
333
234
  /* ---------- Send Request ---------- */
334
235
  let storedSendRequest;
335
236
  /**
@@ -391,11 +292,13 @@ const initClient = (opts) => {
391
292
  storedSendRequest = () => __awaiter(void 0, void 0, void 0, function* () {
392
293
  throw new Error('Cannot send requests because there is no server');
393
294
  });
295
+ noServer = true;
394
296
  }
395
297
  else {
396
298
  // Store values
397
299
  storedSendRequest = opts.sendRequest;
398
300
  sessionExpiredMessage = opts.sessionExpiredMessage;
301
+ noServer = false;
399
302
  }
400
303
  // Handle universal parts
401
304
  darkModeOn = !!opts.darkModeOn;
@@ -412,7 +315,6 @@ const BASE_Z_INDEX = 1000000000;
412
315
  const BASE_Z_INDEX_ON_TOP = 2000000000;
413
316
  // Constants
414
317
  const MS_TO_ANIMATE = 200; // Animation duration
415
- const MS_TO_WAIT_BEFORE_SHOWING_LOADING_INDICATOR = 1000;
416
318
  // Modal type to list of buttons
417
319
  const modalTypeToModalButtonTypes = {
418
320
  [ModalType$1.Okay]: [
@@ -526,7 +428,7 @@ const getNextUniqueId = () => {
526
428
  /*------------------------------------------------------------------------*/
527
429
  /* -------------------------------- Style ------------------------------- */
528
430
  /*------------------------------------------------------------------------*/
529
- const style$9 = `
431
+ const style$a = `
530
432
  .Modal-backdrop {
531
433
  position: fixed;
532
434
  top: 0;
@@ -598,14 +500,13 @@ const Modal = (props) => {
598
500
  /*------------------------------------------------------------------------*/
599
501
  var _a;
600
502
  /* -------------- Props ------------- */
601
- const { type = ModalType$1.NoButtons, size = ModalSize$1.Large, title, largeTitle, children, onClose, dontAllowBackdropExit, dontShowXButton, onTopOfOtherModals, isLoading, isLoadingCancelable, } = props;
503
+ const { type = ModalType$1.NoButtons, size = ModalSize$1.Large, title, largeTitle, children, onClose, dontAllowBackdropExit, dontShowXButton, onTopOfOtherModals, } = props;
602
504
  // Determine if no header either
603
505
  const noHeader = (!title && type === ModalType$1.NoButtons);
604
506
  /* -------------- State ------------- */
605
507
  // True if animation is in use
606
508
  const [animatingIn, setAnimatingIn] = useState(true);
607
509
  const [animatingPop, setAnimatingPop] = useState(false);
608
- const [showModal, setShowModal] = useState(false);
609
510
  /* -------------- Refs -------------- */
610
511
  // Keep track of whether modal is still mounted
611
512
  const mounted = useRef(false);
@@ -632,16 +533,6 @@ const Modal = (props) => {
632
533
  if (mounted.current) {
633
534
  setAnimatingIn(false);
634
535
  }
635
- if (isLoading) {
636
- // wait before showing modal
637
- yield waitMs(MS_TO_WAIT_BEFORE_SHOWING_LOADING_INDICATOR);
638
- if (mounted.current) {
639
- setShowModal(true);
640
- }
641
- }
642
- else {
643
- setShowModal(true);
644
- }
645
536
  }))();
646
537
  return () => {
647
538
  mounted.current = false;
@@ -708,15 +599,6 @@ const Modal = (props) => {
708
599
  else if (animatingPop) {
709
600
  animationClass = 'Modal-animating-pop';
710
601
  }
711
- // default to show close button when not loading and not show close button when loading
712
- // unless downShowXButton or isLoadingCancelable
713
- const showCloseButton = (
714
- // The modal must have an onClose function
715
- onClose
716
- // ...and the close button is allowed to be visible
717
- && !dontShowXButton
718
- // ...and should not be shown if the modal is loading and not cancelable
719
- && !(isLoading && !isLoadingCancelable));
720
602
  // Render the modal
721
603
  const contentToRender = (React__default.createElement("div", { className: "modal show", tabIndex: -1, style: {
722
604
  zIndex: baseZIndex,
@@ -725,7 +607,7 @@ const Modal = (props) => {
725
607
  left: 0,
726
608
  right: 0,
727
609
  } },
728
- React__default.createElement("style", null, style$9),
610
+ React__default.createElement("style", null, style$a),
729
611
  React__default.createElement("div", { className: `Modal-backdrop ${backdropAnimationClass}`, style: {
730
612
  zIndex: baseZIndex + 1,
731
613
  }, onClick: () => __awaiter(void 0, void 0, void 0, function* () {
@@ -743,7 +625,7 @@ const Modal = (props) => {
743
625
  // Handle close
744
626
  handleClose(ModalButtonType$1.Cancel);
745
627
  }) }),
746
- showModal && (React__default.createElement("div", { className: `modal-dialog modal-${size} ${animationClass} modal-dialog-scrollable modal-dialog-centered`, style: {
628
+ React__default.createElement("div", { className: `modal-dialog modal-${size} ${animationClass} modal-dialog-scrollable modal-dialog-centered`, style: {
747
629
  zIndex: baseZIndex + 2,
748
630
  // Override sizes for even larger for XL
749
631
  width: (size === ModalSize$1.ExtraLarge
@@ -775,7 +657,7 @@ const Modal = (props) => {
775
657
  ? '1.6rem'
776
658
  : undefined),
777
659
  } }, title),
778
- showCloseButton && (React__default.createElement("button", { type: "button", className: "Modal-x-button btn-close", "aria-label": "Close", style: {
660
+ (onClose && !dontShowXButton) && (React__default.createElement("button", { type: "button", className: "Modal-x-button btn-close", "aria-label": "Close", style: {
779
661
  backgroundColor: (isDarkModeOn()
780
662
  ? 'white'
781
663
  : undefined),
@@ -783,18 +665,14 @@ const Modal = (props) => {
783
665
  // Handle close
784
666
  handleClose(ModalButtonType$1.Cancel);
785
667
  } })))),
786
- React__default.createElement("div", { className: "modal-body", style: {
668
+ children && (React__default.createElement("div", { className: "modal-body", style: {
787
669
  color: (isDarkModeOn()
788
670
  ? 'white'
789
671
  : undefined),
790
672
  backgroundColor: (isDarkModeOn()
791
673
  ? '#444'
792
674
  : undefined),
793
- } }, isLoading
794
- ? (React__default.createElement(React__default.Fragment, null,
795
- React__default.createElement(LoadingSpinner, null),
796
- React__default.createElement("span", { className: "sr-only" }, "Content loading")))
797
- : children),
675
+ } }, children)),
798
676
  footer && (React__default.createElement("div", { className: "modal-footer pt-1 pb-1", style: {
799
677
  color: (isDarkModeOn()
800
678
  ? 'white'
@@ -805,7 +683,7 @@ const Modal = (props) => {
805
683
  borderTop: (isDarkModeOn()
806
684
  ? '0.1rem solid gray'
807
685
  : undefined),
808
- } }, footer)))))));
686
+ } }, footer))))));
809
687
  // Determine which portal to use
810
688
  const portalNumber = id.current % NUM_MODAL_PORTALS;
811
689
  // Render in a portal
@@ -976,6 +854,10 @@ const setClientEventMetadataPopulator = (newMetadataPopulator) => {
976
854
  */
977
855
  const logClientEvent = (opts) => __awaiter(void 0, void 0, void 0, function* () {
978
856
  var _a, _b, _c, _d, _e, _f, _g;
857
+ // Don't write to the server if there is none
858
+ if (appHasNoServer()) {
859
+ return;
860
+ }
979
861
  // Populate metadata
980
862
  let metadata = ((_a = opts.metadata) !== null && _a !== void 0 ? _a : {});
981
863
  if (metadataPopulator) {
@@ -1372,7 +1254,7 @@ const showSessionExpiredMessage = () => __awaiter(void 0, void 0, void 0, functi
1372
1254
  /*------------------------------------------------------------------------*/
1373
1255
  /* -------------------------------- Style ------------------------------- */
1374
1256
  /*------------------------------------------------------------------------*/
1375
- const style$8 = `
1257
+ const style$9 = `
1376
1258
  .AppWrapper-leave-to-url-notice {
1377
1259
  opacity: 0;
1378
1260
 
@@ -1586,13 +1468,105 @@ const AppWrapper = (props) => {
1586
1468
  /* --------------- Main UI -------------- */
1587
1469
  /*----------------------------------------*/
1588
1470
  return (React__default.createElement(React__default.Fragment, null,
1589
- React__default.createElement("style", null, style$8),
1471
+ React__default.createElement("style", null, style$9),
1590
1472
  React__default.createElement("style", null, tooltipStyle),
1591
1473
  modal,
1592
1474
  customModalPortals,
1593
1475
  body));
1594
1476
  };
1595
1477
 
1478
+ /**
1479
+ * Loading spinner/indicator
1480
+ * @author Gabe Abrams
1481
+ */
1482
+ /*------------------------------------------------------------------------*/
1483
+ /* -------------------------------- Style ------------------------------- */
1484
+ /*------------------------------------------------------------------------*/
1485
+ const style$8 = `
1486
+ /* Container fades in */
1487
+ .LoadingSpinner-container {
1488
+ animation-name: LoadingSpinner-container-fade-in;
1489
+ animation-duration: 0.3s;
1490
+ animation-delay: 0.4s;
1491
+ animation-fill-mode: both;
1492
+ animation-timing-function: ease-out;
1493
+ }
1494
+ @keyframes LoadingSpinner-container-fade-in {
1495
+ 0% {
1496
+ opacity: 0;
1497
+ }
1498
+ 100% {
1499
+ opacity: 1;
1500
+ }
1501
+ }
1502
+
1503
+ /* Blips */
1504
+ .LoadingSpinner-blip-1,
1505
+ .LoadingSpinner-blip-2,
1506
+ .LoadingSpinner-blip-3,
1507
+ .LoadingSpinner-blip-4 {
1508
+ font-size: 1.8rem;
1509
+ opacity: 0.6;
1510
+ margin-top: 1rem;
1511
+ margin-bottom: 1rem;
1512
+ }
1513
+
1514
+ /* First Blip */
1515
+ .LoadingSpinner-blip-1 {
1516
+ animation: LoadingSpinner-pop-animation 2s infinite;
1517
+ }
1518
+
1519
+ /* Second Blip */
1520
+ .LoadingSpinner-blip-2 {
1521
+ animation: LoadingSpinner-pop-animation 2s infinite;
1522
+ -webkit-animation-delay: 0.1s;
1523
+ animation-delay: 0.1s;
1524
+ }
1525
+
1526
+ /* Third Blip */
1527
+ .LoadingSpinner-blip-3 {
1528
+ animation: LoadingSpinner-pop-animation 2s infinite;
1529
+ animation-delay: 0.2s;
1530
+ }
1531
+
1532
+ /* Fourth Blip */
1533
+ .LoadingSpinner-blip-4 {
1534
+ animation: LoadingSpinner-pop-animation 2s infinite;
1535
+ animation-delay: 0.3s;
1536
+ }
1537
+
1538
+ /* Pop Animation for Each Blip */
1539
+ @keyframes LoadingSpinner-pop-animation {
1540
+ 0% {
1541
+ transform: scale(1.0);
1542
+ }
1543
+ 10% {
1544
+ transform: scale(1.5);
1545
+ }
1546
+ 30% {
1547
+ transform: scale(1.0);
1548
+ }
1549
+ 100% {
1550
+ transform: scale(1.0);
1551
+ }
1552
+ }
1553
+ `;
1554
+ /*------------------------------------------------------------------------*/
1555
+ /* ------------------------------ Component ----------------------------- */
1556
+ /*------------------------------------------------------------------------*/
1557
+ const LoadingSpinner = () => {
1558
+ /*------------------------------------------------------------------------*/
1559
+ /* ------------------------------- Render ------------------------------- */
1560
+ /*------------------------------------------------------------------------*/
1561
+ // Add all four blips to a container
1562
+ return (React__default.createElement("div", { className: "text-center LoadingSpinner LoadingSpinner-container" },
1563
+ React__default.createElement("style", null, style$8),
1564
+ React__default.createElement(FontAwesomeIcon, { icon: faCircle, className: "LoadingSpinner-blip-1 me-1" }),
1565
+ React__default.createElement(FontAwesomeIcon, { icon: faCircle, className: "LoadingSpinner-blip-2 me-1" }),
1566
+ React__default.createElement(FontAwesomeIcon, { icon: faCircle, className: "LoadingSpinner-blip-3 me-1" }),
1567
+ React__default.createElement(FontAwesomeIcon, { icon: faCircle, className: "LoadingSpinner-blip-4" })));
1568
+ };
1569
+
1596
1570
  /**
1597
1571
  * A box with a tab on the top that holds buttons and other content
1598
1572
  * @author Gabe Abrams
@@ -1624,9 +1598,7 @@ const style$7 = `
1624
1598
 
1625
1599
  /* Container for Title */
1626
1600
  .TabBox-title-container {
1627
- display: flex;
1628
- justify-content: space-between;
1629
- align-items: center;
1601
+ /* Place on Left */
1630
1602
  position: relative;
1631
1603
  left: 0;
1632
1604
  text-align: left;
@@ -1659,19 +1631,6 @@ const style$7 = `
1659
1631
  background: #fdfdfd;
1660
1632
  }
1661
1633
 
1662
- .TabBox-title-right-container {
1663
- display: flex;
1664
- flex-direction: row;
1665
- align-items: bottom;
1666
- height: 2.4rem;
1667
- overflow: visible;
1668
- }
1669
-
1670
- .TabBox-title-right-contents {
1671
- margin-right: 0.5rem;
1672
- margin-bottom: 0.2rem;
1673
- }
1674
-
1675
1634
  /* Make the TabBox's Children Appear Above Title if Overlap Occurs */
1676
1635
  .TabBox-children {
1677
1636
  position: relative;
@@ -1685,16 +1644,19 @@ const TabBox = (props) => {
1685
1644
  /*------------------------------------------------------------------------*/
1686
1645
  /* -------------------------------- Setup ------------------------------- */
1687
1646
  /*------------------------------------------------------------------------*/
1688
- const { title, children, topRightChildren, noBottomPadding, noBottomMargin, } = props;
1647
+ /* -------------- Props ------------- */
1648
+ const { title, children, noBottomPadding, noBottomMargin, } = props;
1689
1649
  /*------------------------------------------------------------------------*/
1690
1650
  /* ------------------------------- Render ------------------------------- */
1691
1651
  /*------------------------------------------------------------------------*/
1652
+ /*----------------------------------------*/
1653
+ /* --------------- Main UI -------------- */
1654
+ /*----------------------------------------*/
1655
+ // Full UI
1692
1656
  return (React__default.createElement("div", { className: `TabBox-container ${noBottomMargin ? '' : 'mb-2'}` },
1693
1657
  React__default.createElement("style", null, style$7),
1694
1658
  React__default.createElement("div", { className: "TabBox-title-container" },
1695
- React__default.createElement("div", { className: "TabBox-title" }, title),
1696
- topRightChildren && (React__default.createElement("div", { className: "TabBox-title-right-container" },
1697
- React__default.createElement("div", { className: "TabBox-title-right-contents" }, topRightChildren)))),
1659
+ React__default.createElement("div", { className: "TabBox-title" }, title)),
1698
1660
  React__default.createElement("div", { className: `TabBox-box ps-2 pt-2 pe-2 ${noBottomPadding ? '' : 'pb-2'}` },
1699
1661
  React__default.createElement("div", { className: "TabBox-children" }, children))));
1700
1662
  };
@@ -2681,7 +2643,7 @@ const NestableItemList = (props) => {
2681
2643
  React__default.createElement(FontAwesomeIcon, { icon: childExpanded[item.id] ? faChevronDown : faChevronRight })))),
2682
2644
  React__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) => {
2683
2645
  onChanged(changeChecked(item.id, checked, items));
2684
- }, ariaLabel: `Select ${item.name}`, checkedVariant: Variant$1.Light, uncheckedVariant: Variant$1.Light }),
2646
+ }, ariaLabel: `Select ${item.name}`, checkedVariant: Variant$1.Light }),
2685
2647
  (item.isGroup && childExpanded[item.id]) && (React__default.createElement("div", { className: "NestableItemList-children-container", style: {
2686
2648
  paddingLeft: '2.2rem',
2687
2649
  } },
@@ -2704,62 +2666,17 @@ const ItemPicker = (props) => {
2704
2666
  /*------------------------------------------------------------------------*/
2705
2667
  /* -------------- Props ------------- */
2706
2668
  // Destructure all props
2707
- const { title, items, onChanged, noBottomMargin, hideSelectAllOrNoneButtons, } = props;
2669
+ const { title, items, onChanged, noBottomMargin, } = props;
2708
2670
  /*------------------------------------------------------------------------*/
2709
2671
  /* ------------------------- Component Functions ------------------------ */
2710
2672
  /*------------------------------------------------------------------------*/
2711
- /**
2712
- * Updates the checked state of an item (including all children)
2713
- * @author Yuen Ler Chow
2714
- * @param item the item to update
2715
- * @param checked the new checked state
2716
- * @returns the updated item
2717
- */
2718
- const updateItemChecked = (item, checked) => {
2719
- if (item.isGroup) {
2720
- return Object.assign(Object.assign({}, item), { children: item.children.map((child) => {
2721
- return updateItemChecked(child, checked);
2722
- }) });
2723
- }
2724
- return Object.assign(Object.assign({}, item), { checked });
2725
- };
2726
- /**
2727
- * Selects all items in the list
2728
- * @author Yuen Ler Chow
2729
- */
2730
- const handleSelectAll = () => {
2731
- const updatedItems = items.map((item) => {
2732
- return updateItemChecked(item, true);
2733
- });
2734
- onChanged(updatedItems);
2735
- };
2736
- /**
2737
- * Deselects all items in the list
2738
- * @author Yuen Ler Chow
2739
- */
2740
- const handleDeselectAll = () => {
2741
- const updatedItems = items.map((item) => {
2742
- return updateItemChecked(item, false);
2743
- });
2744
- onChanged(updatedItems);
2745
- };
2746
2673
  /*------------------------------------------------------------------------*/
2747
2674
  /* ------------------------------- Render ------------------------------- */
2748
2675
  /*------------------------------------------------------------------------*/
2749
2676
  /*----------------------------------------*/
2750
2677
  /* --------------- Main UI -------------- */
2751
2678
  /*----------------------------------------*/
2752
- // Select all/none
2753
- const selectAllOrNone = (!hideSelectAllOrNoneButtons
2754
- ? (React__default.createElement("div", { className: "d-flex h-100 align-items-end flex-row" },
2755
- React__default.createElement("div", { className: "d-flex justify-content-end" },
2756
- React__default.createElement("div", { className: "me-2", style: { fontSize: '1.2rem' } }, "Select"),
2757
- React__default.createElement("div", { className: "btn-group", role: "group" },
2758
- React__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"),
2759
- React__default.createElement("button", { type: "button", "aria-label": "Deselect all contexts", className: "btn btn-secondary py-0", onClick: handleDeselectAll }, "None")))))
2760
- : undefined);
2761
- // Main UI
2762
- return (React__default.createElement(TabBox, { title: title, noBottomMargin: noBottomMargin, topRightChildren: selectAllOrNone },
2679
+ return (React__default.createElement(TabBox, { title: title, noBottomMargin: noBottomMargin },
2763
2680
  React__default.createElement("div", { style: { overflowX: 'auto' } },
2764
2681
  React__default.createElement(NestableItemList, { items: items, onChanged: onChanged }))));
2765
2682
  };
@@ -2874,16 +2791,16 @@ var LogAction$1 = LogAction;
2874
2791
  */
2875
2792
  var ParamType;
2876
2793
  (function (ParamType) {
2877
- ParamType["Boolean"] = "boolean";
2878
- ParamType["BooleanOptional"] = "boolean-optional";
2879
- ParamType["Float"] = "float";
2880
- ParamType["FloatOptional"] = "float-optional";
2881
- ParamType["Int"] = "int";
2882
- ParamType["IntOptional"] = "int-optional";
2883
- ParamType["JSON"] = "json";
2884
- ParamType["JSONOptional"] = "json-optional";
2885
- ParamType["String"] = "string";
2886
- ParamType["StringOptional"] = "string-optional";
2794
+ ParamType["Boolean"] = "Boolean";
2795
+ ParamType["BooleanOptional"] = "BooleanOptional";
2796
+ ParamType["Float"] = "Float";
2797
+ ParamType["FloatOptional"] = "FloatOptional";
2798
+ ParamType["Int"] = "Int";
2799
+ ParamType["IntOptional"] = "IntOptional";
2800
+ ParamType["JSON"] = "JSON";
2801
+ ParamType["JSONOptional"] = "JSONOptional";
2802
+ ParamType["String"] = "String";
2803
+ ParamType["StringOptional"] = "StringOptional";
2887
2804
  })(ParamType || (ParamType = {}));
2888
2805
  var ParamType$1 = ParamType;
2889
2806
 
@@ -3131,7 +3048,7 @@ const IntelliTable = (props) => {
3131
3048
  param: column.param,
3132
3049
  visible: checked,
3133
3050
  });
3134
- }, checked: columnVisibilityMap[column.param], ariaLabel: `show "${column.title}" column in the ${title} table`, checkedVariant: Variant$1.Light, uncheckedVariant: Variant$1.Light }));
3051
+ }, checked: columnVisibilityMap[column.param], ariaLabel: `show "${column.title}" column in the ${title} table`, checkedVariant: Variant$1.Light, uncheckedVariant: Variant$1.Secondary }));
3135
3052
  }),
3136
3053
  React__default.createElement("div", { className: "mt-3" }, "Or you can:"),
3137
3054
  React__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: () => {
@@ -14093,1295 +14010,23 @@ const padZerosLeft = (num, numDigits) => {
14093
14010
  };
14094
14011
 
14095
14012
  /**
14096
- * Route for checking the status of the current user's
14097
- * access to log review
14013
+ * Stub a server endpoint response
14098
14014
  * @author Gabe Abrams
14015
+ * @param opts object containing all arguments
14016
+ * @param [opts.method=GET] http method to stub
14017
+ * @param opts.path full pathname to stub
14018
+ * @param opts.body body of response if stubbing a successful response
14019
+ * @param opts.errorMessage message of error if stubbing a failed response
14020
+ * @param [opts.errorCode] error code if stubbing a failed response
14099
14021
  */
14100
- const LOG_REVIEW_STATUS_ROUTE = `${ROUTE_PATH_PREFIX}/logs/access_allowed`;
14022
+ const stubServerEndpoint = _setStubResponse;
14101
14023
 
14102
- // Stored copy of caccl functions
14103
- let _cacclGetLaunchInfo;
14104
- // Stored copy of dce-mango log collection
14105
- let _logCollection;
14106
- /*------------------------------------------------------------------------*/
14107
- /* Helpers */
14108
- /*------------------------------------------------------------------------*/
14109
- /**
14110
- * Get launch info via CACCL
14111
- * @author Gabe Abrams
14112
- * @param req express request object
14113
- * @returns object { launched, launchInfo }
14114
- */
14115
- const cacclGetLaunchInfo = (req) => {
14116
- if (!_cacclGetLaunchInfo) {
14117
- throw new ErrorWithCode('Could not get launch info because server was not initialized with dce-reactkit\'s initServer function', ReactKitErrorCode$1.NoCACCLGetLaunchInfoFunction);
14118
- }
14119
- return _cacclGetLaunchInfo(req);
14120
- };
14121
14024
  /**
14122
- * Get log collection
14025
+ * Start a dynamic wait, call the function once the operation has completed and
14026
+ * the dynamic wait will continue waiting for the rest of the minimum time
14123
14027
  * @author Gabe Abrams
14124
- * @returns log collection if one was included during launch or null if we don't
14125
- * have a log collection (yet)
14126
- */
14127
- const internalGetLogCollection = () => {
14128
- return _logCollection !== null && _logCollection !== void 0 ? _logCollection : null;
14129
- };
14130
- /*------------------------------------------------------------------------*/
14131
- /* Main */
14132
- /*------------------------------------------------------------------------*/
14133
- /**
14134
- * Prepare dce-reactkit to run on the server
14135
- * @author Gabe Abrams
14136
- * @param opts object containing all arguments
14137
- * @param opts.app express app from inside of the postprocessor function that
14138
- * we will add routes to
14139
- * @param opts.getLaunchInfo CACCL LTI's get launch info function
14140
- * @param [opts.logCollection] mongo collection from dce-mango to use for
14141
- * storing logs. If none is included, logs are written to the console
14142
- * @param [opts.logReviewAdmins=all] info on which admins can review
14143
- * logs from the client. If not included, all Canvas admins are allowed to
14144
- * review logs. If null, no Canvas admins are allowed to review logs.
14145
- * If an array of Canvas userIds (numbers), only Canvas admins with those
14146
- * userIds are allowed to review logs. If a dce-mango collection, only
14147
- * Canvas admins with entries in that collection ({ userId, ...}) are allowed
14148
- * to review logs
14149
- */
14150
- const initServer = (opts) => {
14151
- _cacclGetLaunchInfo = opts.getLaunchInfo;
14152
- _logCollection = opts.logCollection;
14153
- /*----------------------------------------*/
14154
- /* Logging */
14155
- /*----------------------------------------*/
14156
- /**
14157
- * Log an event
14158
- * @author Gabe Abrams
14159
- * @param {string} context Context of the event (each app determines how to
14160
- * organize its contexts)
14161
- * @param {string} subcontext Subcontext of the event (each app determines
14162
- * how to organize its subcontexts)
14163
- * @param {string} tags stringified list of tags that apply to this action
14164
- * (each app determines tag usage)
14165
- * @param {string} metadata stringified object containing optional custom metadata
14166
- * @param {string} level log level
14167
- * @param {string} [errorMessage] error message if type is an error
14168
- * @param {string} [errorCode] error code if type is an error
14169
- * @param {string} [errorStack] error stack if type is an error
14170
- * @param {string} [target] Target of the action (each app determines the list
14171
- * of targets) These are usually buttons, panels, elements, etc.
14172
- * @param {LogAction} [action] the type of action performed on the target
14173
- * @returns {Log}
14174
- */
14175
- opts.app.post(LOG_ROUTE_PATH, genRouteHandler({
14176
- paramTypes: {
14177
- context: ParamType$1.String,
14178
- subcontext: ParamType$1.String,
14179
- tags: ParamType$1.JSON,
14180
- level: ParamType$1.String,
14181
- metadata: ParamType$1.JSON,
14182
- errorMessage: ParamType$1.StringOptional,
14183
- errorCode: ParamType$1.StringOptional,
14184
- errorStack: ParamType$1.StringOptional,
14185
- target: ParamType$1.StringOptional,
14186
- action: ParamType$1.StringOptional,
14187
- },
14188
- handler: ({ params, logServerEvent }) => {
14189
- // Create log info
14190
- const logInfo = ((params.errorMessage || params.errorCode || params.errorStack)
14191
- // Error
14192
- ? {
14193
- context: params.context,
14194
- subcontext: params.subcontext,
14195
- tags: params.tags,
14196
- level: params.level,
14197
- metadata: params.metadata,
14198
- error: {
14199
- message: params.errorMessage,
14200
- code: params.errorCode,
14201
- stack: params.errorStack,
14202
- },
14203
- }
14204
- // Action
14205
- : {
14206
- context: params.context,
14207
- subcontext: params.subcontext,
14208
- tags: params.tags,
14209
- level: params.level,
14210
- metadata: params.metadata,
14211
- target: params.target,
14212
- action: params.action,
14213
- });
14214
- // Add hidden boolean to change source to "client"
14215
- const logInfoForcedFromClient = Object.assign(Object.assign({}, logInfo), { overrideAsClientEvent: true });
14216
- // Write the log
14217
- const log = logServerEvent(logInfoForcedFromClient);
14218
- // Return
14219
- return log;
14220
- },
14221
- }));
14222
- /*----------------------------------------*/
14223
- /* Log Reviewer */
14224
- /*----------------------------------------*/
14225
- /**
14226
- * Check if a given user is allowed to review logs
14227
- * @author Gabe Abrams
14228
- * @param userId the id of the user
14229
- * @param isAdmin if true, the user is an admin
14230
- * @returns true if the user can review logs
14231
- */
14232
- const canReviewLogs = (userId, isAdmin) => __awaiter(void 0, void 0, void 0, function* () {
14233
- // Immediately deny access if user is not an admin
14234
- if (!isAdmin) {
14235
- return false;
14236
- }
14237
- // If all admins are allowed, we're done
14238
- if (!opts.logReviewAdmins) {
14239
- return true;
14240
- }
14241
- // Do a dynamic check
14242
- try {
14243
- // Array of userIds
14244
- if (Array.isArray(opts.logReviewAdmins)) {
14245
- return opts.logReviewAdmins.some((allowedId) => {
14246
- return (userId === allowedId);
14247
- });
14248
- }
14249
- // Must be a collection
14250
- const matches = yield opts.logReviewAdmins.find({ userId });
14251
- // Make sure at least one entry matches
14252
- return matches.length > 0;
14253
- }
14254
- catch (err) {
14255
- // If an error occurred, simply return false
14256
- return false;
14257
- }
14258
- });
14259
- /**
14260
- * Check if the current user has access to logs
14261
- * @author Gabe Abrams
14262
- * @returns {boolean} true if user has access
14263
- */
14264
- opts.app.get(LOG_REVIEW_STATUS_ROUTE, genRouteHandler({
14265
- handler: ({ params }) => __awaiter(void 0, void 0, void 0, function* () {
14266
- const { userId, isAdmin } = params;
14267
- const canReview = yield canReviewLogs(userId, isAdmin);
14268
- return canReview;
14269
- }),
14270
- }));
14271
- /**
14272
- * Get all logs for a certain month
14273
- * @author Gabe Abrams
14274
- * @param {number} year the year to query (e.g. 2022)
14275
- * @param {number} month the month to query (e.g. 1 = January)
14276
- * @returns {Log[]} list of logs from the given month
14277
- */
14278
- opts.app.get(`${LOG_REVIEW_ROUTE_PATH_PREFIX}/years/:year/months/:month`, genRouteHandler({
14279
- paramTypes: {
14280
- year: ParamType$1.Int,
14281
- month: ParamType$1.Int,
14282
- pageNumber: ParamType$1.Int,
14283
- },
14284
- handler: ({ params }) => __awaiter(void 0, void 0, void 0, function* () {
14285
- // Get user info
14286
- const { year, month, pageNumber, userId, isAdmin, } = params;
14287
- // Validate user
14288
- const canReview = yield canReviewLogs(userId, isAdmin);
14289
- if (!canReview) {
14290
- throw new ErrorWithCode('You cannot access this resource because you do not have the appropriate permissions.', ReactKitErrorCode$1.NotAllowedToReviewLogs);
14291
- }
14292
- // Query for logs
14293
- const response = yield _logCollection.findPaged({
14294
- query: {
14295
- year,
14296
- month,
14297
- },
14298
- perPage: 1000,
14299
- pageNumber,
14300
- });
14301
- // Return response
14302
- return response;
14303
- }),
14304
- }));
14305
- };
14306
-
14307
- // Import shared types
14308
- /**
14309
- * Handle an error and respond to the client
14310
- * @author Gabe Abrams
14311
- * @param res express response
14312
- * @param error error info
14313
- * @param opts.err the error to send to the client
14314
- * or the error message
14315
- * @param [opts.code] an error code (only used if err.code is not
14316
- * included)
14317
- * @param [opts.status=500] the https status code to use
14318
- * defined)
14319
- */
14320
- const handleError = (res, error) => {
14321
- // Get the error message
14322
- let message;
14323
- if (error && error.message) {
14324
- message = (error.message || 'An unknown error occurred.');
14325
- }
14326
- else if (typeof error === 'string') {
14327
- message = (error.trim().length > 0
14328
- ? error
14329
- : 'An unknown error occurred.');
14330
- }
14331
- else {
14332
- message = 'An unknown error occurred.';
14333
- }
14334
- // Get the error code
14335
- const code = (error.code || ReactKitErrorCode$1.NoCode);
14336
- // Get the status code
14337
- const status = (error.status || 500);
14338
- // Respond to user
14339
- res
14340
- // Set the http status code
14341
- .status(status)
14342
- // Send a JSON response
14343
- .json({
14344
- // Error message
14345
- message,
14346
- // Error code
14347
- code,
14348
- // Success = false flag so client can detect server-side errors
14349
- success: false,
14350
- });
14351
- return undefined;
14352
- };
14353
-
14354
- /**
14355
- * Send successful API response
14356
- * @author Gabe Abrams
14357
- * @param res express response
14358
- * @param body the body of the response to send to the client
14359
- */
14360
- const handleSuccess = (res, body) => {
14361
- // Send a http 200 json response
14362
- res.json({
14363
- // Include the body as a parameter
14364
- body,
14365
- // Success = true flag so client can detect successful responses
14366
- success: true,
14367
- });
14368
- return undefined;
14369
- };
14370
-
14371
- // Import shared types
14372
- /**
14373
- * Generate a static error page
14374
- * @author Gabe Abrams
14375
- * @param opts object containing all arguments
14376
- * @param [opts.title=An Error Occurred] title of the error box
14377
- * @param [opts.description=An unknown server error occurred. Please contact support.]
14378
- * a human-readable description of the error
14379
- * @param [opts.code=ReactKitErrorCode.NoCode] error code to show
14380
- * @param [opts.pageTitle=opts.title] title of the page/tab if it differs from
14381
- * the title of the error
14382
- * @returns html of the page
14383
- */
14384
- const genErrorPage = (opts = {}) => {
14385
- var _a, _b, _c, _d;
14386
- const title = ((_a = opts.title) !== null && _a !== void 0 ? _a : 'An Error Occurred');
14387
- const pageTitle = ((_b = opts.pageTitle) !== null && _b !== void 0 ? _b : title);
14388
- const description = ((_c = opts.description) !== null && _c !== void 0 ? _c : 'An unknown server error occurred. Please contact support.');
14389
- const code = ((_d = opts.code) !== null && _d !== void 0 ? _d : ReactKitErrorCode$1.NoCode);
14390
- return `
14391
- <head>
14392
- <!-- Metadata -->
14393
- <meta
14394
- name="viewport"
14395
- content="width=device-width, height=device-height, initial-scale=1.0, minimum-scale=1.0"
14396
- >
14397
-
14398
- <!-- Title -->
14399
- <title>${pageTitle}</title>
14400
-
14401
- <!-- Bootstrap -->
14402
- <link
14403
- rel="stylesheet"
14404
- href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.2.1/css/bootstrap.min.css"
14405
- integrity="sha512-siwe/oXMhSjGCwLn+scraPOWrJxHlUgMBMZXdPe2Tnk3I0x3ESCoLz7WZ5NTH6SZrywMY+PB1cjyqJ5jAluCOg=="
14406
- crossorigin="anonymous"
14407
- referrerpolicy="no-referrer"
14408
- />
14409
- <script
14410
- src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.2.1/js/bootstrap.min.js"
14411
- integrity="sha512-vyRAVI0IEm6LI/fVSv/Wq/d0KUfrg3hJq2Qz5FlfER69sf3ZHlOrsLriNm49FxnpUGmhx+TaJKwJ+ByTLKT+Yg=="
14412
- crossorigin="anonymous"
14413
- referrerpolicy="no-referrer"
14414
- ></script>
14415
-
14416
- <!-- FontAwesome -->
14417
- <link
14418
- rel="stylesheet"
14419
- href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.0/css/all.min.css"
14420
- integrity="sha512-xh6O/CkQoPOWDdYTDqeRdPCVd1SpvCA9XXcUnZS2FmJNp1coAFzvtCN9BmamE+4aHK8yyUHUSCcJHgXloTyT2A=="
14421
- crossorigin="anonymous"
14422
- referrerpolicy="no-referrer"
14423
- />
14424
-
14425
- <!-- Style -->
14426
- <style>
14427
- .DCEReactKit-pop-in {
14428
- animation-name: DCEReactKit-pop-in;
14429
- animation-duration: 0.5s;
14430
- animation-iteration-count: 1;
14431
- animation-timing-function: ease-out;
14432
- animation-fill-mode: both;
14433
-
14434
- transform-origin: center;
14435
- }
14436
-
14437
- @keyframes DCEReactKit-pop-in {
14438
- 0% {
14439
- opacity: 0;
14440
- transform: scale(0.9);
14441
- }
14442
- 100% {
14443
- opacity: 1;
14444
- transform: scale(1);
14445
- }
14446
- }
14447
-
14448
- .DCEReactKit-slide-in {
14449
- animation-name: DCEReactKit-slide-in;
14450
- animation-duration: 1s;
14451
- animation-iteration-count: 1;
14452
- animation-timing-function: ease-out;
14453
- animation-fill-mode: both;
14454
- animation-delay: 0.2s;
14455
- }
14456
-
14457
- @keyframes DCEReactKit-slide-in {
14458
- 0% {
14459
- opacity: 0;
14460
- transform: translate(0, 0.3em);
14461
- }
14462
- 100% {
14463
- opacity: 1;
14464
- transform: translate(0, 0);
14465
- }
14466
- }
14467
- </style>
14468
- </head>
14469
-
14470
- <!-- Body -->
14471
- <body class="bg-dark text-center pt-3 ps-3 pe-3">
14472
- <!-- Alert -->
14473
- <div
14474
- class="DCEReactKit-pop-in alert alert-warning d-inline-block"
14475
- style="width: 50em; max-width: 100%"
14476
- >
14477
- <!-- Title -->
14478
- <h2>
14479
- <i class="me-1 fa-solid fa-triangle-exclamation"></i>
14480
- ${title}
14481
- </h2>
14482
- <!-- Description -->
14483
- <div>
14484
- ${description}
14485
- </div>
14486
- </div>
14487
-
14488
- <!-- Error Code -->
14489
- <div class="DCEReactKit-slide-in text-light">
14490
- <strong>
14491
- Error Code:
14492
- </strong>
14493
- ${code}
14494
- </div>
14495
- </body>
14496
- `;
14497
- };
14498
-
14499
- /**
14500
- * Generate a static info page
14501
- * @author Gabe Abrams
14502
- * @param opts object containing all arguments
14503
- * @param opts.title title of the info box
14504
- * @param opts.body a human-readable text body for the info alert
14505
- * @returns the HTML for the info page
14506
- */
14507
- const genInfoPage = (opts) => {
14508
- const { title, body, } = opts;
14509
- return `
14510
- <head>
14511
- <!-- Metadata -->
14512
- <meta
14513
- name="viewport"
14514
- content="width=device-width, height=device-height, initial-scale=1.0, minimum-scale=1.0"
14515
- >
14516
-
14517
- <!-- Title -->
14518
- <title>${title}</title>
14519
-
14520
- <!-- Bootstrap -->
14521
- <link
14522
- rel="stylesheet"
14523
- href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.2.1/css/bootstrap.min.css"
14524
- integrity="sha512-siwe/oXMhSjGCwLn+scraPOWrJxHlUgMBMZXdPe2Tnk3I0x3ESCoLz7WZ5NTH6SZrywMY+PB1cjyqJ5jAluCOg=="
14525
- crossorigin="anonymous"
14526
- referrerpolicy="no-referrer"
14527
- />
14528
- <script
14529
- src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.2.1/js/bootstrap.min.js"
14530
- integrity="sha512-vyRAVI0IEm6LI/fVSv/Wq/d0KUfrg3hJq2Qz5FlfER69sf3ZHlOrsLriNm49FxnpUGmhx+TaJKwJ+ByTLKT+Yg=="
14531
- crossorigin="anonymous"
14532
- referrerpolicy="no-referrer"
14533
- ></script>
14534
-
14535
- <!-- FontAwesome -->
14536
- <link
14537
- rel="stylesheet"
14538
- href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.0/css/all.min.css"
14539
- integrity="sha512-xh6O/CkQoPOWDdYTDqeRdPCVd1SpvCA9XXcUnZS2FmJNp1coAFzvtCN9BmamE+4aHK8yyUHUSCcJHgXloTyT2A=="
14540
- crossorigin="anonymous"
14541
- referrerpolicy="no-referrer"
14542
- />
14543
-
14544
- <!-- Style -->
14545
- <style>
14546
- .DCEReactKit-pop-in {
14547
- animation-name: DCEReactKit-pop-in;
14548
- animation-duration: 0.5s;
14549
- animation-iteration-count: 1;
14550
- animation-timing-function: ease-out;
14551
- animation-fill-mode: both;
14552
-
14553
- transform-origin: center;
14554
- }
14555
-
14556
- @keyframes DCEReactKit-pop-in {
14557
- 0% {
14558
- opacity: 0;
14559
- transform: scale(0.9);
14560
- }
14561
- 100% {
14562
- opacity: 1;
14563
- transform: scale(1);
14564
- }
14565
- }
14566
- </style>
14567
- </head>
14568
-
14569
- <!-- Body -->
14570
- <body class="bg-dark text-center pt-3 ps-3 pe-3">
14571
- <!-- Alert -->
14572
- <div
14573
- class="DCEReactKit-pop-in alert alert-info d-inline-block"
14574
- style="width: 50em; max-width: 100%"
14575
- >
14576
- <!-- Title -->
14577
- <h2>
14578
- <i class="me-1 fa-solid fa-circle-info"></i>
14579
- ${title}
14580
- </h2>
14581
- <!-- Body -->
14582
- <div>
14583
- ${body}
14584
- </div>
14585
- </div>
14586
- </body>
14587
- `;
14588
- };
14589
-
14590
- /**
14591
- * Perform a rudimentary parsing of the user's browser agent string
14592
- * @author Gabe Abrams
14593
- * @param userAgent the user's browser agent
14594
- * @returns user info
14595
- */
14596
- const parseUserAgent = (userAgent) => {
14597
- /* ------------- Browser ------------ */
14598
- let browser = {
14599
- name: 'Unknown',
14600
- version: 'Unknown',
14601
- };
14602
- // Parse user agent
14603
- let verOffset;
14604
- let nameOffset;
14605
- if ((verOffset = userAgent.indexOf('Opera')) !== -1) {
14606
- // In Opera, the true version is after 'Opera' or after 'Version'
14607
- browser = {
14608
- name: 'Opera',
14609
- version: userAgent.substring(verOffset + 6),
14610
- };
14611
- if ((verOffset = userAgent.indexOf('Version')) !== -1) {
14612
- browser.version = userAgent.substring(verOffset + 8);
14613
- }
14614
- }
14615
- else if ((verOffset = userAgent.indexOf('MSIE')) !== -1) {
14616
- // In MSIE, the true version is after 'MSIE' in userAgent
14617
- browser = {
14618
- name: 'Internet Explorer',
14619
- version: userAgent.substring(verOffset + 5),
14620
- };
14621
- }
14622
- else if ((verOffset = userAgent.indexOf('Chrome')) !== -1) {
14623
- // In Chrome, the true version is after 'Chrome'
14624
- browser = {
14625
- name: 'Chrome',
14626
- version: userAgent.substring(verOffset + 7),
14627
- };
14628
- }
14629
- else if ((verOffset = userAgent.indexOf('Safari')) !== -1) {
14630
- // In Safari, the true version is after 'Safari' or after 'Version'
14631
- browser = {
14632
- name: 'Safari',
14633
- version: userAgent.substring(verOffset + 7),
14634
- };
14635
- if ((verOffset = userAgent.indexOf('Version')) !== -1) {
14636
- browser.version = userAgent.substring(verOffset + 8);
14637
- }
14638
- }
14639
- else if ((verOffset = userAgent.indexOf('Firefox')) != -1) {
14640
- // In Firefox, the true version is after 'Firefox'
14641
- browser = {
14642
- name: 'Firefox',
14643
- version: userAgent.substring(verOffset + 8),
14644
- };
14645
- }
14646
- else if ((nameOffset = userAgent.lastIndexOf(' ') + 1)
14647
- < (verOffset = userAgent.lastIndexOf('/'))) {
14648
- browser = {
14649
- name: userAgent.substring(nameOffset, verOffset),
14650
- version: userAgent.substring(verOffset + 1),
14651
- };
14652
- }
14653
- // Postprocess version
14654
- // trim the fullVersion string at semicolon/space if present
14655
- let ix;
14656
- if ((ix = browser.version.indexOf(';')) !== -1) {
14657
- browser.version = browser.version.substring(0, ix);
14658
- }
14659
- if ((ix = browser.version.indexOf(' ')) !== -1) {
14660
- browser.version = browser.version.substring(0, ix);
14661
- }
14662
- /* ------------- Device ------------- */
14663
- // Detect os
14664
- let os = 'Unknown';
14665
- if (userAgent.includes('Linux')) {
14666
- os = 'Linux';
14667
- }
14668
- else if (userAgent.includes('like Mac')) {
14669
- os = 'iOS';
14670
- }
14671
- else if (userAgent.includes('Mac')) {
14672
- os = 'Mac';
14673
- }
14674
- else if (userAgent.includes('Android')) {
14675
- os = 'Android';
14676
- }
14677
- else if (userAgent.includes('Win')) {
14678
- os = 'Win';
14679
- }
14680
- // Check if mobile
14681
- const isMobile = !!userAgent.match(/(iPhone|iPod|iPad|Android|BlackBerry)/);
14682
- // Device
14683
- const device = {
14684
- isMobile,
14685
- os,
14686
- };
14687
- /* ------------- Finish ------------- */
14688
- // Return info
14689
- return {
14690
- browser,
14691
- device,
14692
- };
14693
- };
14694
-
14695
- /**
14696
- * Generate an express API route handler
14697
- * @author Gabe Abrams
14698
- * @param opts object containing all arguments
14699
- * @param opts.paramTypes map containing the types for each parameter that is
14700
- * included in the request (map: param name => type)
14701
- * @param opts.handler function that processes the request
14702
- * @param [opts.skipSessionCheck] if true, skip the session check (allow users
14703
- * to not be logged in and launched via LTI)
14704
- * @param [opts.allowedHosts] if included, only allow requests from these hosts
14705
- * (start a hostname with a "*" to only check the end of the hostname)
14706
- * you can include just one string instead of an array
14707
- * @param [opts.bannedHosts] if included, do not allow requests from these hosts
14708
- * (start a hostname with a "*" to only check the end of the hostname)
14709
- * you can include just one string instead of an array
14710
- * @param [opts.unhandledErrorMessagePrefix] if included, when an error that
14711
- * is not of type ErrorWithCode is thrown, the client will receive an error
14712
- * where the error message is prefixed with this string. For example,
14713
- * if unhandledErrorMessagePrefix is
14714
- * 'While saving progress, we encountered an error:'
14715
- * and the error is 'progressInfo is not an object',
14716
- * the client will receive an error with the message
14717
- * 'While saving progress, we encountered an error: progressInfo is not an object'
14718
- * @returns express route handler that takes the following arguments:
14719
- * params (map: param name => value),
14720
- * req (express request object),
14721
- * next (express next function),
14722
- * send (a function that sends a string to the client),
14723
- * redirect (takes a url and redirects the user to that url),
14724
- * renderErrorPage (shows a static error page to the user),
14725
- * renderInfoPage (shows a static info page to the user),
14726
- * renderCustomHTML (renders custom html and sends it to the user),
14727
- * and returns the value to send to the client as a JSON API response, or
14728
- * calls next() or redirect(...) or send(...) or renderErrorPage(...).
14729
- * Note: params also has userId, userFirstName,
14730
- * userLastName, userEmail, userAvatarURL, isLearner, isTTM, isAdmin,
14731
- * and any other variables that
14732
- * are directly added to the session, if the user does have a session.
14733
- */
14734
- const genRouteHandler = (opts) => {
14735
- // Return a route handler
14736
- return (req, res, next) => __awaiter(void 0, void 0, void 0, function* () {
14737
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
14738
- // Output params
14739
- const output = {};
14740
- /*----------------------------------------*/
14741
- /* ----------- Hostname Check ----------- */
14742
- /*----------------------------------------*/
14743
- // Get hostnames
14744
- const originURL = String(req.get('origin')
14745
- || req.headers.origin
14746
- || req.headers.referer);
14747
- const originHostname = (originURL
14748
- // Remove protocol
14749
- .replace(/(^\w+:|^)\/\//, '')
14750
- // Remove port
14751
- .replace(/:\d+$/, ''));
14752
- const serverHostname = String(req.hostname);
14753
- // Check allowed
14754
- if (opts.allowedHosts) {
14755
- // Only accept requests from allowed hosts
14756
- const allowedArray = (Array.isArray(opts.allowedHosts)
14757
- ? opts.allowedHosts
14758
- : [opts.allowedHosts]);
14759
- // Check if server is localhost
14760
- if (serverHostname === 'localhost') {
14761
- // Allow localhost
14762
- allowedArray.push('localhost');
14763
- }
14764
- // Check if current host is allowed
14765
- const allowed = allowedArray.some((allowedHost) => {
14766
- if (allowedHost.startsWith('*')) {
14767
- // Check end of hostname
14768
- return originHostname.endsWith(allowedHost.substring(1));
14769
- }
14770
- // Check full hostname
14771
- return originHostname.toLowerCase() === allowedHost.toLowerCase();
14772
- });
14773
- // If not allowed, return error
14774
- if (!allowed) {
14775
- return handleError(res, {
14776
- message: 'You are not allowed to access this endpoint.',
14777
- code: ReactKitErrorCode$1.HostNotAllowed,
14778
- status: 403,
14779
- });
14780
- }
14781
- }
14782
- // Check banned
14783
- if (opts.bannedHosts) {
14784
- // Do not allow requests from banned hosts
14785
- const bannedArray = (Array.isArray(opts.bannedHosts)
14786
- ? opts.bannedHosts
14787
- : [opts.bannedHosts]);
14788
- // Check if current host is banned
14789
- const banned = bannedArray.some((bannedHost) => {
14790
- if (bannedHost.startsWith('*')) {
14791
- // Check end of hostname
14792
- return originHostname.endsWith(bannedHost.substring(1));
14793
- }
14794
- // Check full hostname
14795
- return originHostname.toLowerCase() === bannedHost.toLowerCase();
14796
- });
14797
- // If banned, return error
14798
- if (banned) {
14799
- return handleError(res, {
14800
- message: 'You are not allowed to access this endpoint.',
14801
- code: ReactKitErrorCode$1.HostBanned,
14802
- status: 403,
14803
- });
14804
- }
14805
- }
14806
- /*----------------------------------------*/
14807
- /* ------------ Parse Params ------------ */
14808
- /*----------------------------------------*/
14809
- // Process items one by one
14810
- const paramList = Object.entries((_a = opts.paramTypes) !== null && _a !== void 0 ? _a : {});
14811
- for (let i = 0; i < paramList.length; i++) {
14812
- const [name, type] = paramList[i];
14813
- // Find the value as a string
14814
- const value = (req.params[name]
14815
- || req.query[name]
14816
- || req.body[name]);
14817
- // Parse
14818
- if (type === ParamType$1.Boolean || type === ParamType$1.BooleanOptional) {
14819
- // Boolean
14820
- // Handle case where value doesn't exist
14821
- if (value === undefined) {
14822
- if (type === ParamType$1.BooleanOptional) {
14823
- output[name] = undefined;
14824
- }
14825
- else {
14826
- return handleError(res, {
14827
- message: `Parameter ${name} is required, but it was not included.`,
14828
- code: ReactKitErrorCode$1.MissingParameter,
14829
- status: 422,
14830
- });
14831
- }
14832
- }
14833
- else {
14834
- // Value exists
14835
- // Simplify value
14836
- const simpleVal = (String(value)
14837
- .trim()
14838
- .toLowerCase());
14839
- // Parse
14840
- output[name] = ([
14841
- 'true',
14842
- 'yes',
14843
- 'y',
14844
- '1',
14845
- 't',
14846
- ].indexOf(simpleVal) >= 0);
14847
- }
14848
- }
14849
- else if (type === ParamType$1.Float || type === ParamType$1.FloatOptional) {
14850
- // Float
14851
- // Handle case where value doesn't exist
14852
- if (value === undefined) {
14853
- if (type === ParamType$1.FloatOptional) {
14854
- output[name] = undefined;
14855
- }
14856
- else {
14857
- return handleError(res, {
14858
- message: `Parameter ${name} is required, but it was not included.`,
14859
- code: ReactKitErrorCode$1.MissingParameter,
14860
- status: 422,
14861
- });
14862
- }
14863
- }
14864
- else if (!Number.isNaN(Number.parseFloat(String(value)))) {
14865
- // Value is a number
14866
- output[name] = Number.parseFloat(String(value));
14867
- }
14868
- else {
14869
- // Issue!
14870
- return handleError(res, {
14871
- message: `Request data was malformed: ${name} was not a valid float.`,
14872
- code: ReactKitErrorCode$1.InvalidParameter,
14873
- status: 422,
14874
- });
14875
- }
14876
- }
14877
- else if (type === ParamType$1.Int || type === ParamType$1.IntOptional) {
14878
- // Int
14879
- // Handle case where value doesn't exist
14880
- if (value === undefined) {
14881
- if (type === ParamType$1.IntOptional) {
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.parseInt(String(value), 10))) {
14893
- // Value is a number
14894
- output[name] = Number.parseInt(String(value), 10);
14895
- }
14896
- else {
14897
- // Issue!
14898
- return handleError(res, {
14899
- message: `Request data was malformed: ${name} was not a valid int.`,
14900
- code: ReactKitErrorCode$1.InvalidParameter,
14901
- status: 422,
14902
- });
14903
- }
14904
- }
14905
- else if (type === ParamType$1.JSON || type === ParamType$1.JSONOptional) {
14906
- // Stringified JSON
14907
- // Handle case where value doesn't exist
14908
- if (value === undefined) {
14909
- if (type === ParamType$1.JSONOptional) {
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 {
14921
- // Value exists
14922
- // Parse
14923
- try {
14924
- output[name] = JSON.parse(String(value));
14925
- }
14926
- catch (err) {
14927
- return handleError(res, {
14928
- message: `Request data was malformed: ${name} was not a valid JSON payload.`,
14929
- code: ReactKitErrorCode$1.InvalidParameter,
14930
- status: 422,
14931
- });
14932
- }
14933
- }
14934
- }
14935
- else if (type === ParamType$1.String || type === ParamType$1.StringOptional) {
14936
- // String
14937
- // Handle case where value doesn't exist
14938
- if (value === undefined) {
14939
- if (type === ParamType$1.StringOptional) {
14940
- output[name] = undefined;
14941
- }
14942
- else {
14943
- return handleError(res, {
14944
- message: `Parameter ${name} is required, but it was not included.`,
14945
- code: ReactKitErrorCode$1.MissingParameter,
14946
- status: 422,
14947
- });
14948
- }
14949
- }
14950
- else {
14951
- // Value exists
14952
- // Leave as is
14953
- output[name] = value;
14954
- }
14955
- }
14956
- else {
14957
- // No valid data type
14958
- return handleError(res, {
14959
- message: `An internal error occurred: we could not determine the type of ${name}.`,
14960
- code: ReactKitErrorCode$1.InvalidParameter,
14961
- status: 422,
14962
- });
14963
- }
14964
- }
14965
- /*----------------------------------------*/
14966
- /* ------------- Launch Info ------------ */
14967
- /*----------------------------------------*/
14968
- // Get launch info
14969
- const { launched, launchInfo } = cacclGetLaunchInfo(req);
14970
- if (
14971
- // Not launched
14972
- (!launched || !launchInfo)
14973
- // Not skipping the session check
14974
- && !opts.skipSessionCheck) {
14975
- return handleError(res, {
14976
- message: 'Your session has expired. Please refresh the page and try again.',
14977
- code: ReactKitErrorCode$1.SessionExpired,
14978
- status: 401,
14979
- });
14980
- }
14981
- // Error if user info cannot be found
14982
- if (
14983
- // User information is incomplete
14984
- (!launchInfo
14985
- || !launchInfo.userId
14986
- || !launchInfo.userFirstName
14987
- || !launchInfo.userLastName
14988
- || (launchInfo.notInCourse
14989
- && !launchInfo.isAdmin)
14990
- || (!launchInfo.isTTM
14991
- && !launchInfo.isLearner
14992
- && !launchInfo.isAdmin))
14993
- // Not skipping the session check
14994
- && !opts.skipSessionCheck) {
14995
- return handleError(res, {
14996
- message: 'Your session was invalid. Please refresh the page and try again.',
14997
- code: ReactKitErrorCode$1.SessionExpired,
14998
- status: 401,
14999
- });
15000
- }
15001
- // Add launch info to output
15002
- output.userId = (launchInfo
15003
- ? launchInfo.userId
15004
- : ((_b = output.userId) !== null && _b !== void 0 ? _b : undefined));
15005
- output.userFirstName = (launchInfo
15006
- ? launchInfo.userFirstName
15007
- : ((_c = output.userFirstName) !== null && _c !== void 0 ? _c : undefined));
15008
- output.userLastName = (launchInfo
15009
- ? launchInfo.userLastName
15010
- : ((_d = output.userLastName) !== null && _d !== void 0 ? _d : undefined));
15011
- output.userEmail = (launchInfo
15012
- ? launchInfo.userEmail
15013
- : ((_e = output.userEmail) !== null && _e !== void 0 ? _e : undefined));
15014
- output.userAvatarURL = (launchInfo
15015
- ? ((_f = launchInfo.userImage) !== null && _f !== void 0 ? _f : 'http://www.gravatar.com/avatar/?d=identicon')
15016
- : ((_g = output.userAvatarURL) !== null && _g !== void 0 ? _g : undefined));
15017
- output.isLearner = (launchInfo
15018
- ? !!launchInfo.isLearner
15019
- : ((_h = output.isLearner) !== null && _h !== void 0 ? _h : undefined));
15020
- output.isTTM = (launchInfo
15021
- ? !!launchInfo.isTTM
15022
- : ((_j = output.isTTM) !== null && _j !== void 0 ? _j : undefined));
15023
- output.isAdmin = (launchInfo
15024
- ? !!launchInfo.isAdmin
15025
- : ((_k = output.isAdmin) !== null && _k !== void 0 ? _k : undefined));
15026
- output.courseId = (launchInfo
15027
- ? ((_l = output.courseId) !== null && _l !== void 0 ? _l : launchInfo.courseId)
15028
- : ((_m = output.courseId) !== null && _m !== void 0 ? _m : undefined));
15029
- output.courseName = (launchInfo
15030
- ? launchInfo.contextLabel
15031
- : ((_o = output.courseName) !== null && _o !== void 0 ? _o : undefined));
15032
- // Add other session variables
15033
- Object.keys(req.session).forEach((propName) => {
15034
- // Skip if prop already in output
15035
- if (output[propName] !== undefined) {
15036
- return;
15037
- }
15038
- // Add to output
15039
- const value = req.session[propName];
15040
- if (typeof value === 'string'
15041
- || typeof value === 'boolean'
15042
- || typeof value === 'number') {
15043
- output[propName] = value;
15044
- }
15045
- });
15046
- /*----------------------------------------*/
15047
- /* ----- Require Course Consistency ----- */
15048
- /*----------------------------------------*/
15049
- // Make sure the user actually launched from the appropriate course
15050
- if (output.courseId
15051
- && launchInfo
15052
- && launchInfo.courseId
15053
- && output.courseId !== launchInfo.courseId
15054
- && !output.isTTM
15055
- && !output.isAdmin) {
15056
- // Course of interest is not the launch course
15057
- return handleError(res, {
15058
- message: 'You switched sessions by opening this app in another tab. Please refresh the page and try again.',
15059
- code: ReactKitErrorCode$1.WrongCourse,
15060
- status: 401,
15061
- });
15062
- }
15063
- /*----------------------------------------*/
15064
- /* Require Proper Permissions */
15065
- /*----------------------------------------*/
15066
- // Add TTM endpoint security
15067
- if (
15068
- // This is a TTM endpoint
15069
- req.path.startsWith('/api/ttm')
15070
- // User is not a TTM
15071
- && (
15072
- // User is not a TTM
15073
- !output.isTTM
15074
- // User is not an admin
15075
- && !output.isAdmin)) {
15076
- // User does not have access
15077
- return handleError(res, {
15078
- 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.',
15079
- code: ReactKitErrorCode$1.NotTTM,
15080
- status: 401,
15081
- });
15082
- }
15083
- // Add Admin endpoint security
15084
- if (
15085
- // This is an admin endpoint
15086
- req.path.startsWith('/api/admin')
15087
- // User is not an admin
15088
- && !output.isAdmin) {
15089
- // User does not have access
15090
- return handleError(res, {
15091
- 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.',
15092
- code: ReactKitErrorCode$1.NotAdmin,
15093
- status: 401,
15094
- });
15095
- }
15096
- /*----------------------------------------*/
15097
- /* ------------- Log Handler ------------ */
15098
- /*----------------------------------------*/
15099
- // Create a log handler function
15100
- /**
15101
- * Log an event on the server
15102
- * @author Gabe Abrams
15103
- */
15104
- const logServerEvent = (logOpts) => __awaiter(void 0, void 0, void 0, function* () {
15105
- var _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1;
15106
- // NOTE: internally, we slip through an opts.overrideAsClientEvent boolean
15107
- // that indicates that this is actually a client event, but we don't
15108
- // include that in the LogFunction type because this is internal and
15109
- // hidden from users
15110
- try {
15111
- // Parse user agent
15112
- const { browser, device, } = parseUserAgent(req.headers['user-agent']);
15113
- // Get time info in ET
15114
- const { timestamp, year, month, day, hour, minute, } = getTimeInfoInET();
15115
- // Main log info
15116
- const mainLogInfo = {
15117
- id: `${launchInfo ? launchInfo.userId : 'unknown'}-${Date.now()}-${Math.floor(Math.random() * 100000)}-${Math.floor(Math.random() * 100000)}`,
15118
- userFirstName: (launchInfo ? launchInfo.userFirstName : 'unknown'),
15119
- userLastName: (launchInfo ? launchInfo.userLastName : 'unknown'),
15120
- userEmail: (launchInfo ? launchInfo.userEmail : 'unknown'),
15121
- userId: (launchInfo ? launchInfo.userId : 'unknown'),
15122
- isLearner: (launchInfo && !!launchInfo.isLearner),
15123
- isAdmin: (launchInfo && !!launchInfo.isAdmin),
15124
- isTTM: (launchInfo && !!launchInfo.isTTM),
15125
- courseId: (launchInfo ? launchInfo.courseId : 'unknown'),
15126
- courseName: (launchInfo ? launchInfo.contextLabel : 'unknown'),
15127
- browser,
15128
- device,
15129
- year,
15130
- month,
15131
- day,
15132
- hour,
15133
- minute,
15134
- timestamp,
15135
- context: (typeof logOpts.context === 'string'
15136
- ? logOpts.context
15137
- : ((_q = ((_p = logOpts.context) !== null && _p !== void 0 ? _p : {})._) !== null && _q !== void 0 ? _q : LogBuiltInMetadata.Context.Uncategorized)),
15138
- subcontext: ((_r = logOpts.subcontext) !== null && _r !== void 0 ? _r : LogBuiltInMetadata.Context.Uncategorized),
15139
- tags: ((_s = logOpts.tags) !== null && _s !== void 0 ? _s : []),
15140
- level: ((_t = logOpts.level) !== null && _t !== void 0 ? _t : LogLevel$1.Info),
15141
- metadata: ((_u = logOpts.metadata) !== null && _u !== void 0 ? _u : {}),
15142
- };
15143
- // Type-specific info
15144
- const typeSpecificInfo = (('error' in opts && opts.error)
15145
- ? {
15146
- type: LogType$1.Error,
15147
- errorMessage: (_v = logOpts.error.message) !== null && _v !== void 0 ? _v : 'Unknown message',
15148
- errorCode: (_w = logOpts.error.code) !== null && _w !== void 0 ? _w : ReactKitErrorCode$1.NoCode,
15149
- errorStack: (_x = logOpts.error.stack) !== null && _x !== void 0 ? _x : 'No stack',
15150
- }
15151
- : {
15152
- type: LogType$1.Action,
15153
- target: ((_y = logOpts.target) !== null && _y !== void 0 ? _y : LogBuiltInMetadata.Target.NoTarget),
15154
- action: ((_z = logOpts.action) !== null && _z !== void 0 ? _z : LogAction$1.Unknown),
15155
- });
15156
- // Source-specific info
15157
- const sourceSpecificInfo = (logOpts.overrideAsClientEvent
15158
- ? {
15159
- source: LogSource$1.Client,
15160
- }
15161
- : {
15162
- source: LogSource$1.Server,
15163
- routePath: req.path,
15164
- routeTemplate: req.route.path,
15165
- });
15166
- // Build log event
15167
- const log = Object.assign(Object.assign(Object.assign({}, mainLogInfo), typeSpecificInfo), sourceSpecificInfo);
15168
- // Either print to console or save to db
15169
- const logCollection = internalGetLogCollection();
15170
- if (logCollection) {
15171
- // Store to the log collection
15172
- yield logCollection.insert(log);
15173
- }
15174
- else if (log.type === LogType$1.Error) {
15175
- // Print to console
15176
- // eslint-disable-next-line no-console
15177
- console.error('dce-reactkit error log:', log);
15178
- }
15179
- else {
15180
- // eslint-disable-next-line no-console
15181
- console.log('dce-reactkit action log:', log);
15182
- }
15183
- // Return log entry
15184
- return log;
15185
- }
15186
- catch (err) {
15187
- // Print because we cannot store the error
15188
- // eslint-disable-next-line no-console
15189
- 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);
15190
- // Create a dummy log to return
15191
- const dummyMainInfo = {
15192
- id: '-1',
15193
- userFirstName: 'Unknown',
15194
- userLastName: 'Unknown',
15195
- userEmail: 'unknown@harvard.edu',
15196
- userId: 1,
15197
- isLearner: false,
15198
- isAdmin: false,
15199
- isTTM: false,
15200
- courseId: 1,
15201
- courseName: 'Unknown',
15202
- browser: {
15203
- name: 'Unknown',
15204
- version: 'Unknown',
15205
- },
15206
- device: {
15207
- isMobile: false,
15208
- os: 'Unknown',
15209
- },
15210
- year: 1,
15211
- month: 1,
15212
- day: 1,
15213
- hour: 1,
15214
- minute: 1,
15215
- timestamp: Date.now(),
15216
- tags: [],
15217
- level: LogLevel$1.Warn,
15218
- metadata: {},
15219
- context: LogBuiltInMetadata.Context.Uncategorized,
15220
- subcontext: LogBuiltInMetadata.Context.Uncategorized,
15221
- };
15222
- const dummyTypeSpecificInfo = {
15223
- type: LogType$1.Error,
15224
- errorMessage: 'Unknown',
15225
- errorCode: 'Unknown',
15226
- errorStack: 'No Stack',
15227
- };
15228
- const dummySourceSpecificInfo = {
15229
- source: LogSource$1.Server,
15230
- routePath: req.path,
15231
- routeTemplate: req.route.path,
15232
- };
15233
- const log = Object.assign(Object.assign(Object.assign({}, dummyMainInfo), dummyTypeSpecificInfo), dummySourceSpecificInfo);
15234
- return log;
15235
- }
15236
- });
15237
- /*------------------------------------------------------------------------*/
15238
- /* Call handler */
15239
- /*------------------------------------------------------------------------*/
15240
- // Keep track of whether a response was already sent
15241
- let responseSent = false;
15242
- /**
15243
- * Redirect the user to another path or url
15244
- * @author Gabe Abrams
15245
- * @param pathOrURL the path or url to redirect to
15246
- */
15247
- const redirect = (pathOrURL) => {
15248
- responseSent = true;
15249
- res.redirect(pathOrURL);
15250
- };
15251
- /**
15252
- * Send text to the client (with an optional status code)
15253
- * @author Gabe Abrams
15254
- * @param text the text to send to the client
15255
- * @parm [status=200] the http status code to send
15256
- */
15257
- const send = (text, status = 200) => {
15258
- responseSent = true;
15259
- res.status(status).send(text);
15260
- };
15261
- /**
15262
- * Render an error page
15263
- * @author Gabe Abrams
15264
- * @param renderOpts object containing all arguments
15265
- * @param [renderOpts.title=An Error Occurred] title of the error box
15266
- * @param [renderOpts.description=An unknown server error occurred. Please contact support.]
15267
- * a human-readable description of the error
15268
- * @param [renderOpts.code=ReactKitErrorCode.NoCode] error code to show
15269
- * @param [renderOpts.pageTitle=renderOpts.title] title of the page/tab if it differs from
15270
- * the title of the error
15271
- * @param [renderOpts.status=500] http status code
15272
- */
15273
- const renderErrorPage = (renderOpts = {}) => {
15274
- var _a, _b, _c;
15275
- const html = genErrorPage(renderOpts);
15276
- send(html, (_a = renderOpts.status) !== null && _a !== void 0 ? _a : 500);
15277
- // Log server-side error if not a session expired error or 404
15278
- if (renderOpts.status && renderOpts.status === 404) {
15279
- return;
15280
- }
15281
- if ((_b = renderOpts.title) === null || _b === void 0 ? void 0 : _b.toLowerCase().includes('session expired')) {
15282
- return;
15283
- }
15284
- logServerEvent({
15285
- context: LogBuiltInMetadata.Context.ServerRenderedErrorPage,
15286
- error: {
15287
- message: `${renderOpts.title}: ${renderOpts.description}`,
15288
- code: renderOpts.code,
15289
- },
15290
- metadata: {
15291
- title: renderOpts.title,
15292
- description: renderOpts.description,
15293
- code: renderOpts.code,
15294
- pageTitle: renderOpts.pageTitle,
15295
- status: (_c = renderOpts.status) !== null && _c !== void 0 ? _c : 500,
15296
- },
15297
- });
15298
- };
15299
- /**
15300
- * Render an info page
15301
- * @author Gabe Abrams
15302
- * @param renderOpts object containing all arguments
15303
- * @param renderOpts.title title of the info box
15304
- * @param renderOpts.body a human-readable text body for the info alert
15305
- */
15306
- const renderInfoPage = (renderOpts) => {
15307
- const html = genInfoPage(renderOpts);
15308
- send(html, 200);
15309
- };
15310
- /**
15311
- * Render custom HTML
15312
- * @author Gabe Abrams
15313
- * @param htmlOpts object containing all arguments
15314
- * @param htmlOpts.html the HTML to send to the client
15315
- * @param [ejsOpts.status=200] the http status code to send
15316
- */
15317
- const renderCustomHTML = (htmlOpts) => {
15318
- var _a;
15319
- send(htmlOpts.html, (_a = htmlOpts.status) !== null && _a !== void 0 ? _a : 200);
15320
- };
15321
- // Call the handler
15322
- try {
15323
- const results = yield opts.handler({
15324
- params: output,
15325
- req,
15326
- send,
15327
- next: () => {
15328
- responseSent = true;
15329
- next();
15330
- },
15331
- redirect,
15332
- renderErrorPage,
15333
- renderInfoPage,
15334
- renderCustomHTML,
15335
- logServerEvent,
15336
- });
15337
- // Send results to client (only if next wasn't called)
15338
- if (!responseSent) {
15339
- return handleSuccess(res, results !== null && results !== void 0 ? results : undefined);
15340
- }
15341
- }
15342
- catch (err) {
15343
- // Prefix error message if needed
15344
- if (opts.unhandledErrorMessagePrefix
15345
- && err instanceof Error
15346
- && err.message
15347
- && err.name !== 'ErrorWithCode') {
15348
- err.message = `${opts.unhandledErrorMessagePrefix.trim()} ${err.message.trim()}`;
15349
- }
15350
- // Send error to client (only if next wasn't called)
15351
- if (!responseSent) {
15352
- handleError(res, err);
15353
- // Log server-side error
15354
- logServerEvent({
15355
- context: LogBuiltInMetadata.Context.ServerEndpointError,
15356
- error: err,
15357
- });
15358
- return;
15359
- }
15360
- // Log error that was not responded with
15361
- // eslint-disable-next-line no-console
15362
- console.log('Error occurred but could not be sent to client because a response was already sent:', err);
15363
- }
15364
- });
15365
- };
15366
-
15367
- /**
15368
- * Stub a server endpoint response
15369
- * @author Gabe Abrams
15370
- * @param opts object containing all arguments
15371
- * @param [opts.method=GET] http method to stub
15372
- * @param opts.path full pathname to stub
15373
- * @param opts.body body of response if stubbing a successful response
15374
- * @param opts.errorMessage message of error if stubbing a failed response
15375
- * @param [opts.errorCode] error code if stubbing a failed response
15376
- */
15377
- const stubServerEndpoint = _setStubResponse;
15378
-
15379
- /**
15380
- * Start a dynamic wait, call the function once the operation has completed and
15381
- * the dynamic wait will continue waiting for the rest of the minimum time
15382
- * @author Gabe Abrams
15383
- * @param minWaitMs the minimum number of ms to wait
15384
- * @returns async function to call to finish the wait
14028
+ * @param minWaitMs the minimum number of ms to wait
14029
+ * @returns async function to call to finish the wait
15385
14030
  */
15386
14031
  const startMinWait = (minWaitMs) => {
15387
14032
  const startTimestamp = Date.now();
@@ -15517,27 +14162,11 @@ const parallelLimit = (taskFunctions, limit) => __awaiter(void 0, void 0, void 0
15517
14162
  });
15518
14163
 
15519
14164
  /**
15520
- * Initialize a log collection given the dce-mango Collection class
14165
+ * Route for checking the status of the current user's
14166
+ * access to log review
15521
14167
  * @author Gabe Abrams
15522
- * @param Collection the Collection class from dce-mango
15523
- * @returns initialized logCollection
15524
14168
  */
15525
- const initLogCollection = (Collection) => {
15526
- return new Collection('Log', {
15527
- uniqueIndexKey: 'id',
15528
- indexKeys: [
15529
- 'courseId',
15530
- 'context',
15531
- 'subcontext',
15532
- 'tags',
15533
- 'year',
15534
- 'month',
15535
- 'day',
15536
- 'hour',
15537
- 'type',
15538
- ],
15539
- });
15540
- };
14169
+ const LOG_REVIEW_STATUS_ROUTE = `${ROUTE_PATH_PREFIX}/logs/access_allowed`;
15541
14170
 
15542
14171
  /*------------------------------------------------------------------------*/
15543
14172
  /* -------------------------------- Cache ------------------------------- */
@@ -15679,66 +14308,6 @@ const getLocalTimeInfo = (dateOrTimestamp) => {
15679
14308
  };
15680
14309
  };
15681
14310
 
15682
- /**
15683
- * Add all routes for the DBEditor
15684
- * @author Yuen Ler Chow
15685
- * @param opts object containing all arguments
15686
- * @param opts.app express app to add routes too
15687
- * @param opts.collectionName the name of the collection
15688
- * @param opts.adminsOnly true if the endpoint is for admins only
15689
- * @param opts.collection dce-mango db collection
15690
- */
15691
- const addDBEditorEndpoints = (opts) => {
15692
- const { app, collectionName, adminsOnly, collection, } = opts;
15693
- // Generate the endpoint path
15694
- const endpointPath = generateEndpointPath(collectionName, adminsOnly);
15695
- /**
15696
- * List all items in the collection
15697
- * @author Yuen Ler Chow
15698
- * @returns {any[]} the list of items in the collection
15699
- */
15700
- app.get(endpointPath, genRouteHandler({
15701
- paramTypes: {
15702
- filterQuery: ParamType$1.JSONOptional,
15703
- },
15704
- handler: ({ params }) => __awaiter(void 0, void 0, void 0, function* () {
15705
- var _a;
15706
- const filterQuery = (_a = params.filterQuery) !== null && _a !== void 0 ? _a : {};
15707
- const categories = yield collection.find(filterQuery);
15708
- return categories;
15709
- }),
15710
- }));
15711
- /**
15712
- * Create a new item in the collection
15713
- * @author Yuen Ler Chow
15714
- * @param {any} item the item to create
15715
- */
15716
- app.post(endpointPath, genRouteHandler({
15717
- paramTypes: {
15718
- item: ParamType$1.JSON,
15719
- },
15720
- handler: ({ params, }) => __awaiter(void 0, void 0, void 0, function* () {
15721
- // Destructure params
15722
- const { item, } = params;
15723
- yield collection.insert(item);
15724
- }),
15725
- }));
15726
- /**
15727
- * Remove an item from the collection by id
15728
- * @author Yuen Ler Chow
15729
- */
15730
- app.delete(`${endpointPath}/:id`, genRouteHandler({
15731
- paramTypes: {
15732
- id: ParamType$1.String,
15733
- },
15734
- handler: ({ params, }) => __awaiter(void 0, void 0, void 0, function* () {
15735
- // Destructure params
15736
- const { id, } = params;
15737
- yield collection.delete({ id });
15738
- }),
15739
- }));
15740
- };
15741
-
15742
14311
  /**
15743
14312
  * Given an array of strings, create a single comma-separated string that includes
15744
14313
  * 'and' as well as an oxford comma.
@@ -16280,203 +14849,6 @@ const shuffleArray = (arr) => {
16280
14849
  return newArr;
16281
14850
  };
16282
14851
 
16283
- /**
16284
- * Sends and retries an http request
16285
- * @author Gabriel Abrams
16286
- * @param opts object containing all arguments
16287
- * @param opts.path path to send request to
16288
- * @param [opts.host] host to send request to
16289
- * @param [opts.method=GET] http method to use
16290
- * @param [opts.params] body/data to include in the request
16291
- * @param [opts.headers] headers to include in the request
16292
- * @param [opts.sendCrossDomainCredentials=true if in development mode] if true,
16293
- * send cross-domain credentials even if not in dev mode
16294
- * @param [opts.responseType=JSON] expected response type
16295
- * @returns { body, status, headers } on success
16296
- */
16297
- const sendServerToServerRequest = (opts) => __awaiter(void 0, void 0, void 0, function* () {
16298
- var _a;
16299
- // Process method
16300
- const method = (opts.method || 'GET');
16301
- // Encode objects within params
16302
- let params;
16303
- if (opts.params) {
16304
- params = {};
16305
- Object.entries(opts.params).forEach(([key, val]) => {
16306
- if (typeof val === 'object' && !Array.isArray(val)) {
16307
- params[key] = JSON.stringify(val);
16308
- }
16309
- else {
16310
- params[key] = val;
16311
- }
16312
- });
16313
- }
16314
- // Stringify parameters
16315
- const stringifiedParams = qs.stringify(params || {}, {
16316
- encodeValuesOnly: true,
16317
- arrayFormat: 'brackets',
16318
- });
16319
- // Create url (include query if GET)
16320
- const query = (method === 'GET' ? `?${stringifiedParams}` : '');
16321
- let url;
16322
- if (!opts.host) {
16323
- // No host included at all. Just send to a path
16324
- url = `${opts.path}${query}`;
16325
- }
16326
- else {
16327
- url = `https://${opts.host}${opts.path}${query}`;
16328
- }
16329
- // Update headers
16330
- const headers = opts.headers || {};
16331
- let data = null;
16332
- if (!headers['Content-Type']) {
16333
- // Form encoded
16334
- headers['Content-Type'] = 'application/x-www-form-urlencoded';
16335
- // Add data if applicable
16336
- data = (method !== 'GET' ? stringifiedParams : null);
16337
- }
16338
- else {
16339
- // JSON encode
16340
- data = params;
16341
- }
16342
- // Encode data
16343
- let encodedData;
16344
- if (data) {
16345
- if (headers['Content-Type'] === 'application/x-www-form-urlencoded') {
16346
- encodedData = new URLSearchParams(params);
16347
- }
16348
- else {
16349
- encodedData = JSON.stringify(data);
16350
- }
16351
- }
16352
- // Send request
16353
- try {
16354
- const response = yield fetch(url, {
16355
- method,
16356
- mode: 'cors',
16357
- headers: headers !== null && headers !== void 0 ? headers : {},
16358
- body: ((method !== 'GET' && encodedData)
16359
- ? encodedData
16360
- : undefined),
16361
- redirect: 'follow',
16362
- });
16363
- // Get headers map
16364
- const responseHeaders = {};
16365
- response.headers.forEach((value, key) => {
16366
- responseHeaders[key] = value;
16367
- });
16368
- // Process response based on responseType
16369
- try {
16370
- // Parse response
16371
- let responseBody;
16372
- if (opts.responseType
16373
- && opts.responseType === 'Text') {
16374
- // Response type is text
16375
- responseBody = yield response.text();
16376
- }
16377
- else {
16378
- // Response type is JSON
16379
- responseBody = yield response.json();
16380
- }
16381
- // Return response
16382
- return {
16383
- body: responseBody,
16384
- status: response.status,
16385
- headers: responseHeaders,
16386
- };
16387
- }
16388
- catch (err) {
16389
- throw new ErrorWithCode(`Failed to parse response as ${opts.responseType}: ${err === null || err === void 0 ? void 0 : err.message}`, ReactKitErrorCode$1.ResponseParseError);
16390
- }
16391
- }
16392
- catch (err) {
16393
- // Self-signed certificate error:
16394
- if ((_a = err === null || err === void 0 ? void 0 : err.message) === null || _a === void 0 ? void 0 : _a.includes('self signed certificate')) {
16395
- throw new ErrorWithCode('We refused to send a request because the receiver has self-signed certificates.', ReactKitErrorCode$1.SelfSigned);
16396
- }
16397
- // No tries left
16398
- 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);
16399
- }
16400
- });
16401
-
16402
- /**
16403
- * Send a server-to-server request from this sever to another server that uses
16404
- * dce-reactkit [for server only]
16405
- * @author Gabe Abrams
16406
- * @param opts object containing all arguments
16407
- * @param opts.host - the host of the other server
16408
- * @param opts.path - the path of the other server's endpoint
16409
- * @param [opts.method=GET] - the method of the endpoint
16410
- * @param [opts.params] - query/body parameters to include
16411
- * @param [opts.headers] - headers to include
16412
- * @returns response from server
16413
- */
16414
- const visitEndpointOnAnotherServer = (opts) => __awaiter(void 0, void 0, void 0, function* () {
16415
- var _a;
16416
- // Remove properties with undefined values
16417
- let params;
16418
- if (opts.params) {
16419
- params = Object.fromEntries(Object
16420
- .entries(opts.params)
16421
- .filter(([, value]) => {
16422
- return value !== undefined;
16423
- }));
16424
- }
16425
- // Automatically JSONify arrays and objects
16426
- if (params) {
16427
- params = Object.fromEntries(Object
16428
- .entries(params)
16429
- .map(([key, value]) => {
16430
- if (Array.isArray(value) || typeof value === 'object') {
16431
- return [key, JSON.stringify(value)];
16432
- }
16433
- return [key, value];
16434
- }));
16435
- }
16436
- // Send the request
16437
- const response = yield sendServerToServerRequest({
16438
- host: opts.host,
16439
- path: opts.path,
16440
- method: (_a = opts.method) !== null && _a !== void 0 ? _a : 'GET',
16441
- params,
16442
- });
16443
- // Check for failure
16444
- if (!response || !response.body) {
16445
- throw new ErrorWithCode('We didn\'t get a response from the server. Please check your internet connection.', ReactKitErrorCode$1.NoResponse);
16446
- }
16447
- if (!response.body.success) {
16448
- // Other errors
16449
- throw new ErrorWithCode((response.body.message
16450
- || 'An unknown error occurred. Please contact an admin.'), (response.body.code
16451
- || ReactKitErrorCode$1.NoCode));
16452
- }
16453
- // Success! Extract the body
16454
- const { body } = response.body;
16455
- // Return
16456
- return body;
16457
- });
16458
-
16459
- const punctuationRegex = /[!@#$%^&*(),.?\/;:'"\-\[\]]/g;
16460
- /**
16461
- * Get number of words in string
16462
- * @author Gardenia Liu
16463
- * @author Allison Zhang
16464
- * @author Gabe Abrams
16465
- * @param text the string to check
16466
- * @returns number of words in the string
16467
- */
16468
- const getWordCount = (text) => {
16469
- const trimmedTextWithoutPunctuation = (text
16470
- // Remove leading and trailing whitespace
16471
- .trim()
16472
- // Remove punctuation
16473
- .replace(punctuationRegex, ''));
16474
- if (trimmedTextWithoutPunctuation.length === 0) {
16475
- return 0;
16476
- }
16477
- return trimmedTextWithoutPunctuation.split(/\s+/g).length;
16478
- };
16479
-
16480
14852
  /**
16481
14853
  * Days of the week
16482
14854
  * @author Gabe Abrams
@@ -16493,5 +14865,5 @@ var DayOfWeek;
16493
14865
  })(DayOfWeek || (DayOfWeek = {}));
16494
14866
  var DayOfWeek$1 = DayOfWeek;
16495
14867
 
16496
- export { AppWrapper, AutoscrollToBottomContainer, ButtonInputGroup, CSVDownloadButton, CheckboxButton, CopiableBox, DAY_IN_MS, DBEntryFieldType$1 as DBEntryFieldType, DBEntryManagerPanel, DayOfWeek$1 as DayOfWeek, Drawer, Dropdown, DropdownItemType$1 as DropdownItemType, DynamicWord, ErrorBox, ErrorWithCode, HOUR_IN_MS, IntelliTable, ItemPicker, LoadingSpinner, LogAction$1 as LogAction, LogBuiltInMetadata, LogReviewer, LogSource$1 as LogSource, LogType$1 as LogType, MINUTE_IN_MS, Modal, ModalButtonType$1 as ModalButtonType, ModalSize$1 as ModalSize, ModalType$1 as ModalType, MultiSwitch, ParamType$1 as ParamType, PopFailureMark, PopPendingMark, PopSuccessMark, RadioButton, ReactKitErrorCode$1 as ReactKitErrorCode, SimpleDateChooser, TabBox, ToggleSwitch, Tooltip, Variant$1 as Variant, abbreviate, addDBEditorEndpoints, addFatalErrorHandler, alert, avg, canReviewLogs, capitalize, ceilToNumDecimals, combineClassNames, compareArraysByProp, confirm, everyAsync, extractProp, filterAsync, floorToNumDecimals, forEachAsync, forceNumIntoBounds, genCSV, genCommaList, genRouteHandler, getHumanReadableDate, getLocalTimeInfo, getMonthName, getOrdinal, getPartOfDay, getTimeInfoInET, getWordCount, handleError, handleSuccess, idify, initClient, initLogCollection, initServer, isMobileOrTablet, leaveToURL, logClientEvent, makeLinksClickable, mapAsync, onlyKeepLetters, padDecimalZeros, padZerosLeft, parallelLimit, prefixWithAOrAn, prompt, roundToNumDecimals, setClientEventMetadataPopulator, showFatalError, shuffleArray, someAsync, startMinWait, stringsToHumanReadableList, stubServerEndpoint, sum, useForceRender, validateEmail, validatePhoneNumber, validateString, visitEndpointOnAnotherServer, visitServerEndpoint, waitMs };
14868
+ export { AppWrapper, AutoscrollToBottomContainer, ButtonInputGroup, CSVDownloadButton, CheckboxButton, CopiableBox, DAY_IN_MS, DBEntryFieldType$1 as DBEntryFieldType, DBEntryManagerPanel, DayOfWeek$1 as DayOfWeek, Drawer, Dropdown, DropdownItemType$1 as DropdownItemType, DynamicWord, ErrorBox, ErrorWithCode, HOUR_IN_MS, IntelliTable, ItemPicker, LoadingSpinner, LogAction$1 as LogAction, LogBuiltInMetadata, LogLevel$1 as LogLevel, LogReviewer, LogSource$1 as LogSource, LogType$1 as LogType, MINUTE_IN_MS, Modal, ModalButtonType$1 as ModalButtonType, ModalSize$1 as ModalSize, ModalType$1 as ModalType, MultiSwitch, ParamType$1 as ParamType, PopFailureMark, PopPendingMark, PopSuccessMark, RadioButton, ReactKitErrorCode$1 as ReactKitErrorCode, SimpleDateChooser, TabBox, ToggleSwitch, Tooltip, Variant$1 as Variant, abbreviate, addFatalErrorHandler, alert, avg, canReviewLogs, capitalize, ceilToNumDecimals, combineClassNames, compareArraysByProp, confirm, everyAsync, extractProp, filterAsync, floorToNumDecimals, forEachAsync, forceNumIntoBounds, genCSV, genCommaList, getHumanReadableDate, getLocalTimeInfo, getMonthName, getOrdinal, getPartOfDay, getTimeInfoInET, idify, initClient, isMobileOrTablet, leaveToURL, logClientEvent, makeLinksClickable, mapAsync, onlyKeepLetters, padDecimalZeros, padZerosLeft, parallelLimit, prefixWithAOrAn, prompt, roundToNumDecimals, setClientEventMetadataPopulator, showFatalError, shuffleArray, someAsync, startMinWait, stringsToHumanReadableList, stubServerEndpoint, sum, useForceRender, validateEmail, validatePhoneNumber, validateString, visitServerEndpoint, waitMs };
16497
14869
  //# sourceMappingURL=index.js.map