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/esm/index.js CHANGED
@@ -511,17 +511,29 @@ const LOG_ROUTE_PATH = `${ROUTE_PATH_PREFIX}/log`;
511
511
  const LogBuiltInMetadata = {
512
512
  // Contexts
513
513
  Context: {
514
- Uncategorized: 'n/a',
515
- ServerRenderedErrorPage: '_server-rendered-error-page',
516
- ServerEndpointError: '_server-endpoint-error',
517
- ClientFatalError: '_client-fatal-error',
514
+ Uncategorized: 'Uncategorized',
515
+ ServerRenderedErrorPage: 'ServerRenderedErrorPage',
516
+ ServerEndpointError: 'ServerEndpointError',
517
+ ClientFatalError: 'ClientFatalError',
518
518
  },
519
519
  // Targets
520
520
  Target: {
521
- NoSpecificTarget: 'n/a',
521
+ NoTarget: 'NoTarget',
522
522
  },
523
523
  };
524
524
 
525
+ /**
526
+ * Allowed log levels
527
+ * @author Gabe Abrams
528
+ */
529
+ var LogLevel;
530
+ (function (LogLevel) {
531
+ LogLevel["Warn"] = "Warn";
532
+ LogLevel["Info"] = "Info";
533
+ LogLevel["Debug"] = "Debug";
534
+ })(LogLevel || (LogLevel = {}));
535
+ var LogLevel$1 = LogLevel;
536
+
525
537
  // Keep track of whether or not session expiry has already been handled
526
538
  let sessionAlreadyExpired = false;
527
539
  /*------------------------------------------------------------------------*/
@@ -640,7 +652,7 @@ const visitServerEndpoint = (opts) => __awaiter(void 0, void 0, void 0, function
640
652
  * @author Gabe Abrams
641
653
  */
642
654
  const logClientEvent = (opts) => __awaiter(void 0, void 0, void 0, function* () {
643
- var _a, _b, _c, _d, _e, _f;
655
+ var _a, _b, _c, _d, _e, _f, _g;
644
656
  return visitServerEndpoint({
645
657
  path: LOG_ROUTE_PATH,
646
658
  method: 'POST',
@@ -649,8 +661,9 @@ const logClientEvent = (opts) => __awaiter(void 0, void 0, void 0, function* ()
649
661
  ? opts.context
650
662
  : ((_b = ((_a = opts.context) !== null && _a !== void 0 ? _a : {})._) !== null && _b !== void 0 ? _b : LogBuiltInMetadata.Context.Uncategorized)),
651
663
  subcontext: ((_c = opts.subcontext) !== null && _c !== void 0 ? _c : LogBuiltInMetadata.Context.Uncategorized),
652
- tags: JSON.stringify((_d = opts.tags) !== null && _d !== void 0 ? _d : []),
653
- metadata: JSON.stringify((_e = opts.metadata) !== null && _e !== void 0 ? _e : {}),
664
+ level: ((_d = opts.level) !== null && _d !== void 0 ? _d : LogLevel$1.Info),
665
+ tags: JSON.stringify((_e = opts.tags) !== null && _e !== void 0 ? _e : []),
666
+ metadata: JSON.stringify((_f = opts.metadata) !== null && _f !== void 0 ? _f : {}),
654
667
  errorMessage: (opts.error
655
668
  ? opts.error.message
656
669
  : undefined),
@@ -661,7 +674,7 @@ const logClientEvent = (opts) => __awaiter(void 0, void 0, void 0, function* ()
661
674
  ? opts.error.stack
662
675
  : undefined),
663
676
  target: (opts.action
664
- ? ((_f = opts.target) !== null && _f !== void 0 ? _f : LogBuiltInMetadata.Target.NoSpecificTarget)
677
+ ? ((_g = opts.target) !== null && _g !== void 0 ? _g : LogBuiltInMetadata.Target.NoTarget)
665
678
  : undefined),
666
679
  action: (opts.action
667
680
  ? opts.action
@@ -1167,21 +1180,21 @@ const ButtonInputGroup = (props) => {
1167
1180
  /*------------------------------------------------------------------------*/
1168
1181
  /* -------------- Props ------------- */
1169
1182
  // Destructure all props
1170
- const { label, minLabelWidth, children, } = props;
1183
+ const { label, minLabelWidth, children, className, } = props;
1171
1184
  /*------------------------------------------------------------------------*/
1172
1185
  /* Render */
1173
1186
  /*------------------------------------------------------------------------*/
1174
1187
  /*----------------------------------------*/
1175
1188
  /* Main UI */
1176
1189
  /*----------------------------------------*/
1177
- return (React.createElement("div", { className: "input-group" },
1190
+ return (React.createElement("div", { className: `input-group ${className !== null && className !== void 0 ? className : ''}` },
1178
1191
  React.createElement("div", { className: "input-group-prepend d-flex w-100" },
1179
1192
  React.createElement("span", { className: "input-group-text", style: {
1180
1193
  minWidth: (minLabelWidth !== null && minLabelWidth !== void 0 ? minLabelWidth : undefined),
1181
1194
  borderTopRightRadius: 0,
1182
1195
  borderBottomRightRadius: 0,
1183
1196
  } }, label),
1184
- React.createElement("span", { className: "input-group-text flex-grow-1 rounded-right", style: {
1197
+ React.createElement("span", { className: "input-group-text flex-grow-1 rounded-right d-flex flex-wrap", style: {
1185
1198
  backgroundColor: 'white',
1186
1199
  borderTopLeftRadius: 0,
1187
1200
  borderBottomLeftRadius: 0,
@@ -1344,12 +1357,25 @@ const SimpleDateChooser = (props) => {
1344
1357
  // Figure out which days are allowed
1345
1358
  const days = [];
1346
1359
  const numDaysInMonth = (new Date(year, month, 0)).getDate();
1347
- const firstDay = (month === today.month
1348
- ? today.day // Current month: start at current date
1349
- : 1 // Future month: start at beginning of month
1350
- );
1351
- for (let day = firstDay; day <= numDaysInMonth; day++) {
1352
- days.push(day);
1360
+ if (chooseFromPast) {
1361
+ // Past selection
1362
+ const numDaysToAdd = ((month === today.month)
1363
+ ? today.day // Current month, only add up to today
1364
+ : numDaysInMonth // Past month, add all days
1365
+ );
1366
+ for (let day = 1; day <= numDaysToAdd; day++) {
1367
+ days.push(day);
1368
+ }
1369
+ }
1370
+ else {
1371
+ // Future selection: add all remaining days of the month
1372
+ const firstDay = (month === today.month
1373
+ ? today.day // Current month: start at current date
1374
+ : 1 // Future month: start at beginning of month
1375
+ );
1376
+ for (let day = firstDay; day <= numDaysInMonth; day++) {
1377
+ days.push(day);
1378
+ }
1353
1379
  }
1354
1380
  choices.push({
1355
1381
  choiceName: `${monthName} ${year}`,
@@ -1420,14 +1446,16 @@ const Drawer = (props) => {
1420
1446
  /*------------------------------------------------------------------------*/
1421
1447
  /* -------------- Props ------------- */
1422
1448
  // Destructure all props
1423
- const { children, } = props;
1449
+ const { customBackgroundColor, children, } = props;
1424
1450
  /*------------------------------------------------------------------------*/
1425
1451
  /* Render */
1426
1452
  /*------------------------------------------------------------------------*/
1427
1453
  /*----------------------------------------*/
1428
1454
  /* Main UI */
1429
1455
  /*----------------------------------------*/
1430
- return (React.createElement("div", { className: "Drawer-container" },
1456
+ return (React.createElement("div", { className: "Drawer-container", style: {
1457
+ backgroundColor: (customBackgroundColor !== null && customBackgroundColor !== void 0 ? customBackgroundColor : undefined),
1458
+ } },
1431
1459
  React.createElement("style", null, style$4),
1432
1460
  children));
1433
1461
  };
@@ -1890,24 +1918,25 @@ const CopiableBox = (props) => {
1890
1918
  /**
1891
1919
  * Reusable nested item picker
1892
1920
  * @author Yuen Ler Chow
1921
+ * @author Gabe Abrams
1893
1922
  */
1894
1923
  /* ------------- Actions ------------ */
1895
1924
  // Types of actions
1896
1925
  var ActionType$2;
1897
1926
  (function (ActionType) {
1898
- // Toggle whether the children are being shown
1899
- ActionType["ToggleItems"] = "toggle-items";
1927
+ // Toggle whether a child are being shown
1928
+ ActionType["ToggleChild"] = "toggle-child";
1900
1929
  })(ActionType$2 || (ActionType$2 = {}));
1901
1930
  /**
1902
1931
  * Reducer that executes actions
1903
- * @author Yuen Ler Chow
1932
+ * @author Gabe Abrams
1904
1933
  * @param state current state
1905
1934
  * @param action action to execute
1906
1935
  */
1907
1936
  const reducer$2 = (state, action) => {
1908
1937
  switch (action.type) {
1909
- case ActionType$2.ToggleItems: {
1910
- return { isShowingItems: !state.isShowingItems };
1938
+ case ActionType$2.ToggleChild: {
1939
+ return Object.assign(Object.assign({}, state), { childExpanded: Object.assign(Object.assign({}, state.childExpanded), { [String(action.id)]: !state.childExpanded[String(action.id)] }) });
1911
1940
  }
1912
1941
  default: {
1913
1942
  return state;
@@ -1925,14 +1954,19 @@ const NestableItemList = (props) => {
1925
1954
  // Destructure all props
1926
1955
  const { items, onChanged, } = props;
1927
1956
  /* -------------- State ------------- */
1957
+ // Create initial map of child expanded booleans
1958
+ const initChildExpanded = {};
1959
+ items.forEach((item) => {
1960
+ initChildExpanded[String(item.id)] = false;
1961
+ });
1928
1962
  // Initial state
1929
1963
  const initialState = {
1930
- isShowingItems: false,
1964
+ childExpanded: initChildExpanded,
1931
1965
  };
1932
1966
  // Initialize state
1933
1967
  const [state, dispatch] = useReducer(reducer$2, initialState);
1934
1968
  // Destructure common state
1935
- const { isShowingItems, } = state;
1969
+ const { childExpanded, } = state;
1936
1970
  /*------------------------------------------------------------------------*/
1937
1971
  /* Component Functions */
1938
1972
  /*------------------------------------------------------------------------*/
@@ -2020,14 +2054,15 @@ const NestableItemList = (props) => {
2020
2054
  backgroundColor: 'transparent',
2021
2055
  }, type: "button", onClick: () => {
2022
2056
  dispatch({
2023
- type: ActionType$2.ToggleItems,
2057
+ type: ActionType$2.ToggleChild,
2058
+ id: item.id,
2024
2059
  });
2025
- }, "aria-label": `${isShowingItems ? 'Hide' : 'Show'} items in ${item.name}` },
2026
- React.createElement(FontAwesomeIcon, { icon: isShowingItems ? faChevronDown : faChevronRight })))),
2060
+ }, "aria-label": `${childExpanded[item.id] ? 'Hide' : 'Show'} items in ${item.name}` },
2061
+ React.createElement(FontAwesomeIcon, { icon: childExpanded[item.id] ? faChevronDown : faChevronRight })))),
2027
2062
  React.createElement(CheckboxButton, { className: `NestableItemList-CheckboxButton-${item.id}`, text: item.name, checked: item.isGroup ? allChecked(item.children) : item.checked, dashed: item.isGroup ? !noneChecked(item.children) : false, onChanged: (checked) => {
2028
2063
  onChanged(changeChecked(item.id, checked, items));
2029
2064
  }, ariaLabel: `Select ${item.name}`, checkedVariant: Variant$1.Light }),
2030
- item.isGroup && isShowingItems && (React.createElement("div", { className: "NestableItemList-children-container", style: {
2065
+ (item.isGroup && childExpanded[item.id]) && (React.createElement("div", { className: "NestableItemList-children-container", style: {
2031
2066
  paddingLeft: '2.2rem',
2032
2067
  } },
2033
2068
  React.createElement(NestableItemList, { items: item.children, onChanged: (updatedItems) => {
@@ -2049,7 +2084,7 @@ const ItemPicker = (props) => {
2049
2084
  /*------------------------------------------------------------------------*/
2050
2085
  /* -------------- Props ------------- */
2051
2086
  // Destructure all props
2052
- const { title, items, onChanged, } = props;
2087
+ const { title, items, onChanged, noBottomMargin, } = props;
2053
2088
  /*------------------------------------------------------------------------*/
2054
2089
  /* Component Functions */
2055
2090
  /*------------------------------------------------------------------------*/
@@ -2059,7 +2094,7 @@ const ItemPicker = (props) => {
2059
2094
  /*----------------------------------------*/
2060
2095
  /* Main UI */
2061
2096
  /*----------------------------------------*/
2062
- return (React.createElement(TabBox, { title: title },
2097
+ return (React.createElement(TabBox, { title: title, noBottomMargin: noBottomMargin },
2063
2098
  React.createElement("div", { style: { overflowX: 'auto' } },
2064
2099
  React.createElement(NestableItemList, { items: items, onChanged: onChanged }))));
2065
2100
  };
@@ -2103,37 +2138,37 @@ var LogType$1 = LogType;
2103
2138
  var LogAction;
2104
2139
  (function (LogAction) {
2105
2140
  // Target was opened by the user (it was not on screen, but now it is)
2106
- LogAction["Open"] = "open";
2141
+ LogAction["Open"] = "Open";
2107
2142
  // Target was closed by the user (it was on screen, but now it is not)
2108
- LogAction["Close"] = "close";
2143
+ LogAction["Close"] = "Close";
2109
2144
  // Target was cancelled by the user (it was on closed without saving)
2110
- LogAction["Cancel"] = "cancel";
2145
+ LogAction["Cancel"] = "Cancel";
2111
2146
  // Target was expanded by the user (it always remains on screen, but size was changed)
2112
- LogAction["Expand"] = "expand";
2147
+ LogAction["Expand"] = "Expand";
2113
2148
  // Target was collapsed by the user (it always remains on screen, but size was changed)
2114
- LogAction["Collapse"] = "collapse";
2149
+ LogAction["Collapse"] = "Collapse";
2115
2150
  // Target was viewed by the user (only for items that are not opened or closed, those must use Open/Close actions)
2116
- LogAction["View"] = "view";
2151
+ LogAction["View"] = "View";
2117
2152
  // Target interrupted the user (popup, dialog, validation message, etc. appeared without user prompting)
2118
- LogAction["Interrupt"] = "interrupt";
2153
+ LogAction["Interrupt"] = "Interrupt";
2119
2154
  // Target was created by the user (it did not exist before)
2120
- LogAction["Create"] = "create";
2155
+ LogAction["Create"] = "Create";
2121
2156
  // Target was edited by the user (it existed and was changed)
2122
- LogAction["Edit"] = "edit";
2157
+ LogAction["Edit"] = "Edit";
2123
2158
  // Target was deleted by the user (it existed and now it doesn't)
2124
- LogAction["Delete"] = "delete";
2159
+ LogAction["Delete"] = "Delete";
2125
2160
  // Target was added by the user (it already existed and was added to another place)
2126
- LogAction["Add"] = "add";
2161
+ LogAction["Add"] = "Add";
2127
2162
  // Target was removed by the user (it was removed from something but still exists)
2128
- LogAction["Remove"] = "remove";
2163
+ LogAction["Remove"] = "Remove";
2129
2164
  // Target was activated by the user (click, check, tap, keypress, etc.)
2130
- LogAction["Activate"] = "activate";
2165
+ LogAction["Activate"] = "Activate";
2131
2166
  // Target was deactivated by the user (click away, uncheck, tap outside of, tab away, etc.)
2132
- LogAction["Deactivate"] = "deactivate";
2167
+ LogAction["Deactivate"] = "Deactivate";
2133
2168
  // User showed interest in a target (hover, peek, etc.)
2134
- LogAction["Peek"] = "peek";
2169
+ LogAction["Peek"] = "Peek";
2135
2170
  // Unknown action
2136
- LogAction["Unknown"] = "unknown";
2171
+ LogAction["Unknown"] = "Unknown";
2137
2172
  })(LogAction || (LogAction = {}));
2138
2173
  var LogAction$1 = LogAction;
2139
2174
 
@@ -2175,12 +2210,12 @@ const roundToNumDecimals = (num, numDecimals) => {
2175
2210
  * @returns escaped cell text
2176
2211
  */
2177
2212
  const escapeCellText = (text) => {
2178
- if (!text.includes(',')) {
2213
+ if (!String(text).includes(',')) {
2179
2214
  // No need to escape
2180
- return text;
2215
+ return String(text);
2181
2216
  }
2182
2217
  // Perform escape
2183
- return `"${text.replace(/"/g, '""')}`;
2218
+ return `"${String(text).replace(/"/g, '""')}`;
2184
2219
  };
2185
2220
  /**
2186
2221
  * Generate a CSV file
@@ -2201,7 +2236,23 @@ const genCSV = (data, columns) => {
2201
2236
  data.forEach((datum) => {
2202
2237
  csv += (columns
2203
2238
  .map((column) => {
2204
- return escapeCellText(datum[column.param]);
2239
+ let contents;
2240
+ const cell = datum[column.param];
2241
+ if (typeof cell === 'string'
2242
+ || typeof cell === 'number') {
2243
+ contents = String(cell);
2244
+ }
2245
+ else if (typeof cell === 'undefined'
2246
+ || cell === null) {
2247
+ contents = '';
2248
+ }
2249
+ else if (typeof cell === 'object') {
2250
+ contents = JSON.stringify(cell);
2251
+ }
2252
+ else {
2253
+ contents = '';
2254
+ }
2255
+ return escapeCellText(contents);
2205
2256
  })
2206
2257
  .join(','));
2207
2258
  });
@@ -2234,7 +2285,8 @@ const CSVDownloadButton = (props) => {
2234
2285
  ? `Click to download ${filename}`
2235
2286
  : ariaLabel), style: style, onClick: onClick },
2236
2287
  !children && (React.createElement(React.Fragment, null,
2237
- React.createElement(FontAwesomeIcon, { icon: faCloudDownloadAlt, className: "mr-2" }),
2288
+ React.createElement(FontAwesomeIcon, { icon: faCloudDownloadAlt }),
2289
+ ' ',
2238
2290
  "Download CSV")),
2239
2291
  children));
2240
2292
  };
@@ -2258,7 +2310,7 @@ var ActionType$1;
2258
2310
  // Toggle sort column param
2259
2311
  ActionType["ToggleSortColumn"] = "toggle-sort-column";
2260
2312
  // Toggle the visibility of a column
2261
- ActionType["ToggleColumnVisibility"] = "toggle-column-visibility";
2313
+ ActionType["UpdateColumnVisibility"] = "update-column-visibility";
2262
2314
  // Toggle the column visibility customization modal
2263
2315
  ActionType["ToggleColVisCusModalVisibility"] = "toggle-col-vis-cus-modal-visibility";
2264
2316
  })(ActionType$1 || (ActionType$1 = {}));
@@ -2282,9 +2334,9 @@ const reducer$1 = (state, action) => {
2282
2334
  // Stop sorting by column
2283
2335
  return Object.assign(Object.assign({}, state), { sortColumnParam: undefined, sortType: SortType.Ascending });
2284
2336
  }
2285
- case ActionType$1.ToggleColumnVisibility: {
2337
+ case ActionType$1.UpdateColumnVisibility: {
2286
2338
  const { columnVisibilityMap } = state;
2287
- columnVisibilityMap[action.param] = !columnVisibilityMap[action.param];
2339
+ columnVisibilityMap[action.param] = action.visible;
2288
2340
  return Object.assign(Object.assign({}, state), { columnVisibilityMap });
2289
2341
  }
2290
2342
  case ActionType$1.ToggleColVisCusModalVisibility: {
@@ -2305,7 +2357,11 @@ const IntelliTable = (props) => {
2305
2357
  var _a;
2306
2358
  /* -------------- Props ------------- */
2307
2359
  // Destructure all props
2308
- const { title, id, data, columns, } = props;
2360
+ const { title, id, columns, } = props;
2361
+ // Get data, show empty row if none
2362
+ const data = ((props.data && props.data.length > 0)
2363
+ ? props.data
2364
+ : [{ id: 'empty-row' }]);
2309
2365
  /* -------------- State ------------- */
2310
2366
  // Initial state
2311
2367
  const initColumnVisibilityMap = {};
@@ -2322,20 +2378,29 @@ const IntelliTable = (props) => {
2322
2378
  const [state, dispatch] = useReducer(reducer$1, initialState);
2323
2379
  // Destructure common state
2324
2380
  const { sortColumnParam, sortType, columnVisibilityMap, columnVisibilityCustomizationModalVisible, } = state;
2381
+ /*------------------------------------------------------------------------*/
2382
+ /* Render */
2383
+ /*------------------------------------------------------------------------*/
2384
+ /*----------------------------------------*/
2385
+ /* Modal */
2386
+ /*----------------------------------------*/
2387
+ // Modal that may be defined
2388
+ let modal;
2325
2389
  /* ------- Col Vis Customization Modal ------ */
2326
2390
  if (columnVisibilityCustomizationModalVisible) {
2327
2391
  // Create modal
2328
- (React.createElement(Modal, { type: ModalType$1.Okay, title: "Choose columns to show:", onClose: () => {
2392
+ modal = (React.createElement(Modal, { type: ModalType$1.Okay, title: "Choose columns to show:", onClose: () => {
2329
2393
  dispatch({
2330
2394
  type: ActionType$1.ToggleColVisCusModalVisibility,
2331
2395
  });
2332
- } }, columns.map((column) => {
2333
- return (React.createElement(CheckboxButton, { key: column.param, id: `IntelliTable-${id}-toggle-visibility-${column.param}`, text: column.title, onChanged: () => {
2396
+ }, okayVariant: Variant$1.Light }, columns.map((column) => {
2397
+ return (React.createElement(CheckboxButton, { key: column.param, id: `IntelliTable-${id}-toggle-visibility-${column.param}`, className: "mb-2", text: column.title, onChanged: (checked) => {
2334
2398
  dispatch({
2335
- type: ActionType$1.ToggleColumnVisibility,
2399
+ type: ActionType$1.UpdateColumnVisibility,
2336
2400
  param: column.param,
2401
+ visible: checked,
2337
2402
  });
2338
- }, checked: columnVisibilityMap[column.param], ariaLabel: `show "${column.title}" column` }));
2403
+ }, checked: columnVisibilityMap[column.param], ariaLabel: `show "${column.title}" column`, checkedVariant: Variant$1.Light, uncheckedVariant: Variant$1.Secondary }));
2339
2404
  })));
2340
2405
  }
2341
2406
  /*----------------------------------------*/
@@ -2350,6 +2415,7 @@ const IntelliTable = (props) => {
2350
2415
  // Custom info based on current sort type
2351
2416
  let sortButtonAriaLabel;
2352
2417
  let sortIcon = faSort;
2418
+ let sortingByThisColumn = false;
2353
2419
  if (!sortColumnParam) {
2354
2420
  // Not being sorted yet
2355
2421
  sortButtonAriaLabel = `sort ascending by ${column.title}`;
@@ -2357,6 +2423,7 @@ const IntelliTable = (props) => {
2357
2423
  }
2358
2424
  else if (column.param === sortColumnParam) {
2359
2425
  // Already sorted by this column
2426
+ sortingByThisColumn = true;
2360
2427
  if (sortType === SortType.Ascending) {
2361
2428
  // Sorted ascending
2362
2429
  sortButtonAriaLabel = `sort descending by ${column.title}`;
@@ -2374,11 +2441,14 @@ const IntelliTable = (props) => {
2374
2441
  sortIcon = faSort;
2375
2442
  }
2376
2443
  // Create the cell UI
2377
- return (React.createElement("th", { key: column.param, scope: "col", id: `IntelliTable-${id}-header-${column.param}` },
2378
- React.createElement("div", { className: "d-flex align-items-center justify-content-center flex-row h-100" },
2379
- React.createElement("h4", { className: "m-0" }, column.title),
2444
+ return (React.createElement("th", { key: column.param, scope: "col", id: `IntelliTable-${id}-header-${column.param}`, className: "text-start", style: {
2445
+ borderRight: '0.05rem solid #555',
2446
+ borderLeft: '0.05rem solid #555',
2447
+ } },
2448
+ React.createElement("div", { className: "d-flex align-items-center justify-content-start flex-row h-100" },
2449
+ React.createElement("span", { className: "text-nowrap" }, column.title),
2380
2450
  React.createElement("div", null,
2381
- React.createElement("button", { type: "button", className: "btn btn-light", "aria-label": sortButtonAriaLabel, onClick: () => {
2451
+ React.createElement("button", { type: "button", id: `IntelliTable-${id}-sort-by-${column.param}-button`, className: `btn btn-${sortingByThisColumn ? 'light' : 'secondary'} btn-sm ms-1 ps-1 pe-1 pt-0 pb-0`, "aria-label": sortButtonAriaLabel, onClick: () => {
2382
2452
  dispatch({
2383
2453
  type: ActionType$1.ToggleSortColumn,
2384
2454
  param: column.param,
@@ -2396,8 +2466,28 @@ const IntelliTable = (props) => {
2396
2466
  const descending = (sortType === SortType.Descending);
2397
2467
  if (sortColumnParam) {
2398
2468
  sortedData.sort((a, b) => {
2469
+ var _a, _b;
2399
2470
  const aVal = a[sortColumnParam];
2400
2471
  const bVal = b[sortColumnParam];
2472
+ // Tiebreaker sort by timestamp, most recent first (used if tied)
2473
+ const tiebreaker = (((_a = b.timestamp) !== null && _a !== void 0 ? _a : 0)
2474
+ - ((_b = a.timestamp) !== null && _b !== void 0 ? _b : 0));
2475
+ // Auto-sort undefined and null to end of list
2476
+ if ((aVal === undefined || aVal === null)
2477
+ || (bVal === undefined || bVal === null)) {
2478
+ // At least one was undefined
2479
+ if ((aVal === undefined || aVal === null)
2480
+ && (bVal === undefined || bVal === null)) {
2481
+ // Both undefined
2482
+ return tiebreaker;
2483
+ }
2484
+ if (aVal === undefined || aVal === null) {
2485
+ // First was undefined
2486
+ return 1;
2487
+ }
2488
+ // Second was undefined
2489
+ return -1;
2490
+ }
2401
2491
  // Sort differently based on the data type
2402
2492
  // > Boolean
2403
2493
  if (paramType === ParamType$1.Boolean) {
@@ -2407,7 +2497,7 @@ const IntelliTable = (props) => {
2407
2497
  if (!aVal && bVal) {
2408
2498
  return (descending ? 1 : -1);
2409
2499
  }
2410
- return 0;
2500
+ return tiebreaker;
2411
2501
  }
2412
2502
  // > Number
2413
2503
  if (paramType === ParamType$1.Int
@@ -2419,12 +2509,12 @@ const IntelliTable = (props) => {
2419
2509
  // > String
2420
2510
  if (paramType === ParamType$1.String) {
2421
2511
  if (aVal < bVal) {
2422
- return (descending ? -1 : 1);
2512
+ return (descending ? 1 : -1);
2423
2513
  }
2424
2514
  if (aVal > bVal) {
2425
- return (descending ? 1 : -1);
2515
+ return (descending ? -1 : 1);
2426
2516
  }
2427
- return 0;
2517
+ return tiebreaker;
2428
2518
  }
2429
2519
  // > JSON
2430
2520
  if (paramType === ParamType$1.JSON) {
@@ -2439,7 +2529,7 @@ const IntelliTable = (props) => {
2439
2529
  : (aSize - bSize));
2440
2530
  }
2441
2531
  // No sort
2442
- return 0;
2532
+ return tiebreaker;
2443
2533
  });
2444
2534
  }
2445
2535
  // Table body
@@ -2486,7 +2576,9 @@ const IntelliTable = (props) => {
2486
2576
  }
2487
2577
  else if (column.type === ParamType$1.String) {
2488
2578
  fullValue = String(value).trim();
2489
- const noValue = (value.trim().length) === 0;
2579
+ const noValue = (value === undefined
2580
+ || value === null
2581
+ || String(fullValue).trim().length === 0);
2490
2582
  visibleValue = (noValue
2491
2583
  ? (React.createElement(FontAwesomeIcon, { icon: faMinus }))
2492
2584
  : fullValue);
@@ -2502,7 +2594,10 @@ const IntelliTable = (props) => {
2502
2594
  : fullValue);
2503
2595
  }
2504
2596
  // Create UI
2505
- return (React.createElement("td", { key: `${datum.id}-${column.param}`, title: title }, visibleValue));
2597
+ return (React.createElement("td", { key: `${datum.id}-${column.param}`, title: title, style: {
2598
+ borderRight: '0.05rem solid #555',
2599
+ borderLeft: '0.05rem solid #555',
2600
+ } }, visibleValue));
2506
2601
  }));
2507
2602
  // Add cells to a row
2508
2603
  return (React.createElement("tr", { key: datum.id }, cells));
@@ -2522,11 +2617,12 @@ const IntelliTable = (props) => {
2522
2617
  const csv = genCSV(data, columns);
2523
2618
  // Build main UI
2524
2619
  return (React.createElement("div", { className: `IntelliTable-container-${id}` },
2525
- React.createElement("div", { className: "d-flex align-items-center justify-content-center" },
2620
+ modal,
2621
+ React.createElement("div", { className: "d-flex align-items-center justify-content-start" },
2526
2622
  React.createElement("h3", { className: "m-0" }, title),
2527
2623
  React.createElement("div", { className: "flex-grow-1 text-end" },
2528
2624
  React.createElement(CSVDownloadButton, { "aria-label": `download data as csv for ${title}`, id: `IntelliTable-${id}-download-as-csv`, filename: `${title}.csv`, csv: csv }),
2529
- React.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: () => {
2625
+ React.createElement("button", { type: "button", className: "btn btn-secondary ms-2", "aria-label": `show panel for customizing which columns show in table ${title}`, id: `IntelliTable-${id}-show-column-customization-modal`, onClick: () => {
2530
2626
  dispatch({
2531
2627
  type: ActionType$1.ToggleColVisCusModalVisibility,
2532
2628
  });
@@ -2538,7 +2634,7 @@ const IntelliTable = (props) => {
2538
2634
  numHiddenCols,
2539
2635
  ' ',
2540
2636
  "hidden)"))))),
2541
- React.createElement("div", { className: `IntelliTable-table-${id}`, style: {
2637
+ React.createElement("div", { className: `IntelliTable-table-${id} mt-2`, style: {
2542
2638
  overflowX: 'auto',
2543
2639
  } }, table)));
2544
2640
  };
@@ -2590,6 +2686,11 @@ const style = `
2590
2686
  border: 0.05rem solid black;
2591
2687
  border-radius: 0.5rem;
2592
2688
  overflow: hidden;
2689
+ padding: 0.7rem;
2690
+
2691
+ /* Solid background */
2692
+ background-color: white;
2693
+ color: black;
2593
2694
 
2594
2695
  /* Place contents in flex column */
2595
2696
  flex-direction: column;
@@ -2616,6 +2717,27 @@ const style = `
2616
2717
  /* Vertical scroll */
2617
2718
  overflow-y: auto;
2618
2719
  }
2720
+
2721
+ .LogReviewer-header-close-button {
2722
+ border: 0 !important;
2723
+ background-color: transparent !important;
2724
+ padding-top: 0 !important;
2725
+ padding-bottom: 0 !important;
2726
+ padding-right: 1em !important;
2727
+ margin: 0 !important;
2728
+ color: #444 !important;
2729
+
2730
+ right: 0 !important;
2731
+ position: absolute !important;
2732
+ }
2733
+ .LogReviewer-header-close-button:hover {
2734
+ border: 0 !important;
2735
+ background-color: transparent !important;
2736
+ padding-top: 0 !important;
2737
+ padding-bottom: 0 !important;
2738
+ margin: 0 !important;
2739
+ color: #000 !important;
2740
+ }
2619
2741
  `;
2620
2742
  /*------------------------------------------------------------------------*/
2621
2743
  /* Static Functions */
@@ -2635,7 +2757,7 @@ const genHumanReadableName = (machineReadableName) => {
2635
2757
  // Uppercase! Add a space before
2636
2758
  humanReadableName += ' ';
2637
2759
  }
2638
- humanReadableName += chars;
2760
+ humanReadableName += char;
2639
2761
  });
2640
2762
  // Trim and return
2641
2763
  return humanReadableName.trim();
@@ -2697,7 +2819,19 @@ const reducer = (state, action) => {
2697
2819
  return Object.assign(Object.assign({}, state), { contextFilterState: action.contextFilterState });
2698
2820
  }
2699
2821
  case ActionType.UpdateTagFilterState: {
2700
- return Object.assign(Object.assign({}, state), { tagFilterState: action.tagFilterState });
2822
+ const { tagFilterState } = action;
2823
+ // Select all if every tag is deselected
2824
+ const numTagsSelected = (Object.values(tagFilterState)
2825
+ .filter((isSelected) => {
2826
+ return isSelected;
2827
+ })
2828
+ .length);
2829
+ if (numTagsSelected === 0) {
2830
+ Object.keys(tagFilterState).forEach((tag) => {
2831
+ tagFilterState[tag] = true;
2832
+ });
2833
+ }
2834
+ return Object.assign(Object.assign({}, state), { tagFilterState });
2701
2835
  }
2702
2836
  case ActionType.UpdateActionErrorFilterState: {
2703
2837
  return Object.assign(Object.assign({}, state), { actionErrorFilterState: action.actionErrorFilterState });
@@ -2717,10 +2851,35 @@ const LogReviewer = (props) => {
2717
2851
  /*------------------------------------------------------------------------*/
2718
2852
  /* Setup */
2719
2853
  /*------------------------------------------------------------------------*/
2720
- var _a, _b, _c, _d, _e;
2854
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
2721
2855
  /* -------------- Props ------------- */
2722
2856
  // Destructure props
2723
2857
  const { LogMetadata, onClose, } = props;
2858
+ // Add built-in LogMetadata
2859
+ // > Add "uncategorized" subcontext to each context
2860
+ Object.keys((_a = LogMetadata.Context) !== null && _a !== void 0 ? _a : {}).forEach((context) => {
2861
+ if (
2862
+ // Context exists
2863
+ LogMetadata.Context
2864
+ // Context has children already
2865
+ && typeof LogMetadata.Context[context] !== 'string') {
2866
+ LogMetadata.Context[context][LogBuiltInMetadata.Context.Uncategorized] = (LogBuiltInMetadata.Context.Uncategorized);
2867
+ }
2868
+ });
2869
+ // > Add built-in contexts
2870
+ LogMetadata.Context = ((_b = LogMetadata.Context) !== null && _b !== void 0 ? _b : {});
2871
+ Object.keys(LogBuiltInMetadata.Context).forEach((context) => {
2872
+ if (LogMetadata.Context) {
2873
+ LogMetadata.Context[context] = context;
2874
+ }
2875
+ });
2876
+ // > Add built-in targets
2877
+ LogMetadata.Target = ((_c = LogMetadata.Target) !== null && _c !== void 0 ? _c : {});
2878
+ Object.keys(LogBuiltInMetadata.Target).forEach((target) => {
2879
+ if (LogMetadata.Target) {
2880
+ LogMetadata.Target[target] = target;
2881
+ }
2882
+ });
2724
2883
  /* -------------- State ------------- */
2725
2884
  // Create initial date filter state
2726
2885
  const today = getTimeInfoInET();
@@ -2740,26 +2899,30 @@ const LogReviewer = (props) => {
2740
2899
  };
2741
2900
  // Create initial context filter state
2742
2901
  const initContextFilterState = {};
2743
- Object.keys((_a = LogMetadata.Context) !== null && _a !== void 0 ? _a : {}).forEach((context) => {
2902
+ Object.keys((_d = LogMetadata.Context) !== null && _d !== void 0 ? _d : {}).forEach((context) => {
2744
2903
  var _a, _b;
2745
2904
  const contextValue = ((_a = LogMetadata.Context) !== null && _a !== void 0 ? _a : {})[context];
2746
2905
  if (typeof contextValue === 'string') {
2747
- // Case: no subcontexts
2906
+ // Case: no subcontexts, init as checked
2748
2907
  initContextFilterState[contextValue] = true;
2749
2908
  }
2750
2909
  else {
2751
2910
  // Case: subcontexts exist
2752
- initContextFilterState[contextValue._] = {};
2911
+ initContextFilterState[context] = {};
2753
2912
  Object.values(((_b = LogMetadata.Context) !== null && _b !== void 0 ? _b : {})[context]).forEach((subcontext) => {
2754
- const subcontextValue = contextValue[subcontext];
2755
- initContextFilterState[contextValue._][subcontextValue] = true;
2913
+ // Skip self ("_")
2914
+ if (subcontext === '_') {
2915
+ return;
2916
+ }
2917
+ // Initialize as checked
2918
+ initContextFilterState[context][subcontext] = true;
2756
2919
  });
2757
2920
  }
2758
2921
  });
2759
2922
  // Create initial tag filter state
2760
2923
  const initTagFilterState = {};
2761
- Object.values((_b = LogMetadata.Tag) !== null && _b !== void 0 ? _b : {}).forEach((tagValue) => {
2762
- initTagFilterState[tagValue] = true;
2924
+ Object.keys((_e = LogMetadata.Tag) !== null && _e !== void 0 ? _e : {}).forEach((tag) => {
2925
+ initTagFilterState[tag] = false;
2763
2926
  });
2764
2927
  // Create advanced filter state
2765
2928
  const initAdvancedFilterState = {
@@ -2785,12 +2948,16 @@ const LogReviewer = (props) => {
2785
2948
  target: {},
2786
2949
  action: {},
2787
2950
  };
2788
- Object.values((_c = LogMetadata.Target) !== null && _c !== void 0 ? _c : {}).forEach((target) => {
2951
+ Object.values((_f = LogMetadata.Target) !== null && _f !== void 0 ? _f : {}).forEach((target) => {
2789
2952
  initActionErrorFilterState.target[target] = true;
2790
2953
  });
2791
2954
  Object.values(LogAction$1).forEach((action) => {
2792
2955
  initActionErrorFilterState.action[action] = true;
2793
2956
  });
2957
+ // Add built-in targets
2958
+ Object.values(LogBuiltInMetadata.Target).forEach((target) => {
2959
+ initActionErrorFilterState.target[target] = true;
2960
+ });
2794
2961
  // Initial state
2795
2962
  const initialState = {
2796
2963
  loading: true,
@@ -2824,15 +2991,18 @@ const LogReviewer = (props) => {
2824
2991
  let month = newDateFilterState.startDate.month;
2825
2992
  while (
2826
2993
  // Earlier year
2827
- (year <= newDateFilterState.endDate.year)
2994
+ (year < newDateFilterState.endDate.year)
2828
2995
  // Current year but included month
2829
2996
  || (year === newDateFilterState.endDate.year
2830
2997
  && month <= newDateFilterState.endDate.month)) {
2831
- // Add to list
2832
- toLoad.push({
2833
- year,
2834
- month,
2835
- });
2998
+ // Add to list if not already loaded
2999
+ if (!logMap[year]
3000
+ || !logMap[year][month]) {
3001
+ toLoad.push({
3002
+ year,
3003
+ month,
3004
+ });
3005
+ }
2836
3006
  // Increment
2837
3007
  month += 1;
2838
3008
  if (month > 12) {
@@ -2920,10 +3090,10 @@ const LogReviewer = (props) => {
2920
3090
  /* Filters */
2921
3091
  /*----------------------------------------*/
2922
3092
  // Filter toggle
2923
- const filterToggles = (React.createElement("div", { className: "LogReviewer-filter-toggles d-flex align-items-center justify-content-center" },
3093
+ const filterToggles = (React.createElement("div", { className: "LogReviewer-filter-toggles" },
2924
3094
  React.createElement("h3", { className: "m-0" }, "Filters:"),
2925
- React.createElement("div", { className: "LogReviewer-filter-toggle-buttons" },
2926
- React.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: () => {
3095
+ React.createElement("div", { className: "LogReviewer-filter-toggle-buttons alert alert-secondary p-2 m-0" },
3096
+ React.createElement("button", { type: "button", id: "LogReviewer-toggle-date-filter-drawer", className: `btn btn-${FilterDrawer.Date === expandedFilterDrawer ? 'warning' : 'light'} me-2`, "aria-label": "toggle date filter drawer", onClick: () => {
2927
3097
  dispatch({
2928
3098
  type: ActionType.ToggleFilterDrawer,
2929
3099
  filterDrawer: FilterDrawer.Date,
@@ -2931,7 +3101,7 @@ const LogReviewer = (props) => {
2931
3101
  } },
2932
3102
  React.createElement(FontAwesomeIcon, { icon: faCalendar, className: "me-2" }),
2933
3103
  "Date"),
2934
- React.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: () => {
3104
+ React.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: () => {
2935
3105
  dispatch({
2936
3106
  type: ActionType.ToggleFilterDrawer,
2937
3107
  filterDrawer: FilterDrawer.Context,
@@ -2939,15 +3109,15 @@ const LogReviewer = (props) => {
2939
3109
  } },
2940
3110
  React.createElement(FontAwesomeIcon, { icon: faCircle, className: "me-2" }),
2941
3111
  "Context"),
2942
- React.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: () => {
3112
+ (LogMetadata.Tag && Object.keys(LogMetadata.Tag).length > 0) && (React.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: () => {
2943
3113
  dispatch({
2944
3114
  type: ActionType.ToggleFilterDrawer,
2945
3115
  filterDrawer: FilterDrawer.Tag,
2946
3116
  });
2947
3117
  } },
2948
3118
  React.createElement(FontAwesomeIcon, { icon: faTag, className: "me-2" }),
2949
- "Tag"),
2950
- React.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: () => {
3119
+ "Tag")),
3120
+ React.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: () => {
2951
3121
  dispatch({
2952
3122
  type: ActionType.ToggleFilterDrawer,
2953
3123
  filterDrawer: FilterDrawer.Action,
@@ -2955,7 +3125,7 @@ const LogReviewer = (props) => {
2955
3125
  } },
2956
3126
  React.createElement(FontAwesomeIcon, { icon: faHammer, className: "me-2" }),
2957
3127
  "Action"),
2958
- React.createElement("button", { type: "button", id: "LogReviewer-toggle-advanced-filter-drawer", className: `btn btn-${FilterDrawer.Advanced === expandedFilterDrawer}`, "aria-label": "toggle advanced filter drawer", onClick: () => {
3128
+ React.createElement("button", { type: "button", id: "LogReviewer-toggle-advanced-filter-drawer", className: `btn btn-${FilterDrawer.Advanced === expandedFilterDrawer ? 'warning' : 'light'}`, "aria-label": "toggle advanced filter drawer", onClick: () => {
2959
3129
  dispatch({
2960
3130
  type: ActionType.ToggleFilterDrawer,
2961
3131
  filterDrawer: FilterDrawer.Advanced,
@@ -2968,25 +3138,29 @@ const LogReviewer = (props) => {
2968
3138
  if (expandedFilterDrawer) {
2969
3139
  if (expandedFilterDrawer === FilterDrawer.Date) {
2970
3140
  filterDrawer = (React.createElement(TabBox, { title: "Date" },
2971
- React.createElement(SimpleDateChooser, { ariaLabel: "filter start date", name: "filter-start-date", year: dateFilterState.startDate.year, month: dateFilterState.startDate.month, day: dateFilterState.startDate.day, onChange: (month, day, year) => {
2972
- dispatch({
2973
- type: ActionType.UpdateDateFilterState,
2974
- dateFilterState: Object.assign(Object.assign({}, dateFilterState), { startDate: { month, day, year } }),
2975
- });
3141
+ React.createElement(SimpleDateChooser, { ariaLabel: "filter start date", name: "filter-start-date", year: dateFilterState.startDate.year, month: dateFilterState.startDate.month, day: dateFilterState.startDate.day, chooseFromPast: true, numMonthsToShow: 12, onChange: (month, day, year) => {
3142
+ dateFilterState.startDate = { month, day, year };
3143
+ handleDateRangeUpdated(dateFilterState);
2976
3144
  } }),
2977
3145
  ' ',
2978
3146
  "to",
2979
3147
  ' ',
2980
- React.createElement(SimpleDateChooser, { ariaLabel: "filter end date", name: "filter-end-date", year: dateFilterState.endDate.year, month: dateFilterState.endDate.month, day: dateFilterState.endDate.day, onChange: (month, day, year) => {
2981
- dispatch({
2982
- type: ActionType.UpdateDateFilterState,
2983
- dateFilterState: Object.assign(Object.assign({}, dateFilterState), { endDate: { month, day, year } }),
2984
- });
3148
+ React.createElement(SimpleDateChooser, { ariaLabel: "filter end date", name: "filter-end-date", year: dateFilterState.endDate.year, month: dateFilterState.endDate.month, day: dateFilterState.endDate.day, chooseFromPast: true, numMonthsToShow: 12, onChange: (month, day, year) => {
3149
+ if (year < dateFilterState.startDate.year
3150
+ || (year === dateFilterState.startDate.year
3151
+ && month < dateFilterState.startDate.month)
3152
+ || (year === dateFilterState.startDate.year
3153
+ && month === dateFilterState.startDate.month
3154
+ && day < dateFilterState.startDate.day)) {
3155
+ return alert$1('Invalid Start Date', 'The start date cannot be before the end date.');
3156
+ }
3157
+ dateFilterState.endDate = { month, day, year };
3158
+ handleDateRangeUpdated(dateFilterState);
2985
3159
  } })));
2986
3160
  }
2987
3161
  else if (expandedFilterDrawer === FilterDrawer.Context) {
2988
3162
  // Create item picker items
2989
- const pickableItems = (Object.keys((_d = LogMetadata.Context) !== null && _d !== void 0 ? _d : {})
3163
+ const pickableItems = (Object.keys((_g = LogMetadata.Context) !== null && _g !== void 0 ? _g : {})
2990
3164
  .map((context) => {
2991
3165
  var _a;
2992
3166
  const value = ((_a = LogMetadata.Context) !== null && _a !== void 0 ? _a : {})[context];
@@ -3007,15 +3181,15 @@ const LogReviewer = (props) => {
3007
3181
  })
3008
3182
  .map((subcontext) => {
3009
3183
  return {
3010
- id: `${context}-${subcontext}`,
3184
+ id: subcontext,
3011
3185
  name: genHumanReadableName(subcontext),
3012
3186
  isGroup: false,
3013
- checked: !!value[subcontext],
3187
+ checked: contextFilterState[context][subcontext],
3014
3188
  };
3015
3189
  }));
3016
3190
  const item = {
3017
3191
  id: context,
3018
- name: context,
3192
+ name: genHumanReadableName(context),
3019
3193
  isGroup: true,
3020
3194
  children,
3021
3195
  };
@@ -3028,7 +3202,9 @@ const LogReviewer = (props) => {
3028
3202
  if (pickableItem.isGroup) {
3029
3203
  // Has subcontexts
3030
3204
  pickableItem.children.forEach((subcontextItem) => {
3031
- contextFilterState[pickableItem.id][subcontextItem.id] = (subcontextItem.checked);
3205
+ if (!subcontextItem.isGroup) {
3206
+ contextFilterState[pickableItem.id][subcontextItem.id] = (subcontextItem.checked);
3207
+ }
3032
3208
  });
3033
3209
  }
3034
3210
  else {
@@ -3044,11 +3220,15 @@ const LogReviewer = (props) => {
3044
3220
  }
3045
3221
  else if (expandedFilterDrawer === FilterDrawer.Tag) {
3046
3222
  // Create filter UI
3047
- filterDrawer = (React.createElement(TabBox, { title: "Tags" }, Object.keys(tagFilterState)
3223
+ filterDrawer = (React.createElement(TabBox, { title: "Tags" }, Object.keys((_h = LogMetadata.Tag) !== null && _h !== void 0 ? _h : {})
3048
3224
  .map((tag, i) => {
3049
3225
  const description = genHumanReadableName(tag);
3050
- return (React.createElement(CheckboxButton, { id: `LogReviewer-tag-${tag}-checkbox`, text: description, ariaLabel: `include logs tagged with "${description}" in results`, noMarginOnRight: i === Object.keys(tagFilterState).length - 1, onChanged: (checked) => {
3226
+ return (React.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) => {
3051
3227
  tagFilterState[tag] = checked;
3228
+ dispatch({
3229
+ type: ActionType.UpdateTagFilterState,
3230
+ tagFilterState,
3231
+ });
3052
3232
  } }));
3053
3233
  })));
3054
3234
  }
@@ -3079,29 +3259,33 @@ const LogReviewer = (props) => {
3079
3259
  }, ariaLabel: "only show error logs", selected: actionErrorFilterState.type === LogType$1.Error, noMarginOnRight: true })),
3080
3260
  (actionErrorFilterState.type === undefined
3081
3261
  || actionErrorFilterState.type === LogType$1.Action) && (React.createElement(TabBox, { title: "Action Log Details" },
3082
- React.createElement(ButtonInputGroup, { label: "Action" }, Object.keys(LogAction$1)
3262
+ React.createElement(ButtonInputGroup, { label: "Action", className: "mb-2" }, Object.keys(LogAction$1)
3083
3263
  .map((action, i) => {
3084
3264
  const description = genHumanReadableName(action);
3085
- return (React.createElement(CheckboxButton, { id: `LogReviewer-action-${action}-checkbox`, text: description, ariaLabel: `include logs with action type "${description}" in results`, noMarginOnRight: i === Object.keys(LogAction$1).length - 1, onChanged: (checked) => {
3265
+ return (React.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) => {
3086
3266
  actionErrorFilterState.action[action] = checked;
3267
+ console.log(actionErrorFilterState, action, checked);
3087
3268
  dispatch({
3088
3269
  type: ActionType.UpdateActionErrorFilterState,
3089
3270
  actionErrorFilterState,
3090
3271
  });
3091
3272
  } }));
3092
3273
  })),
3093
- React.createElement(ButtonInputGroup, { label: "Target" }, Object.keys((_e = LogMetadata.Target) !== null && _e !== void 0 ? _e : {})
3094
- .map((target, i) => {
3095
- var _a;
3096
- const description = genHumanReadableName(target);
3097
- return (React.createElement(CheckboxButton, { id: `LogReviewer-target-${target}-checkbox`, text: description, ariaLabel: `include logs with target "${description}" in results`, onChanged: (checked) => {
3098
- actionErrorFilterState.target[target] = checked;
3099
- dispatch({
3100
- type: ActionType.UpdateActionErrorFilterState,
3101
- actionErrorFilterState,
3102
- });
3103
- }, noMarginOnRight: i === Object.keys((_a = LogMetadata.Target) !== null && _a !== void 0 ? _a : {}).length - 1 }));
3104
- })))),
3274
+ React.createElement(ButtonInputGroup, { label: "Target" },
3275
+ (Object.keys((_j = LogMetadata.Target) !== null && _j !== void 0 ? _j : {}).length === 0) && (React.createElement("div", null, "This app does not have any targets yet.")),
3276
+ Object.keys((_k = LogMetadata.Target) !== null && _k !== void 0 ? _k : {})
3277
+ .map((target, i) => {
3278
+ var _a;
3279
+ const description = genHumanReadableName(target);
3280
+ return (React.createElement(CheckboxButton, { key: target, id: `LogReviewer-target-${target}-checkbox`, text: description, ariaLabel: `include logs with target "${description}" in results`, checked: actionErrorFilterState.target[target], onChanged: (checked) => {
3281
+ actionErrorFilterState.target[target] = checked;
3282
+ console.log(actionErrorFilterState, target, checked);
3283
+ dispatch({
3284
+ type: ActionType.UpdateActionErrorFilterState,
3285
+ actionErrorFilterState,
3286
+ });
3287
+ }, noMarginOnRight: i === Object.keys((_a = LogMetadata.Target) !== null && _a !== void 0 ? _a : {}).length - 1 }));
3288
+ })))),
3105
3289
  (actionErrorFilterState.type === undefined
3106
3290
  || actionErrorFilterState.type === LogType$1.Error) && (React.createElement(TabBox, { title: "Error Log Details" },
3107
3291
  React.createElement("div", { className: "input-group mb-2" },
@@ -3115,7 +3299,7 @@ const LogReviewer = (props) => {
3115
3299
  } })),
3116
3300
  React.createElement("div", { className: "input-group mb-2" },
3117
3301
  React.createElement("span", { className: "input-group-text" }, "Error Code"),
3118
- React.createElement("input", { type: "text", className: "form-control", "aria-label": "query for error code", value: actionErrorFilterState.errorMessage, onChange: (e) => {
3302
+ React.createElement("input", { type: "text", className: "form-control", "aria-label": "query for error code", value: actionErrorFilterState.errorCode, onChange: (e) => {
3119
3303
  actionErrorFilterState.errorCode = ((e.target.value)
3120
3304
  .trim()
3121
3305
  .toUpperCase());
@@ -3263,26 +3447,27 @@ const LogReviewer = (props) => {
3263
3447
  advancedFilterState,
3264
3448
  });
3265
3449
  }, noMarginOnRight: true })),
3266
- React.createElement("div", { className: "input-group mb-2" },
3267
- React.createElement("span", { className: "input-group-text" }, "Server Route Path"),
3268
- React.createElement("input", { type: "text", className: "form-control", "aria-label": "query for server route path", placeholder: "e.g. /api/ttm/courses/12345", value: advancedFilterState.routePath, onChange: (e) => {
3269
- advancedFilterState.courseName = ((e.target.value)
3270
- .trim());
3271
- dispatch({
3272
- type: ActionType.UpdateAdvancedFilterState,
3273
- advancedFilterState,
3274
- });
3275
- } })),
3276
- React.createElement("div", { className: "input-group mb-2" },
3277
- React.createElement("span", { className: "input-group-text" }, "Server Route Template"),
3278
- React.createElement("input", { type: "text", className: "form-control", "aria-label": "query for server route template", value: advancedFilterState.routeTemplate, placeholder: "e.g. /api/ttm/courses/:courseId", onChange: (e) => {
3279
- advancedFilterState.courseName = ((e.target.value)
3280
- .trim());
3281
- dispatch({
3282
- type: ActionType.UpdateAdvancedFilterState,
3283
- advancedFilterState,
3284
- });
3285
- } })))));
3450
+ advancedFilterState.source !== LogSource$1.Client && (React.createElement("div", { className: "mt-2" },
3451
+ React.createElement("div", { className: "input-group mb-2" },
3452
+ React.createElement("span", { className: "input-group-text" }, "Server Route Path"),
3453
+ React.createElement("input", { type: "text", className: "form-control", "aria-label": "query for server route path", placeholder: "e.g. /api/ttm/courses/12345", value: advancedFilterState.routePath, onChange: (e) => {
3454
+ advancedFilterState.courseName = ((e.target.value)
3455
+ .trim());
3456
+ dispatch({
3457
+ type: ActionType.UpdateAdvancedFilterState,
3458
+ advancedFilterState,
3459
+ });
3460
+ } })),
3461
+ React.createElement("div", { className: "input-group mb-2" },
3462
+ React.createElement("span", { className: "input-group-text" }, "Server Route Template"),
3463
+ React.createElement("input", { type: "text", className: "form-control", "aria-label": "query for server route template", value: advancedFilterState.routeTemplate, placeholder: "e.g. /api/ttm/courses/:courseId", onChange: (e) => {
3464
+ advancedFilterState.courseName = ((e.target.value)
3465
+ .trim());
3466
+ dispatch({
3467
+ type: ActionType.UpdateAdvancedFilterState,
3468
+ advancedFilterState,
3469
+ });
3470
+ } })))))));
3286
3471
  }
3287
3472
  }
3288
3473
  // Filters UI
@@ -3293,7 +3478,7 @@ const LogReviewer = (props) => {
3293
3478
  // > Perform filters
3294
3479
  const logs = [];
3295
3480
  Object.keys(logMap).forEach((year) => {
3296
- Object.keys(logMap).forEach((month) => {
3481
+ Object.keys(logMap[year]).forEach((month) => {
3297
3482
  logMap[year][month].forEach((log) => {
3298
3483
  /* ----------- Date Filter ---------- */
3299
3484
  var _a;
@@ -3329,16 +3514,23 @@ const LogReviewer = (props) => {
3329
3514
  // Whole context is deselected
3330
3515
  contextFilterState[log.context] === false
3331
3516
  // None of the subcontexts are selected
3332
- || (Object.values((_a = contextFilterState[log.context]) !== null && _a !== void 0 ? _a : {})
3333
- .every((isSelected) => {
3334
- return !isSelected;
3335
- }))) {
3517
+ || (
3518
+ // Has subcontexts
3519
+ typeof contextFilterState[log.context] !== 'boolean'
3520
+ // None of the subcontexts are selected
3521
+ && Object.values((_a = contextFilterState[log.context]) !== null && _a !== void 0 ? _a : {})
3522
+ .every((isSelected) => {
3523
+ return !isSelected;
3524
+ }))) {
3525
+ console.log(log.context, contextFilterState);
3336
3526
  return;
3337
3527
  }
3338
3528
  // Subcontext doesn't match
3339
3529
  if (
3340
- // Log has a subcontext
3341
- log.subcontext
3530
+ // Log context is not "uncategorized" (no point in further filters)
3531
+ log.context !== LogBuiltInMetadata.Context.Uncategorized
3532
+ // Log has a subcontext
3533
+ && log.subcontext
3342
3534
  // Context has subcontexts
3343
3535
  && (contextFilterState[log.context]
3344
3536
  && contextFilterState[log.context] !== false
@@ -3350,12 +3542,16 @@ const LogReviewer = (props) => {
3350
3542
  /* -------------- Tags -------------- */
3351
3543
  // No tags match
3352
3544
  if (
3353
- // Log has at least one tag
3354
- log.tags.length > 0
3545
+ // At least one tag is required
3546
+ Object.values(tagFilterState)
3547
+ .filter((isSelected) => {
3548
+ return isSelected;
3549
+ })
3550
+ .length > 0
3355
3551
  // No tags match
3356
- && (log.tags.every((tag) => {
3552
+ && log.tags.every((tag) => {
3357
3553
  return !tagFilterState[tag];
3358
- }))) {
3554
+ })) {
3359
3555
  return;
3360
3556
  }
3361
3557
  /* ------- Actions and Errors ------- */
@@ -3489,7 +3685,7 @@ const LogReviewer = (props) => {
3489
3685
  // Device info exists
3490
3686
  && log.device
3491
3687
  // Mobile filter doesn't match
3492
- && (advancedFilterState.isMobile === log.device.isMobile)) {
3688
+ && (advancedFilterState.isMobile !== log.device.isMobile)) {
3493
3689
  return;
3494
3690
  }
3495
3691
  // Log source doesn't match
@@ -3554,18 +3750,18 @@ const LogReviewer = (props) => {
3554
3750
  type: ParamType$1.Int,
3555
3751
  },
3556
3752
  {
3557
- title: 'Student',
3753
+ title: 'Student?',
3558
3754
  param: 'isLearner',
3559
3755
  type: ParamType$1.Boolean,
3560
3756
  },
3561
3757
  {
3562
- title: 'Teaching Staff',
3758
+ title: 'Teaching Staff?',
3563
3759
  param: 'isTTM',
3564
3760
  type: ParamType$1.Boolean,
3565
3761
  startsHidden: true,
3566
3762
  },
3567
3763
  {
3568
- title: 'Admin',
3764
+ title: 'Admin?',
3569
3765
  param: 'isAdmin',
3570
3766
  type: ParamType$1.Boolean,
3571
3767
  startsHidden: true,
@@ -3600,7 +3796,7 @@ const LogReviewer = (props) => {
3600
3796
  startsHidden: true,
3601
3797
  },
3602
3798
  {
3603
- title: 'Mobile',
3799
+ title: 'Mobile?',
3604
3800
  param: 'device.isMobile',
3605
3801
  type: ParamType$1.Boolean,
3606
3802
  startsHidden: true,
@@ -3718,12 +3914,20 @@ const LogReviewer = (props) => {
3718
3914
  startsHidden: true,
3719
3915
  },
3720
3916
  ];
3917
+ // Nothing to show notice
3918
+ const noLogsNotice = (logs.length === 0
3919
+ ? (React.createElement("div", { className: "alert alert-warning text-center mt-2" },
3920
+ React.createElement("h4", { className: "m-1" }, "No Logs to Show"),
3921
+ React.createElement("div", null, "Either your filters are too strict or no matching logs have been created yet.")))
3922
+ : undefined);
3721
3923
  // Create intelliTable
3722
- const dataTable = (React.createElement(IntelliTable, { title: "Matching Logs", id: "logs", data: logs, columns: columns }));
3924
+ const dataTable = (React.createElement(IntelliTable, { title: "Matching Logs:", id: "logs", data: logs, columns: columns }));
3723
3925
  // Main body
3724
3926
  body = (React.createElement(React.Fragment, null,
3725
3927
  filters,
3726
- dataTable));
3928
+ React.createElement("div", { className: "mt-2" },
3929
+ dataTable,
3930
+ noLogsNotice)));
3727
3931
  }
3728
3932
  /* ---------- Wrap in Modal --------- */
3729
3933
  return (React.createElement("div", { className: "LogReviewer-outer-container" },
@@ -3731,14 +3935,10 @@ const LogReviewer = (props) => {
3731
3935
  React.createElement("div", { className: "LogReviewer-inner-container" },
3732
3936
  React.createElement("div", { className: "LogReviewer-header" },
3733
3937
  React.createElement("div", { className: "LogReviewer-header-title" },
3734
- React.createElement("h1", { className: "m-0" }, "Log Review Dashboard")),
3735
- React.createElement("button", { type: "button", className: "LogReviewer-header-close-button btn btn-lg", "aria-label": "close log reviewer panel", onClick: onClose, style: {
3736
- border: 0,
3737
- backgroundColor: 'transparent',
3738
- padding: 0,
3739
- margin: 0,
3740
- } },
3741
- React.createElement(FontAwesomeIcon, { icon: faTimes }))),
3938
+ React.createElement("h3", { className: "text-center m-0" }, "Log Review Dashboard")),
3939
+ React.createElement("div", { style: { width: 0 } },
3940
+ React.createElement("button", { type: "button", className: "LogReviewer-header-close-button btn btn-dark btn-lg pe-0", "aria-label": "close log reviewer panel", onClick: onClose },
3941
+ React.createElement(FontAwesomeIcon, { icon: faTimes })))),
3742
3942
  React.createElement("div", { className: "LogReviewer-contents" }, body))));
3743
3943
  };
3744
3944
 
@@ -4062,22 +4262,24 @@ const initServer = (opts) => {
4062
4262
  * @param {number} month the month to query (e.g. 1 = January)
4063
4263
  * @returns {Log[]} list of logs from the given month
4064
4264
  */
4065
- opts.app.post(`${LOG_REVIEW_ROUTE_PATH_PREFIX}/years/:year/months/:month`, genRouteHandler({
4265
+ opts.app.get(`${LOG_REVIEW_ROUTE_PATH_PREFIX}/years/:year/months/:month`, genRouteHandler({
4066
4266
  paramTypes: {
4067
4267
  year: ParamType$1.Int,
4068
4268
  month: ParamType$1.Int,
4069
4269
  },
4070
4270
  handler: ({ params }) => __awaiter(void 0, void 0, void 0, function* () {
4071
4271
  // Get user info
4072
- const { userId, isAdmin } = params;
4272
+ const { year, month, userId, isAdmin, } = params;
4073
4273
  // Validate user
4074
- // isAdmin is already checked because path starts with '/admin'
4075
4274
  const canReview = yield canReviewLogs(userId, isAdmin);
4076
4275
  if (!canReview) {
4077
4276
  throw new ErrorWithCode('You cannot access this resource because you do not have the appropriate permissions.', ReactKitErrorCode$1.NotAllowedToReviewLogs);
4078
4277
  }
4079
4278
  // Query for logs
4080
- const logs = yield _logCollection.find({ userId });
4279
+ const logs = yield _logCollection.find({
4280
+ year,
4281
+ month,
4282
+ });
4081
4283
  // Return logs
4082
4284
  return logs;
4083
4285
  }),
@@ -4381,18 +4583,6 @@ const parseUserAgent = (userAgent) => {
4381
4583
  };
4382
4584
  };
4383
4585
 
4384
- /**
4385
- * Allowed log levels
4386
- * @author Gabe Abrams
4387
- */
4388
- var LogLevel;
4389
- (function (LogLevel) {
4390
- LogLevel["Warn"] = "Warn";
4391
- LogLevel["Info"] = "Info";
4392
- LogLevel["Debug"] = "Debug";
4393
- })(LogLevel || (LogLevel = {}));
4394
- var LogLevel$1 = LogLevel;
4395
-
4396
4586
  /**
4397
4587
  * Generate an express API route handler
4398
4588
  * @author Gabe Abrams
@@ -4738,7 +4928,7 @@ const genRouteHandler = (opts) => {
4738
4928
  isAdmin: !!launchInfo.isAdmin,
4739
4929
  isTTM: !!launchInfo.isTTM,
4740
4930
  courseId: launchInfo.courseId,
4741
- courseName: launchInfo.courseName,
4931
+ courseName: launchInfo.contextLabel,
4742
4932
  browser,
4743
4933
  device,
4744
4934
  year,
@@ -4765,7 +4955,7 @@ const genRouteHandler = (opts) => {
4765
4955
  }
4766
4956
  : {
4767
4957
  type: LogType$1.Action,
4768
- target: ((_m = opts.target) !== null && _m !== void 0 ? _m : LogBuiltInMetadata.Target.NoSpecificTarget),
4958
+ target: ((_m = opts.target) !== null && _m !== void 0 ? _m : LogBuiltInMetadata.Target.NoTarget),
4769
4959
  action: ((_o = opts.action) !== null && _o !== void 0 ? _o : LogAction$1.Unknown),
4770
4960
  });
4771
4961
  // Source-specific info