dce-reactkit 3.2.2-beta.4 → 3.2.2-beta.40

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 (32) hide show
  1. package/dist/cjs/index.js +388 -198
  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/ItemPicker/NestableItemList.d.ts +1 -0
  6. package/dist/cjs/types/components/ItemPicker/index.d.ts +1 -0
  7. package/dist/cjs/types/types/LogAction.d.ts +16 -16
  8. package/dist/cjs/types/types/LogBuiltInMetadata.d.ts +1 -1
  9. package/dist/esm/index.js +388 -198
  10. package/dist/esm/index.js.map +1 -1
  11. package/dist/esm/types/components/ButtonInputGroup.d.ts +1 -0
  12. package/dist/esm/types/components/Drawer.d.ts +1 -0
  13. package/dist/esm/types/components/ItemPicker/NestableItemList.d.ts +1 -0
  14. package/dist/esm/types/components/ItemPicker/index.d.ts +1 -0
  15. package/dist/esm/types/types/LogAction.d.ts +16 -16
  16. package/dist/esm/types/types/LogBuiltInMetadata.d.ts +1 -1
  17. package/dist/index.d.ts +20 -17
  18. package/package.json +1 -1
  19. package/src/components/ButtonInputGroup.tsx +5 -2
  20. package/src/components/CSVDownloadButton.tsx +1 -1
  21. package/src/components/Drawer.tsx +9 -1
  22. package/src/components/IntelliTable.tsx +80 -21
  23. package/src/components/ItemPicker/NestableItemList.tsx +32 -14
  24. package/src/components/ItemPicker/index.tsx +4 -0
  25. package/src/components/LogReviewer.tsx +300 -146
  26. package/src/components/SimpleDateChooser.tsx +20 -7
  27. package/src/helpers/genCSV.ts +21 -4
  28. package/src/helpers/genRouteHandler.ts +2 -2
  29. package/src/helpers/logClientEvent.tsx +6 -1
  30. package/src/server/initServer.ts +11 -4
  31. package/src/types/LogAction.ts +16 -16
  32. package/src/types/LogBuiltInMetadata.ts +5 -5
package/dist/cjs/index.js CHANGED
@@ -519,17 +519,29 @@ 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
 
533
+ /**
534
+ * Allowed log levels
535
+ * @author Gabe Abrams
536
+ */
537
+ var LogLevel;
538
+ (function (LogLevel) {
539
+ LogLevel["Warn"] = "Warn";
540
+ LogLevel["Info"] = "Info";
541
+ LogLevel["Debug"] = "Debug";
542
+ })(LogLevel || (LogLevel = {}));
543
+ var LogLevel$1 = LogLevel;
544
+
533
545
  // Keep track of whether or not session expiry has already been handled
534
546
  let sessionAlreadyExpired = false;
535
547
  /*------------------------------------------------------------------------*/
@@ -648,7 +660,7 @@ const visitServerEndpoint = (opts) => __awaiter(void 0, void 0, void 0, function
648
660
  * @author Gabe Abrams
649
661
  */
650
662
  const logClientEvent = (opts) => __awaiter(void 0, void 0, void 0, function* () {
651
- var _a, _b, _c, _d, _e, _f;
663
+ var _a, _b, _c, _d, _e, _f, _g;
652
664
  return visitServerEndpoint({
653
665
  path: LOG_ROUTE_PATH,
654
666
  method: 'POST',
@@ -657,8 +669,9 @@ const logClientEvent = (opts) => __awaiter(void 0, void 0, void 0, function* ()
657
669
  ? opts.context
658
670
  : ((_b = ((_a = opts.context) !== null && _a !== void 0 ? _a : {})._) !== null && _b !== void 0 ? _b : LogBuiltInMetadata.Context.Uncategorized)),
659
671
  subcontext: ((_c = opts.subcontext) !== null && _c !== void 0 ? _c : LogBuiltInMetadata.Context.Uncategorized),
660
- tags: JSON.stringify((_d = opts.tags) !== null && _d !== void 0 ? _d : []),
661
- metadata: JSON.stringify((_e = opts.metadata) !== null && _e !== void 0 ? _e : {}),
672
+ level: ((_d = opts.level) !== null && _d !== void 0 ? _d : LogLevel$1.Info),
673
+ tags: JSON.stringify((_e = opts.tags) !== null && _e !== void 0 ? _e : []),
674
+ metadata: JSON.stringify((_f = opts.metadata) !== null && _f !== void 0 ? _f : {}),
662
675
  errorMessage: (opts.error
663
676
  ? opts.error.message
664
677
  : undefined),
@@ -669,7 +682,7 @@ const logClientEvent = (opts) => __awaiter(void 0, void 0, void 0, function* ()
669
682
  ? opts.error.stack
670
683
  : undefined),
671
684
  target: (opts.action
672
- ? ((_f = opts.target) !== null && _f !== void 0 ? _f : LogBuiltInMetadata.Target.NoSpecificTarget)
685
+ ? ((_g = opts.target) !== null && _g !== void 0 ? _g : LogBuiltInMetadata.Target.NoTarget)
673
686
  : undefined),
674
687
  action: (opts.action
675
688
  ? opts.action
@@ -1175,21 +1188,21 @@ const ButtonInputGroup = (props) => {
1175
1188
  /*------------------------------------------------------------------------*/
1176
1189
  /* -------------- Props ------------- */
1177
1190
  // Destructure all props
1178
- const { label, minLabelWidth, children, } = props;
1191
+ const { label, minLabelWidth, children, className, } = props;
1179
1192
  /*------------------------------------------------------------------------*/
1180
1193
  /* Render */
1181
1194
  /*------------------------------------------------------------------------*/
1182
1195
  /*----------------------------------------*/
1183
1196
  /* Main UI */
1184
1197
  /*----------------------------------------*/
1185
- 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 : ''}` },
1186
1199
  React__default["default"].createElement("div", { className: "input-group-prepend d-flex w-100" },
1187
1200
  React__default["default"].createElement("span", { className: "input-group-text", style: {
1188
1201
  minWidth: (minLabelWidth !== null && minLabelWidth !== void 0 ? minLabelWidth : undefined),
1189
1202
  borderTopRightRadius: 0,
1190
1203
  borderBottomRightRadius: 0,
1191
1204
  } }, label),
1192
- 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: {
1193
1206
  backgroundColor: 'white',
1194
1207
  borderTopLeftRadius: 0,
1195
1208
  borderBottomLeftRadius: 0,
@@ -1352,12 +1365,25 @@ const SimpleDateChooser = (props) => {
1352
1365
  // Figure out which days are allowed
1353
1366
  const days = [];
1354
1367
  const numDaysInMonth = (new Date(year, month, 0)).getDate();
1355
- const firstDay = (month === today.month
1356
- ? today.day // Current month: start at current date
1357
- : 1 // Future month: start at beginning of month
1358
- );
1359
- for (let day = firstDay; day <= numDaysInMonth; day++) {
1360
- 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
+ }
1361
1387
  }
1362
1388
  choices.push({
1363
1389
  choiceName: `${monthName} ${year}`,
@@ -1428,14 +1454,16 @@ const Drawer = (props) => {
1428
1454
  /*------------------------------------------------------------------------*/
1429
1455
  /* -------------- Props ------------- */
1430
1456
  // Destructure all props
1431
- const { children, } = props;
1457
+ const { customBackgroundColor, children, } = props;
1432
1458
  /*------------------------------------------------------------------------*/
1433
1459
  /* Render */
1434
1460
  /*------------------------------------------------------------------------*/
1435
1461
  /*----------------------------------------*/
1436
1462
  /* Main UI */
1437
1463
  /*----------------------------------------*/
1438
- 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
+ } },
1439
1467
  React__default["default"].createElement("style", null, style$4),
1440
1468
  children));
1441
1469
  };
@@ -1898,24 +1926,25 @@ const CopiableBox = (props) => {
1898
1926
  /**
1899
1927
  * Reusable nested item picker
1900
1928
  * @author Yuen Ler Chow
1929
+ * @author Gabe Abrams
1901
1930
  */
1902
1931
  /* ------------- Actions ------------ */
1903
1932
  // Types of actions
1904
1933
  var ActionType$2;
1905
1934
  (function (ActionType) {
1906
- // Toggle whether the children are being shown
1907
- ActionType["ToggleItems"] = "toggle-items";
1935
+ // Toggle whether a child are being shown
1936
+ ActionType["ToggleChild"] = "toggle-child";
1908
1937
  })(ActionType$2 || (ActionType$2 = {}));
1909
1938
  /**
1910
1939
  * Reducer that executes actions
1911
- * @author Yuen Ler Chow
1940
+ * @author Gabe Abrams
1912
1941
  * @param state current state
1913
1942
  * @param action action to execute
1914
1943
  */
1915
1944
  const reducer$2 = (state, action) => {
1916
1945
  switch (action.type) {
1917
- case ActionType$2.ToggleItems: {
1918
- 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)] }) });
1919
1948
  }
1920
1949
  default: {
1921
1950
  return state;
@@ -1933,14 +1962,19 @@ const NestableItemList = (props) => {
1933
1962
  // Destructure all props
1934
1963
  const { items, onChanged, } = props;
1935
1964
  /* -------------- State ------------- */
1965
+ // Create initial map of child expanded booleans
1966
+ const initChildExpanded = {};
1967
+ items.forEach((item) => {
1968
+ initChildExpanded[String(item.id)] = false;
1969
+ });
1936
1970
  // Initial state
1937
1971
  const initialState = {
1938
- isShowingItems: false,
1972
+ childExpanded: initChildExpanded,
1939
1973
  };
1940
1974
  // Initialize state
1941
1975
  const [state, dispatch] = React.useReducer(reducer$2, initialState);
1942
1976
  // Destructure common state
1943
- const { isShowingItems, } = state;
1977
+ const { childExpanded, } = state;
1944
1978
  /*------------------------------------------------------------------------*/
1945
1979
  /* Component Functions */
1946
1980
  /*------------------------------------------------------------------------*/
@@ -2028,14 +2062,15 @@ const NestableItemList = (props) => {
2028
2062
  backgroundColor: 'transparent',
2029
2063
  }, type: "button", onClick: () => {
2030
2064
  dispatch({
2031
- type: ActionType$2.ToggleItems,
2065
+ type: ActionType$2.ToggleChild,
2066
+ id: item.id,
2032
2067
  });
2033
- }, "aria-label": `${isShowingItems ? 'Hide' : 'Show'} items in ${item.name}` },
2034
- 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 })))),
2035
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) => {
2036
2071
  onChanged(changeChecked(item.id, checked, items));
2037
2072
  }, ariaLabel: `Select ${item.name}`, checkedVariant: Variant$1.Light }),
2038
- 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: {
2039
2074
  paddingLeft: '2.2rem',
2040
2075
  } },
2041
2076
  React__default["default"].createElement(NestableItemList, { items: item.children, onChanged: (updatedItems) => {
@@ -2057,7 +2092,7 @@ const ItemPicker = (props) => {
2057
2092
  /*------------------------------------------------------------------------*/
2058
2093
  /* -------------- Props ------------- */
2059
2094
  // Destructure all props
2060
- const { title, items, onChanged, } = props;
2095
+ const { title, items, onChanged, noBottomMargin, } = props;
2061
2096
  /*------------------------------------------------------------------------*/
2062
2097
  /* Component Functions */
2063
2098
  /*------------------------------------------------------------------------*/
@@ -2067,7 +2102,7 @@ const ItemPicker = (props) => {
2067
2102
  /*----------------------------------------*/
2068
2103
  /* Main UI */
2069
2104
  /*----------------------------------------*/
2070
- return (React__default["default"].createElement(TabBox, { title: title },
2105
+ return (React__default["default"].createElement(TabBox, { title: title, noBottomMargin: noBottomMargin },
2071
2106
  React__default["default"].createElement("div", { style: { overflowX: 'auto' } },
2072
2107
  React__default["default"].createElement(NestableItemList, { items: items, onChanged: onChanged }))));
2073
2108
  };
@@ -2111,37 +2146,37 @@ var LogType$1 = LogType;
2111
2146
  var LogAction;
2112
2147
  (function (LogAction) {
2113
2148
  // Target was opened by the user (it was not on screen, but now it is)
2114
- LogAction["Open"] = "open";
2149
+ LogAction["Open"] = "Open";
2115
2150
  // Target was closed by the user (it was on screen, but now it is not)
2116
- LogAction["Close"] = "close";
2151
+ LogAction["Close"] = "Close";
2117
2152
  // Target was cancelled by the user (it was on closed without saving)
2118
- LogAction["Cancel"] = "cancel";
2153
+ LogAction["Cancel"] = "Cancel";
2119
2154
  // Target was expanded by the user (it always remains on screen, but size was changed)
2120
- LogAction["Expand"] = "expand";
2155
+ LogAction["Expand"] = "Expand";
2121
2156
  // Target was collapsed by the user (it always remains on screen, but size was changed)
2122
- LogAction["Collapse"] = "collapse";
2157
+ LogAction["Collapse"] = "Collapse";
2123
2158
  // Target was viewed by the user (only for items that are not opened or closed, those must use Open/Close actions)
2124
- LogAction["View"] = "view";
2159
+ LogAction["View"] = "View";
2125
2160
  // Target interrupted the user (popup, dialog, validation message, etc. appeared without user prompting)
2126
- LogAction["Interrupt"] = "interrupt";
2161
+ LogAction["Interrupt"] = "Interrupt";
2127
2162
  // Target was created by the user (it did not exist before)
2128
- LogAction["Create"] = "create";
2163
+ LogAction["Create"] = "Create";
2129
2164
  // Target was edited by the user (it existed and was changed)
2130
- LogAction["Edit"] = "edit";
2165
+ LogAction["Edit"] = "Edit";
2131
2166
  // Target was deleted by the user (it existed and now it doesn't)
2132
- LogAction["Delete"] = "delete";
2167
+ LogAction["Delete"] = "Delete";
2133
2168
  // Target was added by the user (it already existed and was added to another place)
2134
- LogAction["Add"] = "add";
2169
+ LogAction["Add"] = "Add";
2135
2170
  // Target was removed by the user (it was removed from something but still exists)
2136
- LogAction["Remove"] = "remove";
2171
+ LogAction["Remove"] = "Remove";
2137
2172
  // Target was activated by the user (click, check, tap, keypress, etc.)
2138
- LogAction["Activate"] = "activate";
2173
+ LogAction["Activate"] = "Activate";
2139
2174
  // Target was deactivated by the user (click away, uncheck, tap outside of, tab away, etc.)
2140
- LogAction["Deactivate"] = "deactivate";
2175
+ LogAction["Deactivate"] = "Deactivate";
2141
2176
  // User showed interest in a target (hover, peek, etc.)
2142
- LogAction["Peek"] = "peek";
2177
+ LogAction["Peek"] = "Peek";
2143
2178
  // Unknown action
2144
- LogAction["Unknown"] = "unknown";
2179
+ LogAction["Unknown"] = "Unknown";
2145
2180
  })(LogAction || (LogAction = {}));
2146
2181
  var LogAction$1 = LogAction;
2147
2182
 
@@ -2183,12 +2218,12 @@ const roundToNumDecimals = (num, numDecimals) => {
2183
2218
  * @returns escaped cell text
2184
2219
  */
2185
2220
  const escapeCellText = (text) => {
2186
- if (!text.includes(',')) {
2221
+ if (!String(text).includes(',')) {
2187
2222
  // No need to escape
2188
- return text;
2223
+ return String(text);
2189
2224
  }
2190
2225
  // Perform escape
2191
- return `"${text.replace(/"/g, '""')}`;
2226
+ return `"${String(text).replace(/"/g, '""')}`;
2192
2227
  };
2193
2228
  /**
2194
2229
  * Generate a CSV file
@@ -2209,7 +2244,23 @@ const genCSV = (data, columns) => {
2209
2244
  data.forEach((datum) => {
2210
2245
  csv += (columns
2211
2246
  .map((column) => {
2212
- return escapeCellText(datum[column.param]);
2247
+ let contents;
2248
+ const cell = datum[column.param];
2249
+ if (typeof cell === 'string'
2250
+ || typeof cell === 'number') {
2251
+ contents = String(cell);
2252
+ }
2253
+ else if (typeof cell === 'undefined'
2254
+ || cell === null) {
2255
+ contents = '';
2256
+ }
2257
+ else if (typeof cell === 'object') {
2258
+ contents = JSON.stringify(cell);
2259
+ }
2260
+ else {
2261
+ contents = '';
2262
+ }
2263
+ return escapeCellText(contents);
2213
2264
  })
2214
2265
  .join(','));
2215
2266
  });
@@ -2242,7 +2293,8 @@ const CSVDownloadButton = (props) => {
2242
2293
  ? `Click to download ${filename}`
2243
2294
  : ariaLabel), style: style, onClick: onClick },
2244
2295
  !children && (React__default["default"].createElement(React__default["default"].Fragment, null,
2245
- React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faCloudDownloadAlt, className: "mr-2" }),
2296
+ React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faCloudDownloadAlt }),
2297
+ ' ',
2246
2298
  "Download CSV")),
2247
2299
  children));
2248
2300
  };
@@ -2266,7 +2318,7 @@ var ActionType$1;
2266
2318
  // Toggle sort column param
2267
2319
  ActionType["ToggleSortColumn"] = "toggle-sort-column";
2268
2320
  // Toggle the visibility of a column
2269
- ActionType["ToggleColumnVisibility"] = "toggle-column-visibility";
2321
+ ActionType["UpdateColumnVisibility"] = "update-column-visibility";
2270
2322
  // Toggle the column visibility customization modal
2271
2323
  ActionType["ToggleColVisCusModalVisibility"] = "toggle-col-vis-cus-modal-visibility";
2272
2324
  })(ActionType$1 || (ActionType$1 = {}));
@@ -2290,9 +2342,9 @@ const reducer$1 = (state, action) => {
2290
2342
  // Stop sorting by column
2291
2343
  return Object.assign(Object.assign({}, state), { sortColumnParam: undefined, sortType: SortType.Ascending });
2292
2344
  }
2293
- case ActionType$1.ToggleColumnVisibility: {
2345
+ case ActionType$1.UpdateColumnVisibility: {
2294
2346
  const { columnVisibilityMap } = state;
2295
- columnVisibilityMap[action.param] = !columnVisibilityMap[action.param];
2347
+ columnVisibilityMap[action.param] = action.visible;
2296
2348
  return Object.assign(Object.assign({}, state), { columnVisibilityMap });
2297
2349
  }
2298
2350
  case ActionType$1.ToggleColVisCusModalVisibility: {
@@ -2313,7 +2365,11 @@ const IntelliTable = (props) => {
2313
2365
  var _a;
2314
2366
  /* -------------- Props ------------- */
2315
2367
  // Destructure all props
2316
- const { title, id, data, columns, } = props;
2368
+ const { title, id, columns, } = props;
2369
+ // Get data, show empty row if none
2370
+ const data = ((props.data && props.data.length > 0)
2371
+ ? props.data
2372
+ : [{ id: 'empty-row' }]);
2317
2373
  /* -------------- State ------------- */
2318
2374
  // Initial state
2319
2375
  const initColumnVisibilityMap = {};
@@ -2330,20 +2386,29 @@ const IntelliTable = (props) => {
2330
2386
  const [state, dispatch] = React.useReducer(reducer$1, initialState);
2331
2387
  // Destructure common state
2332
2388
  const { sortColumnParam, sortType, columnVisibilityMap, columnVisibilityCustomizationModalVisible, } = state;
2389
+ /*------------------------------------------------------------------------*/
2390
+ /* Render */
2391
+ /*------------------------------------------------------------------------*/
2392
+ /*----------------------------------------*/
2393
+ /* Modal */
2394
+ /*----------------------------------------*/
2395
+ // Modal that may be defined
2396
+ let modal;
2333
2397
  /* ------- Col Vis Customization Modal ------ */
2334
2398
  if (columnVisibilityCustomizationModalVisible) {
2335
2399
  // Create modal
2336
- (React__default["default"].createElement(Modal, { type: ModalType$1.Okay, title: "Choose columns to show:", onClose: () => {
2400
+ modal = (React__default["default"].createElement(Modal, { type: ModalType$1.Okay, title: "Choose columns to show:", onClose: () => {
2337
2401
  dispatch({
2338
2402
  type: ActionType$1.ToggleColVisCusModalVisibility,
2339
2403
  });
2340
- } }, columns.map((column) => {
2341
- return (React__default["default"].createElement(CheckboxButton, { key: column.param, id: `IntelliTable-${id}-toggle-visibility-${column.param}`, text: column.title, onChanged: () => {
2404
+ }, okayVariant: Variant$1.Light }, columns.map((column) => {
2405
+ return (React__default["default"].createElement(CheckboxButton, { key: column.param, id: `IntelliTable-${id}-toggle-visibility-${column.param}`, className: "mb-2", text: column.title, onChanged: (checked) => {
2342
2406
  dispatch({
2343
- type: ActionType$1.ToggleColumnVisibility,
2407
+ type: ActionType$1.UpdateColumnVisibility,
2344
2408
  param: column.param,
2409
+ visible: checked,
2345
2410
  });
2346
- }, checked: columnVisibilityMap[column.param], ariaLabel: `show "${column.title}" column` }));
2411
+ }, checked: columnVisibilityMap[column.param], ariaLabel: `show "${column.title}" column`, checkedVariant: Variant$1.Light, uncheckedVariant: Variant$1.Secondary }));
2347
2412
  })));
2348
2413
  }
2349
2414
  /*----------------------------------------*/
@@ -2358,6 +2423,7 @@ const IntelliTable = (props) => {
2358
2423
  // Custom info based on current sort type
2359
2424
  let sortButtonAriaLabel;
2360
2425
  let sortIcon = freeSolidSvgIcons.faSort;
2426
+ let sortingByThisColumn = false;
2361
2427
  if (!sortColumnParam) {
2362
2428
  // Not being sorted yet
2363
2429
  sortButtonAriaLabel = `sort ascending by ${column.title}`;
@@ -2365,6 +2431,7 @@ const IntelliTable = (props) => {
2365
2431
  }
2366
2432
  else if (column.param === sortColumnParam) {
2367
2433
  // Already sorted by this column
2434
+ sortingByThisColumn = true;
2368
2435
  if (sortType === SortType.Ascending) {
2369
2436
  // Sorted ascending
2370
2437
  sortButtonAriaLabel = `sort descending by ${column.title}`;
@@ -2382,11 +2449,14 @@ const IntelliTable = (props) => {
2382
2449
  sortIcon = freeSolidSvgIcons.faSort;
2383
2450
  }
2384
2451
  // Create the cell UI
2385
- return (React__default["default"].createElement("th", { key: column.param, scope: "col", id: `IntelliTable-${id}-header-${column.param}` },
2386
- React__default["default"].createElement("div", { className: "d-flex align-items-center justify-content-center flex-row h-100" },
2387
- React__default["default"].createElement("h4", { className: "m-0" }, column.title),
2452
+ return (React__default["default"].createElement("th", { key: column.param, scope: "col", id: `IntelliTable-${id}-header-${column.param}`, className: "text-start", style: {
2453
+ borderRight: '0.05rem solid #555',
2454
+ borderLeft: '0.05rem solid #555',
2455
+ } },
2456
+ React__default["default"].createElement("div", { className: "d-flex align-items-center justify-content-start flex-row h-100" },
2457
+ React__default["default"].createElement("span", { className: "text-nowrap" }, column.title),
2388
2458
  React__default["default"].createElement("div", null,
2389
- React__default["default"].createElement("button", { type: "button", className: "btn btn-light", "aria-label": sortButtonAriaLabel, onClick: () => {
2459
+ 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: () => {
2390
2460
  dispatch({
2391
2461
  type: ActionType$1.ToggleSortColumn,
2392
2462
  param: column.param,
@@ -2404,8 +2474,28 @@ const IntelliTable = (props) => {
2404
2474
  const descending = (sortType === SortType.Descending);
2405
2475
  if (sortColumnParam) {
2406
2476
  sortedData.sort((a, b) => {
2477
+ var _a, _b;
2407
2478
  const aVal = a[sortColumnParam];
2408
2479
  const bVal = b[sortColumnParam];
2480
+ // Tiebreaker sort by timestamp, most recent first (used if tied)
2481
+ const tiebreaker = (((_a = b.timestamp) !== null && _a !== void 0 ? _a : 0)
2482
+ - ((_b = a.timestamp) !== null && _b !== void 0 ? _b : 0));
2483
+ // Auto-sort undefined and null to end of list
2484
+ if ((aVal === undefined || aVal === null)
2485
+ || (bVal === undefined || bVal === null)) {
2486
+ // At least one was undefined
2487
+ if ((aVal === undefined || aVal === null)
2488
+ && (bVal === undefined || bVal === null)) {
2489
+ // Both undefined
2490
+ return tiebreaker;
2491
+ }
2492
+ if (aVal === undefined || aVal === null) {
2493
+ // First was undefined
2494
+ return 1;
2495
+ }
2496
+ // Second was undefined
2497
+ return -1;
2498
+ }
2409
2499
  // Sort differently based on the data type
2410
2500
  // > Boolean
2411
2501
  if (paramType === ParamType$1.Boolean) {
@@ -2415,7 +2505,7 @@ const IntelliTable = (props) => {
2415
2505
  if (!aVal && bVal) {
2416
2506
  return (descending ? 1 : -1);
2417
2507
  }
2418
- return 0;
2508
+ return tiebreaker;
2419
2509
  }
2420
2510
  // > Number
2421
2511
  if (paramType === ParamType$1.Int
@@ -2427,12 +2517,12 @@ const IntelliTable = (props) => {
2427
2517
  // > String
2428
2518
  if (paramType === ParamType$1.String) {
2429
2519
  if (aVal < bVal) {
2430
- return (descending ? -1 : 1);
2520
+ return (descending ? 1 : -1);
2431
2521
  }
2432
2522
  if (aVal > bVal) {
2433
- return (descending ? 1 : -1);
2523
+ return (descending ? -1 : 1);
2434
2524
  }
2435
- return 0;
2525
+ return tiebreaker;
2436
2526
  }
2437
2527
  // > JSON
2438
2528
  if (paramType === ParamType$1.JSON) {
@@ -2447,7 +2537,7 @@ const IntelliTable = (props) => {
2447
2537
  : (aSize - bSize));
2448
2538
  }
2449
2539
  // No sort
2450
- return 0;
2540
+ return tiebreaker;
2451
2541
  });
2452
2542
  }
2453
2543
  // Table body
@@ -2494,7 +2584,9 @@ const IntelliTable = (props) => {
2494
2584
  }
2495
2585
  else if (column.type === ParamType$1.String) {
2496
2586
  fullValue = String(value).trim();
2497
- const noValue = (value.trim().length) === 0;
2587
+ const noValue = (value === undefined
2588
+ || value === null
2589
+ || String(fullValue).trim().length === 0);
2498
2590
  visibleValue = (noValue
2499
2591
  ? (React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faMinus }))
2500
2592
  : fullValue);
@@ -2510,7 +2602,10 @@ const IntelliTable = (props) => {
2510
2602
  : fullValue);
2511
2603
  }
2512
2604
  // Create UI
2513
- return (React__default["default"].createElement("td", { key: `${datum.id}-${column.param}`, title: title }, visibleValue));
2605
+ return (React__default["default"].createElement("td", { key: `${datum.id}-${column.param}`, title: title, style: {
2606
+ borderRight: '0.05rem solid #555',
2607
+ borderLeft: '0.05rem solid #555',
2608
+ } }, visibleValue));
2514
2609
  }));
2515
2610
  // Add cells to a row
2516
2611
  return (React__default["default"].createElement("tr", { key: datum.id }, cells));
@@ -2530,11 +2625,12 @@ const IntelliTable = (props) => {
2530
2625
  const csv = genCSV(data, columns);
2531
2626
  // Build main UI
2532
2627
  return (React__default["default"].createElement("div", { className: `IntelliTable-container-${id}` },
2533
- React__default["default"].createElement("div", { className: "d-flex align-items-center justify-content-center" },
2628
+ modal,
2629
+ React__default["default"].createElement("div", { className: "d-flex align-items-center justify-content-start" },
2534
2630
  React__default["default"].createElement("h3", { className: "m-0" }, title),
2535
2631
  React__default["default"].createElement("div", { className: "flex-grow-1 text-end" },
2536
2632
  React__default["default"].createElement(CSVDownloadButton, { "aria-label": `download data as csv for ${title}`, id: `IntelliTable-${id}-download-as-csv`, filename: `${title}.csv`, csv: csv }),
2537
- 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: () => {
2633
+ 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: () => {
2538
2634
  dispatch({
2539
2635
  type: ActionType$1.ToggleColVisCusModalVisibility,
2540
2636
  });
@@ -2546,7 +2642,7 @@ const IntelliTable = (props) => {
2546
2642
  numHiddenCols,
2547
2643
  ' ',
2548
2644
  "hidden)"))))),
2549
- React__default["default"].createElement("div", { className: `IntelliTable-table-${id}`, style: {
2645
+ React__default["default"].createElement("div", { className: `IntelliTable-table-${id} mt-2`, style: {
2550
2646
  overflowX: 'auto',
2551
2647
  } }, table)));
2552
2648
  };
@@ -2598,6 +2694,11 @@ const style = `
2598
2694
  border: 0.05rem solid black;
2599
2695
  border-radius: 0.5rem;
2600
2696
  overflow: hidden;
2697
+ padding: 0.7rem;
2698
+
2699
+ /* Solid background */
2700
+ background-color: white;
2701
+ color: black;
2601
2702
 
2602
2703
  /* Place contents in flex column */
2603
2704
  flex-direction: column;
@@ -2624,6 +2725,27 @@ const style = `
2624
2725
  /* Vertical scroll */
2625
2726
  overflow-y: auto;
2626
2727
  }
2728
+
2729
+ .LogReviewer-header-close-button {
2730
+ border: 0 !important;
2731
+ background-color: transparent !important;
2732
+ padding-top: 0 !important;
2733
+ padding-bottom: 0 !important;
2734
+ padding-right: 1em !important;
2735
+ margin: 0 !important;
2736
+ color: #444 !important;
2737
+
2738
+ right: 0 !important;
2739
+ position: absolute !important;
2740
+ }
2741
+ .LogReviewer-header-close-button:hover {
2742
+ border: 0 !important;
2743
+ background-color: transparent !important;
2744
+ padding-top: 0 !important;
2745
+ padding-bottom: 0 !important;
2746
+ margin: 0 !important;
2747
+ color: #000 !important;
2748
+ }
2627
2749
  `;
2628
2750
  /*------------------------------------------------------------------------*/
2629
2751
  /* Static Functions */
@@ -2643,7 +2765,7 @@ const genHumanReadableName = (machineReadableName) => {
2643
2765
  // Uppercase! Add a space before
2644
2766
  humanReadableName += ' ';
2645
2767
  }
2646
- humanReadableName += chars;
2768
+ humanReadableName += char;
2647
2769
  });
2648
2770
  // Trim and return
2649
2771
  return humanReadableName.trim();
@@ -2705,7 +2827,19 @@ const reducer = (state, action) => {
2705
2827
  return Object.assign(Object.assign({}, state), { contextFilterState: action.contextFilterState });
2706
2828
  }
2707
2829
  case ActionType.UpdateTagFilterState: {
2708
- return Object.assign(Object.assign({}, state), { tagFilterState: action.tagFilterState });
2830
+ const { tagFilterState } = action;
2831
+ // Select all if every tag is deselected
2832
+ const numTagsSelected = (Object.values(tagFilterState)
2833
+ .filter((isSelected) => {
2834
+ return isSelected;
2835
+ })
2836
+ .length);
2837
+ if (numTagsSelected === 0) {
2838
+ Object.keys(tagFilterState).forEach((tag) => {
2839
+ tagFilterState[tag] = true;
2840
+ });
2841
+ }
2842
+ return Object.assign(Object.assign({}, state), { tagFilterState });
2709
2843
  }
2710
2844
  case ActionType.UpdateActionErrorFilterState: {
2711
2845
  return Object.assign(Object.assign({}, state), { actionErrorFilterState: action.actionErrorFilterState });
@@ -2725,10 +2859,35 @@ const LogReviewer = (props) => {
2725
2859
  /*------------------------------------------------------------------------*/
2726
2860
  /* Setup */
2727
2861
  /*------------------------------------------------------------------------*/
2728
- var _a, _b, _c, _d, _e;
2862
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
2729
2863
  /* -------------- Props ------------- */
2730
2864
  // Destructure props
2731
2865
  const { LogMetadata, onClose, } = props;
2866
+ // Add built-in LogMetadata
2867
+ // > Add "uncategorized" subcontext to each context
2868
+ Object.keys((_a = LogMetadata.Context) !== null && _a !== void 0 ? _a : {}).forEach((context) => {
2869
+ if (
2870
+ // Context exists
2871
+ LogMetadata.Context
2872
+ // Context has children already
2873
+ && typeof LogMetadata.Context[context] !== 'string') {
2874
+ LogMetadata.Context[context][LogBuiltInMetadata.Context.Uncategorized] = (LogBuiltInMetadata.Context.Uncategorized);
2875
+ }
2876
+ });
2877
+ // > Add built-in contexts
2878
+ LogMetadata.Context = ((_b = LogMetadata.Context) !== null && _b !== void 0 ? _b : {});
2879
+ Object.keys(LogBuiltInMetadata.Context).forEach((context) => {
2880
+ if (LogMetadata.Context) {
2881
+ LogMetadata.Context[context] = context;
2882
+ }
2883
+ });
2884
+ // > Add built-in targets
2885
+ LogMetadata.Target = ((_c = LogMetadata.Target) !== null && _c !== void 0 ? _c : {});
2886
+ Object.keys(LogBuiltInMetadata.Target).forEach((target) => {
2887
+ if (LogMetadata.Target) {
2888
+ LogMetadata.Target[target] = target;
2889
+ }
2890
+ });
2732
2891
  /* -------------- State ------------- */
2733
2892
  // Create initial date filter state
2734
2893
  const today = getTimeInfoInET();
@@ -2748,26 +2907,30 @@ const LogReviewer = (props) => {
2748
2907
  };
2749
2908
  // Create initial context filter state
2750
2909
  const initContextFilterState = {};
2751
- Object.keys((_a = LogMetadata.Context) !== null && _a !== void 0 ? _a : {}).forEach((context) => {
2910
+ Object.keys((_d = LogMetadata.Context) !== null && _d !== void 0 ? _d : {}).forEach((context) => {
2752
2911
  var _a, _b;
2753
2912
  const contextValue = ((_a = LogMetadata.Context) !== null && _a !== void 0 ? _a : {})[context];
2754
2913
  if (typeof contextValue === 'string') {
2755
- // Case: no subcontexts
2914
+ // Case: no subcontexts, init as checked
2756
2915
  initContextFilterState[contextValue] = true;
2757
2916
  }
2758
2917
  else {
2759
2918
  // Case: subcontexts exist
2760
- initContextFilterState[contextValue._] = {};
2919
+ initContextFilterState[context] = {};
2761
2920
  Object.values(((_b = LogMetadata.Context) !== null && _b !== void 0 ? _b : {})[context]).forEach((subcontext) => {
2762
- const subcontextValue = contextValue[subcontext];
2763
- initContextFilterState[contextValue._][subcontextValue] = true;
2921
+ // Skip self ("_")
2922
+ if (subcontext === '_') {
2923
+ return;
2924
+ }
2925
+ // Initialize as checked
2926
+ initContextFilterState[context][subcontext] = true;
2764
2927
  });
2765
2928
  }
2766
2929
  });
2767
2930
  // Create initial tag filter state
2768
2931
  const initTagFilterState = {};
2769
- Object.values((_b = LogMetadata.Tag) !== null && _b !== void 0 ? _b : {}).forEach((tagValue) => {
2770
- initTagFilterState[tagValue] = true;
2932
+ Object.keys((_e = LogMetadata.Tag) !== null && _e !== void 0 ? _e : {}).forEach((tag) => {
2933
+ initTagFilterState[tag] = false;
2771
2934
  });
2772
2935
  // Create advanced filter state
2773
2936
  const initAdvancedFilterState = {
@@ -2793,12 +2956,16 @@ const LogReviewer = (props) => {
2793
2956
  target: {},
2794
2957
  action: {},
2795
2958
  };
2796
- Object.values((_c = LogMetadata.Target) !== null && _c !== void 0 ? _c : {}).forEach((target) => {
2959
+ Object.values((_f = LogMetadata.Target) !== null && _f !== void 0 ? _f : {}).forEach((target) => {
2797
2960
  initActionErrorFilterState.target[target] = true;
2798
2961
  });
2799
2962
  Object.values(LogAction$1).forEach((action) => {
2800
2963
  initActionErrorFilterState.action[action] = true;
2801
2964
  });
2965
+ // Add built-in targets
2966
+ Object.values(LogBuiltInMetadata.Target).forEach((target) => {
2967
+ initActionErrorFilterState.target[target] = true;
2968
+ });
2802
2969
  // Initial state
2803
2970
  const initialState = {
2804
2971
  loading: true,
@@ -2832,15 +2999,18 @@ const LogReviewer = (props) => {
2832
2999
  let month = newDateFilterState.startDate.month;
2833
3000
  while (
2834
3001
  // Earlier year
2835
- (year <= newDateFilterState.endDate.year)
3002
+ (year < newDateFilterState.endDate.year)
2836
3003
  // Current year but included month
2837
3004
  || (year === newDateFilterState.endDate.year
2838
3005
  && month <= newDateFilterState.endDate.month)) {
2839
- // Add to list
2840
- toLoad.push({
2841
- year,
2842
- month,
2843
- });
3006
+ // Add to list if not already loaded
3007
+ if (!logMap[year]
3008
+ || !logMap[year][month]) {
3009
+ toLoad.push({
3010
+ year,
3011
+ month,
3012
+ });
3013
+ }
2844
3014
  // Increment
2845
3015
  month += 1;
2846
3016
  if (month > 12) {
@@ -2928,10 +3098,10 @@ const LogReviewer = (props) => {
2928
3098
  /* Filters */
2929
3099
  /*----------------------------------------*/
2930
3100
  // Filter toggle
2931
- const filterToggles = (React__default["default"].createElement("div", { className: "LogReviewer-filter-toggles d-flex align-items-center justify-content-center" },
3101
+ const filterToggles = (React__default["default"].createElement("div", { className: "LogReviewer-filter-toggles" },
2932
3102
  React__default["default"].createElement("h3", { className: "m-0" }, "Filters:"),
2933
- React__default["default"].createElement("div", { className: "LogReviewer-filter-toggle-buttons" },
2934
- 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: () => {
3103
+ React__default["default"].createElement("div", { className: "LogReviewer-filter-toggle-buttons alert alert-secondary p-2 m-0" },
3104
+ 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: () => {
2935
3105
  dispatch({
2936
3106
  type: ActionType.ToggleFilterDrawer,
2937
3107
  filterDrawer: FilterDrawer.Date,
@@ -2939,7 +3109,7 @@ const LogReviewer = (props) => {
2939
3109
  } },
2940
3110
  React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faCalendar, className: "me-2" }),
2941
3111
  "Date"),
2942
- 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: () => {
3112
+ 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: () => {
2943
3113
  dispatch({
2944
3114
  type: ActionType.ToggleFilterDrawer,
2945
3115
  filterDrawer: FilterDrawer.Context,
@@ -2947,15 +3117,15 @@ const LogReviewer = (props) => {
2947
3117
  } },
2948
3118
  React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faCircle, className: "me-2" }),
2949
3119
  "Context"),
2950
- 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: () => {
3120
+ (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: () => {
2951
3121
  dispatch({
2952
3122
  type: ActionType.ToggleFilterDrawer,
2953
3123
  filterDrawer: FilterDrawer.Tag,
2954
3124
  });
2955
3125
  } },
2956
3126
  React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faTag, className: "me-2" }),
2957
- "Tag"),
2958
- 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: () => {
3127
+ "Tag")),
3128
+ 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: () => {
2959
3129
  dispatch({
2960
3130
  type: ActionType.ToggleFilterDrawer,
2961
3131
  filterDrawer: FilterDrawer.Action,
@@ -2963,7 +3133,7 @@ const LogReviewer = (props) => {
2963
3133
  } },
2964
3134
  React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faHammer, className: "me-2" }),
2965
3135
  "Action"),
2966
- 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: () => {
3136
+ 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: () => {
2967
3137
  dispatch({
2968
3138
  type: ActionType.ToggleFilterDrawer,
2969
3139
  filterDrawer: FilterDrawer.Advanced,
@@ -2976,25 +3146,29 @@ const LogReviewer = (props) => {
2976
3146
  if (expandedFilterDrawer) {
2977
3147
  if (expandedFilterDrawer === FilterDrawer.Date) {
2978
3148
  filterDrawer = (React__default["default"].createElement(TabBox, { title: "Date" },
2979
- 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) => {
2980
- dispatch({
2981
- type: ActionType.UpdateDateFilterState,
2982
- dateFilterState: Object.assign(Object.assign({}, dateFilterState), { startDate: { month, day, year } }),
2983
- });
3149
+ 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: 12, onChange: (month, day, year) => {
3150
+ dateFilterState.startDate = { month, day, year };
3151
+ handleDateRangeUpdated(dateFilterState);
2984
3152
  } }),
2985
3153
  ' ',
2986
3154
  "to",
2987
3155
  ' ',
2988
- 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) => {
2989
- dispatch({
2990
- type: ActionType.UpdateDateFilterState,
2991
- dateFilterState: Object.assign(Object.assign({}, dateFilterState), { endDate: { month, day, year } }),
2992
- });
3156
+ 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) => {
3157
+ if (year < dateFilterState.startDate.year
3158
+ || (year === dateFilterState.startDate.year
3159
+ && month < dateFilterState.startDate.month)
3160
+ || (year === dateFilterState.startDate.year
3161
+ && month === dateFilterState.startDate.month
3162
+ && day < dateFilterState.startDate.day)) {
3163
+ return alert$1('Invalid Start Date', 'The start date cannot be before the end date.');
3164
+ }
3165
+ dateFilterState.endDate = { month, day, year };
3166
+ handleDateRangeUpdated(dateFilterState);
2993
3167
  } })));
2994
3168
  }
2995
3169
  else if (expandedFilterDrawer === FilterDrawer.Context) {
2996
3170
  // Create item picker items
2997
- const pickableItems = (Object.keys((_d = LogMetadata.Context) !== null && _d !== void 0 ? _d : {})
3171
+ const pickableItems = (Object.keys((_g = LogMetadata.Context) !== null && _g !== void 0 ? _g : {})
2998
3172
  .map((context) => {
2999
3173
  var _a;
3000
3174
  const value = ((_a = LogMetadata.Context) !== null && _a !== void 0 ? _a : {})[context];
@@ -3015,15 +3189,15 @@ const LogReviewer = (props) => {
3015
3189
  })
3016
3190
  .map((subcontext) => {
3017
3191
  return {
3018
- id: `${context}-${subcontext}`,
3192
+ id: subcontext,
3019
3193
  name: genHumanReadableName(subcontext),
3020
3194
  isGroup: false,
3021
- checked: !!value[subcontext],
3195
+ checked: contextFilterState[context][subcontext],
3022
3196
  };
3023
3197
  }));
3024
3198
  const item = {
3025
3199
  id: context,
3026
- name: context,
3200
+ name: genHumanReadableName(context),
3027
3201
  isGroup: true,
3028
3202
  children,
3029
3203
  };
@@ -3036,7 +3210,9 @@ const LogReviewer = (props) => {
3036
3210
  if (pickableItem.isGroup) {
3037
3211
  // Has subcontexts
3038
3212
  pickableItem.children.forEach((subcontextItem) => {
3039
- contextFilterState[pickableItem.id][subcontextItem.id] = (subcontextItem.checked);
3213
+ if (!subcontextItem.isGroup) {
3214
+ contextFilterState[pickableItem.id][subcontextItem.id] = (subcontextItem.checked);
3215
+ }
3040
3216
  });
3041
3217
  }
3042
3218
  else {
@@ -3052,11 +3228,15 @@ const LogReviewer = (props) => {
3052
3228
  }
3053
3229
  else if (expandedFilterDrawer === FilterDrawer.Tag) {
3054
3230
  // Create filter UI
3055
- filterDrawer = (React__default["default"].createElement(TabBox, { title: "Tags" }, Object.keys(tagFilterState)
3231
+ filterDrawer = (React__default["default"].createElement(TabBox, { title: "Tags" }, Object.keys((_h = LogMetadata.Tag) !== null && _h !== void 0 ? _h : {})
3056
3232
  .map((tag, i) => {
3057
3233
  const description = genHumanReadableName(tag);
3058
- 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) => {
3234
+ 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) => {
3059
3235
  tagFilterState[tag] = checked;
3236
+ dispatch({
3237
+ type: ActionType.UpdateTagFilterState,
3238
+ tagFilterState,
3239
+ });
3060
3240
  } }));
3061
3241
  })));
3062
3242
  }
@@ -3087,29 +3267,33 @@ const LogReviewer = (props) => {
3087
3267
  }, ariaLabel: "only show error logs", selected: actionErrorFilterState.type === LogType$1.Error, noMarginOnRight: true })),
3088
3268
  (actionErrorFilterState.type === undefined
3089
3269
  || actionErrorFilterState.type === LogType$1.Action) && (React__default["default"].createElement(TabBox, { title: "Action Log Details" },
3090
- React__default["default"].createElement(ButtonInputGroup, { label: "Action" }, Object.keys(LogAction$1)
3270
+ React__default["default"].createElement(ButtonInputGroup, { label: "Action", className: "mb-2" }, Object.keys(LogAction$1)
3091
3271
  .map((action, i) => {
3092
3272
  const description = genHumanReadableName(action);
3093
- 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) => {
3273
+ 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: i === Object.keys(LogAction$1).length - 1, checked: actionErrorFilterState.action[action], onChanged: (checked) => {
3094
3274
  actionErrorFilterState.action[action] = checked;
3275
+ console.log(actionErrorFilterState, action, checked);
3095
3276
  dispatch({
3096
3277
  type: ActionType.UpdateActionErrorFilterState,
3097
3278
  actionErrorFilterState,
3098
3279
  });
3099
3280
  } }));
3100
3281
  })),
3101
- React__default["default"].createElement(ButtonInputGroup, { label: "Target" }, Object.keys((_e = LogMetadata.Target) !== null && _e !== void 0 ? _e : {})
3102
- .map((target, i) => {
3103
- var _a;
3104
- const description = genHumanReadableName(target);
3105
- return (React__default["default"].createElement(CheckboxButton, { id: `LogReviewer-target-${target}-checkbox`, text: description, ariaLabel: `include logs with target "${description}" in results`, onChanged: (checked) => {
3106
- actionErrorFilterState.target[target] = checked;
3107
- dispatch({
3108
- type: ActionType.UpdateActionErrorFilterState,
3109
- actionErrorFilterState,
3110
- });
3111
- }, noMarginOnRight: i === Object.keys((_a = LogMetadata.Target) !== null && _a !== void 0 ? _a : {}).length - 1 }));
3112
- })))),
3282
+ React__default["default"].createElement(ButtonInputGroup, { label: "Target" },
3283
+ (Object.keys((_j = LogMetadata.Target) !== null && _j !== void 0 ? _j : {}).length === 0) && (React__default["default"].createElement("div", null, "This app does not have any targets yet.")),
3284
+ Object.keys((_k = LogMetadata.Target) !== null && _k !== void 0 ? _k : {})
3285
+ .map((target, i) => {
3286
+ var _a;
3287
+ const description = genHumanReadableName(target);
3288
+ 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], onChanged: (checked) => {
3289
+ actionErrorFilterState.target[target] = checked;
3290
+ console.log(actionErrorFilterState, target, checked);
3291
+ dispatch({
3292
+ type: ActionType.UpdateActionErrorFilterState,
3293
+ actionErrorFilterState,
3294
+ });
3295
+ }, noMarginOnRight: i === Object.keys((_a = LogMetadata.Target) !== null && _a !== void 0 ? _a : {}).length - 1 }));
3296
+ })))),
3113
3297
  (actionErrorFilterState.type === undefined
3114
3298
  || actionErrorFilterState.type === LogType$1.Error) && (React__default["default"].createElement(TabBox, { title: "Error Log Details" },
3115
3299
  React__default["default"].createElement("div", { className: "input-group mb-2" },
@@ -3123,7 +3307,7 @@ const LogReviewer = (props) => {
3123
3307
  } })),
3124
3308
  React__default["default"].createElement("div", { className: "input-group mb-2" },
3125
3309
  React__default["default"].createElement("span", { className: "input-group-text" }, "Error Code"),
3126
- React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for error code", value: actionErrorFilterState.errorMessage, onChange: (e) => {
3310
+ React__default["default"].createElement("input", { type: "text", className: "form-control", "aria-label": "query for error code", value: actionErrorFilterState.errorCode, onChange: (e) => {
3127
3311
  actionErrorFilterState.errorCode = ((e.target.value)
3128
3312
  .trim()
3129
3313
  .toUpperCase());
@@ -3271,26 +3455,27 @@ const LogReviewer = (props) => {
3271
3455
  advancedFilterState,
3272
3456
  });
3273
3457
  }, noMarginOnRight: true })),
3274
- React__default["default"].createElement("div", { className: "input-group mb-2" },
3275
- React__default["default"].createElement("span", { className: "input-group-text" }, "Server Route Path"),
3276
- 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) => {
3277
- advancedFilterState.courseName = ((e.target.value)
3278
- .trim());
3279
- dispatch({
3280
- type: ActionType.UpdateAdvancedFilterState,
3281
- advancedFilterState,
3282
- });
3283
- } })),
3284
- React__default["default"].createElement("div", { className: "input-group mb-2" },
3285
- React__default["default"].createElement("span", { className: "input-group-text" }, "Server Route Template"),
3286
- 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) => {
3287
- advancedFilterState.courseName = ((e.target.value)
3288
- .trim());
3289
- dispatch({
3290
- type: ActionType.UpdateAdvancedFilterState,
3291
- advancedFilterState,
3292
- });
3293
- } })))));
3458
+ advancedFilterState.source !== LogSource$1.Client && (React__default["default"].createElement("div", { className: "mt-2" },
3459
+ React__default["default"].createElement("div", { className: "input-group mb-2" },
3460
+ React__default["default"].createElement("span", { className: "input-group-text" }, "Server Route Path"),
3461
+ 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) => {
3462
+ advancedFilterState.courseName = ((e.target.value)
3463
+ .trim());
3464
+ dispatch({
3465
+ type: ActionType.UpdateAdvancedFilterState,
3466
+ advancedFilterState,
3467
+ });
3468
+ } })),
3469
+ React__default["default"].createElement("div", { className: "input-group mb-2" },
3470
+ React__default["default"].createElement("span", { className: "input-group-text" }, "Server Route Template"),
3471
+ 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) => {
3472
+ advancedFilterState.courseName = ((e.target.value)
3473
+ .trim());
3474
+ dispatch({
3475
+ type: ActionType.UpdateAdvancedFilterState,
3476
+ advancedFilterState,
3477
+ });
3478
+ } })))))));
3294
3479
  }
3295
3480
  }
3296
3481
  // Filters UI
@@ -3301,7 +3486,7 @@ const LogReviewer = (props) => {
3301
3486
  // > Perform filters
3302
3487
  const logs = [];
3303
3488
  Object.keys(logMap).forEach((year) => {
3304
- Object.keys(logMap).forEach((month) => {
3489
+ Object.keys(logMap[year]).forEach((month) => {
3305
3490
  logMap[year][month].forEach((log) => {
3306
3491
  /* ----------- Date Filter ---------- */
3307
3492
  var _a;
@@ -3337,16 +3522,23 @@ const LogReviewer = (props) => {
3337
3522
  // Whole context is deselected
3338
3523
  contextFilterState[log.context] === false
3339
3524
  // None of the subcontexts are selected
3340
- || (Object.values((_a = contextFilterState[log.context]) !== null && _a !== void 0 ? _a : {})
3341
- .every((isSelected) => {
3342
- return !isSelected;
3343
- }))) {
3525
+ || (
3526
+ // Has subcontexts
3527
+ typeof contextFilterState[log.context] !== 'boolean'
3528
+ // None of the subcontexts are selected
3529
+ && Object.values((_a = contextFilterState[log.context]) !== null && _a !== void 0 ? _a : {})
3530
+ .every((isSelected) => {
3531
+ return !isSelected;
3532
+ }))) {
3533
+ console.log(log.context, contextFilterState);
3344
3534
  return;
3345
3535
  }
3346
3536
  // Subcontext doesn't match
3347
3537
  if (
3348
- // Log has a subcontext
3349
- log.subcontext
3538
+ // Log context is not "uncategorized" (no point in further filters)
3539
+ log.context !== LogBuiltInMetadata.Context.Uncategorized
3540
+ // Log has a subcontext
3541
+ && log.subcontext
3350
3542
  // Context has subcontexts
3351
3543
  && (contextFilterState[log.context]
3352
3544
  && contextFilterState[log.context] !== false
@@ -3358,12 +3550,16 @@ const LogReviewer = (props) => {
3358
3550
  /* -------------- Tags -------------- */
3359
3551
  // No tags match
3360
3552
  if (
3361
- // Log has at least one tag
3362
- log.tags.length > 0
3553
+ // At least one tag is required
3554
+ Object.values(tagFilterState)
3555
+ .filter((isSelected) => {
3556
+ return isSelected;
3557
+ })
3558
+ .length > 0
3363
3559
  // No tags match
3364
- && (log.tags.every((tag) => {
3560
+ && log.tags.every((tag) => {
3365
3561
  return !tagFilterState[tag];
3366
- }))) {
3562
+ })) {
3367
3563
  return;
3368
3564
  }
3369
3565
  /* ------- Actions and Errors ------- */
@@ -3497,7 +3693,7 @@ const LogReviewer = (props) => {
3497
3693
  // Device info exists
3498
3694
  && log.device
3499
3695
  // Mobile filter doesn't match
3500
- && (advancedFilterState.isMobile === log.device.isMobile)) {
3696
+ && (advancedFilterState.isMobile !== log.device.isMobile)) {
3501
3697
  return;
3502
3698
  }
3503
3699
  // Log source doesn't match
@@ -3562,18 +3758,18 @@ const LogReviewer = (props) => {
3562
3758
  type: ParamType$1.Int,
3563
3759
  },
3564
3760
  {
3565
- title: 'Student',
3761
+ title: 'Student?',
3566
3762
  param: 'isLearner',
3567
3763
  type: ParamType$1.Boolean,
3568
3764
  },
3569
3765
  {
3570
- title: 'Teaching Staff',
3766
+ title: 'Teaching Staff?',
3571
3767
  param: 'isTTM',
3572
3768
  type: ParamType$1.Boolean,
3573
3769
  startsHidden: true,
3574
3770
  },
3575
3771
  {
3576
- title: 'Admin',
3772
+ title: 'Admin?',
3577
3773
  param: 'isAdmin',
3578
3774
  type: ParamType$1.Boolean,
3579
3775
  startsHidden: true,
@@ -3608,7 +3804,7 @@ const LogReviewer = (props) => {
3608
3804
  startsHidden: true,
3609
3805
  },
3610
3806
  {
3611
- title: 'Mobile',
3807
+ title: 'Mobile?',
3612
3808
  param: 'device.isMobile',
3613
3809
  type: ParamType$1.Boolean,
3614
3810
  startsHidden: true,
@@ -3726,12 +3922,20 @@ const LogReviewer = (props) => {
3726
3922
  startsHidden: true,
3727
3923
  },
3728
3924
  ];
3925
+ // Nothing to show notice
3926
+ const noLogsNotice = (logs.length === 0
3927
+ ? (React__default["default"].createElement("div", { className: "alert alert-warning text-center mt-2" },
3928
+ React__default["default"].createElement("h4", { className: "m-1" }, "No Logs to Show"),
3929
+ React__default["default"].createElement("div", null, "Either your filters are too strict or no matching logs have been created yet.")))
3930
+ : undefined);
3729
3931
  // Create intelliTable
3730
- const dataTable = (React__default["default"].createElement(IntelliTable, { title: "Matching Logs", id: "logs", data: logs, columns: columns }));
3932
+ const dataTable = (React__default["default"].createElement(IntelliTable, { title: "Matching Logs:", id: "logs", data: logs, columns: columns }));
3731
3933
  // Main body
3732
3934
  body = (React__default["default"].createElement(React__default["default"].Fragment, null,
3733
3935
  filters,
3734
- dataTable));
3936
+ React__default["default"].createElement("div", { className: "mt-2" },
3937
+ dataTable,
3938
+ noLogsNotice)));
3735
3939
  }
3736
3940
  /* ---------- Wrap in Modal --------- */
3737
3941
  return (React__default["default"].createElement("div", { className: "LogReviewer-outer-container" },
@@ -3739,14 +3943,10 @@ const LogReviewer = (props) => {
3739
3943
  React__default["default"].createElement("div", { className: "LogReviewer-inner-container" },
3740
3944
  React__default["default"].createElement("div", { className: "LogReviewer-header" },
3741
3945
  React__default["default"].createElement("div", { className: "LogReviewer-header-title" },
3742
- React__default["default"].createElement("h1", { className: "m-0" }, "Log Review Dashboard")),
3743
- React__default["default"].createElement("button", { type: "button", className: "LogReviewer-header-close-button btn btn-lg", "aria-label": "close log reviewer panel", onClick: onClose, style: {
3744
- border: 0,
3745
- backgroundColor: 'transparent',
3746
- padding: 0,
3747
- margin: 0,
3748
- } },
3749
- React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faTimes }))),
3946
+ React__default["default"].createElement("h3", { className: "text-center m-0" }, "Log Review Dashboard")),
3947
+ React__default["default"].createElement("div", { style: { width: 0 } },
3948
+ 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 },
3949
+ React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: freeSolidSvgIcons.faTimes })))),
3750
3950
  React__default["default"].createElement("div", { className: "LogReviewer-contents" }, body))));
3751
3951
  };
3752
3952
 
@@ -4070,22 +4270,24 @@ const initServer = (opts) => {
4070
4270
  * @param {number} month the month to query (e.g. 1 = January)
4071
4271
  * @returns {Log[]} list of logs from the given month
4072
4272
  */
4073
- opts.app.post(`${LOG_REVIEW_ROUTE_PATH_PREFIX}/years/:year/months/:month`, genRouteHandler({
4273
+ opts.app.get(`${LOG_REVIEW_ROUTE_PATH_PREFIX}/years/:year/months/:month`, genRouteHandler({
4074
4274
  paramTypes: {
4075
4275
  year: ParamType$1.Int,
4076
4276
  month: ParamType$1.Int,
4077
4277
  },
4078
4278
  handler: ({ params }) => __awaiter(void 0, void 0, void 0, function* () {
4079
4279
  // Get user info
4080
- const { userId, isAdmin } = params;
4280
+ const { year, month, userId, isAdmin, } = params;
4081
4281
  // Validate user
4082
- // isAdmin is already checked because path starts with '/admin'
4083
4282
  const canReview = yield canReviewLogs(userId, isAdmin);
4084
4283
  if (!canReview) {
4085
4284
  throw new ErrorWithCode('You cannot access this resource because you do not have the appropriate permissions.', ReactKitErrorCode$1.NotAllowedToReviewLogs);
4086
4285
  }
4087
4286
  // Query for logs
4088
- const logs = yield _logCollection.find({ userId });
4287
+ const logs = yield _logCollection.find({
4288
+ year,
4289
+ month,
4290
+ });
4089
4291
  // Return logs
4090
4292
  return logs;
4091
4293
  }),
@@ -4389,18 +4591,6 @@ const parseUserAgent = (userAgent) => {
4389
4591
  };
4390
4592
  };
4391
4593
 
4392
- /**
4393
- * Allowed log levels
4394
- * @author Gabe Abrams
4395
- */
4396
- var LogLevel;
4397
- (function (LogLevel) {
4398
- LogLevel["Warn"] = "Warn";
4399
- LogLevel["Info"] = "Info";
4400
- LogLevel["Debug"] = "Debug";
4401
- })(LogLevel || (LogLevel = {}));
4402
- var LogLevel$1 = LogLevel;
4403
-
4404
4594
  /**
4405
4595
  * Generate an express API route handler
4406
4596
  * @author Gabe Abrams
@@ -4746,7 +4936,7 @@ const genRouteHandler = (opts) => {
4746
4936
  isAdmin: !!launchInfo.isAdmin,
4747
4937
  isTTM: !!launchInfo.isTTM,
4748
4938
  courseId: launchInfo.courseId,
4749
- courseName: launchInfo.courseName,
4939
+ courseName: launchInfo.contextLabel,
4750
4940
  browser,
4751
4941
  device,
4752
4942
  year,
@@ -4773,7 +4963,7 @@ const genRouteHandler = (opts) => {
4773
4963
  }
4774
4964
  : {
4775
4965
  type: LogType$1.Action,
4776
- target: ((_m = opts.target) !== null && _m !== void 0 ? _m : LogBuiltInMetadata.Target.NoSpecificTarget),
4966
+ target: ((_m = opts.target) !== null && _m !== void 0 ? _m : LogBuiltInMetadata.Target.NoTarget),
4777
4967
  action: ((_o = opts.action) !== null && _o !== void 0 ? _o : LogAction$1.Unknown),
4778
4968
  });
4779
4969
  // Source-specific info