dce-reactkit 3.2.2-beta.9 → 3.2.4

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 (50) hide show
  1. package/dist/cjs/index.js +742 -435
  2. package/dist/cjs/index.js.map +1 -1
  3. package/dist/cjs/types/components/Drawer.d.ts +1 -0
  4. package/dist/cjs/types/components/IntelliTable.d.ts +1 -0
  5. package/dist/cjs/types/components/ItemPicker/NestableItemList.d.ts +1 -0
  6. package/dist/cjs/types/dynamicConstants/DynamicWord.d.ts +13 -0
  7. package/dist/cjs/types/helpers/isMobileOrTablet.d.ts +7 -0
  8. package/dist/cjs/types/index.d.ts +3 -1
  9. package/dist/cjs/types/types/LogAction.d.ts +16 -16
  10. package/dist/cjs/types/types/LogBuiltInMetadata.d.ts +1 -1
  11. package/dist/cjs/types/types/LogMetadataContextMap.d.ts +11 -0
  12. package/dist/cjs/types/types/LogMetadataTagMap.d.ts +8 -0
  13. package/dist/cjs/types/types/LogMetadataTargetMap.d.ts +8 -0
  14. package/dist/cjs/types/types/LogMetadataType.d.ts +6 -12
  15. package/dist/esm/index.js +742 -437
  16. package/dist/esm/index.js.map +1 -1
  17. package/dist/esm/types/components/Drawer.d.ts +1 -0
  18. package/dist/esm/types/components/IntelliTable.d.ts +1 -0
  19. package/dist/esm/types/components/ItemPicker/NestableItemList.d.ts +1 -0
  20. package/dist/esm/types/dynamicConstants/DynamicWord.d.ts +13 -0
  21. package/dist/esm/types/helpers/isMobileOrTablet.d.ts +7 -0
  22. package/dist/esm/types/index.d.ts +3 -1
  23. package/dist/esm/types/types/LogAction.d.ts +16 -16
  24. package/dist/esm/types/types/LogBuiltInMetadata.d.ts +1 -1
  25. package/dist/esm/types/types/LogMetadataContextMap.d.ts +11 -0
  26. package/dist/esm/types/types/LogMetadataTagMap.d.ts +8 -0
  27. package/dist/esm/types/types/LogMetadataTargetMap.d.ts +8 -0
  28. package/dist/esm/types/types/LogMetadataType.d.ts +6 -12
  29. package/dist/index.d.ts +70 -30
  30. package/package.json +1 -1
  31. package/src/components/ButtonInputGroup.tsx +1 -1
  32. package/src/components/CSVDownloadButton.tsx +2 -2
  33. package/src/components/Drawer.tsx +9 -1
  34. package/src/components/IntelliTable.tsx +178 -23
  35. package/src/components/ItemPicker/NestableItemList.tsx +32 -14
  36. package/src/components/LogReviewer.tsx +584 -424
  37. package/src/components/SimpleDateChooser.tsx +30 -25
  38. package/src/dynamicConstants/DynamicWord.tsx +44 -0
  39. package/src/helpers/genCSV.ts +22 -4
  40. package/src/helpers/genRouteHandler.ts +2 -2
  41. package/src/helpers/isMobileOrTablet.tsx +26 -0
  42. package/src/helpers/logClientEvent.tsx +1 -1
  43. package/src/index.ts +7 -0
  44. package/src/server/initServer.ts +10 -3
  45. package/src/types/LogAction.ts +16 -16
  46. package/src/types/LogBuiltInMetadata.ts +5 -5
  47. package/src/types/LogMetadataContextMap.ts +15 -0
  48. package/src/types/LogMetadataTagMap.ts +9 -0
  49. package/src/types/LogMetadataTargetMap.ts +9 -0
  50. package/src/types/LogMetadataType.ts +8 -15
package/dist/esm/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import React, { useState, useRef, useEffect, useReducer } from 'react';
2
2
  import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
3
- import { faExclamationTriangle, faCircle, faDotCircle, faCheckSquare, faHourglass, faClipboard, faChevronDown, faChevronRight, faCloudDownloadAlt, faCheckCircle, faXmarkCircle, faMinus, faSort, faSortDown, faSortUp, faCalendar, faTag, faHammer, faList, faTimes } from '@fortawesome/free-solid-svg-icons';
3
+ import { faExclamationTriangle, faCircle, faDotCircle, faCheckSquare, faHourglass, faClipboard, faChevronDown, faChevronRight, faCloudDownloadAlt, faMinus, faCheckCircle, faXmarkCircle, faSort, faSortDown, faSortUp, faCalendar, faTag, faHammer, faList, faTimes } from '@fortawesome/free-solid-svg-icons';
4
4
  import { faCircle as faCircle$1, faSquareMinus, faSquare } from '@fortawesome/free-regular-svg-icons';
5
5
 
6
6
  /******************************************************************************
@@ -511,14 +511,14 @@ const LOG_ROUTE_PATH = `${ROUTE_PATH_PREFIX}/log`;
511
511
  const LogBuiltInMetadata = {
512
512
  // Contexts
513
513
  Context: {
514
- Uncategorized: 'n/a',
515
- ServerRenderedErrorPage: '_server-rendered-error-page',
516
- ServerEndpointError: '_server-endpoint-error',
517
- ClientFatalError: '_client-fatal-error',
514
+ Uncategorized: 'Uncategorized',
515
+ ServerRenderedErrorPage: 'ServerRenderedErrorPage',
516
+ ServerEndpointError: 'ServerEndpointError',
517
+ ClientFatalError: 'ClientFatalError',
518
518
  },
519
519
  // Targets
520
520
  Target: {
521
- NoSpecificTarget: 'n/a',
521
+ NoTarget: 'NoTarget',
522
522
  },
523
523
  };
524
524
 
@@ -674,7 +674,7 @@ const logClientEvent = (opts) => __awaiter(void 0, void 0, void 0, function* ()
674
674
  ? opts.error.stack
675
675
  : undefined),
676
676
  target: (opts.action
677
- ? ((_g = opts.target) !== null && _g !== void 0 ? _g : LogBuiltInMetadata.Target.NoSpecificTarget)
677
+ ? ((_g = opts.target) !== null && _g !== void 0 ? _g : LogBuiltInMetadata.Target.NoTarget)
678
678
  : undefined),
679
679
  action: (opts.action
680
680
  ? opts.action
@@ -1194,7 +1194,7 @@ const ButtonInputGroup = (props) => {
1194
1194
  borderTopRightRadius: 0,
1195
1195
  borderBottomRightRadius: 0,
1196
1196
  } }, label),
1197
- React.createElement("span", { className: "input-group-text flex-grow-1 rounded-right", style: {
1197
+ React.createElement("span", { className: "input-group-text flex-grow-1 rounded-right d-flex flex-wrap", style: {
1198
1198
  backgroundColor: 'white',
1199
1199
  borderTopLeftRadius: 0,
1200
1200
  borderBottomLeftRadius: 0,
@@ -1299,18 +1299,6 @@ const getTimeInfoInET = (dateOrTimestamp) => {
1299
1299
  };
1300
1300
  };
1301
1301
 
1302
- /**
1303
- * Force a number to stay within specific bounds
1304
- * @author Gabe Abrams
1305
- * @param num the number to move into the bounds
1306
- * @param min the minimum number in the bound
1307
- * @param max the maximum number in the bound
1308
- * @returns bounded number
1309
- */
1310
- const forceNumIntoBounds = (num, min, max) => {
1311
- return Math.max(min, Math.min(max, num));
1312
- };
1313
-
1314
1302
  /**
1315
1303
  * A very simple, lightweight date chooser
1316
1304
  * @author Gabe Abrams
@@ -1325,7 +1313,7 @@ const SimpleDateChooser = (props) => {
1325
1313
  var _a;
1326
1314
  /* -------------- Props ------------- */
1327
1315
  const { ariaLabel, name, month, day, year, onChange, chooseFromPast, } = props;
1328
- let numMonthsToShow = forceNumIntoBounds(((_a = props.numMonthsToShow) !== null && _a !== void 0 ? _a : 6), 1, 12);
1316
+ const numMonthsToShow = ((_a = props.numMonthsToShow) !== null && _a !== void 0 ? _a : 6);
1329
1317
  /*------------------------------------------------------------------------*/
1330
1318
  /* Render */
1331
1319
  /*------------------------------------------------------------------------*/
@@ -1339,7 +1327,7 @@ const SimpleDateChooser = (props) => {
1339
1327
  let startMonth = today.month;
1340
1328
  if (chooseFromPast) {
1341
1329
  startMonth -= Math.max(0, numMonthsToShow - 1);
1342
- if (startMonth <= 0) {
1330
+ while (startMonth <= 0) {
1343
1331
  startMonth += 12;
1344
1332
  startYear -= 1;
1345
1333
  }
@@ -1347,22 +1335,35 @@ const SimpleDateChooser = (props) => {
1347
1335
  for (let i = 0; i < numMonthsToShow; i++) {
1348
1336
  // Get month and year info
1349
1337
  const unmoddedMonth = (startMonth + i);
1350
- const month = (unmoddedMonth > 12
1351
- ? unmoddedMonth - 12
1352
- : unmoddedMonth);
1338
+ let month = unmoddedMonth;
1339
+ while (month > 12) {
1340
+ month -= 12;
1341
+ }
1353
1342
  const monthName = getMonthName(month).full;
1354
- const year = (unmoddedMonth > 12
1355
- ? startYear + 1
1356
- : startYear);
1343
+ // Year is start year +1 for each 12 months
1344
+ const year = (startYear + (Math.floor(unmoddedMonth / 12)));
1357
1345
  // Figure out which days are allowed
1358
1346
  const days = [];
1359
1347
  const numDaysInMonth = (new Date(year, month, 0)).getDate();
1360
- const firstDay = (month === today.month
1361
- ? today.day // Current month: start at current date
1362
- : 1 // Future month: start at beginning of month
1363
- );
1364
- for (let day = firstDay; day <= numDaysInMonth; day++) {
1365
- days.push(day);
1348
+ if (chooseFromPast) {
1349
+ // Past selection
1350
+ const numDaysToAdd = ((month === today.month)
1351
+ ? today.day // Current month, only add up to today
1352
+ : numDaysInMonth // Past month, add all days
1353
+ );
1354
+ for (let day = 1; day <= numDaysToAdd; day++) {
1355
+ days.push(day);
1356
+ }
1357
+ }
1358
+ else {
1359
+ // Future selection: add all remaining days of the month
1360
+ const firstDay = (month === today.month
1361
+ ? today.day // Current month: start at current date
1362
+ : 1 // Future month: start at beginning of month
1363
+ );
1364
+ for (let day = firstDay; day <= numDaysInMonth; day++) {
1365
+ days.push(day);
1366
+ }
1366
1367
  }
1367
1368
  choices.push({
1368
1369
  choiceName: `${monthName} ${year}`,
@@ -1376,7 +1377,7 @@ const SimpleDateChooser = (props) => {
1376
1377
  const dayOptions = [];
1377
1378
  choices.forEach((choice) => {
1378
1379
  // Create month option
1379
- monthOptions.push(React.createElement("option", { key: choice.month, value: choice.month, "aria-label": `choose ${choice.choiceName}`, onSelect: () => {
1380
+ monthOptions.push(React.createElement("option", { key: choice.month, value: `${choice.month}-${choice.year}`, "aria-label": `choose ${choice.choiceName}`, onSelect: () => {
1380
1381
  onChange(choice.month, choice.days[0], choice.year);
1381
1382
  } }, choice.choiceName));
1382
1383
  if (month === choice.month) {
@@ -1391,7 +1392,7 @@ const SimpleDateChooser = (props) => {
1391
1392
  }
1392
1393
  });
1393
1394
  return (React.createElement("div", { className: "SimpleDateChooser d-inline-block", "aria-label": `date chooser with selected date: ${month} ${day}, ${year}` },
1394
- React.createElement("select", { "aria-label": `month for ${ariaLabel}`, className: "custom-select d-inline-block mr-1", style: { width: 'auto' }, id: `SimpleDateChooser-${name}-month`, value: month, onChange: (e) => {
1395
+ React.createElement("select", { "aria-label": `month for ${ariaLabel}`, className: "custom-select d-inline-block mr-1", style: { width: 'auto' }, id: `SimpleDateChooser-${name}-month`, value: `${month}-${year}`, onChange: (e) => {
1395
1396
  const choice = choices[e.target.selectedIndex];
1396
1397
  // Change day, month, and year
1397
1398
  onChange(choice.month, choice.days[0], choice.year);
@@ -1433,14 +1434,16 @@ const Drawer = (props) => {
1433
1434
  /*------------------------------------------------------------------------*/
1434
1435
  /* -------------- Props ------------- */
1435
1436
  // Destructure all props
1436
- const { children, } = props;
1437
+ const { customBackgroundColor, children, } = props;
1437
1438
  /*------------------------------------------------------------------------*/
1438
1439
  /* Render */
1439
1440
  /*------------------------------------------------------------------------*/
1440
1441
  /*----------------------------------------*/
1441
1442
  /* Main UI */
1442
1443
  /*----------------------------------------*/
1443
- return (React.createElement("div", { className: "Drawer-container" },
1444
+ return (React.createElement("div", { className: "Drawer-container", style: {
1445
+ backgroundColor: (customBackgroundColor !== null && customBackgroundColor !== void 0 ? customBackgroundColor : undefined),
1446
+ } },
1444
1447
  React.createElement("style", null, style$4),
1445
1448
  children));
1446
1449
  };
@@ -1903,24 +1906,25 @@ const CopiableBox = (props) => {
1903
1906
  /**
1904
1907
  * Reusable nested item picker
1905
1908
  * @author Yuen Ler Chow
1909
+ * @author Gabe Abrams
1906
1910
  */
1907
1911
  /* ------------- Actions ------------ */
1908
1912
  // Types of actions
1909
1913
  var ActionType$2;
1910
1914
  (function (ActionType) {
1911
- // Toggle whether the children are being shown
1912
- ActionType["ToggleItems"] = "toggle-items";
1915
+ // Toggle whether a child are being shown
1916
+ ActionType["ToggleChild"] = "toggle-child";
1913
1917
  })(ActionType$2 || (ActionType$2 = {}));
1914
1918
  /**
1915
1919
  * Reducer that executes actions
1916
- * @author Yuen Ler Chow
1920
+ * @author Gabe Abrams
1917
1921
  * @param state current state
1918
1922
  * @param action action to execute
1919
1923
  */
1920
1924
  const reducer$2 = (state, action) => {
1921
1925
  switch (action.type) {
1922
- case ActionType$2.ToggleItems: {
1923
- return { isShowingItems: !state.isShowingItems };
1926
+ case ActionType$2.ToggleChild: {
1927
+ return Object.assign(Object.assign({}, state), { childExpanded: Object.assign(Object.assign({}, state.childExpanded), { [String(action.id)]: !state.childExpanded[String(action.id)] }) });
1924
1928
  }
1925
1929
  default: {
1926
1930
  return state;
@@ -1938,14 +1942,19 @@ const NestableItemList = (props) => {
1938
1942
  // Destructure all props
1939
1943
  const { items, onChanged, } = props;
1940
1944
  /* -------------- State ------------- */
1945
+ // Create initial map of child expanded booleans
1946
+ const initChildExpanded = {};
1947
+ items.forEach((item) => {
1948
+ initChildExpanded[String(item.id)] = false;
1949
+ });
1941
1950
  // Initial state
1942
1951
  const initialState = {
1943
- isShowingItems: false,
1952
+ childExpanded: initChildExpanded,
1944
1953
  };
1945
1954
  // Initialize state
1946
1955
  const [state, dispatch] = useReducer(reducer$2, initialState);
1947
1956
  // Destructure common state
1948
- const { isShowingItems, } = state;
1957
+ const { childExpanded, } = state;
1949
1958
  /*------------------------------------------------------------------------*/
1950
1959
  /* Component Functions */
1951
1960
  /*------------------------------------------------------------------------*/
@@ -2033,14 +2042,15 @@ const NestableItemList = (props) => {
2033
2042
  backgroundColor: 'transparent',
2034
2043
  }, type: "button", onClick: () => {
2035
2044
  dispatch({
2036
- type: ActionType$2.ToggleItems,
2045
+ type: ActionType$2.ToggleChild,
2046
+ id: item.id,
2037
2047
  });
2038
- }, "aria-label": `${isShowingItems ? 'Hide' : 'Show'} items in ${item.name}` },
2039
- React.createElement(FontAwesomeIcon, { icon: isShowingItems ? faChevronDown : faChevronRight })))),
2048
+ }, "aria-label": `${childExpanded[item.id] ? 'Hide' : 'Show'} items in ${item.name}` },
2049
+ React.createElement(FontAwesomeIcon, { icon: childExpanded[item.id] ? faChevronDown : faChevronRight })))),
2040
2050
  React.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) => {
2041
2051
  onChanged(changeChecked(item.id, checked, items));
2042
2052
  }, ariaLabel: `Select ${item.name}`, checkedVariant: Variant$1.Light }),
2043
- item.isGroup && isShowingItems && (React.createElement("div", { className: "NestableItemList-children-container", style: {
2053
+ (item.isGroup && childExpanded[item.id]) && (React.createElement("div", { className: "NestableItemList-children-container", style: {
2044
2054
  paddingLeft: '2.2rem',
2045
2055
  } },
2046
2056
  React.createElement(NestableItemList, { items: item.children, onChanged: (updatedItems) => {
@@ -2077,6 +2087,29 @@ const ItemPicker = (props) => {
2077
2087
  React.createElement(NestableItemList, { items: items, onChanged: onChanged }))));
2078
2088
  };
2079
2089
 
2090
+ // Import shared helpers
2091
+ /**
2092
+ * Get a human-readable description of a date (all in ET)
2093
+ * @author Gabe Abrams
2094
+ * @param [dateOrTimestamp=today] the date or timestamp for the date to describe
2095
+ * @returns human-readable description of the date
2096
+ */
2097
+ const getHumanReadableDate = (dateOrTimestamp) => {
2098
+ // Get the date info
2099
+ const { month, day, year, } = getTimeInfoInET(dateOrTimestamp);
2100
+ const currYear = getTimeInfoInET().year;
2101
+ // Get the short month description
2102
+ const monthName = getMonthName(month).short;
2103
+ // Create start of description
2104
+ let description = `${monthName} ${day}${getOrdinal(day)}`;
2105
+ // Add on year if it's different
2106
+ if (year !== currYear) {
2107
+ description += ` ${year}`;
2108
+ }
2109
+ // Return description
2110
+ return description;
2111
+ };
2112
+
2080
2113
  /**
2081
2114
  * Path of the route for storing client-side logs
2082
2115
  * @author Gabe Abrams
@@ -2116,37 +2149,37 @@ var LogType$1 = LogType;
2116
2149
  var LogAction;
2117
2150
  (function (LogAction) {
2118
2151
  // Target was opened by the user (it was not on screen, but now it is)
2119
- LogAction["Open"] = "open";
2152
+ LogAction["Open"] = "Open";
2120
2153
  // Target was closed by the user (it was on screen, but now it is not)
2121
- LogAction["Close"] = "close";
2154
+ LogAction["Close"] = "Close";
2122
2155
  // Target was cancelled by the user (it was on closed without saving)
2123
- LogAction["Cancel"] = "cancel";
2156
+ LogAction["Cancel"] = "Cancel";
2124
2157
  // Target was expanded by the user (it always remains on screen, but size was changed)
2125
- LogAction["Expand"] = "expand";
2158
+ LogAction["Expand"] = "Expand";
2126
2159
  // Target was collapsed by the user (it always remains on screen, but size was changed)
2127
- LogAction["Collapse"] = "collapse";
2160
+ LogAction["Collapse"] = "Collapse";
2128
2161
  // Target was viewed by the user (only for items that are not opened or closed, those must use Open/Close actions)
2129
- LogAction["View"] = "view";
2162
+ LogAction["View"] = "View";
2130
2163
  // Target interrupted the user (popup, dialog, validation message, etc. appeared without user prompting)
2131
- LogAction["Interrupt"] = "interrupt";
2164
+ LogAction["Interrupt"] = "Interrupt";
2132
2165
  // Target was created by the user (it did not exist before)
2133
- LogAction["Create"] = "create";
2166
+ LogAction["Create"] = "Create";
2134
2167
  // Target was edited by the user (it existed and was changed)
2135
- LogAction["Edit"] = "edit";
2168
+ LogAction["Edit"] = "Edit";
2136
2169
  // Target was deleted by the user (it existed and now it doesn't)
2137
- LogAction["Delete"] = "delete";
2170
+ LogAction["Delete"] = "Delete";
2138
2171
  // Target was added by the user (it already existed and was added to another place)
2139
- LogAction["Add"] = "add";
2172
+ LogAction["Add"] = "Add";
2140
2173
  // Target was removed by the user (it was removed from something but still exists)
2141
- LogAction["Remove"] = "remove";
2174
+ LogAction["Remove"] = "Remove";
2142
2175
  // Target was activated by the user (click, check, tap, keypress, etc.)
2143
- LogAction["Activate"] = "activate";
2176
+ LogAction["Activate"] = "Activate";
2144
2177
  // Target was deactivated by the user (click away, uncheck, tap outside of, tab away, etc.)
2145
- LogAction["Deactivate"] = "deactivate";
2178
+ LogAction["Deactivate"] = "Deactivate";
2146
2179
  // User showed interest in a target (hover, peek, etc.)
2147
- LogAction["Peek"] = "peek";
2180
+ LogAction["Peek"] = "Peek";
2148
2181
  // Unknown action
2149
- LogAction["Unknown"] = "unknown";
2182
+ LogAction["Unknown"] = "Unknown";
2150
2183
  })(LogAction || (LogAction = {}));
2151
2184
  var LogAction$1 = LogAction;
2152
2185
 
@@ -2188,12 +2221,12 @@ const roundToNumDecimals = (num, numDecimals) => {
2188
2221
  * @returns escaped cell text
2189
2222
  */
2190
2223
  const escapeCellText = (text) => {
2191
- if (!text.includes(',')) {
2224
+ if (!String(text).includes(',')) {
2192
2225
  // No need to escape
2193
- return text;
2226
+ return String(text);
2194
2227
  }
2195
2228
  // Perform escape
2196
- return `"${text.replace(/"/g, '""')}`;
2229
+ return `"${String(text).replace(/"/g, '""')}`;
2197
2230
  };
2198
2231
  /**
2199
2232
  * Generate a CSV file
@@ -2212,9 +2245,26 @@ const genCSV = (data, columns) => {
2212
2245
  .join(','));
2213
2246
  // Add each row
2214
2247
  data.forEach((datum) => {
2248
+ csv += '\n';
2215
2249
  csv += (columns
2216
2250
  .map((column) => {
2217
- return escapeCellText(datum[column.param]);
2251
+ let contents;
2252
+ const cell = datum[column.param];
2253
+ if (typeof cell === 'string'
2254
+ || typeof cell === 'number') {
2255
+ contents = String(cell);
2256
+ }
2257
+ else if (typeof cell === 'undefined'
2258
+ || cell === null) {
2259
+ contents = '';
2260
+ }
2261
+ else if (typeof cell === 'object') {
2262
+ contents = JSON.stringify(cell);
2263
+ }
2264
+ else {
2265
+ contents = '';
2266
+ }
2267
+ return escapeCellText(contents);
2218
2268
  })
2219
2269
  .join(','));
2220
2270
  });
@@ -2243,11 +2293,12 @@ const CSVDownloadButton = (props) => {
2243
2293
  /* Main UI */
2244
2294
  /*----------------------------------------*/
2245
2295
  // Render the button
2246
- return (React.createElement("a", { id: id, download: filename, href: `data:application/octet-stream,${csv}`, className: `CSVDownloadButton-button ${className !== null && className !== void 0 ? className : 'btn btn-secondary'}`, "aria-label": (ariaLabel
2296
+ return (React.createElement("a", { id: id, download: filename, href: `data:application/octet-stream,${encodeURIComponent(csv)}`, className: `CSVDownloadButton-button ${className !== null && className !== void 0 ? className : 'btn btn-secondary'}`, "aria-label": (ariaLabel
2247
2297
  ? `Click to download ${filename}`
2248
2298
  : ariaLabel), style: style, onClick: onClick },
2249
2299
  !children && (React.createElement(React.Fragment, null,
2250
- React.createElement(FontAwesomeIcon, { icon: faCloudDownloadAlt, className: "mr-2" }),
2300
+ React.createElement(FontAwesomeIcon, { icon: faCloudDownloadAlt }),
2301
+ ' ',
2251
2302
  "Download CSV")),
2252
2303
  children));
2253
2304
  };
@@ -2271,9 +2322,13 @@ var ActionType$1;
2271
2322
  // Toggle sort column param
2272
2323
  ActionType["ToggleSortColumn"] = "toggle-sort-column";
2273
2324
  // Toggle the visibility of a column
2274
- ActionType["ToggleColumnVisibility"] = "toggle-column-visibility";
2325
+ ActionType["UpdateColumnVisibility"] = "update-column-visibility";
2275
2326
  // Toggle the column visibility customization modal
2276
2327
  ActionType["ToggleColVisCusModalVisibility"] = "toggle-col-vis-cus-modal-visibility";
2328
+ // Show all columns
2329
+ ActionType["ShowAllColumns"] = "show-all-columns";
2330
+ // Hide all columns
2331
+ ActionType["HideAllColumns"] = "hide-all-columns";
2277
2332
  })(ActionType$1 || (ActionType$1 = {}));
2278
2333
  /**
2279
2334
  * Reducer that executes actions
@@ -2295,14 +2350,28 @@ const reducer$1 = (state, action) => {
2295
2350
  // Stop sorting by column
2296
2351
  return Object.assign(Object.assign({}, state), { sortColumnParam: undefined, sortType: SortType.Ascending });
2297
2352
  }
2298
- case ActionType$1.ToggleColumnVisibility: {
2353
+ case ActionType$1.UpdateColumnVisibility: {
2299
2354
  const { columnVisibilityMap } = state;
2300
- columnVisibilityMap[action.param] = !columnVisibilityMap[action.param];
2355
+ columnVisibilityMap[action.param] = action.visible;
2301
2356
  return Object.assign(Object.assign({}, state), { columnVisibilityMap });
2302
2357
  }
2303
2358
  case ActionType$1.ToggleColVisCusModalVisibility: {
2304
2359
  return Object.assign(Object.assign({}, state), { columnVisibilityCustomizationModalVisible: !state.columnVisibilityCustomizationModalVisible });
2305
2360
  }
2361
+ case ActionType$1.ShowAllColumns: {
2362
+ const { columnVisibilityMap } = state;
2363
+ Object.keys(columnVisibilityMap).forEach((param) => {
2364
+ columnVisibilityMap[param] = true;
2365
+ });
2366
+ return Object.assign(Object.assign({}, state), { columnVisibilityMap });
2367
+ }
2368
+ case ActionType$1.HideAllColumns: {
2369
+ const { columnVisibilityMap } = state;
2370
+ Object.keys(columnVisibilityMap).forEach((param) => {
2371
+ columnVisibilityMap[param] = false;
2372
+ });
2373
+ return Object.assign(Object.assign({}, state), { columnVisibilityMap });
2374
+ }
2306
2375
  default: {
2307
2376
  return state;
2308
2377
  }
@@ -2318,7 +2387,18 @@ const IntelliTable = (props) => {
2318
2387
  var _a;
2319
2388
  /* -------------- Props ------------- */
2320
2389
  // Destructure all props
2321
- const { title, id, data, columns, } = props;
2390
+ const { title, id, columns, } = props;
2391
+ // Get data, show empty row if none
2392
+ const data = ((props.data && props.data.length > 0)
2393
+ ? props.data
2394
+ : [{ id: 'empty-row' }]);
2395
+ // Get CSV filename
2396
+ let filename = `${title}.csv`;
2397
+ if (props.csvName) {
2398
+ filename = (props.csvName.endsWith('.csv')
2399
+ ? props.csvName
2400
+ : `${props.csvName}.csv`);
2401
+ }
2322
2402
  /* -------------- State ------------- */
2323
2403
  // Initial state
2324
2404
  const initColumnVisibilityMap = {};
@@ -2335,21 +2415,49 @@ const IntelliTable = (props) => {
2335
2415
  const [state, dispatch] = useReducer(reducer$1, initialState);
2336
2416
  // Destructure common state
2337
2417
  const { sortColumnParam, sortType, columnVisibilityMap, columnVisibilityCustomizationModalVisible, } = state;
2418
+ /*------------------------------------------------------------------------*/
2419
+ /* Render */
2420
+ /*------------------------------------------------------------------------*/
2421
+ /*----------------------------------------*/
2422
+ /* Modal */
2423
+ /*----------------------------------------*/
2424
+ // Modal that may be defined
2425
+ let modal;
2338
2426
  /* ------- Col Vis Customization Modal ------ */
2339
2427
  if (columnVisibilityCustomizationModalVisible) {
2340
2428
  // Create modal
2341
- (React.createElement(Modal, { type: ModalType$1.Okay, title: "Choose columns to show:", onClose: () => {
2429
+ modal = (React.createElement(Modal, { type: ModalType$1.Okay, title: "Choose columns to show:", onClose: () => {
2430
+ const noColumnsSelected = (Object.values(columnVisibilityMap)
2431
+ .every((isSelected) => {
2432
+ return !isSelected;
2433
+ }));
2434
+ if (noColumnsSelected) {
2435
+ return alert$1('Choose at least one column', 'To continue, you have to choose at least one column to show');
2436
+ }
2342
2437
  dispatch({
2343
2438
  type: ActionType$1.ToggleColVisCusModalVisibility,
2344
2439
  });
2345
- } }, columns.map((column) => {
2346
- return (React.createElement(CheckboxButton, { key: column.param, id: `IntelliTable-${id}-toggle-visibility-${column.param}`, text: column.title, onChanged: () => {
2440
+ }, okayVariant: Variant$1.Light },
2441
+ columns.map((column) => {
2442
+ return (React.createElement(CheckboxButton, { key: column.param, id: `IntelliTable-${id}-toggle-visibility-${column.param}`, className: "mb-2", text: column.title, onChanged: (checked) => {
2443
+ dispatch({
2444
+ type: ActionType$1.UpdateColumnVisibility,
2445
+ param: column.param,
2446
+ visible: checked,
2447
+ });
2448
+ }, checked: columnVisibilityMap[column.param], ariaLabel: `show "${column.title}" column in the ${title} table`, checkedVariant: Variant$1.Light, uncheckedVariant: Variant$1.Secondary }));
2449
+ }),
2450
+ React.createElement("div", { className: "mt-3" }, "Or you can:"),
2451
+ React.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: () => {
2452
+ dispatch({
2453
+ type: ActionType$1.ShowAllColumns,
2454
+ });
2455
+ } }, "Select All"),
2456
+ React.createElement("button", { type: "button", id: `IntelliTable-${id}-select-none-columns`, className: "btn btn-secondary", "aria-label": `hide all columns in the ${title} table`, onClick: () => {
2347
2457
  dispatch({
2348
- type: ActionType$1.ToggleColumnVisibility,
2349
- param: column.param,
2458
+ type: ActionType$1.HideAllColumns,
2350
2459
  });
2351
- }, checked: columnVisibilityMap[column.param], ariaLabel: `show "${column.title}" column` }));
2352
- })));
2460
+ } }, "Deselect All")));
2353
2461
  }
2354
2462
  /*----------------------------------------*/
2355
2463
  /* Main UI */
@@ -2363,6 +2471,7 @@ const IntelliTable = (props) => {
2363
2471
  // Custom info based on current sort type
2364
2472
  let sortButtonAriaLabel;
2365
2473
  let sortIcon = faSort;
2474
+ let sortingByThisColumn = false;
2366
2475
  if (!sortColumnParam) {
2367
2476
  // Not being sorted yet
2368
2477
  sortButtonAriaLabel = `sort ascending by ${column.title}`;
@@ -2370,6 +2479,7 @@ const IntelliTable = (props) => {
2370
2479
  }
2371
2480
  else if (column.param === sortColumnParam) {
2372
2481
  // Already sorted by this column
2482
+ sortingByThisColumn = true;
2373
2483
  if (sortType === SortType.Ascending) {
2374
2484
  // Sorted ascending
2375
2485
  sortButtonAriaLabel = `sort descending by ${column.title}`;
@@ -2387,11 +2497,14 @@ const IntelliTable = (props) => {
2387
2497
  sortIcon = faSort;
2388
2498
  }
2389
2499
  // Create the cell UI
2390
- return (React.createElement("th", { key: column.param, scope: "col", id: `IntelliTable-${id}-header-${column.param}` },
2391
- React.createElement("div", { className: "d-flex align-items-center justify-content-center flex-row h-100" },
2500
+ return (React.createElement("th", { key: column.param, scope: "col", id: `IntelliTable-${id}-header-${column.param}`, className: "text-start", style: {
2501
+ borderRight: '0.05rem solid #555',
2502
+ borderLeft: '0.05rem solid #555',
2503
+ } },
2504
+ React.createElement("div", { className: "d-flex align-items-center justify-content-start flex-row h-100" },
2392
2505
  React.createElement("span", { className: "text-nowrap" }, column.title),
2393
2506
  React.createElement("div", null,
2394
- React.createElement("button", { type: "button", className: "btn btn-light btn-sm ms-1", "aria-label": sortButtonAriaLabel, onClick: () => {
2507
+ React.createElement("button", { type: "button", id: `IntelliTable-${id}-sort-by-${column.param}-button`, className: `btn btn-${sortingByThisColumn ? 'light' : 'secondary'} btn-sm ms-1 ps-1 pe-1 pt-0 pb-0`, "aria-label": sortButtonAriaLabel, onClick: () => {
2395
2508
  dispatch({
2396
2509
  type: ActionType$1.ToggleSortColumn,
2397
2510
  param: column.param,
@@ -2409,8 +2522,28 @@ const IntelliTable = (props) => {
2409
2522
  const descending = (sortType === SortType.Descending);
2410
2523
  if (sortColumnParam) {
2411
2524
  sortedData.sort((a, b) => {
2525
+ var _a, _b;
2412
2526
  const aVal = a[sortColumnParam];
2413
2527
  const bVal = b[sortColumnParam];
2528
+ // Tiebreaker sort by timestamp, most recent first (used if tied)
2529
+ const tiebreaker = (((_a = b.timestamp) !== null && _a !== void 0 ? _a : 0)
2530
+ - ((_b = a.timestamp) !== null && _b !== void 0 ? _b : 0));
2531
+ // Auto-sort undefined and null to end of list
2532
+ if ((aVal === undefined || aVal === null)
2533
+ || (bVal === undefined || bVal === null)) {
2534
+ // At least one was undefined
2535
+ if ((aVal === undefined || aVal === null)
2536
+ && (bVal === undefined || bVal === null)) {
2537
+ // Both undefined
2538
+ return tiebreaker;
2539
+ }
2540
+ if (aVal === undefined || aVal === null) {
2541
+ // First was undefined
2542
+ return 1;
2543
+ }
2544
+ // Second was undefined
2545
+ return -1;
2546
+ }
2414
2547
  // Sort differently based on the data type
2415
2548
  // > Boolean
2416
2549
  if (paramType === ParamType$1.Boolean) {
@@ -2420,7 +2553,7 @@ const IntelliTable = (props) => {
2420
2553
  if (!aVal && bVal) {
2421
2554
  return (descending ? 1 : -1);
2422
2555
  }
2423
- return 0;
2556
+ return tiebreaker;
2424
2557
  }
2425
2558
  // > Number
2426
2559
  if (paramType === ParamType$1.Int
@@ -2432,12 +2565,12 @@ const IntelliTable = (props) => {
2432
2565
  // > String
2433
2566
  if (paramType === ParamType$1.String) {
2434
2567
  if (aVal < bVal) {
2435
- return (descending ? -1 : 1);
2568
+ return (descending ? 1 : -1);
2436
2569
  }
2437
2570
  if (aVal > bVal) {
2438
- return (descending ? 1 : -1);
2571
+ return (descending ? -1 : 1);
2439
2572
  }
2440
- return 0;
2573
+ return tiebreaker;
2441
2574
  }
2442
2575
  // > JSON
2443
2576
  if (paramType === ParamType$1.JSON) {
@@ -2452,7 +2585,7 @@ const IntelliTable = (props) => {
2452
2585
  : (aSize - bSize));
2453
2586
  }
2454
2587
  // No sort
2455
- return 0;
2588
+ return tiebreaker;
2456
2589
  });
2457
2590
  }
2458
2591
  // Table body
@@ -2477,9 +2610,12 @@ const IntelliTable = (props) => {
2477
2610
  const noValue = (value === undefined
2478
2611
  || value === null);
2479
2612
  visibleValue = (noValue
2480
- ? (React.createElement(FontAwesomeIcon, { icon: faCheckCircle }))
2481
- : (React.createElement(FontAwesomeIcon, { icon: faXmarkCircle })));
2613
+ ? (React.createElement(FontAwesomeIcon, { icon: faMinus }))
2614
+ : (React.createElement(FontAwesomeIcon, { icon: fullValue ? faCheckCircle : faXmarkCircle })));
2482
2615
  title = (fullValue ? 'True' : 'False');
2616
+ if (noValue) {
2617
+ title = 'Empty Cell';
2618
+ }
2483
2619
  }
2484
2620
  else if (column.type === ParamType$1.Int) {
2485
2621
  fullValue = Number.parseInt(value, 10);
@@ -2488,6 +2624,9 @@ const IntelliTable = (props) => {
2488
2624
  ? (React.createElement(FontAwesomeIcon, { icon: faMinus }))
2489
2625
  : fullValue);
2490
2626
  title = String(fullValue);
2627
+ if (noValue) {
2628
+ title = 'Empty Cell';
2629
+ }
2491
2630
  }
2492
2631
  else if (column.type === ParamType$1.Float) {
2493
2632
  fullValue = Number.parseFloat(value);
@@ -2496,14 +2635,22 @@ const IntelliTable = (props) => {
2496
2635
  ? (React.createElement(FontAwesomeIcon, { icon: faMinus }))
2497
2636
  : roundToNumDecimals(fullValue, 2));
2498
2637
  title = String(fullValue);
2638
+ if (noValue) {
2639
+ title = 'Empty Cell';
2640
+ }
2499
2641
  }
2500
2642
  else if (column.type === ParamType$1.String) {
2501
2643
  fullValue = String(value).trim();
2502
- const noValue = (value.trim().length) === 0;
2644
+ const noValue = (value === undefined
2645
+ || value === null
2646
+ || String(fullValue).trim().length === 0);
2503
2647
  visibleValue = (noValue
2504
2648
  ? (React.createElement(FontAwesomeIcon, { icon: faMinus }))
2505
2649
  : fullValue);
2506
2650
  title = `"${value}"`;
2651
+ if (noValue) {
2652
+ title = 'Empty Cell';
2653
+ }
2507
2654
  }
2508
2655
  else if (column.type === ParamType$1.JSON) {
2509
2656
  fullValue = JSON.stringify(value);
@@ -2513,9 +2660,13 @@ const IntelliTable = (props) => {
2513
2660
  visibleValue = (noValue
2514
2661
  ? (React.createElement(FontAwesomeIcon, { icon: faMinus }))
2515
2662
  : fullValue);
2663
+ title = "JSON Object";
2516
2664
  }
2517
2665
  // Create UI
2518
- return (React.createElement("td", { key: `${datum.id}-${column.param}`, title: title }, visibleValue));
2666
+ return (React.createElement("td", { key: `${datum.id}-${column.param}`, title: title, style: {
2667
+ borderRight: '0.05rem solid #555',
2668
+ borderLeft: '0.05rem solid #555',
2669
+ } }, visibleValue));
2519
2670
  }));
2520
2671
  // Add cells to a row
2521
2672
  return (React.createElement("tr", { key: datum.id }, cells));
@@ -2532,13 +2683,16 @@ const IntelliTable = (props) => {
2532
2683
  })
2533
2684
  .length);
2534
2685
  // Build CSV
2535
- const csv = genCSV(data, columns);
2686
+ const csv = genCSV(data, columns.filter((column) => {
2687
+ return columnVisibilityMap[column.param];
2688
+ }));
2536
2689
  // Build main UI
2537
2690
  return (React.createElement("div", { className: `IntelliTable-container-${id}` },
2538
- React.createElement("div", { className: "d-flex align-items-center justify-content-center" },
2691
+ modal,
2692
+ React.createElement("div", { className: "d-flex align-items-center justify-content-start" },
2539
2693
  React.createElement("h3", { className: "m-0" }, title),
2540
2694
  React.createElement("div", { className: "flex-grow-1 text-end" },
2541
- React.createElement(CSVDownloadButton, { "aria-label": `download data as csv for ${title}`, id: `IntelliTable-${id}-download-as-csv`, filename: `${title}.csv`, csv: csv }),
2695
+ React.createElement(CSVDownloadButton, { "aria-label": `download data as csv for ${title}`, id: `IntelliTable-${id}-download-as-csv`, filename: filename, csv: csv }),
2542
2696
  React.createElement("button", { type: "button", className: "btn btn-secondary ms-2", "aria-label": `show panel for customizing which columns show in table ${title}`, id: `IntelliTable-${id}-show-column-customization-modal`, onClick: () => {
2543
2697
  dispatch({
2544
2698
  type: ActionType$1.ToggleColVisCusModalVisibility,
@@ -2551,7 +2705,7 @@ const IntelliTable = (props) => {
2551
2705
  numHiddenCols,
2552
2706
  ' ',
2553
2707
  "hidden)"))))),
2554
- React.createElement("div", { className: `IntelliTable-table-${id}`, style: {
2708
+ React.createElement("div", { className: `IntelliTable-table-${id} mt-2`, style: {
2555
2709
  overflowX: 'auto',
2556
2710
  } }, table)));
2557
2711
  };
@@ -2640,8 +2794,12 @@ const style = `
2640
2794
  background-color: transparent !important;
2641
2795
  padding-top: 0 !important;
2642
2796
  padding-bottom: 0 !important;
2797
+ padding-right: 1em !important;
2643
2798
  margin: 0 !important;
2644
- color: #333 !important;
2799
+ color: #444 !important;
2800
+
2801
+ right: 0 !important;
2802
+ position: absolute !important;
2645
2803
  }
2646
2804
  .LogReviewer-header-close-button:hover {
2647
2805
  border: 0 !important;
@@ -2653,6 +2811,195 @@ const style = `
2653
2811
  }
2654
2812
  `;
2655
2813
  /*------------------------------------------------------------------------*/
2814
+ /* Constants */
2815
+ /*------------------------------------------------------------------------*/
2816
+ const columns = [
2817
+ {
2818
+ title: 'First Name',
2819
+ param: 'userFirstName',
2820
+ type: ParamType$1.String,
2821
+ },
2822
+ {
2823
+ title: 'Last Name',
2824
+ param: 'userLastName',
2825
+ type: ParamType$1.String,
2826
+ },
2827
+ {
2828
+ title: 'Email',
2829
+ param: 'userEmail',
2830
+ type: ParamType$1.String,
2831
+ },
2832
+ {
2833
+ title: 'Canvas Id',
2834
+ param: 'userId',
2835
+ type: ParamType$1.Int,
2836
+ },
2837
+ {
2838
+ title: 'Student?',
2839
+ param: 'isLearner',
2840
+ type: ParamType$1.Boolean,
2841
+ },
2842
+ {
2843
+ title: 'Teaching Staff?',
2844
+ param: 'isTTM',
2845
+ type: ParamType$1.Boolean,
2846
+ startsHidden: true,
2847
+ },
2848
+ {
2849
+ title: 'Admin?',
2850
+ param: 'isAdmin',
2851
+ type: ParamType$1.Boolean,
2852
+ startsHidden: true,
2853
+ },
2854
+ {
2855
+ title: 'Course Canvas Id',
2856
+ param: 'courseId',
2857
+ type: ParamType$1.Int,
2858
+ startsHidden: true,
2859
+ },
2860
+ {
2861
+ title: 'Course Name',
2862
+ param: 'courseName',
2863
+ type: ParamType$1.String,
2864
+ },
2865
+ {
2866
+ title: 'Browser Name',
2867
+ param: 'browser.name',
2868
+ type: ParamType$1.String,
2869
+ startsHidden: true,
2870
+ },
2871
+ {
2872
+ title: 'Browser Version',
2873
+ param: 'browser.version',
2874
+ type: ParamType$1.String,
2875
+ startsHidden: true,
2876
+ },
2877
+ {
2878
+ title: 'OS',
2879
+ param: 'device.os',
2880
+ type: ParamType$1.String,
2881
+ startsHidden: true,
2882
+ },
2883
+ {
2884
+ title: 'Mobile?',
2885
+ param: 'device.isMobile',
2886
+ type: ParamType$1.Boolean,
2887
+ startsHidden: true,
2888
+ },
2889
+ {
2890
+ title: 'Year',
2891
+ param: 'year',
2892
+ type: ParamType$1.Int,
2893
+ },
2894
+ {
2895
+ title: 'Month',
2896
+ param: 'month',
2897
+ type: ParamType$1.Int,
2898
+ },
2899
+ {
2900
+ title: 'Day',
2901
+ param: 'day',
2902
+ type: ParamType$1.Int,
2903
+ },
2904
+ {
2905
+ title: 'Hour',
2906
+ param: 'hour',
2907
+ type: ParamType$1.Int,
2908
+ },
2909
+ {
2910
+ title: 'Minute',
2911
+ param: 'minute',
2912
+ type: ParamType$1.Int,
2913
+ startsHidden: true,
2914
+ },
2915
+ {
2916
+ title: 'Timestamp',
2917
+ param: 'timestamp',
2918
+ type: ParamType$1.Int,
2919
+ startsHidden: true,
2920
+ },
2921
+ {
2922
+ title: 'Context',
2923
+ param: 'context',
2924
+ type: ParamType$1.String,
2925
+ },
2926
+ {
2927
+ title: 'Subcontext',
2928
+ param: 'subcontext',
2929
+ type: ParamType$1.String,
2930
+ },
2931
+ {
2932
+ title: 'Tags',
2933
+ param: 'tags',
2934
+ type: ParamType$1.JSON,
2935
+ startsHidden: true,
2936
+ },
2937
+ {
2938
+ title: 'Log Level',
2939
+ param: 'level',
2940
+ type: ParamType$1.String,
2941
+ startsHidden: true,
2942
+ },
2943
+ {
2944
+ title: 'Metadata',
2945
+ param: 'metadata',
2946
+ type: ParamType$1.JSON,
2947
+ startsHidden: true,
2948
+ },
2949
+ {
2950
+ title: 'Source',
2951
+ param: 'source',
2952
+ type: ParamType$1.String,
2953
+ },
2954
+ {
2955
+ title: 'Server Route Path',
2956
+ param: 'routePath',
2957
+ type: ParamType$1.String,
2958
+ startsHidden: true,
2959
+ },
2960
+ {
2961
+ title: 'Server Route Template',
2962
+ param: 'routeTemplate',
2963
+ type: ParamType$1.String,
2964
+ startsHidden: true,
2965
+ },
2966
+ {
2967
+ title: 'Type',
2968
+ param: 'type',
2969
+ type: ParamType$1.String,
2970
+ },
2971
+ {
2972
+ title: 'Error Message',
2973
+ param: 'errorMessage',
2974
+ type: ParamType$1.String,
2975
+ startsHidden: true,
2976
+ },
2977
+ {
2978
+ title: 'Error Code',
2979
+ param: 'errorCode',
2980
+ type: ParamType$1.String,
2981
+ startsHidden: true,
2982
+ },
2983
+ {
2984
+ title: 'Error Stack',
2985
+ param: 'errorStack',
2986
+ type: ParamType$1.String,
2987
+ startsHidden: true,
2988
+ },
2989
+ {
2990
+ title: 'Action Target',
2991
+ param: 'target',
2992
+ type: ParamType$1.String,
2993
+ startsHidden: true,
2994
+ },
2995
+ {
2996
+ title: 'Action Type',
2997
+ param: 'action',
2998
+ type: ParamType$1.String,
2999
+ startsHidden: true,
3000
+ },
3001
+ ];
3002
+ /*------------------------------------------------------------------------*/
2656
3003
  /* Static Functions */
2657
3004
  /*------------------------------------------------------------------------*/
2658
3005
  /**
@@ -2752,10 +3099,35 @@ const LogReviewer = (props) => {
2752
3099
  /*------------------------------------------------------------------------*/
2753
3100
  /* Setup */
2754
3101
  /*------------------------------------------------------------------------*/
2755
- var _a, _b, _c, _d, _e, _f;
3102
+ var _a, _b, _c, _d;
2756
3103
  /* -------------- Props ------------- */
2757
3104
  // Destructure props
2758
3105
  const { LogMetadata, onClose, } = props;
3106
+ // Create complete map of contexts
3107
+ const contextMap = {};
3108
+ [
3109
+ ((_a = LogMetadata.Context) !== null && _a !== void 0 ? _a : {}),
3110
+ LogBuiltInMetadata.Context,
3111
+ ].forEach((partialContextMap) => {
3112
+ Object.keys(partialContextMap).forEach((context) => {
3113
+ // Add context
3114
+ contextMap[context] = partialContextMap[context];
3115
+ // If context has children, add an "uncategorized" subcontext
3116
+ if (typeof contextMap[context] !== 'string') {
3117
+ contextMap[context][LogBuiltInMetadata.Context.Uncategorized] = (LogBuiltInMetadata.Context.Uncategorized);
3118
+ }
3119
+ });
3120
+ });
3121
+ // Create complete map of targets
3122
+ const targetMap = {};
3123
+ [
3124
+ ((_b = LogMetadata.Target) !== null && _b !== void 0 ? _b : {}),
3125
+ LogBuiltInMetadata.Target,
3126
+ ].forEach((partialTargetMap) => {
3127
+ Object.keys(partialTargetMap).forEach((target) => {
3128
+ targetMap[target] = partialTargetMap[target];
3129
+ });
3130
+ });
2759
3131
  /* -------------- State ------------- */
2760
3132
  // Create initial date filter state
2761
3133
  const today = getTimeInfoInET();
@@ -2775,26 +3147,29 @@ const LogReviewer = (props) => {
2775
3147
  };
2776
3148
  // Create initial context filter state
2777
3149
  const initContextFilterState = {};
2778
- Object.keys((_a = LogMetadata.Context) !== null && _a !== void 0 ? _a : {}).forEach((context) => {
2779
- var _a, _b;
2780
- const contextValue = ((_a = LogMetadata.Context) !== null && _a !== void 0 ? _a : {})[context];
3150
+ Object.keys(contextMap).forEach((context) => {
3151
+ const contextValue = contextMap[context];
2781
3152
  if (typeof contextValue === 'string') {
2782
- // Case: no subcontexts
3153
+ // Case: no subcontexts, init as checked
2783
3154
  initContextFilterState[contextValue] = true;
2784
3155
  }
2785
3156
  else {
2786
3157
  // Case: subcontexts exist
2787
- initContextFilterState[contextValue._] = {};
2788
- Object.values(((_b = LogMetadata.Context) !== null && _b !== void 0 ? _b : {})[context]).forEach((subcontext) => {
2789
- const subcontextValue = contextValue[subcontext];
2790
- initContextFilterState[contextValue._][subcontextValue] = true;
3158
+ initContextFilterState[context] = {};
3159
+ Object.values(contextMap[context]).forEach((subcontext) => {
3160
+ // Skip self ("_")
3161
+ if (subcontext === '_') {
3162
+ return;
3163
+ }
3164
+ // Initialize as checked
3165
+ initContextFilterState[context][subcontext] = true;
2791
3166
  });
2792
3167
  }
2793
3168
  });
2794
3169
  // Create initial tag filter state
2795
3170
  const initTagFilterState = {};
2796
- Object.values((_b = LogMetadata.Tag) !== null && _b !== void 0 ? _b : {}).forEach((tagValue) => {
2797
- initTagFilterState[tagValue] = true;
3171
+ Object.keys((_c = LogMetadata.Tag) !== null && _c !== void 0 ? _c : {}).forEach((tag) => {
3172
+ initTagFilterState[tag] = false;
2798
3173
  });
2799
3174
  // Create advanced filter state
2800
3175
  const initAdvancedFilterState = {
@@ -2820,7 +3195,7 @@ const LogReviewer = (props) => {
2820
3195
  target: {},
2821
3196
  action: {},
2822
3197
  };
2823
- Object.values((_c = LogMetadata.Target) !== null && _c !== void 0 ? _c : {}).forEach((target) => {
3198
+ Object.values(targetMap).forEach((target) => {
2824
3199
  initActionErrorFilterState.target[target] = true;
2825
3200
  });
2826
3201
  Object.values(LogAction$1).forEach((action) => {
@@ -2859,15 +3234,18 @@ const LogReviewer = (props) => {
2859
3234
  let month = newDateFilterState.startDate.month;
2860
3235
  while (
2861
3236
  // Earlier year
2862
- (year <= newDateFilterState.endDate.year)
3237
+ (year < newDateFilterState.endDate.year)
2863
3238
  // Current year but included month
2864
3239
  || (year === newDateFilterState.endDate.year
2865
3240
  && month <= newDateFilterState.endDate.month)) {
2866
- // Add to list
2867
- toLoad.push({
2868
- year,
2869
- month,
2870
- });
3241
+ // Add to list if not already loaded
3242
+ if (!logMap[year]
3243
+ || !logMap[year][month]) {
3244
+ toLoad.push({
3245
+ year,
3246
+ month,
3247
+ });
3248
+ }
2871
3249
  // Increment
2872
3250
  month += 1;
2873
3251
  if (month > 12) {
@@ -2955,7 +3333,7 @@ const LogReviewer = (props) => {
2955
3333
  /* Filters */
2956
3334
  /*----------------------------------------*/
2957
3335
  // Filter toggle
2958
- const filterToggles = (React.createElement("div", { className: "LogReviewer-filter-toggles mb-2" },
3336
+ const filterToggles = (React.createElement("div", { className: "LogReviewer-filter-toggles" },
2959
3337
  React.createElement("h3", { className: "m-0" }, "Filters:"),
2960
3338
  React.createElement("div", { className: "LogReviewer-filter-toggle-buttons alert alert-secondary p-2 m-0" },
2961
3339
  React.createElement("button", { type: "button", id: "LogReviewer-toggle-date-filter-drawer", className: `btn btn-${FilterDrawer.Date === expandedFilterDrawer ? 'warning' : 'light'} me-2`, "aria-label": "toggle date filter drawer", onClick: () => {
@@ -2990,41 +3368,64 @@ const LogReviewer = (props) => {
2990
3368
  } },
2991
3369
  React.createElement(FontAwesomeIcon, { icon: faHammer, className: "me-2" }),
2992
3370
  "Action"),
2993
- React.createElement("button", { type: "button", id: "LogReviewer-toggle-advanced-filter-drawer", className: `btn btn-${FilterDrawer.Advanced === expandedFilterDrawer ? 'warning' : 'light'}`, "aria-label": "toggle advanced filter drawer", onClick: () => {
3371
+ React.createElement("button", { type: "button", id: "LogReviewer-toggle-advanced-filter-drawer", className: `btn btn-${FilterDrawer.Advanced === expandedFilterDrawer ? 'warning' : 'light'} me-2`, "aria-label": "toggle advanced filter drawer", onClick: () => {
2994
3372
  dispatch({
2995
3373
  type: ActionType.ToggleFilterDrawer,
2996
3374
  filterDrawer: FilterDrawer.Advanced,
2997
3375
  });
2998
3376
  } },
2999
3377
  React.createElement(FontAwesomeIcon, { icon: faList, className: "me-2" }),
3000
- "Advanced"))));
3378
+ "Advanced"),
3379
+ React.createElement("button", { type: "button", id: "LogReviewer-reset-filters-button", className: "btn btn-light", "aria-label": "reset filters", onClick: () => {
3380
+ dispatch({
3381
+ type: ActionType.ResetFilters,
3382
+ initActionErrorFilterState,
3383
+ initAdvancedFilterState,
3384
+ initContextFilterState,
3385
+ initDateFilterState,
3386
+ initTagFilterState,
3387
+ });
3388
+ } },
3389
+ React.createElement(FontAwesomeIcon, { icon: faTimes }),
3390
+ ' ',
3391
+ "Reset"))));
3001
3392
  // Filter drawer
3002
3393
  let filterDrawer;
3003
3394
  if (expandedFilterDrawer) {
3004
3395
  if (expandedFilterDrawer === FilterDrawer.Date) {
3005
3396
  filterDrawer = (React.createElement(TabBox, { title: "Date" },
3006
- React.createElement(SimpleDateChooser, { ariaLabel: "filter start date", name: "filter-start-date", year: dateFilterState.startDate.year, month: dateFilterState.startDate.month, day: dateFilterState.startDate.day, onChange: (month, day, year) => {
3007
- dispatch({
3008
- type: ActionType.UpdateDateFilterState,
3009
- dateFilterState: Object.assign(Object.assign({}, dateFilterState), { startDate: { month, day, year } }),
3010
- });
3397
+ React.createElement(SimpleDateChooser, { ariaLabel: "filter start date", name: "filter-start-date", year: dateFilterState.startDate.year, month: dateFilterState.startDate.month, day: dateFilterState.startDate.day, chooseFromPast: true, numMonthsToShow: 36, onChange: (month, day, year) => {
3398
+ dateFilterState.startDate = { month, day, year };
3399
+ handleDateRangeUpdated(dateFilterState);
3011
3400
  } }),
3012
3401
  ' ',
3013
3402
  "to",
3014
3403
  ' ',
3015
- React.createElement(SimpleDateChooser, { ariaLabel: "filter end date", name: "filter-end-date", year: dateFilterState.endDate.year, month: dateFilterState.endDate.month, day: dateFilterState.endDate.day, onChange: (month, day, year) => {
3016
- dispatch({
3017
- type: ActionType.UpdateDateFilterState,
3018
- dateFilterState: Object.assign(Object.assign({}, dateFilterState), { endDate: { month, day, year } }),
3019
- });
3404
+ React.createElement(SimpleDateChooser, { ariaLabel: "filter end date", name: "filter-end-date", year: dateFilterState.endDate.year, month: dateFilterState.endDate.month, day: dateFilterState.endDate.day, chooseFromPast: true, numMonthsToShow: 12, onChange: (month, day, year) => {
3405
+ if (year < dateFilterState.startDate.year
3406
+ || (year === dateFilterState.startDate.year
3407
+ && month < dateFilterState.startDate.month)
3408
+ || (year === dateFilterState.startDate.year
3409
+ && month === dateFilterState.startDate.month
3410
+ && day < dateFilterState.startDate.day)) {
3411
+ return alert$1('Invalid Start Date', 'The start date cannot be before the end date.');
3412
+ }
3413
+ dateFilterState.endDate = { month, day, year };
3414
+ handleDateRangeUpdated(dateFilterState);
3020
3415
  } })));
3021
3416
  }
3022
3417
  else if (expandedFilterDrawer === FilterDrawer.Context) {
3023
3418
  // Create item picker items
3024
- const pickableItems = (Object.keys((_d = LogMetadata.Context) !== null && _d !== void 0 ? _d : {})
3025
- .map((context) => {
3026
- var _a;
3027
- const value = ((_a = LogMetadata.Context) !== null && _a !== void 0 ? _a : {})[context];
3419
+ const builtInPickableItem = {
3420
+ id: 'built-in-contexts',
3421
+ name: 'Auto-logged',
3422
+ isGroup: true,
3423
+ children: [],
3424
+ };
3425
+ const pickableItems = [];
3426
+ Object.keys(contextMap)
3427
+ .forEach((context) => {
3428
+ const value = contextMap[context];
3028
3429
  if (typeof value === 'string') {
3029
3430
  // No subcategories
3030
3431
  const item = {
@@ -3033,38 +3434,65 @@ const LogReviewer = (props) => {
3033
3434
  isGroup: false,
3034
3435
  checked: !!contextFilterState[context],
3035
3436
  };
3036
- return item;
3437
+ // Add built-in items to its own folder
3438
+ const isBuiltIn = context in LogBuiltInMetadata.Context;
3439
+ if (isBuiltIn) {
3440
+ // Add to built-in pickable item
3441
+ builtInPickableItem.children.push(item);
3442
+ }
3443
+ else {
3444
+ // Add to pickable items list
3445
+ pickableItems.push(item);
3446
+ }
3447
+ return;
3037
3448
  }
3038
3449
  // Has subcategories
3039
3450
  const children = (Object.keys(value)
3451
+ // Remove parent name
3040
3452
  .filter((subcontext) => {
3041
3453
  return subcontext !== '_';
3042
3454
  })
3455
+ // Create child pickable items
3043
3456
  .map((subcontext) => {
3044
3457
  return {
3045
- id: `${context}-${subcontext}`,
3458
+ id: subcontext,
3046
3459
  name: genHumanReadableName(subcontext),
3047
3460
  isGroup: false,
3048
- checked: !!value[subcontext],
3461
+ checked: contextFilterState[context][subcontext],
3049
3462
  };
3050
3463
  }));
3051
3464
  const item = {
3052
3465
  id: context,
3053
- name: context,
3466
+ name: genHumanReadableName(context),
3054
3467
  isGroup: true,
3055
3468
  children,
3056
3469
  };
3057
- return item;
3058
- }));
3470
+ pickableItems.push(item);
3471
+ });
3472
+ // Add built-in contexts to end ofl ist
3473
+ pickableItems.push(builtInPickableItem);
3059
3474
  // Create filter UI
3060
3475
  filterDrawer = (React.createElement(ItemPicker, { title: "Context", items: pickableItems, onChanged: (updatedItems) => {
3061
3476
  // Update our state
3062
3477
  updatedItems.forEach((pickableItem) => {
3063
3478
  if (pickableItem.isGroup) {
3064
3479
  // Has subcontexts
3065
- pickableItem.children.forEach((subcontextItem) => {
3066
- contextFilterState[pickableItem.id][subcontextItem.id] = (subcontextItem.checked);
3067
- });
3480
+ if (pickableItem.id === 'built-in-contexts') {
3481
+ // Built-in
3482
+ // Treat as if these were top-level contexts
3483
+ pickableItem.children.forEach((subcontextItem) => {
3484
+ contextFilterState[subcontextItem.id] = ('checked' in subcontextItem
3485
+ && subcontextItem.checked);
3486
+ });
3487
+ }
3488
+ else {
3489
+ // Not built-in
3490
+ pickableItem.children.forEach((subcontextItem) => {
3491
+ if (!subcontextItem.isGroup) {
3492
+ contextFilterState[pickableItem.id][subcontextItem.id] = (subcontextItem.checked);
3493
+ }
3494
+ });
3495
+ }
3068
3496
  }
3069
3497
  else {
3070
3498
  // No subcontexts
@@ -3079,13 +3507,19 @@ const LogReviewer = (props) => {
3079
3507
  }
3080
3508
  else if (expandedFilterDrawer === FilterDrawer.Tag) {
3081
3509
  // Create filter UI
3082
- filterDrawer = (React.createElement(TabBox, { title: "Tags" }, Object.keys(tagFilterState)
3083
- .map((tag, i) => {
3084
- const description = genHumanReadableName(tag);
3085
- return (React.createElement(CheckboxButton, { id: `LogReviewer-tag-${tag}-checkbox`, text: description, ariaLabel: `include logs tagged with "${description}" in results`, noMarginOnRight: i === Object.keys(tagFilterState).length - 1, onChanged: (checked) => {
3086
- tagFilterState[tag] = checked;
3087
- } }));
3088
- })));
3510
+ filterDrawer = (React.createElement(TabBox, { title: "Tags" },
3511
+ React.createElement("div", null, "If any tags are selected, logs must contain at least one (but not necessarily all) of the selected tags."),
3512
+ React.createElement("div", { className: "d-flex gap-1 flex-wrap" }, Object.keys((_d = LogMetadata.Tag) !== null && _d !== void 0 ? _d : {})
3513
+ .map((tag) => {
3514
+ const description = genHumanReadableName(tag);
3515
+ return (React.createElement(CheckboxButton, { key: tag, id: `LogReviewer-tag-${tag}-checkbox`, text: description, ariaLabel: `require that logs be tagged with "${description}" or any other selected tag`, checked: tagFilterState[tag], onChanged: (checked) => {
3516
+ tagFilterState[tag] = checked;
3517
+ dispatch({
3518
+ type: ActionType.UpdateTagFilterState,
3519
+ tagFilterState,
3520
+ });
3521
+ } }));
3522
+ }))));
3089
3523
  }
3090
3524
  else if (expandedFilterDrawer === FilterDrawer.Action) {
3091
3525
  // Create filter UI
@@ -3114,36 +3548,35 @@ const LogReviewer = (props) => {
3114
3548
  }, ariaLabel: "only show error logs", selected: actionErrorFilterState.type === LogType$1.Error, noMarginOnRight: true })),
3115
3549
  (actionErrorFilterState.type === undefined
3116
3550
  || actionErrorFilterState.type === LogType$1.Action) && (React.createElement(TabBox, { title: "Action Log Details" },
3117
- React.createElement(ButtonInputGroup, { label: "Action", className: "mb-2" }, Object.keys(LogAction$1)
3118
- .map((action, i) => {
3119
- const description = genHumanReadableName(action);
3120
- return (React.createElement(CheckboxButton, { id: `LogReviewer-action-${action}-checkbox`, text: description, ariaLabel: `include logs with action type "${description}" in results`, noMarginOnRight: i === Object.keys(LogAction$1).length - 1, onChanged: (checked) => {
3121
- actionErrorFilterState.action[action] = checked;
3122
- dispatch({
3123
- type: ActionType.UpdateActionErrorFilterState,
3124
- actionErrorFilterState,
3125
- });
3126
- } }));
3127
- })),
3551
+ React.createElement(ButtonInputGroup, { label: "Action", className: "mb-2" },
3552
+ React.createElement("div", { className: "d-flex gap-1 flex-wrap" }, Object.keys(LogAction$1)
3553
+ .map((action) => {
3554
+ const description = genHumanReadableName(action);
3555
+ return (React.createElement(CheckboxButton, { key: action, id: `LogReviewer-action-${action}-checkbox`, text: description, ariaLabel: `include logs with action type "${description}" in results`, noMarginOnRight: true, checked: actionErrorFilterState.action[action], onChanged: (checked) => {
3556
+ actionErrorFilterState.action[action] = checked;
3557
+ dispatch({
3558
+ type: ActionType.UpdateActionErrorFilterState,
3559
+ actionErrorFilterState,
3560
+ });
3561
+ } }));
3562
+ }))),
3128
3563
  React.createElement(ButtonInputGroup, { label: "Target" },
3129
- (Object.keys((_e = LogMetadata.Target) !== null && _e !== void 0 ? _e : {})) && (React.createElement("div", null, "This app does not have any targets yet.")),
3130
- Object.keys((_f = LogMetadata.Target) !== null && _f !== void 0 ? _f : {})
3131
- .map((target, i) => {
3132
- var _a;
3564
+ React.createElement("div", { className: "d-flex gap-1 flex-wrap" }, Object.keys(targetMap)
3565
+ .map((target) => {
3133
3566
  const description = genHumanReadableName(target);
3134
- return (React.createElement(CheckboxButton, { id: `LogReviewer-target-${target}-checkbox`, text: description, ariaLabel: `include logs with target "${description}" in results`, onChanged: (checked) => {
3567
+ return (React.createElement(CheckboxButton, { key: target, id: `LogReviewer-target-${target}-checkbox`, text: description, ariaLabel: `include logs with target "${description}" in results`, checked: actionErrorFilterState.target[target], noMarginOnRight: true, onChanged: (checked) => {
3135
3568
  actionErrorFilterState.target[target] = checked;
3136
3569
  dispatch({
3137
3570
  type: ActionType.UpdateActionErrorFilterState,
3138
3571
  actionErrorFilterState,
3139
3572
  });
3140
- }, noMarginOnRight: i === Object.keys((_a = LogMetadata.Target) !== null && _a !== void 0 ? _a : {}).length - 1 }));
3141
- })))),
3573
+ } }));
3574
+ }))))),
3142
3575
  (actionErrorFilterState.type === undefined
3143
3576
  || actionErrorFilterState.type === LogType$1.Error) && (React.createElement(TabBox, { title: "Error Log Details" },
3144
3577
  React.createElement("div", { className: "input-group mb-2" },
3145
3578
  React.createElement("span", { className: "input-group-text" }, "Error Message"),
3146
- React.createElement("input", { type: "text", className: "form-control", "aria-label": "query for error message", value: actionErrorFilterState.errorMessage, onChange: (e) => {
3579
+ React.createElement("input", { type: "text", className: "form-control", "aria-label": "query for error message", value: actionErrorFilterState.errorMessage, placeholder: "e.g. undefined is not a function", onChange: (e) => {
3147
3580
  actionErrorFilterState.errorMessage = e.target.value;
3148
3581
  dispatch({
3149
3582
  type: ActionType.UpdateActionErrorFilterState,
@@ -3152,7 +3585,7 @@ const LogReviewer = (props) => {
3152
3585
  } })),
3153
3586
  React.createElement("div", { className: "input-group mb-2" },
3154
3587
  React.createElement("span", { className: "input-group-text" }, "Error Code"),
3155
- React.createElement("input", { type: "text", className: "form-control", "aria-label": "query for error code", value: actionErrorFilterState.errorMessage, onChange: (e) => {
3588
+ React.createElement("input", { type: "text", className: "form-control", "aria-label": "query for error code", value: actionErrorFilterState.errorCode, placeholder: "e.g. GC22", onChange: (e) => {
3156
3589
  actionErrorFilterState.errorCode = ((e.target.value)
3157
3590
  .trim()
3158
3591
  .toUpperCase());
@@ -3165,10 +3598,10 @@ const LogReviewer = (props) => {
3165
3598
  else if (expandedFilterDrawer === FilterDrawer.Advanced) {
3166
3599
  // Create advanced filter ui
3167
3600
  filterDrawer = (React.createElement(React.Fragment, null,
3168
- React.createElement(TabBox, { title: "User Info" },
3601
+ React.createElement(TabBox, { title: "User" },
3169
3602
  React.createElement("div", { className: "input-group mb-2" },
3170
3603
  React.createElement("span", { className: "input-group-text" }, "User First Name"),
3171
- React.createElement("input", { type: "text", className: "form-control", "aria-label": "query for user first name", value: advancedFilterState.userFirstName, onChange: (e) => {
3604
+ React.createElement("input", { type: "text", className: "form-control", "aria-label": "query for user first name", value: advancedFilterState.userFirstName, placeholder: "e.g. Divardo", onChange: (e) => {
3172
3605
  advancedFilterState.userFirstName = e.target.value;
3173
3606
  dispatch({
3174
3607
  type: ActionType.UpdateAdvancedFilterState,
@@ -3177,7 +3610,7 @@ const LogReviewer = (props) => {
3177
3610
  } })),
3178
3611
  React.createElement("div", { className: "input-group mb-2" },
3179
3612
  React.createElement("span", { className: "input-group-text" }, "User Last Name"),
3180
- React.createElement("input", { type: "text", className: "form-control", "aria-label": "query for user last name", value: advancedFilterState.userLastName, onChange: (e) => {
3613
+ React.createElement("input", { type: "text", className: "form-control", "aria-label": "query for user last name", value: advancedFilterState.userLastName, placeholder: "e.g. Calicci", onChange: (e) => {
3181
3614
  advancedFilterState.userLastName = e.target.value;
3182
3615
  dispatch({
3183
3616
  type: ActionType.UpdateAdvancedFilterState,
@@ -3186,7 +3619,7 @@ const LogReviewer = (props) => {
3186
3619
  } })),
3187
3620
  React.createElement("div", { className: "input-group mb-2" },
3188
3621
  React.createElement("span", { className: "input-group-text" }, "User Email"),
3189
- React.createElement("input", { type: "text", className: "form-control", "aria-label": "query for user email", value: advancedFilterState.userEmail, onChange: (e) => {
3622
+ React.createElement("input", { type: "text", className: "form-control", "aria-label": "query for user email", value: advancedFilterState.userEmail, placeholder: "e.g. calicci@fas.harvard.edu", onChange: (e) => {
3190
3623
  advancedFilterState.userEmail = ((e.target.value)
3191
3624
  .trim());
3192
3625
  dispatch({
@@ -3196,7 +3629,7 @@ const LogReviewer = (props) => {
3196
3629
  } })),
3197
3630
  React.createElement("div", { className: "input-group mb-2" },
3198
3631
  React.createElement("span", { className: "input-group-text" }, "User Canvas Id"),
3199
- React.createElement("input", { type: "text", className: "form-control", "aria-label": "query for user canvas id", value: advancedFilterState.userId, onChange: (e) => {
3632
+ React.createElement("input", { type: "text", className: "form-control", "aria-label": "query for user canvas id", value: advancedFilterState.userId, placeholder: "e.g. 104985", onChange: (e) => {
3200
3633
  const { value } = e.target;
3201
3634
  // Only update if value contains only numbers
3202
3635
  if (/^\d+$/.test(value)) {
@@ -3230,10 +3663,10 @@ const LogReviewer = (props) => {
3230
3663
  advancedFilterState,
3231
3664
  });
3232
3665
  }, checked: advancedFilterState.includeAdmins, ariaLabel: "show logs from admins" }))),
3233
- React.createElement(TabBox, { title: "Course Info" },
3666
+ React.createElement(TabBox, { title: "Course" },
3234
3667
  React.createElement("div", { className: "input-group mb-2" },
3235
3668
  React.createElement("span", { className: "input-group-text" }, "Course Name"),
3236
- React.createElement("input", { type: "text", className: "form-control", "aria-label": "query for course name", value: advancedFilterState.courseName, onChange: (e) => {
3669
+ React.createElement("input", { type: "text", className: "form-control", "aria-label": "query for course name", value: advancedFilterState.courseName, placeholder: "e.g. GLC 200", onChange: (e) => {
3237
3670
  advancedFilterState.courseName = e.target.value;
3238
3671
  dispatch({
3239
3672
  type: ActionType.UpdateAdvancedFilterState,
@@ -3242,7 +3675,7 @@ const LogReviewer = (props) => {
3242
3675
  } })),
3243
3676
  React.createElement("div", { className: "input-group mb-2" },
3244
3677
  React.createElement("span", { className: "input-group-text" }, "Course Canvas Id"),
3245
- React.createElement("input", { type: "text", className: "form-control", "aria-label": "query for course canvas id", value: advancedFilterState.courseId, onChange: (e) => {
3678
+ React.createElement("input", { type: "text", className: "form-control", "aria-label": "query for course canvas id", value: advancedFilterState.courseId, placeholder: "e.g. 15948", onChange: (e) => {
3246
3679
  const { value } = e.target;
3247
3680
  // Only update if value contains only numbers
3248
3681
  if (/^\d+$/.test(value)) {
@@ -3254,7 +3687,7 @@ const LogReviewer = (props) => {
3254
3687
  advancedFilterState,
3255
3688
  });
3256
3689
  } }))),
3257
- React.createElement(TabBox, { title: "Device Info" },
3690
+ React.createElement(TabBox, { title: "Device" },
3258
3691
  React.createElement(ButtonInputGroup, { label: "Device Type" },
3259
3692
  React.createElement(RadioButton, { text: "All Devices", ariaLabel: "show logs from all devices", selected: advancedFilterState.isMobile === undefined, onSelected: () => {
3260
3693
  advancedFilterState.isMobile = undefined;
@@ -3300,32 +3733,33 @@ const LogReviewer = (props) => {
3300
3733
  advancedFilterState,
3301
3734
  });
3302
3735
  }, noMarginOnRight: true })),
3303
- React.createElement("div", { className: "input-group mb-2" },
3304
- React.createElement("span", { className: "input-group-text" }, "Server Route Path"),
3305
- React.createElement("input", { type: "text", className: "form-control", "aria-label": "query for server route path", placeholder: "e.g. /api/ttm/courses/12345", value: advancedFilterState.routePath, onChange: (e) => {
3306
- advancedFilterState.courseName = ((e.target.value)
3307
- .trim());
3308
- dispatch({
3309
- type: ActionType.UpdateAdvancedFilterState,
3310
- advancedFilterState,
3311
- });
3312
- } })),
3313
- React.createElement("div", { className: "input-group mb-2" },
3314
- React.createElement("span", { className: "input-group-text" }, "Server Route Template"),
3315
- React.createElement("input", { type: "text", className: "form-control", "aria-label": "query for server route template", value: advancedFilterState.routeTemplate, placeholder: "e.g. /api/ttm/courses/:courseId", onChange: (e) => {
3316
- advancedFilterState.courseName = ((e.target.value)
3317
- .trim());
3318
- dispatch({
3319
- type: ActionType.UpdateAdvancedFilterState,
3320
- advancedFilterState,
3321
- });
3322
- } })))));
3736
+ advancedFilterState.source !== LogSource$1.Client && (React.createElement("div", { className: "mt-2" },
3737
+ React.createElement("div", { className: "input-group mb-2" },
3738
+ React.createElement("span", { className: "input-group-text" }, "Server Route Path"),
3739
+ React.createElement("input", { type: "text", className: "form-control", "aria-label": "query for server route path", value: advancedFilterState.routePath, placeholder: "e.g. /api/ttm/courses/12345", onChange: (e) => {
3740
+ advancedFilterState.courseName = ((e.target.value)
3741
+ .trim());
3742
+ dispatch({
3743
+ type: ActionType.UpdateAdvancedFilterState,
3744
+ advancedFilterState,
3745
+ });
3746
+ } })),
3747
+ React.createElement("div", { className: "input-group mb-2" },
3748
+ React.createElement("span", { className: "input-group-text" }, "Server Route Template"),
3749
+ React.createElement("input", { type: "text", className: "form-control", "aria-label": "query for server route template", value: advancedFilterState.routeTemplate, placeholder: "e.g. /api/ttm/courses/:courseId", onChange: (e) => {
3750
+ advancedFilterState.courseName = ((e.target.value)
3751
+ .trim());
3752
+ dispatch({
3753
+ type: ActionType.UpdateAdvancedFilterState,
3754
+ advancedFilterState,
3755
+ });
3756
+ } })))))));
3323
3757
  }
3324
3758
  }
3325
3759
  // Filters UI
3326
3760
  const filters = (React.createElement(React.Fragment, null,
3327
3761
  filterToggles,
3328
- filterDrawer && (React.createElement(Drawer, null, filterDrawer))));
3762
+ filterDrawer && (React.createElement(Drawer, { customBackgroundColor: "#eee" }, filterDrawer))));
3329
3763
  // Actually filter the logs
3330
3764
  // > Perform filters
3331
3765
  const logs = [];
@@ -3366,16 +3800,22 @@ const LogReviewer = (props) => {
3366
3800
  // Whole context is deselected
3367
3801
  contextFilterState[log.context] === false
3368
3802
  // None of the subcontexts are selected
3369
- || (Object.values((_a = contextFilterState[log.context]) !== null && _a !== void 0 ? _a : {})
3370
- .every((isSelected) => {
3371
- return !isSelected;
3372
- }))) {
3803
+ || (
3804
+ // Has subcontexts
3805
+ typeof contextFilterState[log.context] !== 'boolean'
3806
+ // None of the subcontexts are selected
3807
+ && Object.values((_a = contextFilterState[log.context]) !== null && _a !== void 0 ? _a : {})
3808
+ .every((isSelected) => {
3809
+ return !isSelected;
3810
+ }))) {
3373
3811
  return;
3374
3812
  }
3375
3813
  // Subcontext doesn't match
3376
3814
  if (
3377
- // Log has a subcontext
3378
- log.subcontext
3815
+ // Log context is not "uncategorized" (no point in further filters)
3816
+ log.context !== LogBuiltInMetadata.Context.Uncategorized
3817
+ // Log has a subcontext
3818
+ && log.subcontext
3379
3819
  // Context has subcontexts
3380
3820
  && (contextFilterState[log.context]
3381
3821
  && contextFilterState[log.context] !== false
@@ -3387,12 +3827,16 @@ const LogReviewer = (props) => {
3387
3827
  /* -------------- Tags -------------- */
3388
3828
  // No tags match
3389
3829
  if (
3390
- // Log has at least one tag
3391
- log.tags.length > 0
3830
+ // At least one tag is required
3831
+ Object.values(tagFilterState)
3832
+ .filter((isSelected) => {
3833
+ return isSelected;
3834
+ })
3835
+ .length > 0
3392
3836
  // No tags match
3393
- && (log.tags.every((tag) => {
3837
+ && log.tags.every((tag) => {
3394
3838
  return !tagFilterState[tag];
3395
- }))) {
3839
+ })) {
3396
3840
  return;
3397
3841
  }
3398
3842
  /* ------- Actions and Errors ------- */
@@ -3526,7 +3970,7 @@ const LogReviewer = (props) => {
3526
3970
  // Device info exists
3527
3971
  && log.device
3528
3972
  // Mobile filter doesn't match
3529
- && (advancedFilterState.isMobile === log.device.isMobile)) {
3973
+ && (advancedFilterState.isMobile !== log.device.isMobile)) {
3530
3974
  return;
3531
3975
  }
3532
3976
  // Log source doesn't match
@@ -3568,205 +4012,20 @@ const LogReviewer = (props) => {
3568
4012
  /*----------------------------------------*/
3569
4013
  /* Data */
3570
4014
  /*----------------------------------------*/
3571
- // Create data table
3572
- const columns = [
3573
- {
3574
- title: 'First Name',
3575
- param: 'userFirstName',
3576
- type: ParamType$1.String,
3577
- },
3578
- {
3579
- title: 'Last Name',
3580
- param: 'userLastName',
3581
- type: ParamType$1.String,
3582
- },
3583
- {
3584
- title: 'Email',
3585
- param: 'userEmail',
3586
- type: ParamType$1.String,
3587
- },
3588
- {
3589
- title: 'Canvas Id',
3590
- param: 'userId',
3591
- type: ParamType$1.Int,
3592
- },
3593
- {
3594
- title: 'Student',
3595
- param: 'isLearner',
3596
- type: ParamType$1.Boolean,
3597
- },
3598
- {
3599
- title: 'Teaching Staff',
3600
- param: 'isTTM',
3601
- type: ParamType$1.Boolean,
3602
- startsHidden: true,
3603
- },
3604
- {
3605
- title: 'Admin',
3606
- param: 'isAdmin',
3607
- type: ParamType$1.Boolean,
3608
- startsHidden: true,
3609
- },
3610
- {
3611
- title: 'Course Canvas Id',
3612
- param: 'courseId',
3613
- type: ParamType$1.Int,
3614
- startsHidden: true,
3615
- },
3616
- {
3617
- title: 'Course Name',
3618
- param: 'courseName',
3619
- type: ParamType$1.String,
3620
- },
3621
- {
3622
- title: 'Browser Name',
3623
- param: 'browser.name',
3624
- type: ParamType$1.String,
3625
- startsHidden: true,
3626
- },
3627
- {
3628
- title: 'Browser Version',
3629
- param: 'browser.version',
3630
- type: ParamType$1.String,
3631
- startsHidden: true,
3632
- },
3633
- {
3634
- title: 'OS',
3635
- param: 'device.os',
3636
- type: ParamType$1.String,
3637
- startsHidden: true,
3638
- },
3639
- {
3640
- title: 'Mobile',
3641
- param: 'device.isMobile',
3642
- type: ParamType$1.Boolean,
3643
- startsHidden: true,
3644
- },
3645
- {
3646
- title: 'Year',
3647
- param: 'year',
3648
- type: ParamType$1.Int,
3649
- },
3650
- {
3651
- title: 'Month',
3652
- param: 'month',
3653
- type: ParamType$1.Int,
3654
- },
3655
- {
3656
- title: 'Day',
3657
- param: 'day',
3658
- type: ParamType$1.Int,
3659
- },
3660
- {
3661
- title: 'Hour',
3662
- param: 'hour',
3663
- type: ParamType$1.Int,
3664
- },
3665
- {
3666
- title: 'Minute',
3667
- param: 'minute',
3668
- type: ParamType$1.Int,
3669
- startsHidden: true,
3670
- },
3671
- {
3672
- title: 'Timestamp',
3673
- param: 'timestamp',
3674
- type: ParamType$1.Int,
3675
- startsHidden: true,
3676
- },
3677
- {
3678
- title: 'Context',
3679
- param: 'context',
3680
- type: ParamType$1.String,
3681
- },
3682
- {
3683
- title: 'Subcontext',
3684
- param: 'subcontext',
3685
- type: ParamType$1.String,
3686
- },
3687
- {
3688
- title: 'Tags',
3689
- param: 'tags',
3690
- type: ParamType$1.JSON,
3691
- startsHidden: true,
3692
- },
3693
- {
3694
- title: 'Log Level',
3695
- param: 'level',
3696
- type: ParamType$1.String,
3697
- startsHidden: true,
3698
- },
3699
- {
3700
- title: 'Metadata',
3701
- param: 'metadata',
3702
- type: ParamType$1.JSON,
3703
- startsHidden: true,
3704
- },
3705
- {
3706
- title: 'Source',
3707
- param: 'source',
3708
- type: ParamType$1.String,
3709
- },
3710
- {
3711
- title: 'Server Route Path',
3712
- param: 'routePath',
3713
- type: ParamType$1.String,
3714
- startsHidden: true,
3715
- },
3716
- {
3717
- title: 'Server Route Template',
3718
- param: 'routeTemplate',
3719
- type: ParamType$1.String,
3720
- startsHidden: true,
3721
- },
3722
- {
3723
- title: 'Type',
3724
- param: 'type',
3725
- type: ParamType$1.String,
3726
- },
3727
- {
3728
- title: 'Error Message',
3729
- param: 'errorMessage',
3730
- type: ParamType$1.String,
3731
- startsHidden: true,
3732
- },
3733
- {
3734
- title: 'Error Code',
3735
- param: 'errorCode',
3736
- type: ParamType$1.String,
3737
- startsHidden: true,
3738
- },
3739
- {
3740
- title: 'Error Stack',
3741
- param: 'errorStack',
3742
- type: ParamType$1.String,
3743
- startsHidden: true,
3744
- },
3745
- {
3746
- title: 'Action Target',
3747
- param: 'target',
3748
- type: ParamType$1.String,
3749
- startsHidden: true,
3750
- },
3751
- {
3752
- title: 'Action Type',
3753
- param: 'action',
3754
- type: ParamType$1.String,
3755
- startsHidden: true,
3756
- },
3757
- ];
4015
+ // Nothing to show notice
4016
+ const noLogsNotice = (logs.length === 0
4017
+ ? (React.createElement("div", { className: "alert alert-warning text-center mt-2" },
4018
+ React.createElement("h4", { className: "m-1" }, "No Logs to Show"),
4019
+ React.createElement("div", null, "Either your filters are too strict or no matching logs have been created yet.")))
4020
+ : undefined);
3758
4021
  // Create intelliTable
3759
- const dataTable = (logs.length === 0
3760
- ? (React.createElement(React.Fragment, null,
3761
- React.createElement("h3", { className: "m-0" }, "Matching Logs:"),
3762
- React.createElement("div", { className: "alert alert-warning text-center" },
3763
- React.createElement("h4", { className: "m-1" }, "No Logs to Show"),
3764
- React.createElement("div", null, "Either your filters are too strict or no matching logs have been created yet."))))
3765
- : (React.createElement(IntelliTable, { title: "Matching Logs:", id: "logs", data: logs, columns: columns })));
4022
+ const dataTable = (React.createElement(IntelliTable, { title: "Matching Logs:", csvName: `Logs from ${getHumanReadableDate()}`, id: "logs", data: logs, columns: columns }));
3766
4023
  // Main body
3767
4024
  body = (React.createElement(React.Fragment, null,
3768
4025
  filters,
3769
- dataTable));
4026
+ React.createElement("div", { className: "mt-2" },
4027
+ dataTable,
4028
+ noLogsNotice)));
3770
4029
  }
3771
4030
  /* ---------- Wrap in Modal --------- */
3772
4031
  return (React.createElement("div", { className: "LogReviewer-outer-container" },
@@ -3775,8 +4034,9 @@ const LogReviewer = (props) => {
3775
4034
  React.createElement("div", { className: "LogReviewer-header" },
3776
4035
  React.createElement("div", { className: "LogReviewer-header-title" },
3777
4036
  React.createElement("h3", { className: "text-center m-0" }, "Log Review Dashboard")),
3778
- React.createElement("button", { type: "button", className: "LogReviewer-header-close-button btn btn-dark btn-lg", "aria-label": "close log reviewer panel", onClick: onClose },
3779
- React.createElement(FontAwesomeIcon, { icon: faTimes }))),
4037
+ React.createElement("div", { style: { width: 0 } },
4038
+ React.createElement("button", { type: "button", className: "LogReviewer-header-close-button btn btn-dark btn-lg pe-0", "aria-label": "close log reviewer panel", onClick: onClose },
4039
+ React.createElement(FontAwesomeIcon, { icon: faTimes })))),
3780
4040
  React.createElement("div", { className: "LogReviewer-contents" }, body))));
3781
4041
  };
3782
4042
 
@@ -3798,6 +4058,60 @@ const HOUR_IN_MS = 3600000;
3798
4058
  */
3799
4059
  const DAY_IN_MS = 86400000;
3800
4060
 
4061
+ // True if user is on mobile or tablet
4062
+ let cachedResult = undefined;
4063
+ /**
4064
+ * Check if the user is on a mobile device or tablet
4065
+ * from https://stackoverflow.com/questions/11381673/detecting-a-mobile-browser
4066
+ * @returns true if user is on a mobile device
4067
+ */
4068
+ const isMobileOrTablet = () => {
4069
+ var _a;
4070
+ if (cachedResult !== undefined) {
4071
+ return cachedResult;
4072
+ }
4073
+ try {
4074
+ let check = false;
4075
+ ((a) => { if (/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test(a) || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0, 4)))
4076
+ check = true; })((navigator !== null && navigator !== void 0 ? navigator : {}).userAgent || (navigator !== null && navigator !== void 0 ? navigator : {}).vendor || ((_a = window) !== null && _a !== void 0 ? _a : {}).opera);
4077
+ cachedResult = check;
4078
+ return cachedResult;
4079
+ }
4080
+ catch (err) {
4081
+ // Probably this code is being executed on a server
4082
+ cachedResult = false;
4083
+ return cachedResult;
4084
+ }
4085
+ };
4086
+
4087
+ // Import shared types
4088
+ // Check if mobile
4089
+ const isMobile = isMobileOrTablet();
4090
+ /**
4091
+ * Dynamic words determined by the user's platform
4092
+ * @author Gabe Abrams
4093
+ */
4094
+ const DynamicWord = {
4095
+ Click: (isMobile
4096
+ ? 'tap'
4097
+ : 'click'),
4098
+ ClickCapitalized: (isMobile
4099
+ ? 'Tap'
4100
+ : 'Click'),
4101
+ App: (isMobile
4102
+ ? 'app'
4103
+ : 'application'),
4104
+ AppCapitalized: (isMobile
4105
+ ? 'App'
4106
+ : 'Application'),
4107
+ Device: (isMobile
4108
+ ? 'device'
4109
+ : 'computer'),
4110
+ DeviceCapitalized: (isMobile
4111
+ ? 'Device'
4112
+ : 'Computer'),
4113
+ };
4114
+
3801
4115
  /**
3802
4116
  * Shorten text so it fits into a certain number of chars
3803
4117
  * @author Gabe Abrams
@@ -3872,6 +4186,18 @@ const floorToNumDecimals = (num, numDecimals) => {
3872
4186
  return (Math.floor(num * rounder) / rounder);
3873
4187
  };
3874
4188
 
4189
+ /**
4190
+ * Force a number to stay within specific bounds
4191
+ * @author Gabe Abrams
4192
+ * @param num the number to move into the bounds
4193
+ * @param min the minimum number in the bound
4194
+ * @param max the maximum number in the bound
4195
+ * @returns bounded number
4196
+ */
4197
+ const forceNumIntoBounds = (num, min, max) => {
4198
+ return Math.max(min, Math.min(max, num));
4199
+ };
4200
+
3875
4201
  /**
3876
4202
  * Pad a number's decimal with zeros on the right
3877
4203
  * (e.g. 5.2 becomes 5.20 with 2 digit padding)
@@ -4107,15 +4433,17 @@ const initServer = (opts) => {
4107
4433
  },
4108
4434
  handler: ({ params }) => __awaiter(void 0, void 0, void 0, function* () {
4109
4435
  // Get user info
4110
- const { userId, isAdmin } = params;
4436
+ const { year, month, userId, isAdmin, } = params;
4111
4437
  // Validate user
4112
- // isAdmin is already checked because path starts with '/admin'
4113
4438
  const canReview = yield canReviewLogs(userId, isAdmin);
4114
4439
  if (!canReview) {
4115
4440
  throw new ErrorWithCode('You cannot access this resource because you do not have the appropriate permissions.', ReactKitErrorCode$1.NotAllowedToReviewLogs);
4116
4441
  }
4117
4442
  // Query for logs
4118
- const logs = yield _logCollection.find({ userId });
4443
+ const logs = yield _logCollection.find({
4444
+ year,
4445
+ month,
4446
+ });
4119
4447
  // Return logs
4120
4448
  return logs;
4121
4449
  }),
@@ -4764,7 +5092,7 @@ const genRouteHandler = (opts) => {
4764
5092
  isAdmin: !!launchInfo.isAdmin,
4765
5093
  isTTM: !!launchInfo.isTTM,
4766
5094
  courseId: launchInfo.courseId,
4767
- courseName: launchInfo.courseName,
5095
+ courseName: launchInfo.contextLabel,
4768
5096
  browser,
4769
5097
  device,
4770
5098
  year,
@@ -4791,7 +5119,7 @@ const genRouteHandler = (opts) => {
4791
5119
  }
4792
5120
  : {
4793
5121
  type: LogType$1.Action,
4794
- target: ((_m = opts.target) !== null && _m !== void 0 ? _m : LogBuiltInMetadata.Target.NoSpecificTarget),
5122
+ target: ((_m = opts.target) !== null && _m !== void 0 ? _m : LogBuiltInMetadata.Target.NoTarget),
4795
5123
  action: ((_o = opts.action) !== null && _o !== void 0 ? _o : LogAction$1.Unknown),
4796
5124
  });
4797
5125
  // Source-specific info
@@ -5004,29 +5332,6 @@ const startMinWait = (minWaitMs) => {
5004
5332
  });
5005
5333
  };
5006
5334
 
5007
- // Import shared helpers
5008
- /**
5009
- * Get a human-readable description of a date (all in ET)
5010
- * @author Gabe Abrams
5011
- * @param [dateOrTimestamp=today] the date or timestamp for the date to describe
5012
- * @returns human-readable description of the date
5013
- */
5014
- const getHumanReadableDate = (dateOrTimestamp) => {
5015
- // Get the date info
5016
- const { month, day, year, } = getTimeInfoInET(dateOrTimestamp);
5017
- const currYear = getTimeInfoInET().year;
5018
- // Get the short month description
5019
- const monthName = getMonthName(month).short;
5020
- // Create start of description
5021
- let description = `${monthName} ${day}${getOrdinal(day)}`;
5022
- // Add on year if it's different
5023
- if (year !== currYear) {
5024
- description += ` ${year}`;
5025
- }
5026
- // Return description
5027
- return description;
5028
- };
5029
-
5030
5335
  /**
5031
5336
  * Get the current part of day (morning, evening, etc.)
5032
5337
  * @author Gabe Abrams
@@ -5199,5 +5504,5 @@ var DayOfWeek;
5199
5504
  })(DayOfWeek || (DayOfWeek = {}));
5200
5505
  var DayOfWeek$1 = DayOfWeek;
5201
5506
 
5202
- export { AppWrapper, ButtonInputGroup, CSVDownloadButton, CheckboxButton, CopiableBox, DAY_IN_MS, DayOfWeek$1 as DayOfWeek, Drawer, 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, ParamType$1 as ParamType, PopFailureMark, PopPendingMark, PopSuccessMark, RadioButton, ReactKitErrorCode$1 as ReactKitErrorCode, SimpleDateChooser, TabBox, Variant$1 as Variant, abbreviate, alert$1 as alert, avg, canReviewLogs, ceilToNumDecimals, confirm, floorToNumDecimals, forceNumIntoBounds, genCSV, genRouteHandler, getHumanReadableDate, getMonthName, getOrdinal, getPartOfDay, getTimeInfoInET, handleError, handleSuccess, initLogCollection, initServer, logClientEvent, onlyKeepLetters, padDecimalZeros, padZerosLeft, parallelLimit, roundToNumDecimals, showFatalError, startMinWait, stringsToHumanReadableList, stubServerEndpoint, sum, visitServerEndpoint, waitMs };
5507
+ export { AppWrapper, ButtonInputGroup, CSVDownloadButton, CheckboxButton, CopiableBox, DAY_IN_MS, DayOfWeek$1 as DayOfWeek, Drawer, 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, ParamType$1 as ParamType, PopFailureMark, PopPendingMark, PopSuccessMark, RadioButton, ReactKitErrorCode$1 as ReactKitErrorCode, SimpleDateChooser, TabBox, Variant$1 as Variant, abbreviate, alert$1 as alert, avg, canReviewLogs, ceilToNumDecimals, confirm, floorToNumDecimals, forceNumIntoBounds, genCSV, genRouteHandler, getHumanReadableDate, getMonthName, getOrdinal, getPartOfDay, getTimeInfoInET, handleError, handleSuccess, initLogCollection, initServer, isMobileOrTablet, logClientEvent, onlyKeepLetters, padDecimalZeros, padZerosLeft, parallelLimit, roundToNumDecimals, showFatalError, startMinWait, stringsToHumanReadableList, stubServerEndpoint, sum, visitServerEndpoint, waitMs };
5203
5508
  //# sourceMappingURL=index.js.map