@reforgium/statum 3.1.3 → 3.1.4
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.
- package/CHANGELOG.md +12 -1
- package/README.md +3 -3
- package/fesm2022/reforgium-statum.mjs +37 -32
- package/package.json +1 -1
- package/types/reforgium-statum.d.ts +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
|
-
## [3.1.
|
|
1
|
+
## [3.1.4]: 2026-04-10
|
|
2
|
+
|
|
3
|
+
### Fix:
|
|
4
|
+
- `PagedQueryStore`: `totalElements` now preserves `undefined` when `parseResponse` omits the field, so unknown-total datasets stay open-ended instead of being forced back to `0` after reset/refetch.
|
|
5
|
+
- `PagedQueryStore`: `refetchWith(...)` now treats real filters/query/sort changes as dataset replacement, clearing page cache/items and bumping `version` so `@reforgium/data-grid` infinity buffers do not keep stale rows after an empty successful response.
|
|
6
|
+
|
|
7
|
+
### Test:
|
|
8
|
+
- added regression coverage for omitted `totalElements`, reset/refetch clearing, and empty-result unknown-total flows
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## [3.1.3]: 2026-04-06
|
|
2
13
|
|
|
3
14
|
### Fix:
|
|
4
15
|
- `PagedQueryStore`: all public state getters (`filters`, `query`, `page`, `pageSize`, `totalElements`, `sort`) and direct `#routeParams` reads now wrap the underlying signal in `untracked()`; previously calling `fetch()`, `refetchWith()`, `updatePage()`, `setRouteParams()`, etc. from inside an Angular `effect()` or `computed()` would inadvertently subscribe the reactive context to internal state signals, causing cascading re-runs; reactive subscriptions remain available exclusively through the `*State` readonly signals (`filtersState`, `sortState`, `pageState`, …)
|
package/README.md
CHANGED
|
@@ -345,7 +345,7 @@ Lightweight store for server-side pagination with filtering, dynamic query param
|
|
|
345
345
|
| version | `WritableSignal<number>` | Increments when the dataset is reset |
|
|
346
346
|
| page | `number` | Current page (0-based) |
|
|
347
347
|
| pageSize | `number` | Page size |
|
|
348
|
-
| totalElements | `number`
|
|
348
|
+
| totalElements | `number \| undefined` | Total items on server, if known |
|
|
349
349
|
| filters | `Partial<F>` | Active filters |
|
|
350
350
|
| query | `Record<string, unknown>` | Active query params |
|
|
351
351
|
| sort | `ReadonlyArray<{ sort: string; order: 'asc' \| 'desc' }>` | Active sort state |
|
|
@@ -396,7 +396,7 @@ Concurrency can be configured per store:
|
|
|
396
396
|
| Method | Cache read | Cache reset | Notes |
|
|
397
397
|
|----------------|------------|-------------|--------------------------------------------------|
|
|
398
398
|
| fetch | no | yes | Always starts clean from page `0` |
|
|
399
|
-
| refetchWith | no |
|
|
399
|
+
| refetchWith | no | conditional | Reloads current state; when filters/query/sort actually change, resets to page `0` and replaces the dataset |
|
|
400
400
|
| updatePage | yes | no | Can bypass with `ignoreCache: true` |
|
|
401
401
|
| updatePageSize | no | yes | Prevents mixed caches for different page sizes |
|
|
402
402
|
| updateByOffset | yes | no | Internally maps to `page + size` |
|
|
@@ -422,7 +422,7 @@ store.updateSort({ sort: 'name', order: 'asc' });
|
|
|
422
422
|
|
|
423
423
|
`cached()` remains a bounded hot-cache view. It is useful for cache-aware revisit/export/search helpers, but it is not the right datasource for infinity scrolling once cache eviction matters. For `data-grid` infinity mode, prefer passing the whole store as a `GridPagedDataSource` (`[source]="store"`) and let the grid keep its own page buffer.
|
|
424
424
|
|
|
425
|
-
`sort` and `routeParams` should be changed only through `setSort(...)` and `setRouteParams(...)`. Direct state mutation setters for `page`, `pageSize`, `filters`, `query`, and `totalElements` are available for low-level integration scenarios (such as external data-grid source contracts) but prefer the explicit store methods for typical use.
|
|
425
|
+
`sort` and `routeParams` should be changed only through `setSort(...)` and `setRouteParams(...)`. Direct state mutation setters for `page`, `pageSize`, `filters`, `query`, and `totalElements` are available for low-level integration scenarios (such as external data-grid source contracts) but prefer the explicit store methods for typical use. `totalElements` may be `undefined` when the backend does not report a total.
|
|
426
426
|
|
|
427
427
|
### PagedQueryStore + DataGrid source mode
|
|
428
428
|
|
|
@@ -277,10 +277,10 @@ class KeyedScheduler {
|
|
|
277
277
|
class ResourceStore {
|
|
278
278
|
http = inject(HttpClient);
|
|
279
279
|
serializer;
|
|
280
|
-
#value = signal(null, ...(ngDevMode ? [{ debugName: "#value" }] : []));
|
|
281
|
-
#status = signal('idle', ...(ngDevMode ? [{ debugName: "#status" }] : []));
|
|
282
|
-
#error = signal(null, ...(ngDevMode ? [{ debugName: "#error" }] : []));
|
|
283
|
-
#activeRequests = signal(0, ...(ngDevMode ? [{ debugName: "#activeRequests" }] : []));
|
|
280
|
+
#value = signal(null, ...(ngDevMode ? [{ debugName: "#value" }] : /* istanbul ignore next */ []));
|
|
281
|
+
#status = signal('idle', ...(ngDevMode ? [{ debugName: "#status" }] : /* istanbul ignore next */ []));
|
|
282
|
+
#error = signal(null, ...(ngDevMode ? [{ debugName: "#error" }] : /* istanbul ignore next */ []));
|
|
283
|
+
#activeRequests = signal(0, ...(ngDevMode ? [{ debugName: "#activeRequests" }] : /* istanbul ignore next */ []));
|
|
284
284
|
/**
|
|
285
285
|
* Current resource value.
|
|
286
286
|
* Returns `null` if no data yet or the request failed.
|
|
@@ -298,7 +298,7 @@ class ResourceStore {
|
|
|
298
298
|
* Convenience loading flag: `true` when `loading` or `stale`.
|
|
299
299
|
* Useful for spinners and disabling buttons.
|
|
300
300
|
*/
|
|
301
|
-
loading = computed(() => this.#activeRequests() > 0 || this.#status() === 'stale', ...(ngDevMode ? [{ debugName: "loading" }] : []));
|
|
301
|
+
loading = computed(() => this.#activeRequests() > 0 || this.#status() === 'stale', ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
302
302
|
routes;
|
|
303
303
|
opts;
|
|
304
304
|
maxEntries;
|
|
@@ -839,22 +839,22 @@ class PagedQueryStore {
|
|
|
839
839
|
#transport;
|
|
840
840
|
#cache;
|
|
841
841
|
/** Current page data (reactive). */
|
|
842
|
-
items = signal([], ...(ngDevMode ? [{ debugName: "items" }] : []));
|
|
842
|
+
items = signal([], ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
|
|
843
843
|
/** Merged cache of pages (flat list) handy for search/export. */
|
|
844
|
-
cached = signal([], ...(ngDevMode ? [{ debugName: "cached" }] : []));
|
|
844
|
+
cached = signal([], ...(ngDevMode ? [{ debugName: "cached" }] : /* istanbul ignore next */ []));
|
|
845
845
|
/** Loading flag of the current operation. */
|
|
846
|
-
loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : []));
|
|
846
|
+
loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
847
847
|
/** Last request error (if any). */
|
|
848
|
-
error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : []));
|
|
848
|
+
error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
|
|
849
849
|
/** Increments when the current dataset is reset/replaced. Useful for external consumers with local buffers. */
|
|
850
|
-
version = signal(0, ...(ngDevMode ? [{ debugName: "version" }] : []));
|
|
851
|
-
#page = signal(0, ...(ngDevMode ? [{ debugName: "#page" }] : []));
|
|
852
|
-
#pageSize = signal(20, ...(ngDevMode ? [{ debugName: "#pageSize" }] : []));
|
|
853
|
-
#totalElements = signal(
|
|
854
|
-
#filters = signal({}, ...(ngDevMode ? [{ debugName: "#filters" }] : []));
|
|
855
|
-
#query = signal({}, ...(ngDevMode ? [{ debugName: "#query" }] : []));
|
|
856
|
-
#sort = signal([], ...(ngDevMode ? [{ debugName: "#sort" }] : []));
|
|
857
|
-
#routeParams = signal({}, ...(ngDevMode ? [{ debugName: "#routeParams" }] : []));
|
|
850
|
+
version = signal(0, ...(ngDevMode ? [{ debugName: "version" }] : /* istanbul ignore next */ []));
|
|
851
|
+
#page = signal(0, ...(ngDevMode ? [{ debugName: "#page" }] : /* istanbul ignore next */ []));
|
|
852
|
+
#pageSize = signal(20, ...(ngDevMode ? [{ debugName: "#pageSize" }] : /* istanbul ignore next */ []));
|
|
853
|
+
#totalElements = signal(undefined, ...(ngDevMode ? [{ debugName: "#totalElements" }] : /* istanbul ignore next */ []));
|
|
854
|
+
#filters = signal({}, ...(ngDevMode ? [{ debugName: "#filters" }] : /* istanbul ignore next */ []));
|
|
855
|
+
#query = signal({}, ...(ngDevMode ? [{ debugName: "#query" }] : /* istanbul ignore next */ []));
|
|
856
|
+
#sort = signal([], ...(ngDevMode ? [{ debugName: "#sort" }] : /* istanbul ignore next */ []));
|
|
857
|
+
#routeParams = signal({}, ...(ngDevMode ? [{ debugName: "#routeParams" }] : /* istanbul ignore next */ []));
|
|
858
858
|
pageState = this.#page.asReadonly();
|
|
859
859
|
pageSizeState = this.#pageSize.asReadonly();
|
|
860
860
|
totalElementsState = this.#totalElements.asReadonly();
|
|
@@ -891,7 +891,7 @@ class PagedQueryStore {
|
|
|
891
891
|
get pageSize() {
|
|
892
892
|
return untracked(this.#pageSize);
|
|
893
893
|
}
|
|
894
|
-
/** Total number of elements reported by the server. */
|
|
894
|
+
/** Total number of elements reported by the server, or `undefined` when the total is unknown. */
|
|
895
895
|
get totalElements() {
|
|
896
896
|
return untracked(this.#totalElements);
|
|
897
897
|
}
|
|
@@ -931,6 +931,11 @@ class PagedQueryStore {
|
|
|
931
931
|
const nextFilters = filters == null ? this.filters : { ...this.filters, ...filters };
|
|
932
932
|
const nextQuery = query == null ? this.query : mergeQueryParams(this.query, query);
|
|
933
933
|
const nextSort = sort == null ? this.sort : this.normalizeSort(sort);
|
|
934
|
+
const shouldResetDataset = !deepEqual(this.filters, nextFilters) || !deepEqual(this.query, nextQuery) || !deepEqual(this.sort, nextSort);
|
|
935
|
+
if (shouldResetDataset) {
|
|
936
|
+
this.resetDataset();
|
|
937
|
+
return this.#fetchItems({ page: 0, filters: nextFilters, query: nextQuery, sort: nextSort });
|
|
938
|
+
}
|
|
934
939
|
return this.#fetchItems({ filters: nextFilters, query: nextQuery, sort: nextSort });
|
|
935
940
|
}
|
|
936
941
|
/**
|
|
@@ -1169,7 +1174,7 @@ class PagedQueryStore {
|
|
|
1169
1174
|
}
|
|
1170
1175
|
resetDataset() {
|
|
1171
1176
|
this.page = 0;
|
|
1172
|
-
this.totalElements =
|
|
1177
|
+
this.totalElements = undefined;
|
|
1173
1178
|
this.#cache.clear();
|
|
1174
1179
|
this.cached.set([]);
|
|
1175
1180
|
this.items.set([]);
|
|
@@ -1285,19 +1290,19 @@ class DictStore {
|
|
|
1285
1290
|
metaStorage;
|
|
1286
1291
|
ttlMs;
|
|
1287
1292
|
revalidate;
|
|
1288
|
-
cacheUpdatedAt = signal(null, ...(ngDevMode ? [{ debugName: "cacheUpdatedAt" }] : []));
|
|
1293
|
+
cacheUpdatedAt = signal(null, ...(ngDevMode ? [{ debugName: "cacheUpdatedAt" }] : /* istanbul ignore next */ []));
|
|
1289
1294
|
presetFilters = {};
|
|
1290
1295
|
/**
|
|
1291
1296
|
* Search text.
|
|
1292
1297
|
* With `fixed: true` filters the local cache; with `fixed: false` triggers server search.
|
|
1293
1298
|
*/
|
|
1294
|
-
searchText = signal('', ...(ngDevMode ? [{ debugName: "searchText" }] : []));
|
|
1299
|
+
searchText = signal('', ...(ngDevMode ? [{ debugName: "searchText" }] : /* istanbul ignore next */ []));
|
|
1295
1300
|
debouncedSearchText;
|
|
1296
1301
|
/**
|
|
1297
1302
|
* Additional filters for server request (or presets).
|
|
1298
1303
|
*/
|
|
1299
|
-
filters = signal({}, ...(ngDevMode ? [{ debugName: "filters" }] : []));
|
|
1300
|
-
cachedItems = signal([], ...(ngDevMode ? [{ debugName: "cachedItems" }] : []));
|
|
1304
|
+
filters = signal({}, ...(ngDevMode ? [{ debugName: "filters" }] : /* istanbul ignore next */ []));
|
|
1305
|
+
cachedItems = signal([], ...(ngDevMode ? [{ debugName: "cachedItems" }] : /* istanbul ignore next */ []));
|
|
1301
1306
|
/**
|
|
1302
1307
|
* Current list of dictionary items.
|
|
1303
1308
|
* Source — local cache (fixed=true) or data from `PagedQueryStore`.
|
|
@@ -1308,7 +1313,7 @@ class DictStore {
|
|
|
1308
1313
|
return this.debouncedSearchText() || !cached.length ? this.#helper.items() : cached;
|
|
1309
1314
|
}
|
|
1310
1315
|
return cached.length ? this.filterLocal() : this.#helper.items();
|
|
1311
|
-
}, ...(ngDevMode ? [{ debugName: "items" }] : []));
|
|
1316
|
+
}, ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
|
|
1312
1317
|
/**
|
|
1313
1318
|
* Ready-to-use dropdown options: `{ label, value }`.
|
|
1314
1319
|
* Respects `maxOptionsSize` for truncating the list.
|
|
@@ -1316,9 +1321,9 @@ class DictStore {
|
|
|
1316
1321
|
options = computed(() => {
|
|
1317
1322
|
const options = this.items().map((it) => ({ label: String(it[this.labelKey] ?? ''), value: it[this.valueKey] }));
|
|
1318
1323
|
return this.maxOptionsSize ? options.slice(0, this.maxOptionsSize) : options;
|
|
1319
|
-
}, ...(ngDevMode ? [{ debugName: "options" }] : []));
|
|
1324
|
+
}, ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
|
|
1320
1325
|
_lastPromise = null;
|
|
1321
|
-
_armed = signal(false, ...(ngDevMode ? [{ debugName: "_armed" }] : []));
|
|
1326
|
+
_armed = signal(false, ...(ngDevMode ? [{ debugName: "_armed" }] : /* istanbul ignore next */ []));
|
|
1322
1327
|
// todo add i18n support
|
|
1323
1328
|
/**
|
|
1324
1329
|
* @param apiUrl dictionary endpoint (e.g., `'/api/dicts/countries'`)
|
|
@@ -1574,8 +1579,8 @@ class DictLocalStore {
|
|
|
1574
1579
|
* Represents the full, unfiltered list of dictionary entries.
|
|
1575
1580
|
* Used as the base data set for search and option generation.
|
|
1576
1581
|
*/
|
|
1577
|
-
items = signal([], ...(ngDevMode ? [{ debugName: "items" }] : []));
|
|
1578
|
-
#draftItems = signal([], ...(ngDevMode ? [{ debugName: "#draftItems" }] : []));
|
|
1582
|
+
items = signal([], ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
|
|
1583
|
+
#draftItems = signal([], ...(ngDevMode ? [{ debugName: "#draftItems" }] : /* istanbul ignore next */ []));
|
|
1579
1584
|
/**
|
|
1580
1585
|
* Computed list of options in `{ label, value }` format.
|
|
1581
1586
|
*
|
|
@@ -1590,7 +1595,7 @@ class DictLocalStore {
|
|
|
1590
1595
|
value: it[this.valueKey],
|
|
1591
1596
|
}));
|
|
1592
1597
|
return this.maxOptionsSize ? options.slice(0, this.maxOptionsSize) : options;
|
|
1593
|
-
}, ...(ngDevMode ? [{ debugName: "options" }] : []));
|
|
1598
|
+
}, ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
|
|
1594
1599
|
labelKey;
|
|
1595
1600
|
valueKey;
|
|
1596
1601
|
maxOptionsSize;
|
|
@@ -1675,14 +1680,14 @@ class DictLocalStore {
|
|
|
1675
1680
|
class EntityStore {
|
|
1676
1681
|
idKey;
|
|
1677
1682
|
sortIds;
|
|
1678
|
-
byId = signal({}, ...(ngDevMode ? [{ debugName: "byId" }] : []));
|
|
1679
|
-
ids = signal([], ...(ngDevMode ? [{ debugName: "ids" }] : []));
|
|
1683
|
+
byId = signal({}, ...(ngDevMode ? [{ debugName: "byId" }] : /* istanbul ignore next */ []));
|
|
1684
|
+
ids = signal([], ...(ngDevMode ? [{ debugName: "ids" }] : /* istanbul ignore next */ []));
|
|
1680
1685
|
items = computed(() => {
|
|
1681
1686
|
const byId = this.byId();
|
|
1682
1687
|
return this.ids()
|
|
1683
1688
|
.map((id) => byId[String(id)])
|
|
1684
1689
|
.filter((item) => item !== undefined);
|
|
1685
|
-
}, ...(ngDevMode ? [{ debugName: "items" }] : []));
|
|
1690
|
+
}, ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
|
|
1686
1691
|
constructor(config) {
|
|
1687
1692
|
this.idKey = config.idKey;
|
|
1688
1693
|
this.sortIds = config.sortIds;
|
package/package.json
CHANGED
|
@@ -505,7 +505,7 @@ declare class PagedQueryStore<ItemsType extends AnyDict, FilterType extends AnyD
|
|
|
505
505
|
version: WritableSignal<number>;
|
|
506
506
|
readonly pageState: _angular_core.Signal<number>;
|
|
507
507
|
readonly pageSizeState: _angular_core.Signal<number>;
|
|
508
|
-
readonly totalElementsState: _angular_core.Signal<number>;
|
|
508
|
+
readonly totalElementsState: _angular_core.Signal<number | undefined>;
|
|
509
509
|
readonly filtersState: _angular_core.Signal<Partial<FilterType>>;
|
|
510
510
|
readonly queryState: _angular_core.Signal<AnyDict>;
|
|
511
511
|
readonly sortState: _angular_core.Signal<QuerySortRule[]>;
|
|
@@ -523,15 +523,15 @@ declare class PagedQueryStore<ItemsType extends AnyDict, FilterType extends AnyD
|
|
|
523
523
|
get page(): number;
|
|
524
524
|
/** Default page size. */
|
|
525
525
|
get pageSize(): number;
|
|
526
|
-
/** Total number of elements reported by the server. */
|
|
527
|
-
get totalElements(): number;
|
|
526
|
+
/** Total number of elements reported by the server, or `undefined` when the total is unknown. */
|
|
527
|
+
get totalElements(): number | undefined;
|
|
528
528
|
/** Current sort rules. */
|
|
529
529
|
get sort(): QuerySortRule[];
|
|
530
530
|
set filters(value: Partial<FilterType>);
|
|
531
531
|
set query(value: AnyDict);
|
|
532
532
|
set page(value: number);
|
|
533
533
|
set pageSize(value: number);
|
|
534
|
-
set totalElements(value: number);
|
|
534
|
+
set totalElements(value: number | undefined);
|
|
535
535
|
/**
|
|
536
536
|
* Fetch data with explicit filters and query params from the first page.
|
|
537
537
|
* Replaces legacy `setFilters`.
|