@reforgium/statum 3.1.1 → 3.1.3
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 +14 -0
- package/fesm2022/reforgium-statum.mjs +48 -45
- package/package.json +1 -1
- package/types/reforgium-statum.d.ts +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
## [3.1.3]: 2026-04-06
|
|
2
|
+
|
|
3
|
+
### Fix:
|
|
4
|
+
- `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`, …)
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## [3.1.2]: 2026-04-06
|
|
9
|
+
|
|
10
|
+
### Fix:
|
|
11
|
+
- `DictStore`: `presetFilters` were passed to the internal `PagedQueryStore` as its initial filter state, which was immediately overwritten on the first `fetch()` call; `presetFilters` are now stored directly on `DictStore` and explicitly merged into every `fetch` call as `{ name, ...presetFilters, ...runtimeFilters }`, so direction/status constraints are always included regardless of runtime filter state
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
1
15
|
## [3.1.1]: 2026-04-04
|
|
2
16
|
|
|
3
17
|
### Refactor:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Serializer, fillUrlWithParams, mergeQueryParams, LruCache, deepEqual, isNullable, normalizeSortInput, sortInputToTokens, debounceSignal, storageStrategy } from '@reforgium/internal';
|
|
2
2
|
export { LocalStorage, LruCache, MemoryStorage, Serializer, SerializerFieldError, SessionStorage, storageStrategy } from '@reforgium/internal';
|
|
3
3
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
|
4
|
-
import { InjectionToken, makeEnvironmentProviders, inject, signal, computed, EnvironmentInjector, DestroyRef, runInInjectionContext, effect
|
|
4
|
+
import { InjectionToken, makeEnvironmentProviders, inject, signal, computed, EnvironmentInjector, DestroyRef, untracked, runInInjectionContext, effect } from '@angular/core';
|
|
5
5
|
import { Subject, filter, timer, merge, map } from 'rxjs';
|
|
6
6
|
import { debounce, tap, throttle, finalize } from 'rxjs/operators';
|
|
7
7
|
|
|
@@ -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" }] : []));
|
|
281
|
+
#status = signal('idle', ...(ngDevMode ? [{ debugName: "#status" }] : []));
|
|
282
|
+
#error = signal(null, ...(ngDevMode ? [{ debugName: "#error" }] : []));
|
|
283
|
+
#activeRequests = signal(0, ...(ngDevMode ? [{ debugName: "#activeRequests" }] : []));
|
|
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" }] : []));
|
|
302
302
|
routes;
|
|
303
303
|
opts;
|
|
304
304
|
maxEntries;
|
|
@@ -819,7 +819,7 @@ const createResourceProfile = (profile, overrides = {}) => ({
|
|
|
819
819
|
* - reactive signals: `items`, `loading`, `cached`;
|
|
820
820
|
* - methods to control page/size/query;
|
|
821
821
|
* - optional LRU cache by pages;
|
|
822
|
-
* - configurable transport (GET/POST/PATCH
|
|
822
|
+
* - configurable transport (GET/POST/PATCH/…).
|
|
823
823
|
*
|
|
824
824
|
* Example:
|
|
825
825
|
* ```ts
|
|
@@ -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" }] : []));
|
|
843
843
|
/** Merged cache of pages (flat list) handy for search/export. */
|
|
844
|
-
cached = signal([], ...(ngDevMode ? [{ debugName: "cached" }] :
|
|
844
|
+
cached = signal([], ...(ngDevMode ? [{ debugName: "cached" }] : []));
|
|
845
845
|
/** Loading flag of the current operation. */
|
|
846
|
-
loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] :
|
|
846
|
+
loading = signal(false, ...(ngDevMode ? [{ debugName: "loading" }] : []));
|
|
847
847
|
/** Last request error (if any). */
|
|
848
|
-
error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] :
|
|
848
|
+
error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : []));
|
|
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(0, ...(ngDevMode ? [{ debugName: "#totalElements" }] :
|
|
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" }] : []));
|
|
851
|
+
#page = signal(0, ...(ngDevMode ? [{ debugName: "#page" }] : []));
|
|
852
|
+
#pageSize = signal(20, ...(ngDevMode ? [{ debugName: "#pageSize" }] : []));
|
|
853
|
+
#totalElements = signal(0, ...(ngDevMode ? [{ debugName: "#totalElements" }] : []));
|
|
854
|
+
#filters = signal({}, ...(ngDevMode ? [{ debugName: "#filters" }] : []));
|
|
855
|
+
#query = signal({}, ...(ngDevMode ? [{ debugName: "#query" }] : []));
|
|
856
|
+
#sort = signal([], ...(ngDevMode ? [{ debugName: "#sort" }] : []));
|
|
857
|
+
#routeParams = signal({}, ...(ngDevMode ? [{ debugName: "#routeParams" }] : []));
|
|
858
858
|
pageState = this.#page.asReadonly();
|
|
859
859
|
pageSizeState = this.#pageSize.asReadonly();
|
|
860
860
|
totalElementsState = this.#totalElements.asReadonly();
|
|
@@ -877,27 +877,27 @@ class PagedQueryStore {
|
|
|
877
877
|
}
|
|
878
878
|
/** Current filters (applied to requests). */
|
|
879
879
|
get filters() {
|
|
880
|
-
return this.#filters
|
|
880
|
+
return untracked(this.#filters);
|
|
881
881
|
}
|
|
882
882
|
/** Additional query params sent to transport requests. */
|
|
883
883
|
get query() {
|
|
884
|
-
return this.#query
|
|
884
|
+
return untracked(this.#query);
|
|
885
885
|
}
|
|
886
886
|
/** Current page index (0-based). */
|
|
887
887
|
get page() {
|
|
888
|
-
return this.#page
|
|
888
|
+
return untracked(this.#page);
|
|
889
889
|
}
|
|
890
890
|
/** Default page size. */
|
|
891
891
|
get pageSize() {
|
|
892
|
-
return this.#pageSize
|
|
892
|
+
return untracked(this.#pageSize);
|
|
893
893
|
}
|
|
894
894
|
/** Total number of elements reported by the server. */
|
|
895
895
|
get totalElements() {
|
|
896
|
-
return this.#totalElements
|
|
896
|
+
return untracked(this.#totalElements);
|
|
897
897
|
}
|
|
898
898
|
/** Current sort rules. */
|
|
899
899
|
get sort() {
|
|
900
|
-
return this.#sort
|
|
900
|
+
return untracked(this.#sort);
|
|
901
901
|
}
|
|
902
902
|
set filters(value) {
|
|
903
903
|
this.#filters.set(value);
|
|
@@ -998,7 +998,7 @@ class PagedQueryStore {
|
|
|
998
998
|
* ```
|
|
999
999
|
*/
|
|
1000
1000
|
setRouteParams = (params = {}, opts = {}) => {
|
|
1001
|
-
const isChanged = !deepEqual(this.#routeParams
|
|
1001
|
+
const isChanged = !deepEqual(untracked(this.#routeParams), params);
|
|
1002
1002
|
if (!isChanged) {
|
|
1003
1003
|
return;
|
|
1004
1004
|
}
|
|
@@ -1046,7 +1046,7 @@ class PagedQueryStore {
|
|
|
1046
1046
|
}
|
|
1047
1047
|
}
|
|
1048
1048
|
}
|
|
1049
|
-
#fetchItems = async ({ page = this.page, size = this.pageSize, filters = this.filters, query = this.query, routeParams = this.#routeParams
|
|
1049
|
+
#fetchItems = async ({ page = this.page, size = this.pageSize, filters = this.filters, query = this.query, routeParams = untracked(this.#routeParams), sort = this.sort, }) => {
|
|
1050
1050
|
const builtQuery = this.parseQuery({ page, size, filters, query, sort });
|
|
1051
1051
|
const requestId = ++this.#requestId;
|
|
1052
1052
|
this.#activeRequests++;
|
|
@@ -1285,21 +1285,22 @@ class DictStore {
|
|
|
1285
1285
|
metaStorage;
|
|
1286
1286
|
ttlMs;
|
|
1287
1287
|
revalidate;
|
|
1288
|
-
cacheUpdatedAt = signal(null, ...(ngDevMode ? [{ debugName: "cacheUpdatedAt" }] :
|
|
1288
|
+
cacheUpdatedAt = signal(null, ...(ngDevMode ? [{ debugName: "cacheUpdatedAt" }] : []));
|
|
1289
|
+
presetFilters = {};
|
|
1289
1290
|
/**
|
|
1290
1291
|
* Search text.
|
|
1291
1292
|
* With `fixed: true` filters the local cache; with `fixed: false` triggers server search.
|
|
1292
1293
|
*/
|
|
1293
|
-
searchText = signal('', ...(ngDevMode ? [{ debugName: "searchText" }] :
|
|
1294
|
+
searchText = signal('', ...(ngDevMode ? [{ debugName: "searchText" }] : []));
|
|
1294
1295
|
debouncedSearchText;
|
|
1295
1296
|
/**
|
|
1296
1297
|
* Additional filters for server request (or presets).
|
|
1297
1298
|
*/
|
|
1298
|
-
filters = signal({}, ...(ngDevMode ? [{ debugName: "filters" }] :
|
|
1299
|
-
cachedItems = signal([], ...(ngDevMode ? [{ debugName: "cachedItems" }] :
|
|
1299
|
+
filters = signal({}, ...(ngDevMode ? [{ debugName: "filters" }] : []));
|
|
1300
|
+
cachedItems = signal([], ...(ngDevMode ? [{ debugName: "cachedItems" }] : []));
|
|
1300
1301
|
/**
|
|
1301
1302
|
* Current list of dictionary items.
|
|
1302
|
-
* Source
|
|
1303
|
+
* Source — local cache (fixed=true) or data from `PagedQueryStore`.
|
|
1303
1304
|
*/
|
|
1304
1305
|
items = computed(() => {
|
|
1305
1306
|
const cached = this.cachedItems();
|
|
@@ -1307,7 +1308,7 @@ class DictStore {
|
|
|
1307
1308
|
return this.debouncedSearchText() || !cached.length ? this.#helper.items() : cached;
|
|
1308
1309
|
}
|
|
1309
1310
|
return cached.length ? this.filterLocal() : this.#helper.items();
|
|
1310
|
-
}, ...(ngDevMode ? [{ debugName: "items" }] :
|
|
1311
|
+
}, ...(ngDevMode ? [{ debugName: "items" }] : []));
|
|
1311
1312
|
/**
|
|
1312
1313
|
* Ready-to-use dropdown options: `{ label, value }`.
|
|
1313
1314
|
* Respects `maxOptionsSize` for truncating the list.
|
|
@@ -1315,9 +1316,9 @@ class DictStore {
|
|
|
1315
1316
|
options = computed(() => {
|
|
1316
1317
|
const options = this.items().map((it) => ({ label: String(it[this.labelKey] ?? ''), value: it[this.valueKey] }));
|
|
1317
1318
|
return this.maxOptionsSize ? options.slice(0, this.maxOptionsSize) : options;
|
|
1318
|
-
}, ...(ngDevMode ? [{ debugName: "options" }] :
|
|
1319
|
+
}, ...(ngDevMode ? [{ debugName: "options" }] : []));
|
|
1319
1320
|
_lastPromise = null;
|
|
1320
|
-
_armed = signal(false, ...(ngDevMode ? [{ debugName: "_armed" }] :
|
|
1321
|
+
_armed = signal(false, ...(ngDevMode ? [{ debugName: "_armed" }] : []));
|
|
1321
1322
|
// todo add i18n support
|
|
1322
1323
|
/**
|
|
1323
1324
|
* @param apiUrl dictionary endpoint (e.g., `'/api/dicts/countries'`)
|
|
@@ -1329,10 +1330,10 @@ class DictStore {
|
|
|
1329
1330
|
this.storageKey = storageKey;
|
|
1330
1331
|
const searchDebounce = debounceTime ?? 300;
|
|
1331
1332
|
this.debouncedSearchText = debounceSignal(this.searchText, searchDebounce);
|
|
1333
|
+
this.presetFilters = presetFilters ?? {};
|
|
1332
1334
|
this.#helper = new PagedQueryStore(this.apiUrl, {
|
|
1333
1335
|
method: method,
|
|
1334
1336
|
hasCache: false,
|
|
1335
|
-
presetFilters: { name: '', ...presetFilters },
|
|
1336
1337
|
parseResponse: parseResponse,
|
|
1337
1338
|
parseRequest: parseRequest,
|
|
1338
1339
|
debounceTime: debounceTime,
|
|
@@ -1366,12 +1367,14 @@ class DictStore {
|
|
|
1366
1367
|
if (!this.fixed) {
|
|
1367
1368
|
const query = this.debouncedSearchText().trim();
|
|
1368
1369
|
untracked(() => {
|
|
1369
|
-
this._lastPromise = this.#helper.fetch({ filters: { name: query, ...rest } });
|
|
1370
|
+
this._lastPromise = this.#helper.fetch({ filters: { name: query, ...this.presetFilters, ...rest } });
|
|
1370
1371
|
});
|
|
1371
1372
|
}
|
|
1372
1373
|
else if (this.shouldFetchFixedCache()) {
|
|
1373
1374
|
untracked(() => {
|
|
1374
|
-
this._lastPromise = this.#helper
|
|
1375
|
+
this._lastPromise = this.#helper
|
|
1376
|
+
.fetch({ filters: { name: '', ...this.presetFilters, ...rest } })
|
|
1377
|
+
.then((items) => {
|
|
1375
1378
|
items?.length && this.mergeIntoCache(items);
|
|
1376
1379
|
return items;
|
|
1377
1380
|
});
|
|
@@ -1392,7 +1395,7 @@ class DictStore {
|
|
|
1392
1395
|
}
|
|
1393
1396
|
/**
|
|
1394
1397
|
* Set a search query and filters.
|
|
1395
|
-
* With `fixed: false` initiates server search; with `fixed: true`
|
|
1398
|
+
* With `fixed: false` initiates server search; with `fixed: true` — local filtering.
|
|
1396
1399
|
*/
|
|
1397
1400
|
search = (name = '', filters = {}) => {
|
|
1398
1401
|
this._armed.set(true);
|
|
@@ -1571,8 +1574,8 @@ class DictLocalStore {
|
|
|
1571
1574
|
* Represents the full, unfiltered list of dictionary entries.
|
|
1572
1575
|
* Used as the base data set for search and option generation.
|
|
1573
1576
|
*/
|
|
1574
|
-
items = signal([], ...(ngDevMode ? [{ debugName: "items" }] :
|
|
1575
|
-
#draftItems = signal([], ...(ngDevMode ? [{ debugName: "#draftItems" }] :
|
|
1577
|
+
items = signal([], ...(ngDevMode ? [{ debugName: "items" }] : []));
|
|
1578
|
+
#draftItems = signal([], ...(ngDevMode ? [{ debugName: "#draftItems" }] : []));
|
|
1576
1579
|
/**
|
|
1577
1580
|
* Computed list of options in `{ label, value }` format.
|
|
1578
1581
|
*
|
|
@@ -1587,7 +1590,7 @@ class DictLocalStore {
|
|
|
1587
1590
|
value: it[this.valueKey],
|
|
1588
1591
|
}));
|
|
1589
1592
|
return this.maxOptionsSize ? options.slice(0, this.maxOptionsSize) : options;
|
|
1590
|
-
}, ...(ngDevMode ? [{ debugName: "options" }] :
|
|
1593
|
+
}, ...(ngDevMode ? [{ debugName: "options" }] : []));
|
|
1591
1594
|
labelKey;
|
|
1592
1595
|
valueKey;
|
|
1593
1596
|
maxOptionsSize;
|
|
@@ -1672,14 +1675,14 @@ class DictLocalStore {
|
|
|
1672
1675
|
class EntityStore {
|
|
1673
1676
|
idKey;
|
|
1674
1677
|
sortIds;
|
|
1675
|
-
byId = signal({}, ...(ngDevMode ? [{ debugName: "byId" }] :
|
|
1676
|
-
ids = signal([], ...(ngDevMode ? [{ debugName: "ids" }] :
|
|
1678
|
+
byId = signal({}, ...(ngDevMode ? [{ debugName: "byId" }] : []));
|
|
1679
|
+
ids = signal([], ...(ngDevMode ? [{ debugName: "ids" }] : []));
|
|
1677
1680
|
items = computed(() => {
|
|
1678
1681
|
const byId = this.byId();
|
|
1679
1682
|
return this.ids()
|
|
1680
1683
|
.map((id) => byId[String(id)])
|
|
1681
1684
|
.filter((item) => item !== undefined);
|
|
1682
|
-
}, ...(ngDevMode ? [{ debugName: "items" }] :
|
|
1685
|
+
}, ...(ngDevMode ? [{ debugName: "items" }] : []));
|
|
1683
1686
|
constructor(config) {
|
|
1684
1687
|
this.idKey = config.idKey;
|
|
1685
1688
|
this.sortIds = config.sortIds;
|
package/package.json
CHANGED
|
@@ -477,7 +477,7 @@ type PagedQueryStoreProviderConfig = {
|
|
|
477
477
|
* - reactive signals: `items`, `loading`, `cached`;
|
|
478
478
|
* - methods to control page/size/query;
|
|
479
479
|
* - optional LRU cache by pages;
|
|
480
|
-
* - configurable transport (GET/POST/PATCH
|
|
480
|
+
* - configurable transport (GET/POST/PATCH/…).
|
|
481
481
|
*
|
|
482
482
|
* Example:
|
|
483
483
|
* ```ts
|
|
@@ -761,6 +761,7 @@ declare class DictStore<Type extends AnyDict> {
|
|
|
761
761
|
private readonly ttlMs?;
|
|
762
762
|
private readonly revalidate;
|
|
763
763
|
private readonly cacheUpdatedAt;
|
|
764
|
+
private readonly presetFilters;
|
|
764
765
|
/**
|
|
765
766
|
* Search text.
|
|
766
767
|
* With `fixed: true` filters the local cache; with `fixed: false` triggers server search.
|
|
@@ -774,7 +775,7 @@ declare class DictStore<Type extends AnyDict> {
|
|
|
774
775
|
private cachedItems;
|
|
775
776
|
/**
|
|
776
777
|
* Current list of dictionary items.
|
|
777
|
-
* Source
|
|
778
|
+
* Source — local cache (fixed=true) or data from `PagedQueryStore`.
|
|
778
779
|
*/
|
|
779
780
|
items: Signal<readonly Type[]>;
|
|
780
781
|
/**
|
|
@@ -798,7 +799,7 @@ declare class DictStore<Type extends AnyDict> {
|
|
|
798
799
|
clearCache(): void;
|
|
799
800
|
/**
|
|
800
801
|
* Set a search query and filters.
|
|
801
|
-
* With `fixed: false` initiates server search; with `fixed: true`
|
|
802
|
+
* With `fixed: false` initiates server search; with `fixed: true` — local filtering.
|
|
802
803
|
*/
|
|
803
804
|
search: (name?: string, filters?: AnyDict) => void;
|
|
804
805
|
/**
|