dce-reactkit 3.2.2-beta.9 → 3.2.3

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 (47) hide show
  1. package/dist/cjs/index.js +731 -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/index.d.ts +2 -1
  8. package/dist/cjs/types/types/LogAction.d.ts +16 -16
  9. package/dist/cjs/types/types/LogBuiltInMetadata.d.ts +1 -1
  10. package/dist/cjs/types/types/LogMetadataContextMap.d.ts +11 -0
  11. package/dist/cjs/types/types/LogMetadataTagMap.d.ts +8 -0
  12. package/dist/cjs/types/types/LogMetadataTargetMap.d.ts +8 -0
  13. package/dist/cjs/types/types/LogMetadataType.d.ts +6 -12
  14. package/dist/esm/index.js +732 -437
  15. package/dist/esm/index.js.map +1 -1
  16. package/dist/esm/types/components/Drawer.d.ts +1 -0
  17. package/dist/esm/types/components/IntelliTable.d.ts +1 -0
  18. package/dist/esm/types/components/ItemPicker/NestableItemList.d.ts +1 -0
  19. package/dist/esm/types/dynamicConstants/DynamicWord.d.ts +13 -0
  20. package/dist/esm/types/index.d.ts +2 -1
  21. package/dist/esm/types/types/LogAction.d.ts +16 -16
  22. package/dist/esm/types/types/LogBuiltInMetadata.d.ts +1 -1
  23. package/dist/esm/types/types/LogMetadataContextMap.d.ts +11 -0
  24. package/dist/esm/types/types/LogMetadataTagMap.d.ts +8 -0
  25. package/dist/esm/types/types/LogMetadataTargetMap.d.ts +8 -0
  26. package/dist/esm/types/types/LogMetadataType.d.ts +6 -12
  27. package/dist/index.d.ts +63 -30
  28. package/package.json +1 -1
  29. package/src/components/ButtonInputGroup.tsx +1 -1
  30. package/src/components/CSVDownloadButton.tsx +2 -2
  31. package/src/components/Drawer.tsx +9 -1
  32. package/src/components/IntelliTable.tsx +178 -23
  33. package/src/components/ItemPicker/NestableItemList.tsx +32 -14
  34. package/src/components/LogReviewer.tsx +584 -424
  35. package/src/components/SimpleDateChooser.tsx +30 -25
  36. package/src/dynamicConstants/DynamicWord.tsx +55 -0
  37. package/src/helpers/genCSV.ts +22 -4
  38. package/src/helpers/genRouteHandler.ts +2 -2
  39. package/src/helpers/logClientEvent.tsx +1 -1
  40. package/src/index.ts +5 -0
  41. package/src/server/initServer.ts +10 -3
  42. package/src/types/LogAction.ts +16 -16
  43. package/src/types/LogBuiltInMetadata.ts +5 -5
  44. package/src/types/LogMetadataContextMap.ts +15 -0
  45. package/src/types/LogMetadataTagMap.ts +9 -0
  46. package/src/types/LogMetadataTargetMap.ts +9 -0
  47. package/src/types/LogMetadataType.ts +8 -15
package/dist/cjs/index.js CHANGED
@@ -519,14 +519,14 @@ const LOG_ROUTE_PATH = `${ROUTE_PATH_PREFIX}/log`;
519
519
  const LogBuiltInMetadata = {
520
520
  // Contexts
521
521
  Context: {
522
- Uncategorized: 'n/a',
523
- ServerRenderedErrorPage: '_server-rendered-error-page',
524
- ServerEndpointError: '_server-endpoint-error',
525
- ClientFatalError: '_client-fatal-error',
522
+ Uncategorized: 'Uncategorized',
523
+ ServerRenderedErrorPage: 'ServerRenderedErrorPage',
524
+ ServerEndpointError: 'ServerEndpointError',
525
+ ClientFatalError: 'ClientFatalError',
526
526
  },
527
527
  // Targets
528
528
  Target: {
529
- NoSpecificTarget: 'n/a',
529
+ NoTarget: 'NoTarget',
530
530
  },
531
531
  };
532
532
 
@@ -682,7 +682,7 @@ const logClientEvent = (opts) => __awaiter(void 0, void 0, void 0, function* ()
682
682
  ? opts.error.stack
683
683
  : undefined),
684
684
  target: (opts.action
685
- ? ((_g = opts.target) !== null && _g !== void 0 ? _g : LogBuiltInMetadata.Target.NoSpecificTarget)
685
+ ? ((_g = opts.target) !== null && _g !== void 0 ? _g : LogBuiltInMetadata.Target.NoTarget)
686
686
  : undefined),
687
687
  action: (opts.action
688
688
  ? opts.action
@@ -1202,7 +1202,7 @@ const ButtonInputGroup = (props) => {
1202
1202
  borderTopRightRadius: 0,
1203
1203
  borderBottomRightRadius: 0,
1204
1204
  } }, label),
1205
- React__default["default"].createElement("span", { className: "input-group-text flex-grow-1 rounded-right", style: {
1205
+ React__default["default"].createElement("span", { className: "input-group-text flex-grow-1 rounded-right d-flex flex-wrap", style: {
1206
1206
  backgroundColor: 'white',
1207
1207
  borderTopLeftRadius: 0,
1208
1208
  borderBottomLeftRadius: 0,
@@ -1307,18 +1307,6 @@ const getTimeInfoInET = (dateOrTimestamp) => {
1307
1307
  };
1308
1308
  };
1309
1309
 
1310
- /**
1311
- * Force a number to stay within specific bounds
1312
- * @author Gabe Abrams
1313
- * @param num the number to move into the bounds
1314
- * @param min the minimum number in the bound
1315
- * @param max the maximum number in the bound
1316
- * @returns bounded number
1317
- */
1318
- const forceNumIntoBounds = (num, min, max) => {
1319
- return Math.max(min, Math.min(max, num));
1320
- };
1321
-
1322
1310
  /**
1323
1311
  * A very simple, lightweight date chooser
1324
1312
  * @author Gabe Abrams
@@ -1333,7 +1321,7 @@ const SimpleDateChooser = (props) => {
1333
1321
  var _a;
1334
1322
  /* -------------- Props ------------- */
1335
1323
  const { ariaLabel, name, month, day, year, onChange, chooseFromPast, } = props;
1336
- let numMonthsToShow = forceNumIntoBounds(((_a = props.numMonthsToShow) !== null && _a !== void 0 ? _a : 6), 1, 12);
1324
+ const numMonthsToShow = ((_a = props.numMonthsToShow) !== null && _a !== void 0 ? _a : 6);
1337
1325
  /*------------------------------------------------------------------------*/
1338
1326
  /* Render */
1339
1327
  /*------------------------------------------------------------------------*/
@@ -1347,7 +1335,7 @@ const SimpleDateChooser = (props) => {
1347
1335
  let startMonth = today.month;
1348
1336
  if (chooseFromPast) {
1349
1337
  startMonth -= Math.max(0, numMonthsToShow - 1);
1350
- if (startMonth <= 0) {
1338
+ while (startMonth <= 0) {
1351
1339
  startMonth += 12;
1352
1340
  startYear -= 1;
1353
1341
  }
@@ -1355,22 +1343,35 @@ const SimpleDateChooser = (props) => {
1355
1343
  for (let i = 0; i < numMonthsToShow; i++) {
1356
1344
  // Get month and year info
1357
1345
  const unmoddedMonth = (startMonth + i);
1358
- const month = (unmoddedMonth > 12
1359
- ? unmoddedMonth - 12
1360
- : unmoddedMonth);
1346
+ let month = unmoddedMonth;
1347
+ while (month > 12) {
1348
+ month -= 12;
1349
+ }
1361
1350
  const monthName = getMonthName(month).full;
1362
- const year = (unmoddedMonth > 12
1363
- ? startYear + 1
1364
- : startYear);
1351
+ // Year is start year +1 for each 12 months
1352
+ const year = (startYear + (Math.floor(unmoddedMonth / 12)));
1365
1353
  // Figure out which days are allowed
1366
1354
  const days = [];
1367
1355
  const numDaysInMonth = (new Date(year, month, 0)).getDate();
1368
- const firstDay = (month === today.month
1369
- ? today.day // Current month: start at current date
1370
- : 1 // Future month: start at beginning of month
1371
- );
1372
- for (let day = firstDay; day <= numDaysInMonth; day++) {
1373
- days.push(day);
1356
+ if (chooseFromPast) {
1357
+ // Past selection
1358
+ const numDaysToAdd = ((month === today.month)
1359
+ ? today.day // Current month, only add up to today
1360
+ : numDaysInMonth // Past month, add all days
1361
+ );
1362
+ for (let day = 1; day <= numDaysToAdd; day++) {
1363
+ days.push(day);
1364
+ }
1365
+ }
1366
+ else {
1367
+ // Future selection: add all remaining days of the month
1368
+ const firstDay = (month === today.month
1369
+ ? today.day // Current month: start at current date
1370
+ : 1 // Future month: start at beginning of month
1371
+ );
1372
+ for (let day = firstDay; day <= numDaysInMonth; day++) {
1373
+ days.push(day);
1374
+ }
1374
1375
  }
1375
1376
  choices.push({
1376
1377
  choiceName: `${monthName} ${year}`,
@@ -1384,7 +1385,7 @@ const SimpleDateChooser = (props) => {
1384
1385
  const dayOptions = [];
1385
1386
  choices.forEach((choice) => {
1386
1387
  // Create month option
1387
- monthOptions.push(React__default["default"].createElement("option", { key: choice.month, value: choice.month, "aria-label": `choose ${choice.choiceName}`, onSelect: () => {
1388
+ monthOptions.push(React__default["default"].createElement("option", { key: choice.month, value: `${choice.month}-${choice.year}`, "aria-label": `choose ${choice.choiceName}`, onSelect: () => {
1388
1389
  onChange(choice.month, choice.days[0], choice.year);
1389
1390
  } }, choice.choiceName));
1390
1391
  if (month === choice.month) {
@@ -1399,7 +1400,7 @@ const SimpleDateChooser = (props) => {
1399
1400
  }
1400
1401
  });
1401
1402
  return (React__default["default"].createElement("div", { className: "SimpleDateChooser d-inline-block", "aria-label": `date chooser with selected date: ${month} ${day}, ${year}` },
1402
- React__default["default"].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) => {
1403
+ React__default["default"].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) => {
1403
1404
  const choice = choices[e.target.selectedIndex];
1404
1405
  // Change day, month, and year
1405
1406
  onChange(choice.month, choice.days[0], choice.year);
@@ -1441,14 +1442,16 @@ const Drawer = (props) => {
1441
1442
  /*------------------------------------------------------------------------*/
1442
1443
  /* -------------- Props ------------- */
1443
1444
  // Destructure all props
1444
- const { children, } = props;
1445
+ const { customBackgroundColor, children, } = props;
1445
1446
  /*------------------------------------------------------------------------*/
1446
1447
  /* Render */
1447
1448
  /*------------------------------------------------------------------------*/
1448
1449
  /*----------------------------------------*/
1449
1450
  /* Main UI */
1450
1451
  /*----------------------------------------*/
1451
- return (React__default["default"].createElement("div", { className: "Drawer-container" },
1452
+ return (React__default["default"].createElement("div", { className: "Drawer-container", style: {
1453
+ backgroundColor: (customBackgroundColor !== null && customBackgroundColor !== void 0 ? customBackgroundColor : undefined),
1454
+ } },
1452
1455
  React__default["default"].createElement("style", null, style$4),
1453
1456
  children));
1454
1457
  };
@@ -1911,24 +1914,25 @@ const CopiableBox = (props) => {
1911
1914
  /**
1912
1915
  * Reusable nested item picker
1913
1916
  * @author Yuen Ler Chow
1917
+ * @author Gabe Abrams
1914
1918
  */
1915
1919
  /* ------------- Actions ------------ */
1916
1920
  // Types of actions
1917
1921
  var ActionType$2;
1918
1922
  (function (ActionType) {
1919
- // Toggle whether the children are being shown
1920
- ActionType["ToggleItems"] = "toggle-items";
1923
+ // Toggle whether a child are being shown
1924
+ ActionType["ToggleChild"] = "toggle-child";
1921
1925
  })(ActionType$2 || (ActionType$2 = {}));
1922
1926
  /**
1923
1927
  * Reducer that executes actions
1924
- * @author Yuen Ler Chow
1928
+ * @author Gabe Abrams
1925
1929
  * @param state current state
1926
1930
  * @param action action to execute
1927
1931
  */
1928
1932
  const reducer$2 = (state, action) => {
1929
1933
  switch (action.type) {
1930
- case ActionType$2.ToggleItems: {
1931
- return { isShowingItems: !state.isShowingItems };
1934
+ case ActionType$2.ToggleChild: {
1935
+ return Object.assign(Object.assign({}, state), { childExpanded: Object.assign(Object.assign({}, state.childExpanded), { [String(action.id)]: !state.childExpanded[String(action.id)] }) });
1932
1936
  }
1933
1937
  default: {
1934
1938
  return state;
@@ -1946,14 +1950,19 @@ const NestableItemList = (props) => {
1946
1950
  // Destructure all props
1947
1951
  const { items, onChanged, } = props;
1948
1952
  /* -------------- State ------------- */
1953
+ // Create initial map of child expanded booleans
1954
+ const initChildExpanded = {};
1955
+ items.forEach((item) => {
1956
+ initChildExpanded[String(item.id)] = false;
1957
+ });
1949
1958
  // Initial state
1950
1959
  const initialState = {
1951
- isShowingItems: false,
1960
+ childExpanded: initChildExpanded,
1952
1961
  };
1953
1962
  // Initialize state
1954
1963
  const [state, dispatch] = React.useReducer(reducer$2, initialState);
1955
1964
  // Destructure common state
1956
- const { isShowingItems, } = state;
1965
+ const { childExpanded, } = state;
1957
1966
  /*------------------------------------------------------------------------*/
1958
1967
  /* Component Functions */
1959
1968
  /*------------------------------------------------------------------------*/
@@ -2041,14 +2050,15 @@ const NestableItemList = (props) => {
2041
2050
  backgroundColor: 'transparent',
2042
2051
  }, type: "button", onClick: () => {
2043
2052
  dispatch({
2044
- type: ActionType$2.ToggleItems,
2053
+ type: ActionType$2.ToggleChild,
2054
+ id: item.id,
2045
2055
  });
2046
- }, "aria-label": `${isShowingItems ? 'Hide' : 'Show'} items in ${item.name}` },
2047
- React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: isShowingItems ? freeSolidSvgIcons.faChevronDown : freeSolidSvgIcons.faChevronRight })))),
2056
+ }, "aria-label": `${childExpanded[item.id] ? 'Hide' : 'Show'} items in ${item.name}` },
2057
+ React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: childExpanded[item.id] ? freeSolidSvgIcons.faChevronDown : freeSolidSvgIcons.faChevronRight })))),
2048
2058
  React__default["default"].createElement(CheckboxButton, { className: `NestableItemList-CheckboxButton-${item.id}`, text: item.name, checked: item.isGroup ? allChecked(item.children) : item.checked, dashed: item.isGroup ? !noneChecked(item.children) : false, onChanged: (checked) => {
2049
2059
  onChanged(changeChecked(item.id, checked, items));
2050
2060
  }, ariaLabel: `Select ${item.name}`, checkedVariant: Variant$1.Light }),
2051
- item.isGroup && isShowingItems && (React__default["default"].createElement("div", { className: "NestableItemList-children-container", style: {
2061
+ (item.isGroup && childExpanded[item.id]) && (React__default["default"].createElement("div", { className: "NestableItemList-children-container", style: {
2052
2062
  paddingLeft: '2.2rem',
2053
2063
  } },
2054
2064
  React__default["default"].createElement(NestableItemList, { items: item.children, onChanged: (updatedItems) => {
@@ -2085,6 +2095,29 @@ const ItemPicker = (props) => {
2085
2095
  React__default["default"].createElement(NestableItemList, { items: items, onChanged: onChanged }))));
2086
2096
  };
2087
2097
 
2098
+ // Import shared helpers
2099
+ /**
2100
+ * Get a human-readable description of a date (all in ET)
2101
+ * @author Gabe Abrams
2102
+ * @param [dateOrTimestamp=today] the date or timestamp for the date to describe
2103
+ * @returns human-readable description of the date
2104
+ */
2105
+ const getHumanReadableDate = (dateOrTimestamp) => {
2106
+ // Get the date info
2107
+ const { month, day, year, } = getTimeInfoInET(dateOrTimestamp);
2108
+ const currYear = getTimeInfoInET().year;
2109
+ // Get the short month description
2110
+ const monthName = getMonthName(month).short;
2111
+ // Create start of description
2112
+ let description = `${monthName} ${day}${getOrdinal(day)}`;
2113
+ // Add on year if it's different
2114
+ if (year !== currYear) {
2115
+ description += ` ${year}`;
2116
+ }
2117
+ // Return description
2118
+ return description;
2119
+ };
2120
+
2088
2121
  /**
2089
2122
  * Path of the route for storing client-side logs
2090
2123
  * @author Gabe Abrams
@@ -2124,37 +2157,37 @@ var LogType$1 = LogType;
2124
2157
  var LogAction;
2125
2158
  (function (LogAction) {
2126
2159
  // Target was opened by the user (it was not on screen, but now it is)
2127
- LogAction["Open"] = "open";
2160
+ LogAction["Open"] = "Open";
2128
2161
  // Target was closed by the user (it was on screen, but now it is not)
2129
- LogAction["Close"] = "close";
2162
+ LogAction["Close"] = "Close";
2130
2163
  // Target was cancelled by the user (it was on closed without saving)
2131
- LogAction["Cancel"] = "cancel";
2164
+ LogAction["Cancel"] = "Cancel";
2132
2165
  // Target was expanded by the user (it always remains on screen, but size was changed)
2133
- LogAction["Expand"] = "expand";
2166
+ LogAction["Expand"] = "Expand";
2134
2167
  // Target was collapsed by the user (it always remains on screen, but size was changed)
2135
- LogAction["Collapse"] = "collapse";
2168
+ LogAction["Collapse"] = "Collapse";
2136
2169
  // Target was viewed by the user (only for items that are not opened or closed, those must use Open/Close actions)
2137
- LogAction["View"] = "view";
2170
+ LogAction["View"] = "View";
2138
2171
  // Target interrupted the user (popup, dialog, validation message, etc. appeared without user prompting)
2139
- LogAction["Interrupt"] = "interrupt";
2172
+ LogAction["Interrupt"] = "Interrupt";
2140
2173
  // Target was created by the user (it did not exist before)
2141
- LogAction["Create"] = "create";
2174
+ LogAction["Create"] = "Create";
2142
2175
  // Target was edited by the user (it existed and was changed)
2143
- LogAction["Edit"] = "edit";
2176
+ LogAction["Edit"] = "Edit";
2144
2177
  // Target was deleted by the user (it existed and now it doesn't)
2145
- LogAction["Delete"] = "delete";
2178
+ LogAction["Delete"] = "Delete";
2146
2179
  // Target was added by the user (it already existed and was added to another place)
2147
- LogAction["Add"] = "add";
2180
+ LogAction["Add"] = "Add";
2148
2181
  // Target was removed by the user (it was removed from something but still exists)
2149
- LogAction["Remove"] = "remove";
2182
+ LogAction["Remove"] = "Remove";
2150
2183
  // Target was activated by the user (click, check, tap, keypress, etc.)
2151
- LogAction["Activate"] = "activate";
2184
+ LogAction["Activate"] = "Activate";
2152
2185
  // Target was deactivated by the user (click away, uncheck, tap outside of, tab away, etc.)
2153
- LogAction["Deactivate"] = "deactivate";
2186
+ LogAction["Deactivate"] = "Deactivate";
2154
2187
  // User showed interest in a target (hover, peek, etc.)
2155
- LogAction["Peek"] = "peek";
2188
+ LogAction["Peek"] = "Peek";
2156
2189
  // Unknown action
2157
- LogAction["Unknown"] = "unknown";
2190
+ LogAction["Unknown"] = "Unknown";
2158
2191
  })(LogAction || (LogAction = {}));
2159
2192
  var LogAction$1 = LogAction;
2160
2193
 
@@ -2196,12 +2229,12 @@ const roundToNumDecimals = (num, numDecimals) => {
2196
2229
  * @returns escaped cell text
2197
2230
  */
2198
2231
  const escapeCellText = (text) => {
2199
- if (!text.includes(',')) {
2232
+ if (!String(text).includes(',')) {
2200
2233
  // No need to escape
2201
- return text;
2234
+ return String(text);
2202
2235
  }
2203
2236
  // Perform escape
2204
- return `"${text.replace(/"/g, '""')}`;
2237
+ return `"${String(text).replace(/"/g, '""')}`;
2205
2238
  };
2206
2239
  /**
2207
2240
  * Generate a CSV file
@@ -2220,9 +2253,26 @@ const genCSV = (data, columns) => {
2220
2253
  .join(','));
2221
2254
  // Add each row
2222
2255
  data.forEach((datum) => {
2256
+ csv += '\n';
2223
2257
  csv += (columns
2224
2258
  .map((column) => {
2225
- return escapeCellText(datum[column.param]);
2259
+ let contents;
2260
+ const cell = datum[column.param];
2261
+ if (typeof cell === 'string'
2262
+ || typeof cell === 'number') {
2263
+ contents = String(cell);
2264
+ }
2265
+ else if (typeof cell === 'undefined'
2266
+ || cell === null) {
2267
+ contents = '';
2268
+ }
2269
+ else if (typeof cell === 'object') {
2270
+ contents = JSON.stringify(cell);
2271
+ }
2272
+ else {
2273
+ contents = '';
2274
+ }
2275
+ return escapeCellText(contents);
2226
2276
  })
2227
2277
  .join(','));
2228
2278
  });
@@ -2251,11 +2301,12 @@ const CSVDownloadButton = (props) => {
2251
2301
  /* Main UI */
2252
2302
  /*----------------------------------------*/
2253
2303
  // Render the button
2254
- return (React__default["default"].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
2304
+ return (React__default["default"].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
2255
2305
  ? `Click to download ${filename}`
2256
2306
  : ariaLabel), style: style, onClick: onClick },
2257
2307
  !children && (React__default["default"].createElement(React__default["default"].Fragment, null,
2258
- React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faCloudDownloadAlt, className: "mr-2" }),
2308
+ React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faCloudDownloadAlt }),
2309
+ ' ',
2259
2310
  "Download CSV")),
2260
2311
  children));
2261
2312
  };
@@ -2279,9 +2330,13 @@ var ActionType$1;
2279
2330
  // Toggle sort column param
2280
2331
  ActionType["ToggleSortColumn"] = "toggle-sort-column";
2281
2332
  // Toggle the visibility of a column
2282
- ActionType["ToggleColumnVisibility"] = "toggle-column-visibility";
2333
+ ActionType["UpdateColumnVisibility"] = "update-column-visibility";
2283
2334
  // Toggle the column visibility customization modal
2284
2335
  ActionType["ToggleColVisCusModalVisibility"] = "toggle-col-vis-cus-modal-visibility";
2336
+ // Show all columns
2337
+ ActionType["ShowAllColumns"] = "show-all-columns";
2338
+ // Hide all columns
2339
+ ActionType["HideAllColumns"] = "hide-all-columns";
2285
2340
  })(ActionType$1 || (ActionType$1 = {}));
2286
2341
  /**
2287
2342
  * Reducer that executes actions
@@ -2303,14 +2358,28 @@ const reducer$1 = (state, action) => {
2303
2358
  // Stop sorting by column
2304
2359
  return Object.assign(Object.assign({}, state), { sortColumnParam: undefined, sortType: SortType.Ascending });
2305
2360
  }
2306
- case ActionType$1.ToggleColumnVisibility: {
2361
+ case ActionType$1.UpdateColumnVisibility: {
2307
2362
  const { columnVisibilityMap } = state;
2308
- columnVisibilityMap[action.param] = !columnVisibilityMap[action.param];
2363
+ columnVisibilityMap[action.param] = action.visible;
2309
2364
  return Object.assign(Object.assign({}, state), { columnVisibilityMap });
2310
2365
  }
2311
2366
  case ActionType$1.ToggleColVisCusModalVisibility: {
2312
2367
  return Object.assign(Object.assign({}, state), { columnVisibilityCustomizationModalVisible: !state.columnVisibilityCustomizationModalVisible });
2313
2368
  }
2369
+ case ActionType$1.ShowAllColumns: {
2370
+ const { columnVisibilityMap } = state;
2371
+ Object.keys(columnVisibilityMap).forEach((param) => {
2372
+ columnVisibilityMap[param] = true;
2373
+ });
2374
+ return Object.assign(Object.assign({}, state), { columnVisibilityMap });
2375
+ }
2376
+ case ActionType$1.HideAllColumns: {
2377
+ const { columnVisibilityMap } = state;
2378
+ Object.keys(columnVisibilityMap).forEach((param) => {
2379
+ columnVisibilityMap[param] = false;
2380
+ });
2381
+ return Object.assign(Object.assign({}, state), { columnVisibilityMap });
2382
+ }
2314
2383
  default: {
2315
2384
  return state;
2316
2385
  }
@@ -2326,7 +2395,18 @@ const IntelliTable = (props) => {
2326
2395
  var _a;
2327
2396
  /* -------------- Props ------------- */
2328
2397
  // Destructure all props
2329
- const { title, id, data, columns, } = props;
2398
+ const { title, id, columns, } = props;
2399
+ // Get data, show empty row if none
2400
+ const data = ((props.data && props.data.length > 0)
2401
+ ? props.data
2402
+ : [{ id: 'empty-row' }]);
2403
+ // Get CSV filename
2404
+ let filename = `${title}.csv`;
2405
+ if (props.csvName) {
2406
+ filename = (props.csvName.endsWith('.csv')
2407
+ ? props.csvName
2408
+ : `${props.csvName}.csv`);
2409
+ }
2330
2410
  /* -------------- State ------------- */
2331
2411
  // Initial state
2332
2412
  const initColumnVisibilityMap = {};
@@ -2343,21 +2423,49 @@ const IntelliTable = (props) => {
2343
2423
  const [state, dispatch] = React.useReducer(reducer$1, initialState);
2344
2424
  // Destructure common state
2345
2425
  const { sortColumnParam, sortType, columnVisibilityMap, columnVisibilityCustomizationModalVisible, } = state;
2426
+ /*------------------------------------------------------------------------*/
2427
+ /* Render */
2428
+ /*------------------------------------------------------------------------*/
2429
+ /*----------------------------------------*/
2430
+ /* Modal */
2431
+ /*----------------------------------------*/
2432
+ // Modal that may be defined
2433
+ let modal;
2346
2434
  /* ------- Col Vis Customization Modal ------ */
2347
2435
  if (columnVisibilityCustomizationModalVisible) {
2348
2436
  // Create modal
2349
- (React__default["default"].createElement(Modal, { type: ModalType$1.Okay, title: "Choose columns to show:", onClose: () => {
2437
+ modal = (React__default["default"].createElement(Modal, { type: ModalType$1.Okay, title: "Choose columns to show:", onClose: () => {
2438
+ const noColumnsSelected = (Object.values(columnVisibilityMap)
2439
+ .every((isSelected) => {
2440
+ return !isSelected;
2441
+ }));
2442
+ if (noColumnsSelected) {
2443
+ return alert$1('Choose at least one column', 'To continue, you have to choose at least one column to show');
2444
+ }
2350
2445
  dispatch({
2351
2446
  type: ActionType$1.ToggleColVisCusModalVisibility,
2352
2447
  });
2353
- } }, columns.map((column) => {
2354
- return (React__default["default"].createElement(CheckboxButton, { key: column.param, id: `IntelliTable-${id}-toggle-visibility-${column.param}`, text: column.title, onChanged: () => {
2448
+ }, okayVariant: Variant$1.Light },
2449
+ columns.map((column) => {
2450
+ return (React__default["default"].createElement(CheckboxButton, { key: column.param, id: `IntelliTable-${id}-toggle-visibility-${column.param}`, className: "mb-2", text: column.title, onChanged: (checked) => {
2451
+ dispatch({
2452
+ type: ActionType$1.UpdateColumnVisibility,
2453
+ param: column.param,
2454
+ visible: checked,
2455
+ });
2456
+ }, checked: columnVisibilityMap[column.param], ariaLabel: `show "${column.title}" column in the ${title} table`, checkedVariant: Variant$1.Light, uncheckedVariant: Variant$1.Secondary }));
2457
+ }),
2458
+ React__default["default"].createElement("div", { className: "mt-3" }, "Or you can:"),
2459
+ React__default["default"].createElement("button", { type: "button", id: `IntelliTable-${id}-select-all-columns`, className: "btn btn-secondary me-2", "aria-label": `show all columns in the ${title} table`, onClick: () => {
2460
+ dispatch({
2461
+ type: ActionType$1.ShowAllColumns,
2462
+ });
2463
+ } }, "Select All"),
2464
+ React__default["default"].createElement("button", { type: "button", id: `IntelliTable-${id}-select-none-columns`, className: "btn btn-secondary", "aria-label": `hide all columns in the ${title} table`, onClick: () => {
2355
2465
  dispatch({
2356
- type: ActionType$1.ToggleColumnVisibility,
2357
- param: column.param,
2466
+ type: ActionType$1.HideAllColumns,
2358
2467
  });
2359
- }, checked: columnVisibilityMap[column.param], ariaLabel: `show "${column.title}" column` }));
2360
- })));
2468
+ } }, "Deselect All")));
2361
2469
  }
2362
2470
  /*----------------------------------------*/
2363
2471
  /* Main UI */
@@ -2371,6 +2479,7 @@ const IntelliTable = (props) => {
2371
2479
  // Custom info based on current sort type
2372
2480
  let sortButtonAriaLabel;
2373
2481
  let sortIcon = freeSolidSvgIcons.faSort;
2482
+ let sortingByThisColumn = false;
2374
2483
  if (!sortColumnParam) {
2375
2484
  // Not being sorted yet
2376
2485
  sortButtonAriaLabel = `sort ascending by ${column.title}`;
@@ -2378,6 +2487,7 @@ const IntelliTable = (props) => {
2378
2487
  }
2379
2488
  else if (column.param === sortColumnParam) {
2380
2489
  // Already sorted by this column
2490
+ sortingByThisColumn = true;
2381
2491
  if (sortType === SortType.Ascending) {
2382
2492
  // Sorted ascending
2383
2493
  sortButtonAriaLabel = `sort descending by ${column.title}`;
@@ -2395,11 +2505,14 @@ const IntelliTable = (props) => {
2395
2505
  sortIcon = freeSolidSvgIcons.faSort;
2396
2506
  }
2397
2507
  // Create the cell UI
2398
- return (React__default["default"].createElement("th", { key: column.param, scope: "col", id: `IntelliTable-${id}-header-${column.param}` },
2399
- React__default["default"].createElement("div", { className: "d-flex align-items-center justify-content-center flex-row h-100" },
2508
+ return (React__default["default"].createElement("th", { key: column.param, scope: "col", id: `IntelliTable-${id}-header-${column.param}`, className: "text-start", style: {
2509
+ borderRight: '0.05rem solid #555',
2510
+ borderLeft: '0.05rem solid #555',
2511
+ } },
2512
+ React__default["default"].createElement("div", { className: "d-flex align-items-center justify-content-start flex-row h-100" },
2400
2513
  React__default["default"].createElement("span", { className: "text-nowrap" }, column.title),
2401
2514
  React__default["default"].createElement("div", null,
2402
- React__default["default"].createElement("button", { type: "button", className: "btn btn-light btn-sm ms-1", "aria-label": sortButtonAriaLabel, onClick: () => {
2515
+ React__default["default"].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: () => {
2403
2516
  dispatch({
2404
2517
  type: ActionType$1.ToggleSortColumn,
2405
2518
  param: column.param,
@@ -2417,8 +2530,28 @@ const IntelliTable = (props) => {
2417
2530
  const descending = (sortType === SortType.Descending);
2418
2531
  if (sortColumnParam) {
2419
2532
  sortedData.sort((a, b) => {
2533
+ var _a, _b;
2420
2534
  const aVal = a[sortColumnParam];
2421
2535
  const bVal = b[sortColumnParam];
2536
+ // Tiebreaker sort by timestamp, most recent first (used if tied)
2537
+ const tiebreaker = (((_a = b.timestamp) !== null && _a !== void 0 ? _a : 0)
2538
+ - ((_b = a.timestamp) !== null && _b !== void 0 ? _b : 0));
2539
+ // Auto-sort undefined and null to end of list
2540
+ if ((aVal === undefined || aVal === null)
2541
+ || (bVal === undefined || bVal === null)) {
2542
+ // At least one was undefined
2543
+ if ((aVal === undefined || aVal === null)
2544
+ && (bVal === undefined || bVal === null)) {
2545
+ // Both undefined
2546
+ return tiebreaker;
2547
+ }
2548
+ if (aVal === undefined || aVal === null) {
2549
+ // First was undefined
2550
+ return 1;
2551
+ }
2552
+ // Second was undefined
2553
+ return -1;
2554
+ }
2422
2555
  // Sort differently based on the data type
2423
2556
  // > Boolean
2424
2557
  if (paramType === ParamType$1.Boolean) {
@@ -2428,7 +2561,7 @@ const IntelliTable = (props) => {
2428
2561
  if (!aVal && bVal) {
2429
2562
  return (descending ? 1 : -1);
2430
2563
  }
2431
- return 0;
2564
+ return tiebreaker;
2432
2565
  }
2433
2566
  // > Number
2434
2567
  if (paramType === ParamType$1.Int
@@ -2440,12 +2573,12 @@ const IntelliTable = (props) => {
2440
2573
  // > String
2441
2574
  if (paramType === ParamType$1.String) {
2442
2575
  if (aVal < bVal) {
2443
- return (descending ? -1 : 1);
2576
+ return (descending ? 1 : -1);
2444
2577
  }
2445
2578
  if (aVal > bVal) {
2446
- return (descending ? 1 : -1);
2579
+ return (descending ? -1 : 1);
2447
2580
  }
2448
- return 0;
2581
+ return tiebreaker;
2449
2582
  }
2450
2583
  // > JSON
2451
2584
  if (paramType === ParamType$1.JSON) {
@@ -2460,7 +2593,7 @@ const IntelliTable = (props) => {
2460
2593
  : (aSize - bSize));
2461
2594
  }
2462
2595
  // No sort
2463
- return 0;
2596
+ return tiebreaker;
2464
2597
  });
2465
2598
  }
2466
2599
  // Table body
@@ -2485,9 +2618,12 @@ const IntelliTable = (props) => {
2485
2618
  const noValue = (value === undefined
2486
2619
  || value === null);
2487
2620
  visibleValue = (noValue
2488
- ? (React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faCheckCircle }))
2489
- : (React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faXmarkCircle })));
2621
+ ? (React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faMinus }))
2622
+ : (React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: fullValue ? freeSolidSvgIcons.faCheckCircle : freeSolidSvgIcons.faXmarkCircle })));
2490
2623
  title = (fullValue ? 'True' : 'False');
2624
+ if (noValue) {
2625
+ title = 'Empty Cell';
2626
+ }
2491
2627
  }
2492
2628
  else if (column.type === ParamType$1.Int) {
2493
2629
  fullValue = Number.parseInt(value, 10);
@@ -2496,6 +2632,9 @@ const IntelliTable = (props) => {
2496
2632
  ? (React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faMinus }))
2497
2633
  : fullValue);
2498
2634
  title = String(fullValue);
2635
+ if (noValue) {
2636
+ title = 'Empty Cell';
2637
+ }
2499
2638
  }
2500
2639
  else if (column.type === ParamType$1.Float) {
2501
2640
  fullValue = Number.parseFloat(value);
@@ -2504,14 +2643,22 @@ const IntelliTable = (props) => {
2504
2643
  ? (React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faMinus }))
2505
2644
  : roundToNumDecimals(fullValue, 2));
2506
2645
  title = String(fullValue);
2646
+ if (noValue) {
2647
+ title = 'Empty Cell';
2648
+ }
2507
2649
  }
2508
2650
  else if (column.type === ParamType$1.String) {
2509
2651
  fullValue = String(value).trim();
2510
- const noValue = (value.trim().length) === 0;
2652
+ const noValue = (value === undefined
2653
+ || value === null
2654
+ || String(fullValue).trim().length === 0);
2511
2655
  visibleValue = (noValue
2512
2656
  ? (React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faMinus }))
2513
2657
  : fullValue);
2514
2658
  title = `"${value}"`;
2659
+ if (noValue) {
2660
+ title = 'Empty Cell';
2661
+ }
2515
2662
  }
2516
2663
  else if (column.type === ParamType$1.JSON) {
2517
2664
  fullValue = JSON.stringify(value);
@@ -2521,9 +2668,13 @@ const IntelliTable = (props) => {
2521
2668
  visibleValue = (noValue
2522
2669
  ? (React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faMinus }))
2523
2670
  : fullValue);
2671
+ title = "JSON Object";
2524
2672
  }
2525
2673
  // Create UI
2526
- return (React__default["default"].createElement("td", { key: `${datum.id}-${column.param}`, title: title }, visibleValue));
2674
+ return (React__default["default"].createElement("td", { key: `${datum.id}-${column.param}`, title: title, style: {
2675
+ borderRight: '0.05rem solid #555',
2676
+ borderLeft: '0.05rem solid #555',
2677
+ } }, visibleValue));
2527
2678
  }));
2528
2679
  // Add cells to a row
2529
2680
  return (React__default["default"].createElement("tr", { key: datum.id }, cells));
@@ -2540,13 +2691,16 @@ const IntelliTable = (props) => {
2540
2691
  })
2541
2692
  .length);
2542
2693
  // Build CSV
2543
- const csv = genCSV(data, columns);
2694
+ const csv = genCSV(data, columns.filter((column) => {
2695
+ return columnVisibilityMap[column.param];
2696
+ }));
2544
2697
  // Build main UI
2545
2698
  return (React__default["default"].createElement("div", { className: `IntelliTable-container-${id}` },
2546
- React__default["default"].createElement("div", { className: "d-flex align-items-center justify-content-center" },
2699
+ modal,
2700
+ React__default["default"].createElement("div", { className: "d-flex align-items-center justify-content-start" },
2547
2701
  React__default["default"].createElement("h3", { className: "m-0" }, title),
2548
2702
  React__default["default"].createElement("div", { className: "flex-grow-1 text-end" },
2549
- React__default["default"].createElement(CSVDownloadButton, { "aria-label": `download data as csv for ${title}`, id: `IntelliTable-${id}-download-as-csv`, filename: `${title}.csv`, csv: csv }),
2703
+ React__default["default"].createElement(CSVDownloadButton, { "aria-label": `download data as csv for ${title}`, id: `IntelliTable-${id}-download-as-csv`, filename: filename, csv: csv }),
2550
2704
  React__default["default"].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: () => {
2551
2705
  dispatch({
2552
2706
  type: ActionType$1.ToggleColVisCusModalVisibility,
@@ -2559,7 +2713,7 @@ const IntelliTable = (props) => {
2559
2713
  numHiddenCols,
2560
2714
  ' ',
2561
2715
  "hidden)"))))),
2562
- React__default["default"].createElement("div", { className: `IntelliTable-table-${id}`, style: {
2716
+ React__default["default"].createElement("div", { className: `IntelliTable-table-${id} mt-2`, style: {
2563
2717
  overflowX: 'auto',
2564
2718
  } }, table)));
2565
2719
  };
@@ -2648,8 +2802,12 @@ const style = `
2648
2802
  background-color: transparent !important;
2649
2803
  padding-top: 0 !important;
2650
2804
  padding-bottom: 0 !important;
2805
+ padding-right: 1em !important;
2651
2806
  margin: 0 !important;
2652
- color: #333 !important;
2807
+ color: #444 !important;
2808
+
2809
+ right: 0 !important;
2810
+ position: absolute !important;
2653
2811
  }
2654
2812
  .LogReviewer-header-close-button:hover {
2655
2813
  border: 0 !important;
@@ -2661,6 +2819,195 @@ const style = `
2661
2819
  }
2662
2820
  `;
2663
2821
  /*------------------------------------------------------------------------*/
2822
+ /* Constants */
2823
+ /*------------------------------------------------------------------------*/
2824
+ const columns = [
2825
+ {
2826
+ title: 'First Name',
2827
+ param: 'userFirstName',
2828
+ type: ParamType$1.String,
2829
+ },
2830
+ {
2831
+ title: 'Last Name',
2832
+ param: 'userLastName',
2833
+ type: ParamType$1.String,
2834
+ },
2835
+ {
2836
+ title: 'Email',
2837
+ param: 'userEmail',
2838
+ type: ParamType$1.String,
2839
+ },
2840
+ {
2841
+ title: 'Canvas Id',
2842
+ param: 'userId',
2843
+ type: ParamType$1.Int,
2844
+ },
2845
+ {
2846
+ title: 'Student?',
2847
+ param: 'isLearner',
2848
+ type: ParamType$1.Boolean,
2849
+ },
2850
+ {
2851
+ title: 'Teaching Staff?',
2852
+ param: 'isTTM',
2853
+ type: ParamType$1.Boolean,
2854
+ startsHidden: true,
2855
+ },
2856
+ {
2857
+ title: 'Admin?',
2858
+ param: 'isAdmin',
2859
+ type: ParamType$1.Boolean,
2860
+ startsHidden: true,
2861
+ },
2862
+ {
2863
+ title: 'Course Canvas Id',
2864
+ param: 'courseId',
2865
+ type: ParamType$1.Int,
2866
+ startsHidden: true,
2867
+ },
2868
+ {
2869
+ title: 'Course Name',
2870
+ param: 'courseName',
2871
+ type: ParamType$1.String,
2872
+ },
2873
+ {
2874
+ title: 'Browser Name',
2875
+ param: 'browser.name',
2876
+ type: ParamType$1.String,
2877
+ startsHidden: true,
2878
+ },
2879
+ {
2880
+ title: 'Browser Version',
2881
+ param: 'browser.version',
2882
+ type: ParamType$1.String,
2883
+ startsHidden: true,
2884
+ },
2885
+ {
2886
+ title: 'OS',
2887
+ param: 'device.os',
2888
+ type: ParamType$1.String,
2889
+ startsHidden: true,
2890
+ },
2891
+ {
2892
+ title: 'Mobile?',
2893
+ param: 'device.isMobile',
2894
+ type: ParamType$1.Boolean,
2895
+ startsHidden: true,
2896
+ },
2897
+ {
2898
+ title: 'Year',
2899
+ param: 'year',
2900
+ type: ParamType$1.Int,
2901
+ },
2902
+ {
2903
+ title: 'Month',
2904
+ param: 'month',
2905
+ type: ParamType$1.Int,
2906
+ },
2907
+ {
2908
+ title: 'Day',
2909
+ param: 'day',
2910
+ type: ParamType$1.Int,
2911
+ },
2912
+ {
2913
+ title: 'Hour',
2914
+ param: 'hour',
2915
+ type: ParamType$1.Int,
2916
+ },
2917
+ {
2918
+ title: 'Minute',
2919
+ param: 'minute',
2920
+ type: ParamType$1.Int,
2921
+ startsHidden: true,
2922
+ },
2923
+ {
2924
+ title: 'Timestamp',
2925
+ param: 'timestamp',
2926
+ type: ParamType$1.Int,
2927
+ startsHidden: true,
2928
+ },
2929
+ {
2930
+ title: 'Context',
2931
+ param: 'context',
2932
+ type: ParamType$1.String,
2933
+ },
2934
+ {
2935
+ title: 'Subcontext',
2936
+ param: 'subcontext',
2937
+ type: ParamType$1.String,
2938
+ },
2939
+ {
2940
+ title: 'Tags',
2941
+ param: 'tags',
2942
+ type: ParamType$1.JSON,
2943
+ startsHidden: true,
2944
+ },
2945
+ {
2946
+ title: 'Log Level',
2947
+ param: 'level',
2948
+ type: ParamType$1.String,
2949
+ startsHidden: true,
2950
+ },
2951
+ {
2952
+ title: 'Metadata',
2953
+ param: 'metadata',
2954
+ type: ParamType$1.JSON,
2955
+ startsHidden: true,
2956
+ },
2957
+ {
2958
+ title: 'Source',
2959
+ param: 'source',
2960
+ type: ParamType$1.String,
2961
+ },
2962
+ {
2963
+ title: 'Server Route Path',
2964
+ param: 'routePath',
2965
+ type: ParamType$1.String,
2966
+ startsHidden: true,
2967
+ },
2968
+ {
2969
+ title: 'Server Route Template',
2970
+ param: 'routeTemplate',
2971
+ type: ParamType$1.String,
2972
+ startsHidden: true,
2973
+ },
2974
+ {
2975
+ title: 'Type',
2976
+ param: 'type',
2977
+ type: ParamType$1.String,
2978
+ },
2979
+ {
2980
+ title: 'Error Message',
2981
+ param: 'errorMessage',
2982
+ type: ParamType$1.String,
2983
+ startsHidden: true,
2984
+ },
2985
+ {
2986
+ title: 'Error Code',
2987
+ param: 'errorCode',
2988
+ type: ParamType$1.String,
2989
+ startsHidden: true,
2990
+ },
2991
+ {
2992
+ title: 'Error Stack',
2993
+ param: 'errorStack',
2994
+ type: ParamType$1.String,
2995
+ startsHidden: true,
2996
+ },
2997
+ {
2998
+ title: 'Action Target',
2999
+ param: 'target',
3000
+ type: ParamType$1.String,
3001
+ startsHidden: true,
3002
+ },
3003
+ {
3004
+ title: 'Action Type',
3005
+ param: 'action',
3006
+ type: ParamType$1.String,
3007
+ startsHidden: true,
3008
+ },
3009
+ ];
3010
+ /*------------------------------------------------------------------------*/
2664
3011
  /* Static Functions */
2665
3012
  /*------------------------------------------------------------------------*/
2666
3013
  /**
@@ -2760,10 +3107,35 @@ const LogReviewer = (props) => {
2760
3107
  /*------------------------------------------------------------------------*/
2761
3108
  /* Setup */
2762
3109
  /*------------------------------------------------------------------------*/
2763
- var _a, _b, _c, _d, _e, _f;
3110
+ var _a, _b, _c, _d;
2764
3111
  /* -------------- Props ------------- */
2765
3112
  // Destructure props
2766
3113
  const { LogMetadata, onClose, } = props;
3114
+ // Create complete map of contexts
3115
+ const contextMap = {};
3116
+ [
3117
+ ((_a = LogMetadata.Context) !== null && _a !== void 0 ? _a : {}),
3118
+ LogBuiltInMetadata.Context,
3119
+ ].forEach((partialContextMap) => {
3120
+ Object.keys(partialContextMap).forEach((context) => {
3121
+ // Add context
3122
+ contextMap[context] = partialContextMap[context];
3123
+ // If context has children, add an "uncategorized" subcontext
3124
+ if (typeof contextMap[context] !== 'string') {
3125
+ contextMap[context][LogBuiltInMetadata.Context.Uncategorized] = (LogBuiltInMetadata.Context.Uncategorized);
3126
+ }
3127
+ });
3128
+ });
3129
+ // Create complete map of targets
3130
+ const targetMap = {};
3131
+ [
3132
+ ((_b = LogMetadata.Target) !== null && _b !== void 0 ? _b : {}),
3133
+ LogBuiltInMetadata.Target,
3134
+ ].forEach((partialTargetMap) => {
3135
+ Object.keys(partialTargetMap).forEach((target) => {
3136
+ targetMap[target] = partialTargetMap[target];
3137
+ });
3138
+ });
2767
3139
  /* -------------- State ------------- */
2768
3140
  // Create initial date filter state
2769
3141
  const today = getTimeInfoInET();
@@ -2783,26 +3155,29 @@ const LogReviewer = (props) => {
2783
3155
  };
2784
3156
  // Create initial context filter state
2785
3157
  const initContextFilterState = {};
2786
- Object.keys((_a = LogMetadata.Context) !== null && _a !== void 0 ? _a : {}).forEach((context) => {
2787
- var _a, _b;
2788
- const contextValue = ((_a = LogMetadata.Context) !== null && _a !== void 0 ? _a : {})[context];
3158
+ Object.keys(contextMap).forEach((context) => {
3159
+ const contextValue = contextMap[context];
2789
3160
  if (typeof contextValue === 'string') {
2790
- // Case: no subcontexts
3161
+ // Case: no subcontexts, init as checked
2791
3162
  initContextFilterState[contextValue] = true;
2792
3163
  }
2793
3164
  else {
2794
3165
  // Case: subcontexts exist
2795
- initContextFilterState[contextValue._] = {};
2796
- Object.values(((_b = LogMetadata.Context) !== null && _b !== void 0 ? _b : {})[context]).forEach((subcontext) => {
2797
- const subcontextValue = contextValue[subcontext];
2798
- initContextFilterState[contextValue._][subcontextValue] = true;
3166
+ initContextFilterState[context] = {};
3167
+ Object.values(contextMap[context]).forEach((subcontext) => {
3168
+ // Skip self ("_")
3169
+ if (subcontext === '_') {
3170
+ return;
3171
+ }
3172
+ // Initialize as checked
3173
+ initContextFilterState[context][subcontext] = true;
2799
3174
  });
2800
3175
  }
2801
3176
  });
2802
3177
  // Create initial tag filter state
2803
3178
  const initTagFilterState = {};
2804
- Object.values((_b = LogMetadata.Tag) !== null && _b !== void 0 ? _b : {}).forEach((tagValue) => {
2805
- initTagFilterState[tagValue] = true;
3179
+ Object.keys((_c = LogMetadata.Tag) !== null && _c !== void 0 ? _c : {}).forEach((tag) => {
3180
+ initTagFilterState[tag] = false;
2806
3181
  });
2807
3182
  // Create advanced filter state
2808
3183
  const initAdvancedFilterState = {
@@ -2828,7 +3203,7 @@ const LogReviewer = (props) => {
2828
3203
  target: {},
2829
3204
  action: {},
2830
3205
  };
2831
- Object.values((_c = LogMetadata.Target) !== null && _c !== void 0 ? _c : {}).forEach((target) => {
3206
+ Object.values(targetMap).forEach((target) => {
2832
3207
  initActionErrorFilterState.target[target] = true;
2833
3208
  });
2834
3209
  Object.values(LogAction$1).forEach((action) => {
@@ -2867,15 +3242,18 @@ const LogReviewer = (props) => {
2867
3242
  let month = newDateFilterState.startDate.month;
2868
3243
  while (
2869
3244
  // Earlier year
2870
- (year <= newDateFilterState.endDate.year)
3245
+ (year < newDateFilterState.endDate.year)
2871
3246
  // Current year but included month
2872
3247
  || (year === newDateFilterState.endDate.year
2873
3248
  && month <= newDateFilterState.endDate.month)) {
2874
- // Add to list
2875
- toLoad.push({
2876
- year,
2877
- month,
2878
- });
3249
+ // Add to list if not already loaded
3250
+ if (!logMap[year]
3251
+ || !logMap[year][month]) {
3252
+ toLoad.push({
3253
+ year,
3254
+ month,
3255
+ });
3256
+ }
2879
3257
  // Increment
2880
3258
  month += 1;
2881
3259
  if (month > 12) {
@@ -2963,7 +3341,7 @@ const LogReviewer = (props) => {
2963
3341
  /* Filters */
2964
3342
  /*----------------------------------------*/
2965
3343
  // Filter toggle
2966
- const filterToggles = (React__default["default"].createElement("div", { className: "LogReviewer-filter-toggles mb-2" },
3344
+ const filterToggles = (React__default["default"].createElement("div", { className: "LogReviewer-filter-toggles" },
2967
3345
  React__default["default"].createElement("h3", { className: "m-0" }, "Filters:"),
2968
3346
  React__default["default"].createElement("div", { className: "LogReviewer-filter-toggle-buttons alert alert-secondary p-2 m-0" },
2969
3347
  React__default["default"].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: () => {
@@ -2998,41 +3376,64 @@ const LogReviewer = (props) => {
2998
3376
  } },
2999
3377
  React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faHammer, className: "me-2" }),
3000
3378
  "Action"),
3001
- React__default["default"].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: () => {
3379
+ React__default["default"].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: () => {
3002
3380
  dispatch({
3003
3381
  type: ActionType.ToggleFilterDrawer,
3004
3382
  filterDrawer: FilterDrawer.Advanced,
3005
3383
  });
3006
3384
  } },
3007
3385
  React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faList, className: "me-2" }),
3008
- "Advanced"))));
3386
+ "Advanced"),
3387
+ React__default["default"].createElement("button", { type: "button", id: "LogReviewer-reset-filters-button", className: "btn btn-light", "aria-label": "reset filters", onClick: () => {
3388
+ dispatch({
3389
+ type: ActionType.ResetFilters,
3390
+ initActionErrorFilterState,
3391
+ initAdvancedFilterState,
3392
+ initContextFilterState,
3393
+ initDateFilterState,
3394
+ initTagFilterState,
3395
+ });
3396
+ } },
3397
+ React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faTimes }),
3398
+ ' ',
3399
+ "Reset"))));
3009
3400
  // Filter drawer
3010
3401
  let filterDrawer;
3011
3402
  if (expandedFilterDrawer) {
3012
3403
  if (expandedFilterDrawer === FilterDrawer.Date) {
3013
3404
  filterDrawer = (React__default["default"].createElement(TabBox, { title: "Date" },
3014
- React__default["default"].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) => {
3015
- dispatch({
3016
- type: ActionType.UpdateDateFilterState,
3017
- dateFilterState: Object.assign(Object.assign({}, dateFilterState), { startDate: { month, day, year } }),
3018
- });
3405
+ React__default["default"].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) => {
3406
+ dateFilterState.startDate = { month, day, year };
3407
+ handleDateRangeUpdated(dateFilterState);
3019
3408
  } }),
3020
3409
  ' ',
3021
3410
  "to",
3022
3411
  ' ',
3023
- React__default["default"].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) => {
3024
- dispatch({
3025
- type: ActionType.UpdateDateFilterState,
3026
- dateFilterState: Object.assign(Object.assign({}, dateFilterState), { endDate: { month, day, year } }),
3027
- });
3412
+ React__default["default"].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) => {
3413
+ if (year < dateFilterState.startDate.year
3414
+ || (year === dateFilterState.startDate.year
3415
+ && month < dateFilterState.startDate.month)
3416
+ || (year === dateFilterState.startDate.year
3417
+ && month === dateFilterState.startDate.month
3418
+ && day < dateFilterState.startDate.day)) {
3419
+ return alert$1('Invalid Start Date', 'The start date cannot be before the end date.');
3420
+ }
3421
+ dateFilterState.endDate = { month, day, year };
3422
+ handleDateRangeUpdated(dateFilterState);
3028
3423
  } })));
3029
3424
  }
3030
3425
  else if (expandedFilterDrawer === FilterDrawer.Context) {
3031
3426
  // Create item picker items
3032
- const pickableItems = (Object.keys((_d = LogMetadata.Context) !== null && _d !== void 0 ? _d : {})
3033
- .map((context) => {
3034
- var _a;
3035
- const value = ((_a = LogMetadata.Context) !== null && _a !== void 0 ? _a : {})[context];
3427
+ const builtInPickableItem = {
3428
+ id: 'built-in-contexts',
3429
+ name: 'Auto-logged',
3430
+ isGroup: true,
3431
+ children: [],
3432
+ };
3433
+ const pickableItems = [];
3434
+ Object.keys(contextMap)
3435
+ .forEach((context) => {
3436
+ const value = contextMap[context];
3036
3437
  if (typeof value === 'string') {
3037
3438
  // No subcategories
3038
3439
  const item = {
@@ -3041,38 +3442,65 @@ const LogReviewer = (props) => {
3041
3442
  isGroup: false,
3042
3443
  checked: !!contextFilterState[context],
3043
3444
  };
3044
- return item;
3445
+ // Add built-in items to its own folder
3446
+ const isBuiltIn = context in LogBuiltInMetadata.Context;
3447
+ if (isBuiltIn) {
3448
+ // Add to built-in pickable item
3449
+ builtInPickableItem.children.push(item);
3450
+ }
3451
+ else {
3452
+ // Add to pickable items list
3453
+ pickableItems.push(item);
3454
+ }
3455
+ return;
3045
3456
  }
3046
3457
  // Has subcategories
3047
3458
  const children = (Object.keys(value)
3459
+ // Remove parent name
3048
3460
  .filter((subcontext) => {
3049
3461
  return subcontext !== '_';
3050
3462
  })
3463
+ // Create child pickable items
3051
3464
  .map((subcontext) => {
3052
3465
  return {
3053
- id: `${context}-${subcontext}`,
3466
+ id: subcontext,
3054
3467
  name: genHumanReadableName(subcontext),
3055
3468
  isGroup: false,
3056
- checked: !!value[subcontext],
3469
+ checked: contextFilterState[context][subcontext],
3057
3470
  };
3058
3471
  }));
3059
3472
  const item = {
3060
3473
  id: context,
3061
- name: context,
3474
+ name: genHumanReadableName(context),
3062
3475
  isGroup: true,
3063
3476
  children,
3064
3477
  };
3065
- return item;
3066
- }));
3478
+ pickableItems.push(item);
3479
+ });
3480
+ // Add built-in contexts to end ofl ist
3481
+ pickableItems.push(builtInPickableItem);
3067
3482
  // Create filter UI
3068
3483
  filterDrawer = (React__default["default"].createElement(ItemPicker, { title: "Context", items: pickableItems, onChanged: (updatedItems) => {
3069
3484
  // Update our state
3070
3485
  updatedItems.forEach((pickableItem) => {
3071
3486
  if (pickableItem.isGroup) {
3072
3487
  // Has subcontexts
3073
- pickableItem.children.forEach((subcontextItem) => {
3074
- contextFilterState[pickableItem.id][subcontextItem.id] = (subcontextItem.checked);
3075
- });
3488
+ if (pickableItem.id === 'built-in-contexts') {
3489
+ // Built-in
3490
+ // Treat as if these were top-level contexts
3491
+ pickableItem.children.forEach((subcontextItem) => {
3492
+ contextFilterState[subcontextItem.id] = ('checked' in subcontextItem
3493
+ && subcontextItem.checked);
3494
+ });
3495
+ }
3496
+ else {
3497
+ // Not built-in
3498
+ pickableItem.children.forEach((subcontextItem) => {
3499
+ if (!subcontextItem.isGroup) {
3500
+ contextFilterState[pickableItem.id][subcontextItem.id] = (subcontextItem.checked);
3501
+ }
3502
+ });
3503
+ }
3076
3504
  }
3077
3505
  else {
3078
3506
  // No subcontexts
@@ -3087,13 +3515,19 @@ const LogReviewer = (props) => {
3087
3515
  }
3088
3516
  else if (expandedFilterDrawer === FilterDrawer.Tag) {
3089
3517
  // Create filter UI
3090
- filterDrawer = (React__default["default"].createElement(TabBox, { title: "Tags" }, Object.keys(tagFilterState)
3091
- .map((tag, i) => {
3092
- const description = genHumanReadableName(tag);
3093
- return (React__default["default"].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) => {
3094
- tagFilterState[tag] = checked;
3095
- } }));
3096
- })));
3518
+ filterDrawer = (React__default["default"].createElement(TabBox, { title: "Tags" },
3519
+ React__default["default"].createElement("div", null, "If any tags are selected, logs must contain at least one (but not necessarily all) of the selected tags."),
3520
+ React__default["default"].createElement("div", { className: "d-flex gap-1 flex-wrap" }, Object.keys((_d = LogMetadata.Tag) !== null && _d !== void 0 ? _d : {})
3521
+ .map((tag) => {
3522
+ const description = genHumanReadableName(tag);
3523
+ return (React__default["default"].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) => {
3524
+ tagFilterState[tag] = checked;
3525
+ dispatch({
3526
+ type: ActionType.UpdateTagFilterState,
3527
+ tagFilterState,
3528
+ });
3529
+ } }));
3530
+ }))));
3097
3531
  }
3098
3532
  else if (expandedFilterDrawer === FilterDrawer.Action) {
3099
3533
  // Create filter UI
@@ -3122,36 +3556,35 @@ const LogReviewer = (props) => {
3122
3556
  }, ariaLabel: "only show error logs", selected: actionErrorFilterState.type === LogType$1.Error, noMarginOnRight: true })),
3123
3557
  (actionErrorFilterState.type === undefined
3124
3558
  || actionErrorFilterState.type === LogType$1.Action) && (React__default["default"].createElement(TabBox, { title: "Action Log Details" },
3125
- React__default["default"].createElement(ButtonInputGroup, { label: "Action", className: "mb-2" }, Object.keys(LogAction$1)
3126
- .map((action, i) => {
3127
- const description = genHumanReadableName(action);
3128
- return (React__default["default"].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) => {
3129
- actionErrorFilterState.action[action] = checked;
3130
- dispatch({
3131
- type: ActionType.UpdateActionErrorFilterState,
3132
- actionErrorFilterState,
3133
- });
3134
- } }));
3135
- })),
3559
+ React__default["default"].createElement(ButtonInputGroup, { label: "Action", className: "mb-2" },
3560
+ React__default["default"].createElement("div", { className: "d-flex gap-1 flex-wrap" }, Object.keys(LogAction$1)
3561
+ .map((action) => {
3562
+ const description = genHumanReadableName(action);
3563
+ return (React__default["default"].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) => {
3564
+ actionErrorFilterState.action[action] = checked;
3565
+ dispatch({
3566
+ type: ActionType.UpdateActionErrorFilterState,
3567
+ actionErrorFilterState,
3568
+ });
3569
+ } }));
3570
+ }))),
3136
3571
  React__default["default"].createElement(ButtonInputGroup, { label: "Target" },
3137
- (Object.keys((_e = LogMetadata.Target) !== null && _e !== void 0 ? _e : {})) && (React__default["default"].createElement("div", null, "This app does not have any targets yet.")),
3138
- Object.keys((_f = LogMetadata.Target) !== null && _f !== void 0 ? _f : {})
3139
- .map((target, i) => {
3140
- var _a;
3572
+ React__default["default"].createElement("div", { className: "d-flex gap-1 flex-wrap" }, Object.keys(targetMap)
3573
+ .map((target) => {
3141
3574
  const description = genHumanReadableName(target);
3142
- return (React__default["default"].createElement(CheckboxButton, { id: `LogReviewer-target-${target}-checkbox`, text: description, ariaLabel: `include logs with target "${description}" in results`, onChanged: (checked) => {
3575
+ return (React__default["default"].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) => {
3143
3576
  actionErrorFilterState.target[target] = checked;
3144
3577
  dispatch({
3145
3578
  type: ActionType.UpdateActionErrorFilterState,
3146
3579
  actionErrorFilterState,
3147
3580
  });
3148
- }, noMarginOnRight: i === Object.keys((_a = LogMetadata.Target) !== null && _a !== void 0 ? _a : {}).length - 1 }));
3149
- })))),
3581
+ } }));
3582
+ }))))),
3150
3583
  (actionErrorFilterState.type === undefined
3151
3584
  || actionErrorFilterState.type === LogType$1.Error) && (React__default["default"].createElement(TabBox, { title: "Error Log Details" },
3152
3585
  React__default["default"].createElement("div", { className: "input-group mb-2" },
3153
3586
  React__default["default"].createElement("span", { className: "input-group-text" }, "Error Message"),
3154
- React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for error message", value: actionErrorFilterState.errorMessage, onChange: (e) => {
3587
+ React__default["default"].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) => {
3155
3588
  actionErrorFilterState.errorMessage = e.target.value;
3156
3589
  dispatch({
3157
3590
  type: ActionType.UpdateActionErrorFilterState,
@@ -3160,7 +3593,7 @@ const LogReviewer = (props) => {
3160
3593
  } })),
3161
3594
  React__default["default"].createElement("div", { className: "input-group mb-2" },
3162
3595
  React__default["default"].createElement("span", { className: "input-group-text" }, "Error Code"),
3163
- React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for error code", value: actionErrorFilterState.errorMessage, onChange: (e) => {
3596
+ React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for error code", value: actionErrorFilterState.errorCode, placeholder: "e.g. GC22", onChange: (e) => {
3164
3597
  actionErrorFilterState.errorCode = ((e.target.value)
3165
3598
  .trim()
3166
3599
  .toUpperCase());
@@ -3173,10 +3606,10 @@ const LogReviewer = (props) => {
3173
3606
  else if (expandedFilterDrawer === FilterDrawer.Advanced) {
3174
3607
  // Create advanced filter ui
3175
3608
  filterDrawer = (React__default["default"].createElement(React__default["default"].Fragment, null,
3176
- React__default["default"].createElement(TabBox, { title: "User Info" },
3609
+ React__default["default"].createElement(TabBox, { title: "User" },
3177
3610
  React__default["default"].createElement("div", { className: "input-group mb-2" },
3178
3611
  React__default["default"].createElement("span", { className: "input-group-text" }, "User First Name"),
3179
- React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for user first name", value: advancedFilterState.userFirstName, onChange: (e) => {
3612
+ React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for user first name", value: advancedFilterState.userFirstName, placeholder: "e.g. Divardo", onChange: (e) => {
3180
3613
  advancedFilterState.userFirstName = e.target.value;
3181
3614
  dispatch({
3182
3615
  type: ActionType.UpdateAdvancedFilterState,
@@ -3185,7 +3618,7 @@ const LogReviewer = (props) => {
3185
3618
  } })),
3186
3619
  React__default["default"].createElement("div", { className: "input-group mb-2" },
3187
3620
  React__default["default"].createElement("span", { className: "input-group-text" }, "User Last Name"),
3188
- React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for user last name", value: advancedFilterState.userLastName, onChange: (e) => {
3621
+ React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for user last name", value: advancedFilterState.userLastName, placeholder: "e.g. Calicci", onChange: (e) => {
3189
3622
  advancedFilterState.userLastName = e.target.value;
3190
3623
  dispatch({
3191
3624
  type: ActionType.UpdateAdvancedFilterState,
@@ -3194,7 +3627,7 @@ const LogReviewer = (props) => {
3194
3627
  } })),
3195
3628
  React__default["default"].createElement("div", { className: "input-group mb-2" },
3196
3629
  React__default["default"].createElement("span", { className: "input-group-text" }, "User Email"),
3197
- React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for user email", value: advancedFilterState.userEmail, onChange: (e) => {
3630
+ React__default["default"].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) => {
3198
3631
  advancedFilterState.userEmail = ((e.target.value)
3199
3632
  .trim());
3200
3633
  dispatch({
@@ -3204,7 +3637,7 @@ const LogReviewer = (props) => {
3204
3637
  } })),
3205
3638
  React__default["default"].createElement("div", { className: "input-group mb-2" },
3206
3639
  React__default["default"].createElement("span", { className: "input-group-text" }, "User Canvas Id"),
3207
- React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for user canvas id", value: advancedFilterState.userId, onChange: (e) => {
3640
+ React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for user canvas id", value: advancedFilterState.userId, placeholder: "e.g. 104985", onChange: (e) => {
3208
3641
  const { value } = e.target;
3209
3642
  // Only update if value contains only numbers
3210
3643
  if (/^\d+$/.test(value)) {
@@ -3238,10 +3671,10 @@ const LogReviewer = (props) => {
3238
3671
  advancedFilterState,
3239
3672
  });
3240
3673
  }, checked: advancedFilterState.includeAdmins, ariaLabel: "show logs from admins" }))),
3241
- React__default["default"].createElement(TabBox, { title: "Course Info" },
3674
+ React__default["default"].createElement(TabBox, { title: "Course" },
3242
3675
  React__default["default"].createElement("div", { className: "input-group mb-2" },
3243
3676
  React__default["default"].createElement("span", { className: "input-group-text" }, "Course Name"),
3244
- React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for course name", value: advancedFilterState.courseName, onChange: (e) => {
3677
+ React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for course name", value: advancedFilterState.courseName, placeholder: "e.g. GLC 200", onChange: (e) => {
3245
3678
  advancedFilterState.courseName = e.target.value;
3246
3679
  dispatch({
3247
3680
  type: ActionType.UpdateAdvancedFilterState,
@@ -3250,7 +3683,7 @@ const LogReviewer = (props) => {
3250
3683
  } })),
3251
3684
  React__default["default"].createElement("div", { className: "input-group mb-2" },
3252
3685
  React__default["default"].createElement("span", { className: "input-group-text" }, "Course Canvas Id"),
3253
- React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for course canvas id", value: advancedFilterState.courseId, onChange: (e) => {
3686
+ React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for course canvas id", value: advancedFilterState.courseId, placeholder: "e.g. 15948", onChange: (e) => {
3254
3687
  const { value } = e.target;
3255
3688
  // Only update if value contains only numbers
3256
3689
  if (/^\d+$/.test(value)) {
@@ -3262,7 +3695,7 @@ const LogReviewer = (props) => {
3262
3695
  advancedFilterState,
3263
3696
  });
3264
3697
  } }))),
3265
- React__default["default"].createElement(TabBox, { title: "Device Info" },
3698
+ React__default["default"].createElement(TabBox, { title: "Device" },
3266
3699
  React__default["default"].createElement(ButtonInputGroup, { label: "Device Type" },
3267
3700
  React__default["default"].createElement(RadioButton, { text: "All Devices", ariaLabel: "show logs from all devices", selected: advancedFilterState.isMobile === undefined, onSelected: () => {
3268
3701
  advancedFilterState.isMobile = undefined;
@@ -3308,32 +3741,33 @@ const LogReviewer = (props) => {
3308
3741
  advancedFilterState,
3309
3742
  });
3310
3743
  }, noMarginOnRight: true })),
3311
- React__default["default"].createElement("div", { className: "input-group mb-2" },
3312
- React__default["default"].createElement("span", { className: "input-group-text" }, "Server Route Path"),
3313
- React__default["default"].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) => {
3314
- advancedFilterState.courseName = ((e.target.value)
3315
- .trim());
3316
- dispatch({
3317
- type: ActionType.UpdateAdvancedFilterState,
3318
- advancedFilterState,
3319
- });
3320
- } })),
3321
- React__default["default"].createElement("div", { className: "input-group mb-2" },
3322
- React__default["default"].createElement("span", { className: "input-group-text" }, "Server Route Template"),
3323
- React__default["default"].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) => {
3324
- advancedFilterState.courseName = ((e.target.value)
3325
- .trim());
3326
- dispatch({
3327
- type: ActionType.UpdateAdvancedFilterState,
3328
- advancedFilterState,
3329
- });
3330
- } })))));
3744
+ advancedFilterState.source !== LogSource$1.Client && (React__default["default"].createElement("div", { className: "mt-2" },
3745
+ React__default["default"].createElement("div", { className: "input-group mb-2" },
3746
+ React__default["default"].createElement("span", { className: "input-group-text" }, "Server Route Path"),
3747
+ React__default["default"].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) => {
3748
+ advancedFilterState.courseName = ((e.target.value)
3749
+ .trim());
3750
+ dispatch({
3751
+ type: ActionType.UpdateAdvancedFilterState,
3752
+ advancedFilterState,
3753
+ });
3754
+ } })),
3755
+ React__default["default"].createElement("div", { className: "input-group mb-2" },
3756
+ React__default["default"].createElement("span", { className: "input-group-text" }, "Server Route Template"),
3757
+ React__default["default"].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) => {
3758
+ advancedFilterState.courseName = ((e.target.value)
3759
+ .trim());
3760
+ dispatch({
3761
+ type: ActionType.UpdateAdvancedFilterState,
3762
+ advancedFilterState,
3763
+ });
3764
+ } })))))));
3331
3765
  }
3332
3766
  }
3333
3767
  // Filters UI
3334
3768
  const filters = (React__default["default"].createElement(React__default["default"].Fragment, null,
3335
3769
  filterToggles,
3336
- filterDrawer && (React__default["default"].createElement(Drawer, null, filterDrawer))));
3770
+ filterDrawer && (React__default["default"].createElement(Drawer, { customBackgroundColor: "#eee" }, filterDrawer))));
3337
3771
  // Actually filter the logs
3338
3772
  // > Perform filters
3339
3773
  const logs = [];
@@ -3374,16 +3808,22 @@ const LogReviewer = (props) => {
3374
3808
  // Whole context is deselected
3375
3809
  contextFilterState[log.context] === false
3376
3810
  // None of the subcontexts are selected
3377
- || (Object.values((_a = contextFilterState[log.context]) !== null && _a !== void 0 ? _a : {})
3378
- .every((isSelected) => {
3379
- return !isSelected;
3380
- }))) {
3811
+ || (
3812
+ // Has subcontexts
3813
+ typeof contextFilterState[log.context] !== 'boolean'
3814
+ // None of the subcontexts are selected
3815
+ && Object.values((_a = contextFilterState[log.context]) !== null && _a !== void 0 ? _a : {})
3816
+ .every((isSelected) => {
3817
+ return !isSelected;
3818
+ }))) {
3381
3819
  return;
3382
3820
  }
3383
3821
  // Subcontext doesn't match
3384
3822
  if (
3385
- // Log has a subcontext
3386
- log.subcontext
3823
+ // Log context is not "uncategorized" (no point in further filters)
3824
+ log.context !== LogBuiltInMetadata.Context.Uncategorized
3825
+ // Log has a subcontext
3826
+ && log.subcontext
3387
3827
  // Context has subcontexts
3388
3828
  && (contextFilterState[log.context]
3389
3829
  && contextFilterState[log.context] !== false
@@ -3395,12 +3835,16 @@ const LogReviewer = (props) => {
3395
3835
  /* -------------- Tags -------------- */
3396
3836
  // No tags match
3397
3837
  if (
3398
- // Log has at least one tag
3399
- log.tags.length > 0
3838
+ // At least one tag is required
3839
+ Object.values(tagFilterState)
3840
+ .filter((isSelected) => {
3841
+ return isSelected;
3842
+ })
3843
+ .length > 0
3400
3844
  // No tags match
3401
- && (log.tags.every((tag) => {
3845
+ && log.tags.every((tag) => {
3402
3846
  return !tagFilterState[tag];
3403
- }))) {
3847
+ })) {
3404
3848
  return;
3405
3849
  }
3406
3850
  /* ------- Actions and Errors ------- */
@@ -3534,7 +3978,7 @@ const LogReviewer = (props) => {
3534
3978
  // Device info exists
3535
3979
  && log.device
3536
3980
  // Mobile filter doesn't match
3537
- && (advancedFilterState.isMobile === log.device.isMobile)) {
3981
+ && (advancedFilterState.isMobile !== log.device.isMobile)) {
3538
3982
  return;
3539
3983
  }
3540
3984
  // Log source doesn't match
@@ -3576,205 +4020,20 @@ const LogReviewer = (props) => {
3576
4020
  /*----------------------------------------*/
3577
4021
  /* Data */
3578
4022
  /*----------------------------------------*/
3579
- // Create data table
3580
- const columns = [
3581
- {
3582
- title: 'First Name',
3583
- param: 'userFirstName',
3584
- type: ParamType$1.String,
3585
- },
3586
- {
3587
- title: 'Last Name',
3588
- param: 'userLastName',
3589
- type: ParamType$1.String,
3590
- },
3591
- {
3592
- title: 'Email',
3593
- param: 'userEmail',
3594
- type: ParamType$1.String,
3595
- },
3596
- {
3597
- title: 'Canvas Id',
3598
- param: 'userId',
3599
- type: ParamType$1.Int,
3600
- },
3601
- {
3602
- title: 'Student',
3603
- param: 'isLearner',
3604
- type: ParamType$1.Boolean,
3605
- },
3606
- {
3607
- title: 'Teaching Staff',
3608
- param: 'isTTM',
3609
- type: ParamType$1.Boolean,
3610
- startsHidden: true,
3611
- },
3612
- {
3613
- title: 'Admin',
3614
- param: 'isAdmin',
3615
- type: ParamType$1.Boolean,
3616
- startsHidden: true,
3617
- },
3618
- {
3619
- title: 'Course Canvas Id',
3620
- param: 'courseId',
3621
- type: ParamType$1.Int,
3622
- startsHidden: true,
3623
- },
3624
- {
3625
- title: 'Course Name',
3626
- param: 'courseName',
3627
- type: ParamType$1.String,
3628
- },
3629
- {
3630
- title: 'Browser Name',
3631
- param: 'browser.name',
3632
- type: ParamType$1.String,
3633
- startsHidden: true,
3634
- },
3635
- {
3636
- title: 'Browser Version',
3637
- param: 'browser.version',
3638
- type: ParamType$1.String,
3639
- startsHidden: true,
3640
- },
3641
- {
3642
- title: 'OS',
3643
- param: 'device.os',
3644
- type: ParamType$1.String,
3645
- startsHidden: true,
3646
- },
3647
- {
3648
- title: 'Mobile',
3649
- param: 'device.isMobile',
3650
- type: ParamType$1.Boolean,
3651
- startsHidden: true,
3652
- },
3653
- {
3654
- title: 'Year',
3655
- param: 'year',
3656
- type: ParamType$1.Int,
3657
- },
3658
- {
3659
- title: 'Month',
3660
- param: 'month',
3661
- type: ParamType$1.Int,
3662
- },
3663
- {
3664
- title: 'Day',
3665
- param: 'day',
3666
- type: ParamType$1.Int,
3667
- },
3668
- {
3669
- title: 'Hour',
3670
- param: 'hour',
3671
- type: ParamType$1.Int,
3672
- },
3673
- {
3674
- title: 'Minute',
3675
- param: 'minute',
3676
- type: ParamType$1.Int,
3677
- startsHidden: true,
3678
- },
3679
- {
3680
- title: 'Timestamp',
3681
- param: 'timestamp',
3682
- type: ParamType$1.Int,
3683
- startsHidden: true,
3684
- },
3685
- {
3686
- title: 'Context',
3687
- param: 'context',
3688
- type: ParamType$1.String,
3689
- },
3690
- {
3691
- title: 'Subcontext',
3692
- param: 'subcontext',
3693
- type: ParamType$1.String,
3694
- },
3695
- {
3696
- title: 'Tags',
3697
- param: 'tags',
3698
- type: ParamType$1.JSON,
3699
- startsHidden: true,
3700
- },
3701
- {
3702
- title: 'Log Level',
3703
- param: 'level',
3704
- type: ParamType$1.String,
3705
- startsHidden: true,
3706
- },
3707
- {
3708
- title: 'Metadata',
3709
- param: 'metadata',
3710
- type: ParamType$1.JSON,
3711
- startsHidden: true,
3712
- },
3713
- {
3714
- title: 'Source',
3715
- param: 'source',
3716
- type: ParamType$1.String,
3717
- },
3718
- {
3719
- title: 'Server Route Path',
3720
- param: 'routePath',
3721
- type: ParamType$1.String,
3722
- startsHidden: true,
3723
- },
3724
- {
3725
- title: 'Server Route Template',
3726
- param: 'routeTemplate',
3727
- type: ParamType$1.String,
3728
- startsHidden: true,
3729
- },
3730
- {
3731
- title: 'Type',
3732
- param: 'type',
3733
- type: ParamType$1.String,
3734
- },
3735
- {
3736
- title: 'Error Message',
3737
- param: 'errorMessage',
3738
- type: ParamType$1.String,
3739
- startsHidden: true,
3740
- },
3741
- {
3742
- title: 'Error Code',
3743
- param: 'errorCode',
3744
- type: ParamType$1.String,
3745
- startsHidden: true,
3746
- },
3747
- {
3748
- title: 'Error Stack',
3749
- param: 'errorStack',
3750
- type: ParamType$1.String,
3751
- startsHidden: true,
3752
- },
3753
- {
3754
- title: 'Action Target',
3755
- param: 'target',
3756
- type: ParamType$1.String,
3757
- startsHidden: true,
3758
- },
3759
- {
3760
- title: 'Action Type',
3761
- param: 'action',
3762
- type: ParamType$1.String,
3763
- startsHidden: true,
3764
- },
3765
- ];
4023
+ // Nothing to show notice
4024
+ const noLogsNotice = (logs.length === 0
4025
+ ? (React__default["default"].createElement("div", { className: "alert alert-warning text-center mt-2" },
4026
+ React__default["default"].createElement("h4", { className: "m-1" }, "No Logs to Show"),
4027
+ React__default["default"].createElement("div", null, "Either your filters are too strict or no matching logs have been created yet.")))
4028
+ : undefined);
3766
4029
  // Create intelliTable
3767
- const dataTable = (logs.length === 0
3768
- ? (React__default["default"].createElement(React__default["default"].Fragment, null,
3769
- React__default["default"].createElement("h3", { className: "m-0" }, "Matching Logs:"),
3770
- React__default["default"].createElement("div", { className: "alert alert-warning text-center" },
3771
- React__default["default"].createElement("h4", { className: "m-1" }, "No Logs to Show"),
3772
- React__default["default"].createElement("div", null, "Either your filters are too strict or no matching logs have been created yet."))))
3773
- : (React__default["default"].createElement(IntelliTable, { title: "Matching Logs:", id: "logs", data: logs, columns: columns })));
4030
+ const dataTable = (React__default["default"].createElement(IntelliTable, { title: "Matching Logs:", csvName: `Logs from ${getHumanReadableDate()}`, id: "logs", data: logs, columns: columns }));
3774
4031
  // Main body
3775
4032
  body = (React__default["default"].createElement(React__default["default"].Fragment, null,
3776
4033
  filters,
3777
- dataTable));
4034
+ React__default["default"].createElement("div", { className: "mt-2" },
4035
+ dataTable,
4036
+ noLogsNotice)));
3778
4037
  }
3779
4038
  /* ---------- Wrap in Modal --------- */
3780
4039
  return (React__default["default"].createElement("div", { className: "LogReviewer-outer-container" },
@@ -3783,8 +4042,9 @@ const LogReviewer = (props) => {
3783
4042
  React__default["default"].createElement("div", { className: "LogReviewer-header" },
3784
4043
  React__default["default"].createElement("div", { className: "LogReviewer-header-title" },
3785
4044
  React__default["default"].createElement("h3", { className: "text-center m-0" }, "Log Review Dashboard")),
3786
- React__default["default"].createElement("button", { type: "button", className: "LogReviewer-header-close-button btn btn-dark btn-lg", "aria-label": "close log reviewer panel", onClick: onClose },
3787
- React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faTimes }))),
4045
+ React__default["default"].createElement("div", { style: { width: 0 } },
4046
+ React__default["default"].createElement("button", { type: "button", className: "LogReviewer-header-close-button btn btn-dark btn-lg pe-0", "aria-label": "close log reviewer panel", onClick: onClose },
4047
+ React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faTimes })))),
3788
4048
  React__default["default"].createElement("div", { className: "LogReviewer-contents" }, body))));
3789
4049
  };
3790
4050
 
@@ -3806,6 +4066,50 @@ const HOUR_IN_MS = 3600000;
3806
4066
  */
3807
4067
  const DAY_IN_MS = 86400000;
3808
4068
 
4069
+ /**
4070
+ * Check if the user is on mobile
4071
+ * from https://stackoverflow.com/questions/11381673/detecting-a-mobile-browser
4072
+ * @returns true if user is on a mobile device
4073
+ */
4074
+ const checkIfMobileOrTablet = () => {
4075
+ var _a;
4076
+ try {
4077
+ let check = false;
4078
+ ((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)))
4079
+ check = true; })((navigator !== null && navigator !== void 0 ? navigator : {}).userAgent || (navigator !== null && navigator !== void 0 ? navigator : {}).vendor || ((_a = window) !== null && _a !== void 0 ? _a : {}).opera);
4080
+ return check;
4081
+ }
4082
+ catch (err) {
4083
+ // Probably this code is being executed on a server
4084
+ return false;
4085
+ }
4086
+ };
4087
+ const isMobileOrTablet = checkIfMobileOrTablet();
4088
+ /**
4089
+ * Dynamic words determined by the user's platform
4090
+ * @author Gabe Abrams
4091
+ */
4092
+ const DynamicWord = {
4093
+ Click: (isMobileOrTablet
4094
+ ? 'tap'
4095
+ : 'click'),
4096
+ ClickCapitalized: (isMobileOrTablet
4097
+ ? 'Tap'
4098
+ : 'Click'),
4099
+ App: (isMobileOrTablet
4100
+ ? 'app'
4101
+ : 'application'),
4102
+ AppCapitalized: (isMobileOrTablet
4103
+ ? 'App'
4104
+ : 'Application'),
4105
+ Device: (isMobileOrTablet
4106
+ ? 'device'
4107
+ : 'computer'),
4108
+ DeviceCapitalized: (isMobileOrTablet
4109
+ ? 'Device'
4110
+ : 'Computer'),
4111
+ };
4112
+
3809
4113
  /**
3810
4114
  * Shorten text so it fits into a certain number of chars
3811
4115
  * @author Gabe Abrams
@@ -3880,6 +4184,18 @@ const floorToNumDecimals = (num, numDecimals) => {
3880
4184
  return (Math.floor(num * rounder) / rounder);
3881
4185
  };
3882
4186
 
4187
+ /**
4188
+ * Force a number to stay within specific bounds
4189
+ * @author Gabe Abrams
4190
+ * @param num the number to move into the bounds
4191
+ * @param min the minimum number in the bound
4192
+ * @param max the maximum number in the bound
4193
+ * @returns bounded number
4194
+ */
4195
+ const forceNumIntoBounds = (num, min, max) => {
4196
+ return Math.max(min, Math.min(max, num));
4197
+ };
4198
+
3883
4199
  /**
3884
4200
  * Pad a number's decimal with zeros on the right
3885
4201
  * (e.g. 5.2 becomes 5.20 with 2 digit padding)
@@ -4115,15 +4431,17 @@ const initServer = (opts) => {
4115
4431
  },
4116
4432
  handler: ({ params }) => __awaiter(void 0, void 0, void 0, function* () {
4117
4433
  // Get user info
4118
- const { userId, isAdmin } = params;
4434
+ const { year, month, userId, isAdmin, } = params;
4119
4435
  // Validate user
4120
- // isAdmin is already checked because path starts with '/admin'
4121
4436
  const canReview = yield canReviewLogs(userId, isAdmin);
4122
4437
  if (!canReview) {
4123
4438
  throw new ErrorWithCode('You cannot access this resource because you do not have the appropriate permissions.', ReactKitErrorCode$1.NotAllowedToReviewLogs);
4124
4439
  }
4125
4440
  // Query for logs
4126
- const logs = yield _logCollection.find({ userId });
4441
+ const logs = yield _logCollection.find({
4442
+ year,
4443
+ month,
4444
+ });
4127
4445
  // Return logs
4128
4446
  return logs;
4129
4447
  }),
@@ -4772,7 +5090,7 @@ const genRouteHandler = (opts) => {
4772
5090
  isAdmin: !!launchInfo.isAdmin,
4773
5091
  isTTM: !!launchInfo.isTTM,
4774
5092
  courseId: launchInfo.courseId,
4775
- courseName: launchInfo.courseName,
5093
+ courseName: launchInfo.contextLabel,
4776
5094
  browser,
4777
5095
  device,
4778
5096
  year,
@@ -4799,7 +5117,7 @@ const genRouteHandler = (opts) => {
4799
5117
  }
4800
5118
  : {
4801
5119
  type: LogType$1.Action,
4802
- target: ((_m = opts.target) !== null && _m !== void 0 ? _m : LogBuiltInMetadata.Target.NoSpecificTarget),
5120
+ target: ((_m = opts.target) !== null && _m !== void 0 ? _m : LogBuiltInMetadata.Target.NoTarget),
4803
5121
  action: ((_o = opts.action) !== null && _o !== void 0 ? _o : LogAction$1.Unknown),
4804
5122
  });
4805
5123
  // Source-specific info
@@ -5012,29 +5330,6 @@ const startMinWait = (minWaitMs) => {
5012
5330
  });
5013
5331
  };
5014
5332
 
5015
- // Import shared helpers
5016
- /**
5017
- * Get a human-readable description of a date (all in ET)
5018
- * @author Gabe Abrams
5019
- * @param [dateOrTimestamp=today] the date or timestamp for the date to describe
5020
- * @returns human-readable description of the date
5021
- */
5022
- const getHumanReadableDate = (dateOrTimestamp) => {
5023
- // Get the date info
5024
- const { month, day, year, } = getTimeInfoInET(dateOrTimestamp);
5025
- const currYear = getTimeInfoInET().year;
5026
- // Get the short month description
5027
- const monthName = getMonthName(month).short;
5028
- // Create start of description
5029
- let description = `${monthName} ${day}${getOrdinal(day)}`;
5030
- // Add on year if it's different
5031
- if (year !== currYear) {
5032
- description += ` ${year}`;
5033
- }
5034
- // Return description
5035
- return description;
5036
- };
5037
-
5038
5333
  /**
5039
5334
  * Get the current part of day (morning, evening, etc.)
5040
5335
  * @author Gabe Abrams
@@ -5215,6 +5510,7 @@ exports.CopiableBox = CopiableBox;
5215
5510
  exports.DAY_IN_MS = DAY_IN_MS;
5216
5511
  exports.DayOfWeek = DayOfWeek$1;
5217
5512
  exports.Drawer = Drawer;
5513
+ exports.DynamicWord = DynamicWord;
5218
5514
  exports.ErrorBox = ErrorBox;
5219
5515
  exports.ErrorWithCode = ErrorWithCode;
5220
5516
  exports.HOUR_IN_MS = HOUR_IN_MS;