dce-reactkit 3.2.2-beta.5 → 3.2.2-beta.50

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 (46) hide show
  1. package/dist/cjs/index.js +728 -433
  2. package/dist/cjs/index.js.map +1 -1
  3. package/dist/cjs/types/components/ButtonInputGroup.d.ts +1 -0
  4. package/dist/cjs/types/components/Drawer.d.ts +1 -0
  5. package/dist/cjs/types/components/IntelliTable.d.ts +1 -0
  6. package/dist/cjs/types/components/ItemPicker/NestableItemList.d.ts +1 -0
  7. package/dist/cjs/types/components/ItemPicker/index.d.ts +1 -0
  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 +729 -434
  15. package/dist/esm/index.js.map +1 -1
  16. package/dist/esm/types/components/ButtonInputGroup.d.ts +1 -0
  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/components/ItemPicker/index.d.ts +1 -0
  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 +51 -29
  28. package/package.json +1 -1
  29. package/src/components/ButtonInputGroup.tsx +5 -2
  30. package/src/components/CSVDownloadButton.tsx +2 -2
  31. package/src/components/Drawer.tsx +9 -1
  32. package/src/components/IntelliTable.tsx +181 -26
  33. package/src/components/ItemPicker/NestableItemList.tsx +32 -14
  34. package/src/components/ItemPicker/index.tsx +4 -0
  35. package/src/components/LogReviewer.tsx +635 -406
  36. package/src/components/SimpleDateChooser.tsx +27 -18
  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/server/initServer.ts +10 -3
  41. package/src/types/LogAction.ts +16 -16
  42. package/src/types/LogBuiltInMetadata.ts +5 -5
  43. package/src/types/LogMetadataContextMap.ts +15 -0
  44. package/src/types/LogMetadataTagMap.ts +9 -0
  45. package/src/types/LogMetadataTargetMap.ts +9 -0
  46. 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
@@ -1188,21 +1188,21 @@ const ButtonInputGroup = (props) => {
1188
1188
  /*------------------------------------------------------------------------*/
1189
1189
  /* -------------- Props ------------- */
1190
1190
  // Destructure all props
1191
- const { label, minLabelWidth, children, } = props;
1191
+ const { label, minLabelWidth, children, className, } = props;
1192
1192
  /*------------------------------------------------------------------------*/
1193
1193
  /* Render */
1194
1194
  /*------------------------------------------------------------------------*/
1195
1195
  /*----------------------------------------*/
1196
1196
  /* Main UI */
1197
1197
  /*----------------------------------------*/
1198
- return (React__default["default"].createElement("div", { className: "input-group" },
1198
+ return (React__default["default"].createElement("div", { className: `input-group ${className !== null && className !== void 0 ? className : ''}` },
1199
1199
  React__default["default"].createElement("div", { className: "input-group-prepend d-flex w-100" },
1200
1200
  React__default["default"].createElement("span", { className: "input-group-text", style: {
1201
1201
  minWidth: (minLabelWidth !== null && minLabelWidth !== void 0 ? minLabelWidth : undefined),
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,
@@ -1347,7 +1347,7 @@ const SimpleDateChooser = (props) => {
1347
1347
  let startMonth = today.month;
1348
1348
  if (chooseFromPast) {
1349
1349
  startMonth -= Math.max(0, numMonthsToShow - 1);
1350
- if (startMonth <= 0) {
1350
+ while (startMonth <= 0) {
1351
1351
  startMonth += 12;
1352
1352
  startYear -= 1;
1353
1353
  }
@@ -1355,22 +1355,35 @@ const SimpleDateChooser = (props) => {
1355
1355
  for (let i = 0; i < numMonthsToShow; i++) {
1356
1356
  // Get month and year info
1357
1357
  const unmoddedMonth = (startMonth + i);
1358
- const month = (unmoddedMonth > 12
1359
- ? unmoddedMonth - 12
1360
- : unmoddedMonth);
1358
+ let month = unmoddedMonth;
1359
+ while (month > 12) {
1360
+ month -= 12;
1361
+ }
1361
1362
  const monthName = getMonthName(month).full;
1362
- const year = (unmoddedMonth > 12
1363
- ? startYear + 1
1364
- : startYear);
1363
+ // Year is start year +1 for each 12 months
1364
+ const year = (startYear + (Math.floor(unmoddedMonth / 12)));
1365
1365
  // Figure out which days are allowed
1366
1366
  const days = [];
1367
1367
  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);
1368
+ if (chooseFromPast) {
1369
+ // Past selection
1370
+ const numDaysToAdd = ((month === today.month)
1371
+ ? today.day // Current month, only add up to today
1372
+ : numDaysInMonth // Past month, add all days
1373
+ );
1374
+ for (let day = 1; day <= numDaysToAdd; day++) {
1375
+ days.push(day);
1376
+ }
1377
+ }
1378
+ else {
1379
+ // Future selection: add all remaining days of the month
1380
+ const firstDay = (month === today.month
1381
+ ? today.day // Current month: start at current date
1382
+ : 1 // Future month: start at beginning of month
1383
+ );
1384
+ for (let day = firstDay; day <= numDaysInMonth; day++) {
1385
+ days.push(day);
1386
+ }
1374
1387
  }
1375
1388
  choices.push({
1376
1389
  choiceName: `${monthName} ${year}`,
@@ -1441,14 +1454,16 @@ const Drawer = (props) => {
1441
1454
  /*------------------------------------------------------------------------*/
1442
1455
  /* -------------- Props ------------- */
1443
1456
  // Destructure all props
1444
- const { children, } = props;
1457
+ const { customBackgroundColor, children, } = props;
1445
1458
  /*------------------------------------------------------------------------*/
1446
1459
  /* Render */
1447
1460
  /*------------------------------------------------------------------------*/
1448
1461
  /*----------------------------------------*/
1449
1462
  /* Main UI */
1450
1463
  /*----------------------------------------*/
1451
- return (React__default["default"].createElement("div", { className: "Drawer-container" },
1464
+ return (React__default["default"].createElement("div", { className: "Drawer-container", style: {
1465
+ backgroundColor: (customBackgroundColor !== null && customBackgroundColor !== void 0 ? customBackgroundColor : undefined),
1466
+ } },
1452
1467
  React__default["default"].createElement("style", null, style$4),
1453
1468
  children));
1454
1469
  };
@@ -1911,24 +1926,25 @@ const CopiableBox = (props) => {
1911
1926
  /**
1912
1927
  * Reusable nested item picker
1913
1928
  * @author Yuen Ler Chow
1929
+ * @author Gabe Abrams
1914
1930
  */
1915
1931
  /* ------------- Actions ------------ */
1916
1932
  // Types of actions
1917
1933
  var ActionType$2;
1918
1934
  (function (ActionType) {
1919
- // Toggle whether the children are being shown
1920
- ActionType["ToggleItems"] = "toggle-items";
1935
+ // Toggle whether a child are being shown
1936
+ ActionType["ToggleChild"] = "toggle-child";
1921
1937
  })(ActionType$2 || (ActionType$2 = {}));
1922
1938
  /**
1923
1939
  * Reducer that executes actions
1924
- * @author Yuen Ler Chow
1940
+ * @author Gabe Abrams
1925
1941
  * @param state current state
1926
1942
  * @param action action to execute
1927
1943
  */
1928
1944
  const reducer$2 = (state, action) => {
1929
1945
  switch (action.type) {
1930
- case ActionType$2.ToggleItems: {
1931
- return { isShowingItems: !state.isShowingItems };
1946
+ case ActionType$2.ToggleChild: {
1947
+ return Object.assign(Object.assign({}, state), { childExpanded: Object.assign(Object.assign({}, state.childExpanded), { [String(action.id)]: !state.childExpanded[String(action.id)] }) });
1932
1948
  }
1933
1949
  default: {
1934
1950
  return state;
@@ -1946,14 +1962,19 @@ const NestableItemList = (props) => {
1946
1962
  // Destructure all props
1947
1963
  const { items, onChanged, } = props;
1948
1964
  /* -------------- State ------------- */
1965
+ // Create initial map of child expanded booleans
1966
+ const initChildExpanded = {};
1967
+ items.forEach((item) => {
1968
+ initChildExpanded[String(item.id)] = false;
1969
+ });
1949
1970
  // Initial state
1950
1971
  const initialState = {
1951
- isShowingItems: false,
1972
+ childExpanded: initChildExpanded,
1952
1973
  };
1953
1974
  // Initialize state
1954
1975
  const [state, dispatch] = React.useReducer(reducer$2, initialState);
1955
1976
  // Destructure common state
1956
- const { isShowingItems, } = state;
1977
+ const { childExpanded, } = state;
1957
1978
  /*------------------------------------------------------------------------*/
1958
1979
  /* Component Functions */
1959
1980
  /*------------------------------------------------------------------------*/
@@ -2041,14 +2062,15 @@ const NestableItemList = (props) => {
2041
2062
  backgroundColor: 'transparent',
2042
2063
  }, type: "button", onClick: () => {
2043
2064
  dispatch({
2044
- type: ActionType$2.ToggleItems,
2065
+ type: ActionType$2.ToggleChild,
2066
+ id: item.id,
2045
2067
  });
2046
- }, "aria-label": `${isShowingItems ? 'Hide' : 'Show'} items in ${item.name}` },
2047
- React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: isShowingItems ? freeSolidSvgIcons.faChevronDown : freeSolidSvgIcons.faChevronRight })))),
2068
+ }, "aria-label": `${childExpanded[item.id] ? 'Hide' : 'Show'} items in ${item.name}` },
2069
+ React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: childExpanded[item.id] ? freeSolidSvgIcons.faChevronDown : freeSolidSvgIcons.faChevronRight })))),
2048
2070
  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
2071
  onChanged(changeChecked(item.id, checked, items));
2050
2072
  }, ariaLabel: `Select ${item.name}`, checkedVariant: Variant$1.Light }),
2051
- item.isGroup && isShowingItems && (React__default["default"].createElement("div", { className: "NestableItemList-children-container", style: {
2073
+ (item.isGroup && childExpanded[item.id]) && (React__default["default"].createElement("div", { className: "NestableItemList-children-container", style: {
2052
2074
  paddingLeft: '2.2rem',
2053
2075
  } },
2054
2076
  React__default["default"].createElement(NestableItemList, { items: item.children, onChanged: (updatedItems) => {
@@ -2070,7 +2092,7 @@ const ItemPicker = (props) => {
2070
2092
  /*------------------------------------------------------------------------*/
2071
2093
  /* -------------- Props ------------- */
2072
2094
  // Destructure all props
2073
- const { title, items, onChanged, } = props;
2095
+ const { title, items, onChanged, noBottomMargin, } = props;
2074
2096
  /*------------------------------------------------------------------------*/
2075
2097
  /* Component Functions */
2076
2098
  /*------------------------------------------------------------------------*/
@@ -2080,11 +2102,34 @@ const ItemPicker = (props) => {
2080
2102
  /*----------------------------------------*/
2081
2103
  /* Main UI */
2082
2104
  /*----------------------------------------*/
2083
- return (React__default["default"].createElement(TabBox, { title: title },
2105
+ return (React__default["default"].createElement(TabBox, { title: title, noBottomMargin: noBottomMargin },
2084
2106
  React__default["default"].createElement("div", { style: { overflowX: 'auto' } },
2085
2107
  React__default["default"].createElement(NestableItemList, { items: items, onChanged: onChanged }))));
2086
2108
  };
2087
2109
 
2110
+ // Import shared helpers
2111
+ /**
2112
+ * Get a human-readable description of a date (all in ET)
2113
+ * @author Gabe Abrams
2114
+ * @param [dateOrTimestamp=today] the date or timestamp for the date to describe
2115
+ * @returns human-readable description of the date
2116
+ */
2117
+ const getHumanReadableDate = (dateOrTimestamp) => {
2118
+ // Get the date info
2119
+ const { month, day, year, } = getTimeInfoInET(dateOrTimestamp);
2120
+ const currYear = getTimeInfoInET().year;
2121
+ // Get the short month description
2122
+ const monthName = getMonthName(month).short;
2123
+ // Create start of description
2124
+ let description = `${monthName} ${day}${getOrdinal(day)}`;
2125
+ // Add on year if it's different
2126
+ if (year !== currYear) {
2127
+ description += ` ${year}`;
2128
+ }
2129
+ // Return description
2130
+ return description;
2131
+ };
2132
+
2088
2133
  /**
2089
2134
  * Path of the route for storing client-side logs
2090
2135
  * @author Gabe Abrams
@@ -2124,37 +2169,37 @@ var LogType$1 = LogType;
2124
2169
  var LogAction;
2125
2170
  (function (LogAction) {
2126
2171
  // Target was opened by the user (it was not on screen, but now it is)
2127
- LogAction["Open"] = "open";
2172
+ LogAction["Open"] = "Open";
2128
2173
  // Target was closed by the user (it was on screen, but now it is not)
2129
- LogAction["Close"] = "close";
2174
+ LogAction["Close"] = "Close";
2130
2175
  // Target was cancelled by the user (it was on closed without saving)
2131
- LogAction["Cancel"] = "cancel";
2176
+ LogAction["Cancel"] = "Cancel";
2132
2177
  // Target was expanded by the user (it always remains on screen, but size was changed)
2133
- LogAction["Expand"] = "expand";
2178
+ LogAction["Expand"] = "Expand";
2134
2179
  // Target was collapsed by the user (it always remains on screen, but size was changed)
2135
- LogAction["Collapse"] = "collapse";
2180
+ LogAction["Collapse"] = "Collapse";
2136
2181
  // 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";
2182
+ LogAction["View"] = "View";
2138
2183
  // Target interrupted the user (popup, dialog, validation message, etc. appeared without user prompting)
2139
- LogAction["Interrupt"] = "interrupt";
2184
+ LogAction["Interrupt"] = "Interrupt";
2140
2185
  // Target was created by the user (it did not exist before)
2141
- LogAction["Create"] = "create";
2186
+ LogAction["Create"] = "Create";
2142
2187
  // Target was edited by the user (it existed and was changed)
2143
- LogAction["Edit"] = "edit";
2188
+ LogAction["Edit"] = "Edit";
2144
2189
  // Target was deleted by the user (it existed and now it doesn't)
2145
- LogAction["Delete"] = "delete";
2190
+ LogAction["Delete"] = "Delete";
2146
2191
  // Target was added by the user (it already existed and was added to another place)
2147
- LogAction["Add"] = "add";
2192
+ LogAction["Add"] = "Add";
2148
2193
  // Target was removed by the user (it was removed from something but still exists)
2149
- LogAction["Remove"] = "remove";
2194
+ LogAction["Remove"] = "Remove";
2150
2195
  // Target was activated by the user (click, check, tap, keypress, etc.)
2151
- LogAction["Activate"] = "activate";
2196
+ LogAction["Activate"] = "Activate";
2152
2197
  // Target was deactivated by the user (click away, uncheck, tap outside of, tab away, etc.)
2153
- LogAction["Deactivate"] = "deactivate";
2198
+ LogAction["Deactivate"] = "Deactivate";
2154
2199
  // User showed interest in a target (hover, peek, etc.)
2155
- LogAction["Peek"] = "peek";
2200
+ LogAction["Peek"] = "Peek";
2156
2201
  // Unknown action
2157
- LogAction["Unknown"] = "unknown";
2202
+ LogAction["Unknown"] = "Unknown";
2158
2203
  })(LogAction || (LogAction = {}));
2159
2204
  var LogAction$1 = LogAction;
2160
2205
 
@@ -2196,12 +2241,12 @@ const roundToNumDecimals = (num, numDecimals) => {
2196
2241
  * @returns escaped cell text
2197
2242
  */
2198
2243
  const escapeCellText = (text) => {
2199
- if (!text.includes(',')) {
2244
+ if (!String(text).includes(',')) {
2200
2245
  // No need to escape
2201
- return text;
2246
+ return String(text);
2202
2247
  }
2203
2248
  // Perform escape
2204
- return `"${text.replace(/"/g, '""')}`;
2249
+ return `"${String(text).replace(/"/g, '""')}`;
2205
2250
  };
2206
2251
  /**
2207
2252
  * Generate a CSV file
@@ -2220,9 +2265,26 @@ const genCSV = (data, columns) => {
2220
2265
  .join(','));
2221
2266
  // Add each row
2222
2267
  data.forEach((datum) => {
2268
+ csv += '\n';
2223
2269
  csv += (columns
2224
2270
  .map((column) => {
2225
- return escapeCellText(datum[column.param]);
2271
+ let contents;
2272
+ const cell = datum[column.param];
2273
+ if (typeof cell === 'string'
2274
+ || typeof cell === 'number') {
2275
+ contents = String(cell);
2276
+ }
2277
+ else if (typeof cell === 'undefined'
2278
+ || cell === null) {
2279
+ contents = '';
2280
+ }
2281
+ else if (typeof cell === 'object') {
2282
+ contents = JSON.stringify(cell);
2283
+ }
2284
+ else {
2285
+ contents = '';
2286
+ }
2287
+ return escapeCellText(contents);
2226
2288
  })
2227
2289
  .join(','));
2228
2290
  });
@@ -2251,11 +2313,12 @@ const CSVDownloadButton = (props) => {
2251
2313
  /* Main UI */
2252
2314
  /*----------------------------------------*/
2253
2315
  // 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
2316
+ 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
2317
  ? `Click to download ${filename}`
2256
2318
  : ariaLabel), style: style, onClick: onClick },
2257
2319
  !children && (React__default["default"].createElement(React__default["default"].Fragment, null,
2258
- React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faCloudDownloadAlt, className: "mr-2" }),
2320
+ React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faCloudDownloadAlt }),
2321
+ ' ',
2259
2322
  "Download CSV")),
2260
2323
  children));
2261
2324
  };
@@ -2279,9 +2342,13 @@ var ActionType$1;
2279
2342
  // Toggle sort column param
2280
2343
  ActionType["ToggleSortColumn"] = "toggle-sort-column";
2281
2344
  // Toggle the visibility of a column
2282
- ActionType["ToggleColumnVisibility"] = "toggle-column-visibility";
2345
+ ActionType["UpdateColumnVisibility"] = "update-column-visibility";
2283
2346
  // Toggle the column visibility customization modal
2284
2347
  ActionType["ToggleColVisCusModalVisibility"] = "toggle-col-vis-cus-modal-visibility";
2348
+ // Show all columns
2349
+ ActionType["ShowAllColumns"] = "show-all-columns";
2350
+ // Hide all columns
2351
+ ActionType["HideAllColumns"] = "hide-all-columns";
2285
2352
  })(ActionType$1 || (ActionType$1 = {}));
2286
2353
  /**
2287
2354
  * Reducer that executes actions
@@ -2303,14 +2370,28 @@ const reducer$1 = (state, action) => {
2303
2370
  // Stop sorting by column
2304
2371
  return Object.assign(Object.assign({}, state), { sortColumnParam: undefined, sortType: SortType.Ascending });
2305
2372
  }
2306
- case ActionType$1.ToggleColumnVisibility: {
2373
+ case ActionType$1.UpdateColumnVisibility: {
2307
2374
  const { columnVisibilityMap } = state;
2308
- columnVisibilityMap[action.param] = !columnVisibilityMap[action.param];
2375
+ columnVisibilityMap[action.param] = action.visible;
2309
2376
  return Object.assign(Object.assign({}, state), { columnVisibilityMap });
2310
2377
  }
2311
2378
  case ActionType$1.ToggleColVisCusModalVisibility: {
2312
2379
  return Object.assign(Object.assign({}, state), { columnVisibilityCustomizationModalVisible: !state.columnVisibilityCustomizationModalVisible });
2313
2380
  }
2381
+ case ActionType$1.ShowAllColumns: {
2382
+ const { columnVisibilityMap } = state;
2383
+ Object.keys(columnVisibilityMap).forEach((param) => {
2384
+ columnVisibilityMap[param] = true;
2385
+ });
2386
+ return Object.assign(Object.assign({}, state), { columnVisibilityMap });
2387
+ }
2388
+ case ActionType$1.HideAllColumns: {
2389
+ const { columnVisibilityMap } = state;
2390
+ Object.keys(columnVisibilityMap).forEach((param) => {
2391
+ columnVisibilityMap[param] = false;
2392
+ });
2393
+ return Object.assign(Object.assign({}, state), { columnVisibilityMap });
2394
+ }
2314
2395
  default: {
2315
2396
  return state;
2316
2397
  }
@@ -2326,7 +2407,18 @@ const IntelliTable = (props) => {
2326
2407
  var _a;
2327
2408
  /* -------------- Props ------------- */
2328
2409
  // Destructure all props
2329
- const { title, id, data, columns, } = props;
2410
+ const { title, id, columns, } = props;
2411
+ // Get data, show empty row if none
2412
+ const data = ((props.data && props.data.length > 0)
2413
+ ? props.data
2414
+ : [{ id: 'empty-row' }]);
2415
+ // Get CSV filename
2416
+ let filename = `${title}.csv`;
2417
+ if (props.csvName) {
2418
+ filename = (props.csvName.endsWith('.csv')
2419
+ ? props.csvName
2420
+ : `${props.csvName}.csv`);
2421
+ }
2330
2422
  /* -------------- State ------------- */
2331
2423
  // Initial state
2332
2424
  const initColumnVisibilityMap = {};
@@ -2343,21 +2435,49 @@ const IntelliTable = (props) => {
2343
2435
  const [state, dispatch] = React.useReducer(reducer$1, initialState);
2344
2436
  // Destructure common state
2345
2437
  const { sortColumnParam, sortType, columnVisibilityMap, columnVisibilityCustomizationModalVisible, } = state;
2438
+ /*------------------------------------------------------------------------*/
2439
+ /* Render */
2440
+ /*------------------------------------------------------------------------*/
2441
+ /*----------------------------------------*/
2442
+ /* Modal */
2443
+ /*----------------------------------------*/
2444
+ // Modal that may be defined
2445
+ let modal;
2346
2446
  /* ------- Col Vis Customization Modal ------ */
2347
2447
  if (columnVisibilityCustomizationModalVisible) {
2348
2448
  // Create modal
2349
- (React__default["default"].createElement(Modal, { type: ModalType$1.Okay, title: "Choose columns to show:", onClose: () => {
2449
+ modal = (React__default["default"].createElement(Modal, { type: ModalType$1.Okay, title: "Choose columns to show:", onClose: () => {
2450
+ const noColumnsSelected = (Object.values(columnVisibilityMap)
2451
+ .every((isSelected) => {
2452
+ return !isSelected;
2453
+ }));
2454
+ if (noColumnsSelected) {
2455
+ return alert$1('Choose at least one column', 'To continue, you have to choose at least one column to show');
2456
+ }
2350
2457
  dispatch({
2351
2458
  type: ActionType$1.ToggleColVisCusModalVisibility,
2352
2459
  });
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: () => {
2460
+ }, okayVariant: Variant$1.Light },
2461
+ columns.map((column) => {
2462
+ return (React__default["default"].createElement(CheckboxButton, { key: column.param, id: `IntelliTable-${id}-toggle-visibility-${column.param}`, className: "mb-2", text: column.title, onChanged: (checked) => {
2463
+ dispatch({
2464
+ type: ActionType$1.UpdateColumnVisibility,
2465
+ param: column.param,
2466
+ visible: checked,
2467
+ });
2468
+ }, checked: columnVisibilityMap[column.param], ariaLabel: `show "${column.title}" column in the ${title} table`, checkedVariant: Variant$1.Light, uncheckedVariant: Variant$1.Secondary }));
2469
+ }),
2470
+ React__default["default"].createElement("div", { className: "mt-3" }, "Or you can:"),
2471
+ 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: () => {
2472
+ dispatch({
2473
+ type: ActionType$1.ShowAllColumns,
2474
+ });
2475
+ } }, "Select All"),
2476
+ 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
2477
  dispatch({
2356
- type: ActionType$1.ToggleColumnVisibility,
2357
- param: column.param,
2478
+ type: ActionType$1.HideAllColumns,
2358
2479
  });
2359
- }, checked: columnVisibilityMap[column.param], ariaLabel: `show "${column.title}" column` }));
2360
- })));
2480
+ } }, "Deselect All")));
2361
2481
  }
2362
2482
  /*----------------------------------------*/
2363
2483
  /* Main UI */
@@ -2371,6 +2491,7 @@ const IntelliTable = (props) => {
2371
2491
  // Custom info based on current sort type
2372
2492
  let sortButtonAriaLabel;
2373
2493
  let sortIcon = freeSolidSvgIcons.faSort;
2494
+ let sortingByThisColumn = false;
2374
2495
  if (!sortColumnParam) {
2375
2496
  // Not being sorted yet
2376
2497
  sortButtonAriaLabel = `sort ascending by ${column.title}`;
@@ -2378,6 +2499,7 @@ const IntelliTable = (props) => {
2378
2499
  }
2379
2500
  else if (column.param === sortColumnParam) {
2380
2501
  // Already sorted by this column
2502
+ sortingByThisColumn = true;
2381
2503
  if (sortType === SortType.Ascending) {
2382
2504
  // Sorted ascending
2383
2505
  sortButtonAriaLabel = `sort descending by ${column.title}`;
@@ -2395,11 +2517,14 @@ const IntelliTable = (props) => {
2395
2517
  sortIcon = freeSolidSvgIcons.faSort;
2396
2518
  }
2397
2519
  // 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" },
2400
- React__default["default"].createElement("h4", { className: "m-0" }, column.title),
2520
+ return (React__default["default"].createElement("th", { key: column.param, scope: "col", id: `IntelliTable-${id}-header-${column.param}`, className: "text-start", style: {
2521
+ borderRight: '0.05rem solid #555',
2522
+ borderLeft: '0.05rem solid #555',
2523
+ } },
2524
+ React__default["default"].createElement("div", { className: "d-flex align-items-center justify-content-start flex-row h-100" },
2525
+ React__default["default"].createElement("span", { className: "text-nowrap" }, column.title),
2401
2526
  React__default["default"].createElement("div", null,
2402
- React__default["default"].createElement("button", { type: "button", className: "btn btn-light", "aria-label": sortButtonAriaLabel, onClick: () => {
2527
+ 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
2528
  dispatch({
2404
2529
  type: ActionType$1.ToggleSortColumn,
2405
2530
  param: column.param,
@@ -2417,8 +2542,28 @@ const IntelliTable = (props) => {
2417
2542
  const descending = (sortType === SortType.Descending);
2418
2543
  if (sortColumnParam) {
2419
2544
  sortedData.sort((a, b) => {
2545
+ var _a, _b;
2420
2546
  const aVal = a[sortColumnParam];
2421
2547
  const bVal = b[sortColumnParam];
2548
+ // Tiebreaker sort by timestamp, most recent first (used if tied)
2549
+ const tiebreaker = (((_a = b.timestamp) !== null && _a !== void 0 ? _a : 0)
2550
+ - ((_b = a.timestamp) !== null && _b !== void 0 ? _b : 0));
2551
+ // Auto-sort undefined and null to end of list
2552
+ if ((aVal === undefined || aVal === null)
2553
+ || (bVal === undefined || bVal === null)) {
2554
+ // At least one was undefined
2555
+ if ((aVal === undefined || aVal === null)
2556
+ && (bVal === undefined || bVal === null)) {
2557
+ // Both undefined
2558
+ return tiebreaker;
2559
+ }
2560
+ if (aVal === undefined || aVal === null) {
2561
+ // First was undefined
2562
+ return 1;
2563
+ }
2564
+ // Second was undefined
2565
+ return -1;
2566
+ }
2422
2567
  // Sort differently based on the data type
2423
2568
  // > Boolean
2424
2569
  if (paramType === ParamType$1.Boolean) {
@@ -2428,7 +2573,7 @@ const IntelliTable = (props) => {
2428
2573
  if (!aVal && bVal) {
2429
2574
  return (descending ? 1 : -1);
2430
2575
  }
2431
- return 0;
2576
+ return tiebreaker;
2432
2577
  }
2433
2578
  // > Number
2434
2579
  if (paramType === ParamType$1.Int
@@ -2440,12 +2585,12 @@ const IntelliTable = (props) => {
2440
2585
  // > String
2441
2586
  if (paramType === ParamType$1.String) {
2442
2587
  if (aVal < bVal) {
2443
- return (descending ? -1 : 1);
2588
+ return (descending ? 1 : -1);
2444
2589
  }
2445
2590
  if (aVal > bVal) {
2446
- return (descending ? 1 : -1);
2591
+ return (descending ? -1 : 1);
2447
2592
  }
2448
- return 0;
2593
+ return tiebreaker;
2449
2594
  }
2450
2595
  // > JSON
2451
2596
  if (paramType === ParamType$1.JSON) {
@@ -2460,7 +2605,7 @@ const IntelliTable = (props) => {
2460
2605
  : (aSize - bSize));
2461
2606
  }
2462
2607
  // No sort
2463
- return 0;
2608
+ return tiebreaker;
2464
2609
  });
2465
2610
  }
2466
2611
  // Table body
@@ -2485,9 +2630,12 @@ const IntelliTable = (props) => {
2485
2630
  const noValue = (value === undefined
2486
2631
  || value === null);
2487
2632
  visibleValue = (noValue
2488
- ? (React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faCheckCircle }))
2489
- : (React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faXmarkCircle })));
2633
+ ? (React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faMinus }))
2634
+ : (React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: fullValue ? freeSolidSvgIcons.faCheckCircle : freeSolidSvgIcons.faXmarkCircle })));
2490
2635
  title = (fullValue ? 'True' : 'False');
2636
+ if (noValue) {
2637
+ title = 'Empty Cell';
2638
+ }
2491
2639
  }
2492
2640
  else if (column.type === ParamType$1.Int) {
2493
2641
  fullValue = Number.parseInt(value, 10);
@@ -2496,6 +2644,9 @@ const IntelliTable = (props) => {
2496
2644
  ? (React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faMinus }))
2497
2645
  : fullValue);
2498
2646
  title = String(fullValue);
2647
+ if (noValue) {
2648
+ title = 'Empty Cell';
2649
+ }
2499
2650
  }
2500
2651
  else if (column.type === ParamType$1.Float) {
2501
2652
  fullValue = Number.parseFloat(value);
@@ -2504,14 +2655,22 @@ const IntelliTable = (props) => {
2504
2655
  ? (React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faMinus }))
2505
2656
  : roundToNumDecimals(fullValue, 2));
2506
2657
  title = String(fullValue);
2658
+ if (noValue) {
2659
+ title = 'Empty Cell';
2660
+ }
2507
2661
  }
2508
2662
  else if (column.type === ParamType$1.String) {
2509
2663
  fullValue = String(value).trim();
2510
- const noValue = (value.trim().length) === 0;
2664
+ const noValue = (value === undefined
2665
+ || value === null
2666
+ || String(fullValue).trim().length === 0);
2511
2667
  visibleValue = (noValue
2512
2668
  ? (React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faMinus }))
2513
2669
  : fullValue);
2514
2670
  title = `"${value}"`;
2671
+ if (noValue) {
2672
+ title = 'Empty Cell';
2673
+ }
2515
2674
  }
2516
2675
  else if (column.type === ParamType$1.JSON) {
2517
2676
  fullValue = JSON.stringify(value);
@@ -2521,9 +2680,13 @@ const IntelliTable = (props) => {
2521
2680
  visibleValue = (noValue
2522
2681
  ? (React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faMinus }))
2523
2682
  : fullValue);
2683
+ title = "JSON Object";
2524
2684
  }
2525
2685
  // Create UI
2526
- return (React__default["default"].createElement("td", { key: `${datum.id}-${column.param}`, title: title }, visibleValue));
2686
+ return (React__default["default"].createElement("td", { key: `${datum.id}-${column.param}`, title: title, style: {
2687
+ borderRight: '0.05rem solid #555',
2688
+ borderLeft: '0.05rem solid #555',
2689
+ } }, visibleValue));
2527
2690
  }));
2528
2691
  // Add cells to a row
2529
2692
  return (React__default["default"].createElement("tr", { key: datum.id }, cells));
@@ -2540,14 +2703,17 @@ const IntelliTable = (props) => {
2540
2703
  })
2541
2704
  .length);
2542
2705
  // Build CSV
2543
- const csv = genCSV(data, columns);
2706
+ const csv = genCSV(data, columns.filter((column) => {
2707
+ return columnVisibilityMap[column.param];
2708
+ }));
2544
2709
  // Build main UI
2545
2710
  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" },
2711
+ modal,
2712
+ React__default["default"].createElement("div", { className: "d-flex align-items-center justify-content-start" },
2547
2713
  React__default["default"].createElement("h3", { className: "m-0" }, title),
2548
2714
  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 }),
2550
- React__default["default"].createElement("button", { type: "button", className: "btn btn-secondary", "aria-label": `show panel for customizing which columns show in table ${title}`, id: `IntelliTable-${id}-show-column-customization-modal`, onClick: () => {
2715
+ React__default["default"].createElement(CSVDownloadButton, { "aria-label": `download data as csv for ${title}`, id: `IntelliTable-${id}-download-as-csv`, filename: filename, csv: csv }),
2716
+ 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
2717
  dispatch({
2552
2718
  type: ActionType$1.ToggleColVisCusModalVisibility,
2553
2719
  });
@@ -2559,7 +2725,7 @@ const IntelliTable = (props) => {
2559
2725
  numHiddenCols,
2560
2726
  ' ',
2561
2727
  "hidden)"))))),
2562
- React__default["default"].createElement("div", { className: `IntelliTable-table-${id}`, style: {
2728
+ React__default["default"].createElement("div", { className: `IntelliTable-table-${id} mt-2`, style: {
2563
2729
  overflowX: 'auto',
2564
2730
  } }, table)));
2565
2731
  };
@@ -2611,6 +2777,11 @@ const style = `
2611
2777
  border: 0.05rem solid black;
2612
2778
  border-radius: 0.5rem;
2613
2779
  overflow: hidden;
2780
+ padding: 0.7rem;
2781
+
2782
+ /* Solid background */
2783
+ background-color: white;
2784
+ color: black;
2614
2785
 
2615
2786
  /* Place contents in flex column */
2616
2787
  flex-direction: column;
@@ -2637,8 +2808,218 @@ const style = `
2637
2808
  /* Vertical scroll */
2638
2809
  overflow-y: auto;
2639
2810
  }
2811
+
2812
+ .LogReviewer-header-close-button {
2813
+ border: 0 !important;
2814
+ background-color: transparent !important;
2815
+ padding-top: 0 !important;
2816
+ padding-bottom: 0 !important;
2817
+ padding-right: 1em !important;
2818
+ margin: 0 !important;
2819
+ color: #444 !important;
2820
+
2821
+ right: 0 !important;
2822
+ position: absolute !important;
2823
+ }
2824
+ .LogReviewer-header-close-button:hover {
2825
+ border: 0 !important;
2826
+ background-color: transparent !important;
2827
+ padding-top: 0 !important;
2828
+ padding-bottom: 0 !important;
2829
+ margin: 0 !important;
2830
+ color: #000 !important;
2831
+ }
2640
2832
  `;
2641
2833
  /*------------------------------------------------------------------------*/
2834
+ /* Constants */
2835
+ /*------------------------------------------------------------------------*/
2836
+ const columns = [
2837
+ {
2838
+ title: 'First Name',
2839
+ param: 'userFirstName',
2840
+ type: ParamType$1.String,
2841
+ },
2842
+ {
2843
+ title: 'Last Name',
2844
+ param: 'userLastName',
2845
+ type: ParamType$1.String,
2846
+ },
2847
+ {
2848
+ title: 'Email',
2849
+ param: 'userEmail',
2850
+ type: ParamType$1.String,
2851
+ },
2852
+ {
2853
+ title: 'Canvas Id',
2854
+ param: 'userId',
2855
+ type: ParamType$1.Int,
2856
+ },
2857
+ {
2858
+ title: 'Student?',
2859
+ param: 'isLearner',
2860
+ type: ParamType$1.Boolean,
2861
+ },
2862
+ {
2863
+ title: 'Teaching Staff?',
2864
+ param: 'isTTM',
2865
+ type: ParamType$1.Boolean,
2866
+ startsHidden: true,
2867
+ },
2868
+ {
2869
+ title: 'Admin?',
2870
+ param: 'isAdmin',
2871
+ type: ParamType$1.Boolean,
2872
+ startsHidden: true,
2873
+ },
2874
+ {
2875
+ title: 'Course Canvas Id',
2876
+ param: 'courseId',
2877
+ type: ParamType$1.Int,
2878
+ startsHidden: true,
2879
+ },
2880
+ {
2881
+ title: 'Course Name',
2882
+ param: 'courseName',
2883
+ type: ParamType$1.String,
2884
+ },
2885
+ {
2886
+ title: 'Browser Name',
2887
+ param: 'browser.name',
2888
+ type: ParamType$1.String,
2889
+ startsHidden: true,
2890
+ },
2891
+ {
2892
+ title: 'Browser Version',
2893
+ param: 'browser.version',
2894
+ type: ParamType$1.String,
2895
+ startsHidden: true,
2896
+ },
2897
+ {
2898
+ title: 'OS',
2899
+ param: 'device.os',
2900
+ type: ParamType$1.String,
2901
+ startsHidden: true,
2902
+ },
2903
+ {
2904
+ title: 'Mobile?',
2905
+ param: 'device.isMobile',
2906
+ type: ParamType$1.Boolean,
2907
+ startsHidden: true,
2908
+ },
2909
+ {
2910
+ title: 'Year',
2911
+ param: 'year',
2912
+ type: ParamType$1.Int,
2913
+ },
2914
+ {
2915
+ title: 'Month',
2916
+ param: 'month',
2917
+ type: ParamType$1.Int,
2918
+ },
2919
+ {
2920
+ title: 'Day',
2921
+ param: 'day',
2922
+ type: ParamType$1.Int,
2923
+ },
2924
+ {
2925
+ title: 'Hour',
2926
+ param: 'hour',
2927
+ type: ParamType$1.Int,
2928
+ },
2929
+ {
2930
+ title: 'Minute',
2931
+ param: 'minute',
2932
+ type: ParamType$1.Int,
2933
+ startsHidden: true,
2934
+ },
2935
+ {
2936
+ title: 'Timestamp',
2937
+ param: 'timestamp',
2938
+ type: ParamType$1.Int,
2939
+ startsHidden: true,
2940
+ },
2941
+ {
2942
+ title: 'Context',
2943
+ param: 'context',
2944
+ type: ParamType$1.String,
2945
+ },
2946
+ {
2947
+ title: 'Subcontext',
2948
+ param: 'subcontext',
2949
+ type: ParamType$1.String,
2950
+ },
2951
+ {
2952
+ title: 'Tags',
2953
+ param: 'tags',
2954
+ type: ParamType$1.JSON,
2955
+ startsHidden: true,
2956
+ },
2957
+ {
2958
+ title: 'Log Level',
2959
+ param: 'level',
2960
+ type: ParamType$1.String,
2961
+ startsHidden: true,
2962
+ },
2963
+ {
2964
+ title: 'Metadata',
2965
+ param: 'metadata',
2966
+ type: ParamType$1.JSON,
2967
+ startsHidden: true,
2968
+ },
2969
+ {
2970
+ title: 'Source',
2971
+ param: 'source',
2972
+ type: ParamType$1.String,
2973
+ },
2974
+ {
2975
+ title: 'Server Route Path',
2976
+ param: 'routePath',
2977
+ type: ParamType$1.String,
2978
+ startsHidden: true,
2979
+ },
2980
+ {
2981
+ title: 'Server Route Template',
2982
+ param: 'routeTemplate',
2983
+ type: ParamType$1.String,
2984
+ startsHidden: true,
2985
+ },
2986
+ {
2987
+ title: 'Type',
2988
+ param: 'type',
2989
+ type: ParamType$1.String,
2990
+ },
2991
+ {
2992
+ title: 'Error Message',
2993
+ param: 'errorMessage',
2994
+ type: ParamType$1.String,
2995
+ startsHidden: true,
2996
+ },
2997
+ {
2998
+ title: 'Error Code',
2999
+ param: 'errorCode',
3000
+ type: ParamType$1.String,
3001
+ startsHidden: true,
3002
+ },
3003
+ {
3004
+ title: 'Error Stack',
3005
+ param: 'errorStack',
3006
+ type: ParamType$1.String,
3007
+ startsHidden: true,
3008
+ },
3009
+ {
3010
+ title: 'Action Target',
3011
+ param: 'target',
3012
+ type: ParamType$1.String,
3013
+ startsHidden: true,
3014
+ },
3015
+ {
3016
+ title: 'Action Type',
3017
+ param: 'action',
3018
+ type: ParamType$1.String,
3019
+ startsHidden: true,
3020
+ },
3021
+ ];
3022
+ /*------------------------------------------------------------------------*/
2642
3023
  /* Static Functions */
2643
3024
  /*------------------------------------------------------------------------*/
2644
3025
  /**
@@ -2656,7 +3037,7 @@ const genHumanReadableName = (machineReadableName) => {
2656
3037
  // Uppercase! Add a space before
2657
3038
  humanReadableName += ' ';
2658
3039
  }
2659
- humanReadableName += chars;
3040
+ humanReadableName += char;
2660
3041
  });
2661
3042
  // Trim and return
2662
3043
  return humanReadableName.trim();
@@ -2718,7 +3099,19 @@ const reducer = (state, action) => {
2718
3099
  return Object.assign(Object.assign({}, state), { contextFilterState: action.contextFilterState });
2719
3100
  }
2720
3101
  case ActionType.UpdateTagFilterState: {
2721
- return Object.assign(Object.assign({}, state), { tagFilterState: action.tagFilterState });
3102
+ const { tagFilterState } = action;
3103
+ // Select all if every tag is deselected
3104
+ const numTagsSelected = (Object.values(tagFilterState)
3105
+ .filter((isSelected) => {
3106
+ return isSelected;
3107
+ })
3108
+ .length);
3109
+ if (numTagsSelected === 0) {
3110
+ Object.keys(tagFilterState).forEach((tag) => {
3111
+ tagFilterState[tag] = true;
3112
+ });
3113
+ }
3114
+ return Object.assign(Object.assign({}, state), { tagFilterState });
2722
3115
  }
2723
3116
  case ActionType.UpdateActionErrorFilterState: {
2724
3117
  return Object.assign(Object.assign({}, state), { actionErrorFilterState: action.actionErrorFilterState });
@@ -2738,10 +3131,35 @@ const LogReviewer = (props) => {
2738
3131
  /*------------------------------------------------------------------------*/
2739
3132
  /* Setup */
2740
3133
  /*------------------------------------------------------------------------*/
2741
- var _a, _b, _c, _d, _e;
3134
+ var _a, _b, _c, _d, _e, _f;
2742
3135
  /* -------------- Props ------------- */
2743
3136
  // Destructure props
2744
3137
  const { LogMetadata, onClose, } = props;
3138
+ // Create complete map of contexts
3139
+ const contextMap = {};
3140
+ [
3141
+ ((_a = LogMetadata.Context) !== null && _a !== void 0 ? _a : {}),
3142
+ LogBuiltInMetadata.Context,
3143
+ ].forEach((partialContextMap) => {
3144
+ Object.keys(partialContextMap).forEach((context) => {
3145
+ // Add context
3146
+ contextMap[context] = context;
3147
+ // If context has children, add an "uncategorized" subcontext
3148
+ if (typeof contextMap[context] !== 'string') {
3149
+ contextMap[context][LogBuiltInMetadata.Context.Uncategorized] = (LogBuiltInMetadata.Context.Uncategorized);
3150
+ }
3151
+ });
3152
+ });
3153
+ // Create complete map of targets
3154
+ const targetMap = {};
3155
+ [
3156
+ ((_b = LogMetadata.Target) !== null && _b !== void 0 ? _b : {}),
3157
+ LogBuiltInMetadata.Target,
3158
+ ].forEach((partialTargetMap) => {
3159
+ Object.keys(partialTargetMap).forEach((target) => {
3160
+ targetMap[target] = target;
3161
+ });
3162
+ });
2745
3163
  /* -------------- State ------------- */
2746
3164
  // Create initial date filter state
2747
3165
  const today = getTimeInfoInET();
@@ -2761,26 +3179,30 @@ const LogReviewer = (props) => {
2761
3179
  };
2762
3180
  // Create initial context filter state
2763
3181
  const initContextFilterState = {};
2764
- Object.keys((_a = LogMetadata.Context) !== null && _a !== void 0 ? _a : {}).forEach((context) => {
3182
+ Object.keys((_c = LogMetadata.Context) !== null && _c !== void 0 ? _c : {}).forEach((context) => {
2765
3183
  var _a, _b;
2766
3184
  const contextValue = ((_a = LogMetadata.Context) !== null && _a !== void 0 ? _a : {})[context];
2767
3185
  if (typeof contextValue === 'string') {
2768
- // Case: no subcontexts
3186
+ // Case: no subcontexts, init as checked
2769
3187
  initContextFilterState[contextValue] = true;
2770
3188
  }
2771
3189
  else {
2772
3190
  // Case: subcontexts exist
2773
- initContextFilterState[contextValue._] = {};
3191
+ initContextFilterState[context] = {};
2774
3192
  Object.values(((_b = LogMetadata.Context) !== null && _b !== void 0 ? _b : {})[context]).forEach((subcontext) => {
2775
- const subcontextValue = contextValue[subcontext];
2776
- initContextFilterState[contextValue._][subcontextValue] = true;
3193
+ // Skip self ("_")
3194
+ if (subcontext === '_') {
3195
+ return;
3196
+ }
3197
+ // Initialize as checked
3198
+ initContextFilterState[context][subcontext] = true;
2777
3199
  });
2778
3200
  }
2779
3201
  });
2780
3202
  // Create initial tag filter state
2781
3203
  const initTagFilterState = {};
2782
- Object.values((_b = LogMetadata.Tag) !== null && _b !== void 0 ? _b : {}).forEach((tagValue) => {
2783
- initTagFilterState[tagValue] = true;
3204
+ Object.keys((_d = LogMetadata.Tag) !== null && _d !== void 0 ? _d : {}).forEach((tag) => {
3205
+ initTagFilterState[tag] = false;
2784
3206
  });
2785
3207
  // Create advanced filter state
2786
3208
  const initAdvancedFilterState = {
@@ -2806,12 +3228,16 @@ const LogReviewer = (props) => {
2806
3228
  target: {},
2807
3229
  action: {},
2808
3230
  };
2809
- Object.values((_c = LogMetadata.Target) !== null && _c !== void 0 ? _c : {}).forEach((target) => {
3231
+ Object.values((_e = LogMetadata.Target) !== null && _e !== void 0 ? _e : {}).forEach((target) => {
2810
3232
  initActionErrorFilterState.target[target] = true;
2811
3233
  });
2812
3234
  Object.values(LogAction$1).forEach((action) => {
2813
3235
  initActionErrorFilterState.action[action] = true;
2814
3236
  });
3237
+ // Add built-in targets
3238
+ Object.values(LogBuiltInMetadata.Target).forEach((target) => {
3239
+ initActionErrorFilterState.target[target] = true;
3240
+ });
2815
3241
  // Initial state
2816
3242
  const initialState = {
2817
3243
  loading: true,
@@ -2845,15 +3271,18 @@ const LogReviewer = (props) => {
2845
3271
  let month = newDateFilterState.startDate.month;
2846
3272
  while (
2847
3273
  // Earlier year
2848
- (year <= newDateFilterState.endDate.year)
3274
+ (year < newDateFilterState.endDate.year)
2849
3275
  // Current year but included month
2850
3276
  || (year === newDateFilterState.endDate.year
2851
3277
  && month <= newDateFilterState.endDate.month)) {
2852
- // Add to list
2853
- toLoad.push({
2854
- year,
2855
- month,
2856
- });
3278
+ // Add to list if not already loaded
3279
+ if (!logMap[year]
3280
+ || !logMap[year][month]) {
3281
+ toLoad.push({
3282
+ year,
3283
+ month,
3284
+ });
3285
+ }
2857
3286
  // Increment
2858
3287
  month += 1;
2859
3288
  if (month > 12) {
@@ -2941,10 +3370,10 @@ const LogReviewer = (props) => {
2941
3370
  /* Filters */
2942
3371
  /*----------------------------------------*/
2943
3372
  // Filter toggle
2944
- const filterToggles = (React__default["default"].createElement("div", { className: "LogReviewer-filter-toggles d-flex align-items-center justify-content-center" },
3373
+ const filterToggles = (React__default["default"].createElement("div", { className: "LogReviewer-filter-toggles" },
2945
3374
  React__default["default"].createElement("h3", { className: "m-0" }, "Filters:"),
2946
- React__default["default"].createElement("div", { className: "LogReviewer-filter-toggle-buttons" },
2947
- React__default["default"].createElement("button", { type: "button", id: "LogReviewer-toggle-date-filter-drawer", className: `btn btn-${FilterDrawer.Date === expandedFilterDrawer} me-2`, "aria-label": "toggle date filter drawer", onClick: () => {
3375
+ React__default["default"].createElement("div", { className: "LogReviewer-filter-toggle-buttons alert alert-secondary p-2 m-0" },
3376
+ 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: () => {
2948
3377
  dispatch({
2949
3378
  type: ActionType.ToggleFilterDrawer,
2950
3379
  filterDrawer: FilterDrawer.Date,
@@ -2952,7 +3381,7 @@ const LogReviewer = (props) => {
2952
3381
  } },
2953
3382
  React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faCalendar, className: "me-2" }),
2954
3383
  "Date"),
2955
- React__default["default"].createElement("button", { type: "button", id: "LogReviewer-toggle-context-filter-drawer", className: `btn btn-${FilterDrawer.Context === expandedFilterDrawer} me-2`, "aria-label": "toggle context filter drawer", onClick: () => {
3384
+ React__default["default"].createElement("button", { type: "button", id: "LogReviewer-toggle-context-filter-drawer", className: `btn btn-${FilterDrawer.Context === expandedFilterDrawer ? 'warning' : 'light'} me-2`, "aria-label": "toggle context filter drawer", onClick: () => {
2956
3385
  dispatch({
2957
3386
  type: ActionType.ToggleFilterDrawer,
2958
3387
  filterDrawer: FilterDrawer.Context,
@@ -2960,15 +3389,15 @@ const LogReviewer = (props) => {
2960
3389
  } },
2961
3390
  React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faCircle, className: "me-2" }),
2962
3391
  "Context"),
2963
- React__default["default"].createElement("button", { type: "button", id: "LogReviewer-toggle-tag-filter-drawer", className: `btn btn-${FilterDrawer.Tag === expandedFilterDrawer} me-2`, "aria-label": "toggle tag filter drawer", onClick: () => {
3392
+ (LogMetadata.Tag && Object.keys(LogMetadata.Tag).length > 0) && (React__default["default"].createElement("button", { type: "button", id: "LogReviewer-toggle-tag-filter-drawer", className: `btn btn-${FilterDrawer.Tag === expandedFilterDrawer ? 'warning' : 'light'} me-2`, "aria-label": "toggle tag filter drawer", onClick: () => {
2964
3393
  dispatch({
2965
3394
  type: ActionType.ToggleFilterDrawer,
2966
3395
  filterDrawer: FilterDrawer.Tag,
2967
3396
  });
2968
3397
  } },
2969
3398
  React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faTag, className: "me-2" }),
2970
- "Tag"),
2971
- React__default["default"].createElement("button", { type: "button", id: "LogReviewer-toggle-action-filter-drawer", className: `btn btn-${FilterDrawer.Action === expandedFilterDrawer} me-2`, "aria-label": "toggle action and error filter drawer", onClick: () => {
3399
+ "Tag")),
3400
+ React__default["default"].createElement("button", { type: "button", id: "LogReviewer-toggle-action-filter-drawer", className: `btn btn-${FilterDrawer.Action === expandedFilterDrawer ? 'warning' : 'light'} me-2`, "aria-label": "toggle action and error filter drawer", onClick: () => {
2972
3401
  dispatch({
2973
3402
  type: ActionType.ToggleFilterDrawer,
2974
3403
  filterDrawer: FilterDrawer.Action,
@@ -2976,41 +3405,64 @@ const LogReviewer = (props) => {
2976
3405
  } },
2977
3406
  React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faHammer, className: "me-2" }),
2978
3407
  "Action"),
2979
- React__default["default"].createElement("button", { type: "button", id: "LogReviewer-toggle-advanced-filter-drawer", className: `btn btn-${FilterDrawer.Advanced === expandedFilterDrawer}`, "aria-label": "toggle advanced filter drawer", onClick: () => {
3408
+ 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: () => {
2980
3409
  dispatch({
2981
3410
  type: ActionType.ToggleFilterDrawer,
2982
3411
  filterDrawer: FilterDrawer.Advanced,
2983
3412
  });
2984
3413
  } },
2985
3414
  React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faList, className: "me-2" }),
2986
- "Advanced"))));
3415
+ "Advanced"),
3416
+ React__default["default"].createElement("button", { type: "button", id: "LogReviewer-reset-filters-button", className: "btn btn-light", "aria-label": "reset filters", onClick: () => {
3417
+ dispatch({
3418
+ type: ActionType.ResetFilters,
3419
+ initActionErrorFilterState,
3420
+ initAdvancedFilterState,
3421
+ initContextFilterState,
3422
+ initDateFilterState,
3423
+ initTagFilterState,
3424
+ });
3425
+ } },
3426
+ React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faTimes }),
3427
+ ' ',
3428
+ "Reset"))));
2987
3429
  // Filter drawer
2988
3430
  let filterDrawer;
2989
3431
  if (expandedFilterDrawer) {
2990
3432
  if (expandedFilterDrawer === FilterDrawer.Date) {
2991
3433
  filterDrawer = (React__default["default"].createElement(TabBox, { title: "Date" },
2992
- 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) => {
2993
- dispatch({
2994
- type: ActionType.UpdateDateFilterState,
2995
- dateFilterState: Object.assign(Object.assign({}, dateFilterState), { startDate: { month, day, year } }),
2996
- });
3434
+ 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) => {
3435
+ dateFilterState.startDate = { month, day, year };
3436
+ handleDateRangeUpdated(dateFilterState);
2997
3437
  } }),
2998
3438
  ' ',
2999
3439
  "to",
3000
3440
  ' ',
3001
- 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) => {
3002
- dispatch({
3003
- type: ActionType.UpdateDateFilterState,
3004
- dateFilterState: Object.assign(Object.assign({}, dateFilterState), { endDate: { month, day, year } }),
3005
- });
3441
+ 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) => {
3442
+ if (year < dateFilterState.startDate.year
3443
+ || (year === dateFilterState.startDate.year
3444
+ && month < dateFilterState.startDate.month)
3445
+ || (year === dateFilterState.startDate.year
3446
+ && month === dateFilterState.startDate.month
3447
+ && day < dateFilterState.startDate.day)) {
3448
+ return alert$1('Invalid Start Date', 'The start date cannot be before the end date.');
3449
+ }
3450
+ dateFilterState.endDate = { month, day, year };
3451
+ handleDateRangeUpdated(dateFilterState);
3006
3452
  } })));
3007
3453
  }
3008
3454
  else if (expandedFilterDrawer === FilterDrawer.Context) {
3009
3455
  // Create item picker items
3010
- const pickableItems = (Object.keys((_d = LogMetadata.Context) !== null && _d !== void 0 ? _d : {})
3011
- .map((context) => {
3012
- var _a;
3013
- const value = ((_a = LogMetadata.Context) !== null && _a !== void 0 ? _a : {})[context];
3456
+ const builtInPickableItem = {
3457
+ id: '_',
3458
+ name: 'Auto-logged',
3459
+ isGroup: true,
3460
+ children: [],
3461
+ };
3462
+ const pickableItems = [];
3463
+ Object.keys(contextMap)
3464
+ .forEach((context) => {
3465
+ const value = (contextMap)[context];
3014
3466
  if (typeof value === 'string') {
3015
3467
  // No subcategories
3016
3468
  const item = {
@@ -3019,38 +3471,64 @@ const LogReviewer = (props) => {
3019
3471
  isGroup: false,
3020
3472
  checked: !!contextFilterState[context],
3021
3473
  };
3022
- return item;
3474
+ // Add built-in items to its own folder
3475
+ const isBuiltIn = context in LogBuiltInMetadata.Context;
3476
+ if (isBuiltIn) {
3477
+ // Add to built-in pickable item
3478
+ builtInPickableItem.children.push(item);
3479
+ }
3480
+ else {
3481
+ // Add to pickable items list
3482
+ pickableItems.push(item);
3483
+ }
3023
3484
  }
3024
3485
  // Has subcategories
3025
3486
  const children = (Object.keys(value)
3487
+ // Remove parent name
3026
3488
  .filter((subcontext) => {
3027
3489
  return subcontext !== '_';
3028
3490
  })
3491
+ // Create child pickable items
3029
3492
  .map((subcontext) => {
3030
3493
  return {
3031
- id: `${context}-${subcontext}`,
3494
+ id: subcontext,
3032
3495
  name: genHumanReadableName(subcontext),
3033
3496
  isGroup: false,
3034
- checked: !!value[subcontext],
3497
+ checked: contextFilterState[context][subcontext],
3035
3498
  };
3036
3499
  }));
3037
3500
  const item = {
3038
3501
  id: context,
3039
- name: context,
3502
+ name: genHumanReadableName(context),
3040
3503
  isGroup: true,
3041
3504
  children,
3042
3505
  };
3043
- return item;
3044
- }));
3506
+ pickableItems.push(item);
3507
+ });
3508
+ // Add built-in contexts to end ofl ist
3509
+ pickableItems.push(builtInPickableItem);
3045
3510
  // Create filter UI
3046
3511
  filterDrawer = (React__default["default"].createElement(ItemPicker, { title: "Context", items: pickableItems, onChanged: (updatedItems) => {
3047
3512
  // Update our state
3048
3513
  updatedItems.forEach((pickableItem) => {
3049
3514
  if (pickableItem.isGroup) {
3050
3515
  // Has subcontexts
3051
- pickableItem.children.forEach((subcontextItem) => {
3052
- contextFilterState[pickableItem.id][subcontextItem.id] = (subcontextItem.checked);
3053
- });
3516
+ if (pickableItem.id === '_') {
3517
+ // Built-in
3518
+ // Treat as if these were top-level contexts
3519
+ pickableItem.children.forEach((subcontextItem) => {
3520
+ contextFilterState[subcontextItem.id] = ('checked' in subcontextItem
3521
+ && subcontextItem.checked);
3522
+ });
3523
+ }
3524
+ else {
3525
+ // Not built-in
3526
+ pickableItem.children.forEach((subcontextItem) => {
3527
+ if (!subcontextItem.isGroup) {
3528
+ contextFilterState[pickableItem.id][subcontextItem.id] = (subcontextItem.checked);
3529
+ }
3530
+ });
3531
+ }
3054
3532
  }
3055
3533
  else {
3056
3534
  // No subcontexts
@@ -3065,13 +3543,19 @@ const LogReviewer = (props) => {
3065
3543
  }
3066
3544
  else if (expandedFilterDrawer === FilterDrawer.Tag) {
3067
3545
  // Create filter UI
3068
- filterDrawer = (React__default["default"].createElement(TabBox, { title: "Tags" }, Object.keys(tagFilterState)
3069
- .map((tag, i) => {
3070
- const description = genHumanReadableName(tag);
3071
- 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) => {
3072
- tagFilterState[tag] = checked;
3073
- } }));
3074
- })));
3546
+ filterDrawer = (React__default["default"].createElement(TabBox, { title: "Tags" },
3547
+ React__default["default"].createElement("div", null, "If any are selected, logs must contain at least one but not necessarily all of the selected tags."),
3548
+ Object.keys((_f = LogMetadata.Tag) !== null && _f !== void 0 ? _f : {})
3549
+ .map((tag, i) => {
3550
+ const description = genHumanReadableName(tag);
3551
+ return (React__default["default"].createElement(CheckboxButton, { id: `LogReviewer-tag-${tag}-checkbox`, text: description, ariaLabel: `require that logs be tagged with "${description}" or any other selected tag`, noMarginOnRight: i === Object.keys(tagFilterState).length - 1, checked: tagFilterState[tag], onChanged: (checked) => {
3552
+ tagFilterState[tag] = checked;
3553
+ dispatch({
3554
+ type: ActionType.UpdateTagFilterState,
3555
+ tagFilterState,
3556
+ });
3557
+ } }));
3558
+ })));
3075
3559
  }
3076
3560
  else if (expandedFilterDrawer === FilterDrawer.Action) {
3077
3561
  // Create filter UI
@@ -3100,34 +3584,37 @@ const LogReviewer = (props) => {
3100
3584
  }, ariaLabel: "only show error logs", selected: actionErrorFilterState.type === LogType$1.Error, noMarginOnRight: true })),
3101
3585
  (actionErrorFilterState.type === undefined
3102
3586
  || actionErrorFilterState.type === LogType$1.Action) && (React__default["default"].createElement(TabBox, { title: "Action Log Details" },
3103
- React__default["default"].createElement(ButtonInputGroup, { label: "Action" }, Object.keys(LogAction$1)
3104
- .map((action, i) => {
3105
- const description = genHumanReadableName(action);
3106
- 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) => {
3107
- actionErrorFilterState.action[action] = checked;
3108
- dispatch({
3109
- type: ActionType.UpdateActionErrorFilterState,
3110
- actionErrorFilterState,
3111
- });
3112
- } }));
3113
- })),
3114
- React__default["default"].createElement(ButtonInputGroup, { label: "Target" }, Object.keys((_e = LogMetadata.Target) !== null && _e !== void 0 ? _e : {})
3115
- .map((target, i) => {
3116
- var _a;
3117
- const description = genHumanReadableName(target);
3118
- return (React__default["default"].createElement(CheckboxButton, { id: `LogReviewer-target-${target}-checkbox`, text: description, ariaLabel: `include logs with target "${description}" in results`, onChanged: (checked) => {
3119
- actionErrorFilterState.target[target] = checked;
3120
- dispatch({
3121
- type: ActionType.UpdateActionErrorFilterState,
3122
- actionErrorFilterState,
3123
- });
3124
- }, noMarginOnRight: i === Object.keys((_a = LogMetadata.Target) !== null && _a !== void 0 ? _a : {}).length - 1 }));
3125
- })))),
3587
+ React__default["default"].createElement(ButtonInputGroup, { label: "Action", className: "mb-2" },
3588
+ React__default["default"].createElement("div", { className: "d-flex gap-1 flex-wrap" }, Object.keys(LogAction$1)
3589
+ .map((action) => {
3590
+ const description = genHumanReadableName(action);
3591
+ 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) => {
3592
+ actionErrorFilterState.action[action] = checked;
3593
+ console.log(actionErrorFilterState, action, checked);
3594
+ dispatch({
3595
+ type: ActionType.UpdateActionErrorFilterState,
3596
+ actionErrorFilterState,
3597
+ });
3598
+ } }));
3599
+ }))),
3600
+ React__default["default"].createElement(ButtonInputGroup, { label: "Target" },
3601
+ React__default["default"].createElement("div", { className: "d-flex gap-1 flex-wrap" }, Object.keys(targetMap)
3602
+ .map((target) => {
3603
+ const description = genHumanReadableName(target);
3604
+ 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) => {
3605
+ actionErrorFilterState.target[target] = checked;
3606
+ console.log(actionErrorFilterState, target, checked);
3607
+ dispatch({
3608
+ type: ActionType.UpdateActionErrorFilterState,
3609
+ actionErrorFilterState,
3610
+ });
3611
+ } }));
3612
+ }))))),
3126
3613
  (actionErrorFilterState.type === undefined
3127
3614
  || actionErrorFilterState.type === LogType$1.Error) && (React__default["default"].createElement(TabBox, { title: "Error Log Details" },
3128
3615
  React__default["default"].createElement("div", { className: "input-group mb-2" },
3129
3616
  React__default["default"].createElement("span", { className: "input-group-text" }, "Error Message"),
3130
- React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for error message", value: actionErrorFilterState.errorMessage, onChange: (e) => {
3617
+ 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) => {
3131
3618
  actionErrorFilterState.errorMessage = e.target.value;
3132
3619
  dispatch({
3133
3620
  type: ActionType.UpdateActionErrorFilterState,
@@ -3136,7 +3623,7 @@ const LogReviewer = (props) => {
3136
3623
  } })),
3137
3624
  React__default["default"].createElement("div", { className: "input-group mb-2" },
3138
3625
  React__default["default"].createElement("span", { className: "input-group-text" }, "Error Code"),
3139
- React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for error code", value: actionErrorFilterState.errorMessage, onChange: (e) => {
3626
+ 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) => {
3140
3627
  actionErrorFilterState.errorCode = ((e.target.value)
3141
3628
  .trim()
3142
3629
  .toUpperCase());
@@ -3152,7 +3639,7 @@ const LogReviewer = (props) => {
3152
3639
  React__default["default"].createElement(TabBox, { title: "User Info" },
3153
3640
  React__default["default"].createElement("div", { className: "input-group mb-2" },
3154
3641
  React__default["default"].createElement("span", { className: "input-group-text" }, "User First Name"),
3155
- React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for user first name", value: advancedFilterState.userFirstName, onChange: (e) => {
3642
+ 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) => {
3156
3643
  advancedFilterState.userFirstName = e.target.value;
3157
3644
  dispatch({
3158
3645
  type: ActionType.UpdateAdvancedFilterState,
@@ -3161,7 +3648,7 @@ const LogReviewer = (props) => {
3161
3648
  } })),
3162
3649
  React__default["default"].createElement("div", { className: "input-group mb-2" },
3163
3650
  React__default["default"].createElement("span", { className: "input-group-text" }, "User Last Name"),
3164
- React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for user last name", value: advancedFilterState.userLastName, onChange: (e) => {
3651
+ 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) => {
3165
3652
  advancedFilterState.userLastName = e.target.value;
3166
3653
  dispatch({
3167
3654
  type: ActionType.UpdateAdvancedFilterState,
@@ -3170,7 +3657,7 @@ const LogReviewer = (props) => {
3170
3657
  } })),
3171
3658
  React__default["default"].createElement("div", { className: "input-group mb-2" },
3172
3659
  React__default["default"].createElement("span", { className: "input-group-text" }, "User Email"),
3173
- React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for user email", value: advancedFilterState.userEmail, onChange: (e) => {
3660
+ 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) => {
3174
3661
  advancedFilterState.userEmail = ((e.target.value)
3175
3662
  .trim());
3176
3663
  dispatch({
@@ -3180,7 +3667,7 @@ const LogReviewer = (props) => {
3180
3667
  } })),
3181
3668
  React__default["default"].createElement("div", { className: "input-group mb-2" },
3182
3669
  React__default["default"].createElement("span", { className: "input-group-text" }, "User Canvas Id"),
3183
- React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for user canvas id", value: advancedFilterState.userId, onChange: (e) => {
3670
+ 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) => {
3184
3671
  const { value } = e.target;
3185
3672
  // Only update if value contains only numbers
3186
3673
  if (/^\d+$/.test(value)) {
@@ -3217,7 +3704,7 @@ const LogReviewer = (props) => {
3217
3704
  React__default["default"].createElement(TabBox, { title: "Course Info" },
3218
3705
  React__default["default"].createElement("div", { className: "input-group mb-2" },
3219
3706
  React__default["default"].createElement("span", { className: "input-group-text" }, "Course Name"),
3220
- React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for course name", value: advancedFilterState.courseName, onChange: (e) => {
3707
+ 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) => {
3221
3708
  advancedFilterState.courseName = e.target.value;
3222
3709
  dispatch({
3223
3710
  type: ActionType.UpdateAdvancedFilterState,
@@ -3226,7 +3713,7 @@ const LogReviewer = (props) => {
3226
3713
  } })),
3227
3714
  React__default["default"].createElement("div", { className: "input-group mb-2" },
3228
3715
  React__default["default"].createElement("span", { className: "input-group-text" }, "Course Canvas Id"),
3229
- React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for course canvas id", value: advancedFilterState.courseId, onChange: (e) => {
3716
+ 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) => {
3230
3717
  const { value } = e.target;
3231
3718
  // Only update if value contains only numbers
3232
3719
  if (/^\d+$/.test(value)) {
@@ -3284,37 +3771,38 @@ const LogReviewer = (props) => {
3284
3771
  advancedFilterState,
3285
3772
  });
3286
3773
  }, noMarginOnRight: true })),
3287
- React__default["default"].createElement("div", { className: "input-group mb-2" },
3288
- React__default["default"].createElement("span", { className: "input-group-text" }, "Server Route Path"),
3289
- 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) => {
3290
- advancedFilterState.courseName = ((e.target.value)
3291
- .trim());
3292
- dispatch({
3293
- type: ActionType.UpdateAdvancedFilterState,
3294
- advancedFilterState,
3295
- });
3296
- } })),
3297
- React__default["default"].createElement("div", { className: "input-group mb-2" },
3298
- React__default["default"].createElement("span", { className: "input-group-text" }, "Server Route Template"),
3299
- 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) => {
3300
- advancedFilterState.courseName = ((e.target.value)
3301
- .trim());
3302
- dispatch({
3303
- type: ActionType.UpdateAdvancedFilterState,
3304
- advancedFilterState,
3305
- });
3306
- } })))));
3774
+ advancedFilterState.source !== LogSource$1.Client && (React__default["default"].createElement("div", { className: "mt-2" },
3775
+ React__default["default"].createElement("div", { className: "input-group mb-2" },
3776
+ React__default["default"].createElement("span", { className: "input-group-text" }, "Server Route Path"),
3777
+ 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) => {
3778
+ advancedFilterState.courseName = ((e.target.value)
3779
+ .trim());
3780
+ dispatch({
3781
+ type: ActionType.UpdateAdvancedFilterState,
3782
+ advancedFilterState,
3783
+ });
3784
+ } })),
3785
+ React__default["default"].createElement("div", { className: "input-group mb-2" },
3786
+ React__default["default"].createElement("span", { className: "input-group-text" }, "Server Route Template"),
3787
+ 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) => {
3788
+ advancedFilterState.courseName = ((e.target.value)
3789
+ .trim());
3790
+ dispatch({
3791
+ type: ActionType.UpdateAdvancedFilterState,
3792
+ advancedFilterState,
3793
+ });
3794
+ } })))))));
3307
3795
  }
3308
3796
  }
3309
3797
  // Filters UI
3310
3798
  const filters = (React__default["default"].createElement(React__default["default"].Fragment, null,
3311
3799
  filterToggles,
3312
- filterDrawer && (React__default["default"].createElement(Drawer, null, filterDrawer))));
3800
+ filterDrawer && (React__default["default"].createElement(Drawer, { customBackgroundColor: "#eee" }, filterDrawer))));
3313
3801
  // Actually filter the logs
3314
3802
  // > Perform filters
3315
3803
  const logs = [];
3316
3804
  Object.keys(logMap).forEach((year) => {
3317
- Object.keys(logMap).forEach((month) => {
3805
+ Object.keys(logMap[year]).forEach((month) => {
3318
3806
  logMap[year][month].forEach((log) => {
3319
3807
  /* ----------- Date Filter ---------- */
3320
3808
  var _a;
@@ -3350,16 +3838,23 @@ const LogReviewer = (props) => {
3350
3838
  // Whole context is deselected
3351
3839
  contextFilterState[log.context] === false
3352
3840
  // None of the subcontexts are selected
3353
- || (Object.values((_a = contextFilterState[log.context]) !== null && _a !== void 0 ? _a : {})
3354
- .every((isSelected) => {
3355
- return !isSelected;
3356
- }))) {
3841
+ || (
3842
+ // Has subcontexts
3843
+ typeof contextFilterState[log.context] !== 'boolean'
3844
+ // None of the subcontexts are selected
3845
+ && Object.values((_a = contextFilterState[log.context]) !== null && _a !== void 0 ? _a : {})
3846
+ .every((isSelected) => {
3847
+ return !isSelected;
3848
+ }))) {
3849
+ console.log(log.context, contextFilterState);
3357
3850
  return;
3358
3851
  }
3359
3852
  // Subcontext doesn't match
3360
3853
  if (
3361
- // Log has a subcontext
3362
- log.subcontext
3854
+ // Log context is not "uncategorized" (no point in further filters)
3855
+ log.context !== LogBuiltInMetadata.Context.Uncategorized
3856
+ // Log has a subcontext
3857
+ && log.subcontext
3363
3858
  // Context has subcontexts
3364
3859
  && (contextFilterState[log.context]
3365
3860
  && contextFilterState[log.context] !== false
@@ -3371,12 +3866,16 @@ const LogReviewer = (props) => {
3371
3866
  /* -------------- Tags -------------- */
3372
3867
  // No tags match
3373
3868
  if (
3374
- // Log has at least one tag
3375
- log.tags.length > 0
3869
+ // At least one tag is required
3870
+ Object.values(tagFilterState)
3871
+ .filter((isSelected) => {
3872
+ return isSelected;
3873
+ })
3874
+ .length > 0
3376
3875
  // No tags match
3377
- && (log.tags.every((tag) => {
3876
+ && log.tags.every((tag) => {
3378
3877
  return !tagFilterState[tag];
3379
- }))) {
3878
+ })) {
3380
3879
  return;
3381
3880
  }
3382
3881
  /* ------- Actions and Errors ------- */
@@ -3510,7 +4009,7 @@ const LogReviewer = (props) => {
3510
4009
  // Device info exists
3511
4010
  && log.device
3512
4011
  // Mobile filter doesn't match
3513
- && (advancedFilterState.isMobile === log.device.isMobile)) {
4012
+ && (advancedFilterState.isMobile !== log.device.isMobile)) {
3514
4013
  return;
3515
4014
  }
3516
4015
  // Log source doesn't match
@@ -3552,199 +4051,20 @@ const LogReviewer = (props) => {
3552
4051
  /*----------------------------------------*/
3553
4052
  /* Data */
3554
4053
  /*----------------------------------------*/
3555
- // Create data table
3556
- const columns = [
3557
- {
3558
- title: 'First Name',
3559
- param: 'userFirstName',
3560
- type: ParamType$1.String,
3561
- },
3562
- {
3563
- title: 'Last Name',
3564
- param: 'userLastName',
3565
- type: ParamType$1.String,
3566
- },
3567
- {
3568
- title: 'Email',
3569
- param: 'userEmail',
3570
- type: ParamType$1.String,
3571
- },
3572
- {
3573
- title: 'Canvas Id',
3574
- param: 'userId',
3575
- type: ParamType$1.Int,
3576
- },
3577
- {
3578
- title: 'Student',
3579
- param: 'isLearner',
3580
- type: ParamType$1.Boolean,
3581
- },
3582
- {
3583
- title: 'Teaching Staff',
3584
- param: 'isTTM',
3585
- type: ParamType$1.Boolean,
3586
- startsHidden: true,
3587
- },
3588
- {
3589
- title: 'Admin',
3590
- param: 'isAdmin',
3591
- type: ParamType$1.Boolean,
3592
- startsHidden: true,
3593
- },
3594
- {
3595
- title: 'Course Canvas Id',
3596
- param: 'courseId',
3597
- type: ParamType$1.Int,
3598
- startsHidden: true,
3599
- },
3600
- {
3601
- title: 'Course Name',
3602
- param: 'courseName',
3603
- type: ParamType$1.String,
3604
- },
3605
- {
3606
- title: 'Browser Name',
3607
- param: 'browser.name',
3608
- type: ParamType$1.String,
3609
- startsHidden: true,
3610
- },
3611
- {
3612
- title: 'Browser Version',
3613
- param: 'browser.version',
3614
- type: ParamType$1.String,
3615
- startsHidden: true,
3616
- },
3617
- {
3618
- title: 'OS',
3619
- param: 'device.os',
3620
- type: ParamType$1.String,
3621
- startsHidden: true,
3622
- },
3623
- {
3624
- title: 'Mobile',
3625
- param: 'device.isMobile',
3626
- type: ParamType$1.Boolean,
3627
- startsHidden: true,
3628
- },
3629
- {
3630
- title: 'Year',
3631
- param: 'year',
3632
- type: ParamType$1.Int,
3633
- },
3634
- {
3635
- title: 'Month',
3636
- param: 'month',
3637
- type: ParamType$1.Int,
3638
- },
3639
- {
3640
- title: 'Day',
3641
- param: 'day',
3642
- type: ParamType$1.Int,
3643
- },
3644
- {
3645
- title: 'Hour',
3646
- param: 'hour',
3647
- type: ParamType$1.Int,
3648
- },
3649
- {
3650
- title: 'Minute',
3651
- param: 'minute',
3652
- type: ParamType$1.Int,
3653
- startsHidden: true,
3654
- },
3655
- {
3656
- title: 'Timestamp',
3657
- param: 'timestamp',
3658
- type: ParamType$1.Int,
3659
- startsHidden: true,
3660
- },
3661
- {
3662
- title: 'Context',
3663
- param: 'context',
3664
- type: ParamType$1.String,
3665
- },
3666
- {
3667
- title: 'Subcontext',
3668
- param: 'subcontext',
3669
- type: ParamType$1.String,
3670
- },
3671
- {
3672
- title: 'Tags',
3673
- param: 'tags',
3674
- type: ParamType$1.JSON,
3675
- startsHidden: true,
3676
- },
3677
- {
3678
- title: 'Log Level',
3679
- param: 'level',
3680
- type: ParamType$1.String,
3681
- startsHidden: true,
3682
- },
3683
- {
3684
- title: 'Metadata',
3685
- param: 'metadata',
3686
- type: ParamType$1.JSON,
3687
- startsHidden: true,
3688
- },
3689
- {
3690
- title: 'Source',
3691
- param: 'source',
3692
- type: ParamType$1.String,
3693
- },
3694
- {
3695
- title: 'Server Route Path',
3696
- param: 'routePath',
3697
- type: ParamType$1.String,
3698
- startsHidden: true,
3699
- },
3700
- {
3701
- title: 'Server Route Template',
3702
- param: 'routeTemplate',
3703
- type: ParamType$1.String,
3704
- startsHidden: true,
3705
- },
3706
- {
3707
- title: 'Type',
3708
- param: 'type',
3709
- type: ParamType$1.String,
3710
- },
3711
- {
3712
- title: 'Error Message',
3713
- param: 'errorMessage',
3714
- type: ParamType$1.String,
3715
- startsHidden: true,
3716
- },
3717
- {
3718
- title: 'Error Code',
3719
- param: 'errorCode',
3720
- type: ParamType$1.String,
3721
- startsHidden: true,
3722
- },
3723
- {
3724
- title: 'Error Stack',
3725
- param: 'errorStack',
3726
- type: ParamType$1.String,
3727
- startsHidden: true,
3728
- },
3729
- {
3730
- title: 'Action Target',
3731
- param: 'target',
3732
- type: ParamType$1.String,
3733
- startsHidden: true,
3734
- },
3735
- {
3736
- title: 'Action Type',
3737
- param: 'action',
3738
- type: ParamType$1.String,
3739
- startsHidden: true,
3740
- },
3741
- ];
4054
+ // Nothing to show notice
4055
+ const noLogsNotice = (logs.length === 0
4056
+ ? (React__default["default"].createElement("div", { className: "alert alert-warning text-center mt-2" },
4057
+ React__default["default"].createElement("h4", { className: "m-1" }, "No Logs to Show"),
4058
+ React__default["default"].createElement("div", null, "Either your filters are too strict or no matching logs have been created yet.")))
4059
+ : undefined);
3742
4060
  // Create intelliTable
3743
- const dataTable = (React__default["default"].createElement(IntelliTable, { title: "Matching Logs", id: "logs", data: logs, columns: columns }));
4061
+ const dataTable = (React__default["default"].createElement(IntelliTable, { title: "Matching Logs:", csvName: `Logs from ${getHumanReadableDate()}`, id: "logs", data: logs, columns: columns }));
3744
4062
  // Main body
3745
4063
  body = (React__default["default"].createElement(React__default["default"].Fragment, null,
3746
4064
  filters,
3747
- dataTable));
4065
+ React__default["default"].createElement("div", { className: "mt-2" },
4066
+ dataTable,
4067
+ noLogsNotice)));
3748
4068
  }
3749
4069
  /* ---------- Wrap in Modal --------- */
3750
4070
  return (React__default["default"].createElement("div", { className: "LogReviewer-outer-container" },
@@ -3752,14 +4072,10 @@ const LogReviewer = (props) => {
3752
4072
  React__default["default"].createElement("div", { className: "LogReviewer-inner-container" },
3753
4073
  React__default["default"].createElement("div", { className: "LogReviewer-header" },
3754
4074
  React__default["default"].createElement("div", { className: "LogReviewer-header-title" },
3755
- React__default["default"].createElement("h1", { className: "m-0" }, "Log Review Dashboard")),
3756
- React__default["default"].createElement("button", { type: "button", className: "LogReviewer-header-close-button btn btn-lg", "aria-label": "close log reviewer panel", onClick: onClose, style: {
3757
- border: 0,
3758
- backgroundColor: 'transparent',
3759
- padding: 0,
3760
- margin: 0,
3761
- } },
3762
- React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faTimes }))),
4075
+ React__default["default"].createElement("h3", { className: "text-center m-0" }, "Log Review Dashboard")),
4076
+ React__default["default"].createElement("div", { style: { width: 0 } },
4077
+ 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 },
4078
+ React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faTimes })))),
3763
4079
  React__default["default"].createElement("div", { className: "LogReviewer-contents" }, body))));
3764
4080
  };
3765
4081
 
@@ -4090,15 +4406,17 @@ const initServer = (opts) => {
4090
4406
  },
4091
4407
  handler: ({ params }) => __awaiter(void 0, void 0, void 0, function* () {
4092
4408
  // Get user info
4093
- const { userId, isAdmin } = params;
4409
+ const { year, month, userId, isAdmin, } = params;
4094
4410
  // Validate user
4095
- // isAdmin is already checked because path starts with '/admin'
4096
4411
  const canReview = yield canReviewLogs(userId, isAdmin);
4097
4412
  if (!canReview) {
4098
4413
  throw new ErrorWithCode('You cannot access this resource because you do not have the appropriate permissions.', ReactKitErrorCode$1.NotAllowedToReviewLogs);
4099
4414
  }
4100
4415
  // Query for logs
4101
- const logs = yield _logCollection.find({ userId });
4416
+ const logs = yield _logCollection.find({
4417
+ year,
4418
+ month,
4419
+ });
4102
4420
  // Return logs
4103
4421
  return logs;
4104
4422
  }),
@@ -4747,7 +5065,7 @@ const genRouteHandler = (opts) => {
4747
5065
  isAdmin: !!launchInfo.isAdmin,
4748
5066
  isTTM: !!launchInfo.isTTM,
4749
5067
  courseId: launchInfo.courseId,
4750
- courseName: launchInfo.courseName,
5068
+ courseName: launchInfo.contextLabel,
4751
5069
  browser,
4752
5070
  device,
4753
5071
  year,
@@ -4774,7 +5092,7 @@ const genRouteHandler = (opts) => {
4774
5092
  }
4775
5093
  : {
4776
5094
  type: LogType$1.Action,
4777
- target: ((_m = opts.target) !== null && _m !== void 0 ? _m : LogBuiltInMetadata.Target.NoSpecificTarget),
5095
+ target: ((_m = opts.target) !== null && _m !== void 0 ? _m : LogBuiltInMetadata.Target.NoTarget),
4778
5096
  action: ((_o = opts.action) !== null && _o !== void 0 ? _o : LogAction$1.Unknown),
4779
5097
  });
4780
5098
  // Source-specific info
@@ -4987,29 +5305,6 @@ const startMinWait = (minWaitMs) => {
4987
5305
  });
4988
5306
  };
4989
5307
 
4990
- // Import shared helpers
4991
- /**
4992
- * Get a human-readable description of a date (all in ET)
4993
- * @author Gabe Abrams
4994
- * @param [dateOrTimestamp=today] the date or timestamp for the date to describe
4995
- * @returns human-readable description of the date
4996
- */
4997
- const getHumanReadableDate = (dateOrTimestamp) => {
4998
- // Get the date info
4999
- const { month, day, year, } = getTimeInfoInET(dateOrTimestamp);
5000
- const currYear = getTimeInfoInET().year;
5001
- // Get the short month description
5002
- const monthName = getMonthName(month).short;
5003
- // Create start of description
5004
- let description = `${monthName} ${day}${getOrdinal(day)}`;
5005
- // Add on year if it's different
5006
- if (year !== currYear) {
5007
- description += ` ${year}`;
5008
- }
5009
- // Return description
5010
- return description;
5011
- };
5012
-
5013
5308
  /**
5014
5309
  * Get the current part of day (morning, evening, etc.)
5015
5310
  * @author Gabe Abrams