@elasticias/screens 1.0.40 → 1.0.42
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.
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
2
|
import { Input, Component, signal, InjectionToken, inject, ChangeDetectorRef, ViewChild, Injector, computed, DestroyRef } from '@angular/core';
|
|
3
3
|
import { PersistedState, StorageUtils, AppUtils, CsvUtils } from '@elasticias/utils';
|
|
4
|
-
import { Subject, combineLatest, take } from 'rxjs';
|
|
4
|
+
import { Subject, combineLatest, take, merge, map, distinctUntilChanged } from 'rxjs';
|
|
5
5
|
import { Permissions } from '@elasticias/types';
|
|
6
6
|
import { Router, ActivatedRoute } from '@angular/router';
|
|
7
7
|
import { CacheService, ToastService, ConfirmDialogService, EfShortcutService } from '@elasticias/core';
|
|
@@ -458,6 +458,13 @@ class AbstractScreenComponent extends AbstractComponent {
|
|
|
458
458
|
const v = raw;
|
|
459
459
|
if (!v || typeof v !== 'object' || !v.presetKey)
|
|
460
460
|
return null;
|
|
461
|
+
// An open range stores null bounds. `new Date(null)` is 1970, not
|
|
462
|
+
// NaN, so it must be handled before parsing, not after.
|
|
463
|
+
if (v.start == null && v.end == null) {
|
|
464
|
+
return { start: null, end: null, presetKey: v.presetKey, label: v.label ?? '', labelKey: v.labelKey };
|
|
465
|
+
}
|
|
466
|
+
if (v.start == null || v.end == null)
|
|
467
|
+
return null;
|
|
461
468
|
const start = new Date(v.start);
|
|
462
469
|
const end = new Date(v.end);
|
|
463
470
|
if (isNaN(start.getTime()) || isNaN(end.getTime()))
|
|
@@ -1075,14 +1082,27 @@ class AbstractSearchScreenV2 extends AbstractScreenComponent {
|
|
|
1075
1082
|
criteria = signal(new SearchEntity({}), ...(ngDevMode ? [{ debugName: "criteria" }] : /* istanbul ignore next */ []));
|
|
1076
1083
|
/**
|
|
1077
1084
|
* Active date-range filter shown in `ef-datepicker-advanced`'s
|
|
1078
|
-
* trigger
|
|
1079
|
-
*
|
|
1080
|
-
*
|
|
1085
|
+
* trigger, and the single source of the criteria's `start`/`end`.
|
|
1086
|
+
* The first search, `clear()` and a picked range all send exactly
|
|
1087
|
+
* what this holds, and a cache restore rebuilds it from the restored
|
|
1088
|
+
* dates, so the chip never reads one period while the list shows
|
|
1089
|
+
* another.
|
|
1081
1090
|
*
|
|
1082
|
-
*
|
|
1083
|
-
*
|
|
1091
|
+
* Defaults to `'all_time'` (no dates sent). Override
|
|
1092
|
+
* `buildDefaultDateRange()` per screen to open on a window instead.
|
|
1084
1093
|
*/
|
|
1085
1094
|
dateRange = signal(this.buildDefaultDateRange(), ...(ngDevMode ? [{ debugName: "dateRange" }] : /* istanbul ignore next */ []));
|
|
1095
|
+
/**
|
|
1096
|
+
* Whether this screen offers the date chip at all. Set it to `false`
|
|
1097
|
+
* (`override readonly showDateFilter = false;`) on a screen whose
|
|
1098
|
+
* backend query has no date field, and guard the template's
|
|
1099
|
+
* `<ef-datepicker-advanced>` with `@if (showDateFilter)` (or drop it).
|
|
1100
|
+
* The base then never sends `start`/`end`: not on first load, not
|
|
1101
|
+
* after `clear()`, not from criteria cached by an older session, and
|
|
1102
|
+
* not from `onDateRangeChange()` / `setDateRange()`, which become
|
|
1103
|
+
* no-ops, so a picker left in the template cannot filter either.
|
|
1104
|
+
*/
|
|
1105
|
+
showDateFilter = true;
|
|
1086
1106
|
/* ── Filter-bar UI state (DRY) ──────────────────────────────────
|
|
1087
1107
|
Identical across every list screen — lifted up so subclasses
|
|
1088
1108
|
don't re-declare. `clear()` resets all four signals. */
|
|
@@ -1196,14 +1216,16 @@ class AbstractSearchScreenV2 extends AbstractScreenComponent {
|
|
|
1196
1216
|
}
|
|
1197
1217
|
/* ── Date-range filter (DRY) ────────────────────────────────────
|
|
1198
1218
|
Wired to ef-datepicker-advanced. The default range is
|
|
1199
|
-
`
|
|
1200
|
-
if a
|
|
1219
|
+
`all_time`, which sends no dates. Override
|
|
1220
|
+
`buildDefaultDateRange()` per screen if a window is needed. */
|
|
1201
1221
|
/**
|
|
1202
1222
|
* Wired to `<ef-datepicker-advanced (rangeChange)>` — stores the
|
|
1203
1223
|
* range for trigger display and pushes it into the search criteria
|
|
1204
1224
|
* so the next `search()` filters by it.
|
|
1205
1225
|
*/
|
|
1206
1226
|
onDateRangeChange(range) {
|
|
1227
|
+
if (!this.showDateFilter)
|
|
1228
|
+
return;
|
|
1207
1229
|
this.dateRange.set(range);
|
|
1208
1230
|
this.setDateRange(range.start, range.end);
|
|
1209
1231
|
}
|
|
@@ -1216,19 +1238,52 @@ class AbstractSearchScreenV2 extends AbstractScreenComponent {
|
|
|
1216
1238
|
this.dateRange.set(this.buildDefaultDateRange());
|
|
1217
1239
|
}
|
|
1218
1240
|
/**
|
|
1219
|
-
* Build the default `EfDateRange`.
|
|
1220
|
-
*
|
|
1221
|
-
*
|
|
1241
|
+
* Build the default `EfDateRange`. Default: `all_time`, no bounds,
|
|
1242
|
+
* so a typed search finds an old record instead of silently missing
|
|
1243
|
+
* it behind a window nobody chose. Override per screen to open on a
|
|
1244
|
+
* window (e.g. `daysAgo(89)` to `startOfToday()` for `last_90_days`);
|
|
1245
|
+
* the first search and `clear()` then send that window.
|
|
1222
1246
|
*/
|
|
1223
1247
|
buildDefaultDateRange() {
|
|
1224
1248
|
return {
|
|
1225
|
-
start:
|
|
1226
|
-
end:
|
|
1227
|
-
presetKey: '
|
|
1228
|
-
labelKey: '
|
|
1229
|
-
label: '
|
|
1249
|
+
start: null,
|
|
1250
|
+
end: null,
|
|
1251
|
+
presetKey: 'all_time',
|
|
1252
|
+
labelKey: 'date_preset_all_time',
|
|
1253
|
+
label: '',
|
|
1230
1254
|
};
|
|
1231
1255
|
}
|
|
1256
|
+
/** Copy the chip's range onto fresh criteria, so what is shown is
|
|
1257
|
+
* what is sent. Sends nothing when the screen has no date chip. */
|
|
1258
|
+
seedDateCriteria(criteria) {
|
|
1259
|
+
const range = this.showDateFilter ? this.dateRange() : null;
|
|
1260
|
+
criteria.start = range?.start ?? null;
|
|
1261
|
+
criteria.end = range?.end ?? null;
|
|
1262
|
+
}
|
|
1263
|
+
/** Rebuild the chip from restored criteria: both dates are shown as
|
|
1264
|
+
* the custom range they filter on; anything less is `all_time`, and
|
|
1265
|
+
* `bootstrapInitialSearch()` drops a lone bound so nothing is sent.
|
|
1266
|
+
* No literal label: the picker formats a custom range itself. */
|
|
1267
|
+
dateRangeFromCriteria(startRaw, endRaw) {
|
|
1268
|
+
const start = this.toDateOrNull(startRaw);
|
|
1269
|
+
const end = this.toDateOrNull(endRaw);
|
|
1270
|
+
if (!start || !end) {
|
|
1271
|
+
return {
|
|
1272
|
+
start: null,
|
|
1273
|
+
end: null,
|
|
1274
|
+
presetKey: 'all_time',
|
|
1275
|
+
labelKey: 'date_preset_all_time',
|
|
1276
|
+
label: '',
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
return { start, end, presetKey: 'custom', label: '' };
|
|
1280
|
+
}
|
|
1281
|
+
toDateOrNull(raw) {
|
|
1282
|
+
if (raw == null || raw === '')
|
|
1283
|
+
return null;
|
|
1284
|
+
const d = raw instanceof Date ? raw : new Date(raw);
|
|
1285
|
+
return isNaN(d.getTime()) ? null : d;
|
|
1286
|
+
}
|
|
1232
1287
|
/** Today at 00:00 local time. */
|
|
1233
1288
|
startOfToday() {
|
|
1234
1289
|
const d = new Date();
|
|
@@ -1430,7 +1485,17 @@ class AbstractSearchScreenV2 extends AbstractScreenComponent {
|
|
|
1430
1485
|
bootstrapInitialSearch() {
|
|
1431
1486
|
const cached = this.cacheService.getCache(this.screenStateKey);
|
|
1432
1487
|
if (cached) {
|
|
1433
|
-
|
|
1488
|
+
const restored = new SearchEntity(cached);
|
|
1489
|
+
const bothBounds = this.toDateOrNull(cached['start']) && this.toDateOrNull(cached['end']);
|
|
1490
|
+
if (!this.showDateFilter || !bothBounds) {
|
|
1491
|
+
// Criteria cached while this screen still had a chip may carry
|
|
1492
|
+
// dates its backend ignores at best; and a lone bound would be
|
|
1493
|
+
// sent under a chip reading "all time". Drop them.
|
|
1494
|
+
restored.start = null;
|
|
1495
|
+
restored.end = null;
|
|
1496
|
+
restored.startEndDateRanges = null;
|
|
1497
|
+
}
|
|
1498
|
+
this.criteria.set(restored);
|
|
1434
1499
|
this.restoreFilterUiFromCriteria(cached);
|
|
1435
1500
|
}
|
|
1436
1501
|
else {
|
|
@@ -1448,6 +1513,7 @@ class AbstractSearchScreenV2 extends AbstractScreenComponent {
|
|
|
1448
1513
|
pageNumber: PaginationEnum.DEFAULT_PAGE,
|
|
1449
1514
|
pageSize: PaginationEnum.DEFAULT_PAGE_SIZE,
|
|
1450
1515
|
};
|
|
1516
|
+
this.seedDateCriteria(fresh);
|
|
1451
1517
|
this.criteria.set(fresh);
|
|
1452
1518
|
}
|
|
1453
1519
|
this.search();
|
|
@@ -1458,11 +1524,15 @@ class AbstractSearchScreenV2 extends AbstractScreenComponent {
|
|
|
1458
1524
|
* what the drawer / search box displays matches what the search will
|
|
1459
1525
|
* actually send (otherwise a stale criteria filter keeps applying
|
|
1460
1526
|
* while every control reads "Tous"). Base handles the free-text
|
|
1461
|
-
* input and the declared advanced filters; override
|
|
1462
|
-
* to sync screen-specific state
|
|
1527
|
+
* input, the date chip and the declared advanced filters; override
|
|
1528
|
+
* (calling super) to sync screen-specific state, such as a status pill
|
|
1529
|
+
* or custom switches.
|
|
1463
1530
|
*/
|
|
1464
1531
|
restoreFilterUiFromCriteria(cached) {
|
|
1465
1532
|
this.searchQuery.set(cached['searchText'] ?? '');
|
|
1533
|
+
if (this.showDateFilter) {
|
|
1534
|
+
this.dateRange.set(this.dateRangeFromCriteria(cached['start'], cached['end']));
|
|
1535
|
+
}
|
|
1466
1536
|
const next = {};
|
|
1467
1537
|
for (const f of this.advancedFilters) {
|
|
1468
1538
|
const value = cached[f.key];
|
|
@@ -1508,7 +1578,13 @@ class AbstractSearchScreenV2 extends AbstractScreenComponent {
|
|
|
1508
1578
|
}
|
|
1509
1579
|
/* ── Convenience criteria mutators ───────────────────────────── */
|
|
1510
1580
|
setSearchText(text) {
|
|
1511
|
-
|
|
1581
|
+
// Templates bind this to ef-smart-bar's (search), and that binding has
|
|
1582
|
+
// received the native DOM 'search' Event, which reached the API as
|
|
1583
|
+
// {"isTrusted":true} and failed. ef-smart-bar now stops the event, but
|
|
1584
|
+
// whatever arrives that is not text, the field's own value is the
|
|
1585
|
+
// search the user meant.
|
|
1586
|
+
const value = typeof text === 'string' ? text : this.searchQuery();
|
|
1587
|
+
this.criteria.update((c) => this.cloneCriteria(c, { searchText: value || undefined, page: 1 }));
|
|
1512
1588
|
this.search();
|
|
1513
1589
|
}
|
|
1514
1590
|
setPage(pageNumber, pageSize) {
|
|
@@ -1529,6 +1605,8 @@ class AbstractSearchScreenV2 extends AbstractScreenComponent {
|
|
|
1529
1605
|
this.search();
|
|
1530
1606
|
}
|
|
1531
1607
|
setDateRange(start, end) {
|
|
1608
|
+
if (!this.showDateFilter)
|
|
1609
|
+
return;
|
|
1532
1610
|
this.criteria.update((c) => this.cloneCriteria(c, { start, end, page: 1 }));
|
|
1533
1611
|
this.search();
|
|
1534
1612
|
}
|
|
@@ -1551,6 +1629,8 @@ class AbstractSearchScreenV2 extends AbstractScreenComponent {
|
|
|
1551
1629
|
},
|
|
1552
1630
|
];
|
|
1553
1631
|
}
|
|
1632
|
+
this.resetDateRange();
|
|
1633
|
+
this.seedDateCriteria(fresh);
|
|
1554
1634
|
this.criteria.set(fresh);
|
|
1555
1635
|
// Reset all the shared filter-bar / selection signals so
|
|
1556
1636
|
// subclasses don't have to track each one individually.
|
|
@@ -1560,7 +1640,6 @@ class AbstractSearchScreenV2 extends AbstractScreenComponent {
|
|
|
1560
1640
|
this.activeFilters.set([]);
|
|
1561
1641
|
this.advancedValues.set({});
|
|
1562
1642
|
this.selected.set(new Set());
|
|
1563
|
-
this.resetDateRange();
|
|
1564
1643
|
this.cacheService.setCache(this.screenStateKey, fresh);
|
|
1565
1644
|
this.search();
|
|
1566
1645
|
}
|
|
@@ -1847,6 +1926,12 @@ class AbstractDetailScreenV2 extends AbstractScreenComponent {
|
|
|
1847
1926
|
destroyRef = inject(DestroyRef);
|
|
1848
1927
|
auditHistoryService = inject(AUDIT_HISTORY_SERVICE, { optional: true });
|
|
1849
1928
|
shortcuts = inject(EfShortcutService);
|
|
1929
|
+
/** The in-flight entity / history requests. A newer load drops the
|
|
1930
|
+
* older one, so a slow response for an id the screen has moved off
|
|
1931
|
+
* can never overwrite the record it now shows, and destroying the
|
|
1932
|
+
* screen drops both. */
|
|
1933
|
+
loadSub;
|
|
1934
|
+
auditSub;
|
|
1850
1935
|
/** Distinguishes this instance's registration from any other detail
|
|
1851
1936
|
* screen alive at once (the outgoing and incoming routed component
|
|
1852
1937
|
* can briefly coexist), the same convention ef-row-actions uses:
|
|
@@ -1857,6 +1942,12 @@ class AbstractDetailScreenV2 extends AbstractScreenComponent {
|
|
|
1857
1942
|
serviceInstance;
|
|
1858
1943
|
constructor() {
|
|
1859
1944
|
super();
|
|
1945
|
+
// A response landing after the screen is gone would still run
|
|
1946
|
+
// afterLoad() and start a history request for a screen nobody sees.
|
|
1947
|
+
this.destroyRef.onDestroy(() => {
|
|
1948
|
+
this.loadSub?.unsubscribe();
|
|
1949
|
+
this.auditSub?.unsubscribe();
|
|
1950
|
+
});
|
|
1860
1951
|
// Every detail screen inherits mod+s with no per-screen wiring, and
|
|
1861
1952
|
// it doubles as the fix for the browser's own Save Page dialog
|
|
1862
1953
|
// otherwise popping up on top of it.
|
|
@@ -1980,11 +2071,21 @@ class AbstractDetailScreenV2 extends AbstractScreenComponent {
|
|
|
1980
2071
|
this.loadReferenceData(cfg.REF_DATA_OPTIONS);
|
|
1981
2072
|
}
|
|
1982
2073
|
// Wire route params → load (or reset to empty on /details).
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
2074
|
+
//
|
|
2075
|
+
// One navigation can emit on both maps: the router pushes
|
|
2076
|
+
// queryParams first, then params. Combining the two emissions saw
|
|
2077
|
+
// a half-updated route on details/SRC?mode=duplicate → details/NEW
|
|
2078
|
+
// (the old id without the mode) and loaded SRC on a screen that
|
|
2079
|
+
// was moving to NEW. The router sets `snapshot` to the next one
|
|
2080
|
+
// before either emits, so each emission reads id and mode together
|
|
2081
|
+
// from it, and the second emission of the same navigation (same
|
|
2082
|
+
// snapshot object) is dropped. Any navigation that changes params
|
|
2083
|
+
// or query still gets a new snapshot, so it still reloads once.
|
|
2084
|
+
merge(this.route.paramMap, this.route.queryParamMap)
|
|
2085
|
+
.pipe(map(() => this.route.snapshot), distinctUntilChanged(), takeUntilDestroyed(this.destroyRef))
|
|
2086
|
+
.subscribe(snapshot => {
|
|
2087
|
+
const id = snapshot.paramMap.get('id');
|
|
2088
|
+
const isDuplicate = snapshot.queryParamMap.get('mode') === 'duplicate';
|
|
1988
2089
|
this.entityId.set(id);
|
|
1989
2090
|
this.duplicateMode.set(isDuplicate);
|
|
1990
2091
|
this.context.duplicateMode = isDuplicate;
|
|
@@ -1994,6 +2095,12 @@ class AbstractDetailScreenV2 extends AbstractScreenComponent {
|
|
|
1994
2095
|
}
|
|
1995
2096
|
else {
|
|
1996
2097
|
// New mode — reset to a fresh empty entity, no history yet.
|
|
2098
|
+
// A load still in flight for the previous id is dropped.
|
|
2099
|
+
if (this.loadSub && !this.loadSub.closed) {
|
|
2100
|
+
this.loadSub.unsubscribe();
|
|
2101
|
+
this.loading.set(false);
|
|
2102
|
+
}
|
|
2103
|
+
this.auditSub?.unsubscribe();
|
|
1997
2104
|
this.entity.set({});
|
|
1998
2105
|
this.auditEntries.set([]);
|
|
1999
2106
|
this.afterLoad();
|
|
@@ -2035,7 +2142,15 @@ class AbstractDetailScreenV2 extends AbstractScreenComponent {
|
|
|
2035
2142
|
return;
|
|
2036
2143
|
this.loading.set(true);
|
|
2037
2144
|
this.errorMsg.set(null);
|
|
2038
|
-
|
|
2145
|
+
// Moving to another record: the previous one's history must not
|
|
2146
|
+
// land under it while this load is in flight.
|
|
2147
|
+
const showsAnotherRecord = this.entity()?.id !== id;
|
|
2148
|
+
if (showsAnotherRecord) {
|
|
2149
|
+
this.auditSub?.unsubscribe();
|
|
2150
|
+
this.auditEntries.set([]);
|
|
2151
|
+
}
|
|
2152
|
+
this.loadSub?.unsubscribe();
|
|
2153
|
+
this.loadSub = this.serviceInstance.get(id).subscribe({
|
|
2039
2154
|
next: (result) => {
|
|
2040
2155
|
const vm = new ViewModelEntity(result);
|
|
2041
2156
|
vm.id = id;
|
|
@@ -2048,6 +2163,13 @@ class AbstractDetailScreenV2 extends AbstractScreenComponent {
|
|
|
2048
2163
|
console.error('AbstractDetailScreenV2.loadData failed', err);
|
|
2049
2164
|
this.errorMsg.set(err?.message ?? 'Erreur de chargement');
|
|
2050
2165
|
this.loading.set(false);
|
|
2166
|
+
// Save and the status actions target entityId(). Leaving the
|
|
2167
|
+
// previous record on screen would have them act on a record
|
|
2168
|
+
// other than the one shown. A failed reload of the same
|
|
2169
|
+
// record keeps it: what is shown is still what is targeted.
|
|
2170
|
+
if (showsAnotherRecord) {
|
|
2171
|
+
this.entity.set({});
|
|
2172
|
+
}
|
|
2051
2173
|
},
|
|
2052
2174
|
});
|
|
2053
2175
|
}
|
|
@@ -2201,6 +2323,7 @@ class AbstractDetailScreenV2 extends AbstractScreenComponent {
|
|
|
2201
2323
|
* an empty history rather than blocking the screen.
|
|
2202
2324
|
*/
|
|
2203
2325
|
loadAuditHistory() {
|
|
2326
|
+
this.auditSub?.unsubscribe();
|
|
2204
2327
|
this.auditEntries.set([]);
|
|
2205
2328
|
const entityType = this.getConfig()?.AUDIT_ENTITY_TYPE;
|
|
2206
2329
|
const id = this.entityId();
|
|
@@ -2216,7 +2339,7 @@ class AbstractDetailScreenV2 extends AbstractScreenComponent {
|
|
|
2216
2339
|
if (!result$ || typeof result$.subscribe !== 'function') {
|
|
2217
2340
|
return;
|
|
2218
2341
|
}
|
|
2219
|
-
result$.subscribe({
|
|
2342
|
+
this.auditSub = result$.subscribe({
|
|
2220
2343
|
next: (entries) => this.auditEntries.set(entries ?? []),
|
|
2221
2344
|
error: () => this.auditEntries.set([]),
|
|
2222
2345
|
});
|
|
@@ -2351,7 +2474,7 @@ class AbstractReportScreenV2 extends AbstractScreenComponent {
|
|
|
2351
2474
|
this.reportsService = this.injector.get(this.getConfig().SERVICE);
|
|
2352
2475
|
// Reinstate the period the user last chose before anything loads, so the
|
|
2353
2476
|
// widgets fetch it once instead of fetching the default and refetching.
|
|
2354
|
-
this.dateRange.set(this.restorePersistedDateRange(this.dateRange(), key => this.rangeFromPreset(key)));
|
|
2477
|
+
this.dateRange.set(this.boundedOrDefault(this.restorePersistedDateRange(this.dateRange(), key => this.rangeFromPreset(key))));
|
|
2355
2478
|
const lists = this.getConfig()?.REPORT_STATIC_LISTS ?? [];
|
|
2356
2479
|
if (lists.length > 0) {
|
|
2357
2480
|
this.initializeStaticLists(lists);
|
|
@@ -2365,10 +2488,22 @@ class AbstractReportScreenV2 extends AbstractScreenComponent {
|
|
|
2365
2488
|
/* ── Period ─────────────────────────────────────────────────── */
|
|
2366
2489
|
/** Wired to `<ef-datepicker-advanced (rangeChange)>`. */
|
|
2367
2490
|
onDateRangeChange(range) {
|
|
2368
|
-
this.
|
|
2369
|
-
this.
|
|
2491
|
+
const bounded = this.boundedOrDefault(range);
|
|
2492
|
+
this.dateRange.set(bounded);
|
|
2493
|
+
this.persistDateRange(bounded);
|
|
2370
2494
|
this.loadAll();
|
|
2371
2495
|
}
|
|
2496
|
+
/**
|
|
2497
|
+
* Every widget query is a period, so an open range (`all_time`) has no
|
|
2498
|
+
* meaning here. The picker only offers it with `allowAllTime`, which a
|
|
2499
|
+
* report should not set; if one arrives anyway, fall back to the
|
|
2500
|
+
* default period rather than send a null bound.
|
|
2501
|
+
*/
|
|
2502
|
+
boundedOrDefault(range) {
|
|
2503
|
+
return range.start && range.end
|
|
2504
|
+
? range
|
|
2505
|
+
: this.buildDefaultDateRange();
|
|
2506
|
+
}
|
|
2372
2507
|
/** Default period from the config's `DEFAULT_PERIOD` preset. */
|
|
2373
2508
|
buildDefaultDateRange() {
|
|
2374
2509
|
return this.rangeFromPreset(this.getConfig()?.DEFAULT_PERIOD ?? 'last_30_days');
|