@reforgium/statum 1.0.2 → 2.0.0
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/README.md +18 -16
- package/fesm2022/reforgium-statum.mjs +198 -58
- package/fesm2022/reforgium-statum.mjs.map +1 -1
- package/package.json +2 -1
- package/types/reforgium-statum.d.ts +157 -23
- package/types/reforgium-statum.d.ts.map +1 -1
package/README.md
CHANGED
|
@@ -58,6 +58,7 @@ All cache strategies implement the same contract.
|
|
|
58
58
|
|
|
59
59
|
```ts
|
|
60
60
|
interface StorageInterface<T> {
|
|
61
|
+
prefix?: string;
|
|
61
62
|
get(key: string): T | null;
|
|
62
63
|
set(key: string, value: T): void;
|
|
63
64
|
remove(key: string): void;
|
|
@@ -148,16 +149,16 @@ Lightweight store for server-side pagination with sorting, filtering and page ca
|
|
|
148
149
|
|
|
149
150
|
### State
|
|
150
151
|
|
|
151
|
-
| Field / Signal | Type
|
|
152
|
-
|
|
153
|
-
| items
|
|
154
|
-
| cached
|
|
155
|
-
| loading
|
|
156
|
-
| page
|
|
157
|
-
| pageSize
|
|
158
|
-
| totalElements
|
|
159
|
-
| filters
|
|
160
|
-
| sort
|
|
152
|
+
| Field / Signal | Type | Description |
|
|
153
|
+
|----------------|---------------------------|---------------------------------|
|
|
154
|
+
| items | `WritableSignal<T[]>` | Current page items |
|
|
155
|
+
| cached | `WritableSignal<T[]>` | Flattened cache of cached pages |
|
|
156
|
+
| loading | `WritableSignal<boolean>` | Loading indicator |
|
|
157
|
+
| page | `number` | Current page (0-based) |
|
|
158
|
+
| pageSize | `number` | Page size |
|
|
159
|
+
| totalElements | `number` | Total items on server |
|
|
160
|
+
| filters | `Partial<F>` | Active filters |
|
|
161
|
+
| sort | `string \| undefined` | Sort as `"field,asc |
|
|
161
162
|
|
|
162
163
|
### Methods
|
|
163
164
|
|
|
@@ -170,6 +171,7 @@ Lightweight store for server-side pagination with sorting, filtering and page ca
|
|
|
170
171
|
| setFilters | Replace filters, reset cache + sort, go to page 0 |
|
|
171
172
|
| updateQuery | Map table events to `page`/`sort` |
|
|
172
173
|
| updateRoute | Change endpoint, reset meta/cache |
|
|
174
|
+
| setRouteParams | Fill route url with params |
|
|
173
175
|
| updateConfig | Patch config and apply presets |
|
|
174
176
|
| copy | Copy config/meta from another store |
|
|
175
177
|
| destroy | Manual destroying of caches and abort requests |
|
|
@@ -270,12 +272,12 @@ Utility for serialization/deserialization between layers (UI ↔ API, objects
|
|
|
270
272
|
|
|
271
273
|
Core API:
|
|
272
274
|
|
|
273
|
-
| Method
|
|
274
|
-
|
|
275
|
-
| serialize
|
|
276
|
-
| deserialize | Parse incoming data / query string
|
|
277
|
-
| toQuery
|
|
278
|
-
| withConfig
|
|
275
|
+
| Method | Description |
|
|
276
|
+
|-------------|------------------------------------------------|
|
|
277
|
+
| serialize | Prepare data for transport |
|
|
278
|
+
| deserialize | Parse incoming data / query string |
|
|
279
|
+
| toQuery | Build query string |
|
|
280
|
+
| withConfig | Clone serializer with partial config overrides |
|
|
279
281
|
|
|
280
282
|
Config:
|
|
281
283
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { formatDate, isNullable, isDatePeriod, parseToDate, makeQuery, isNumber, isObject, parseToDatePeriod, parseQueryArray, fillUrlWithParams, concatArray } from '@reforgium/internal';
|
|
2
|
-
import { InjectionToken, inject, signal, computed, DestroyRef, effect, untracked } from '@angular/core';
|
|
1
|
+
import { formatDate, isNullable, isDatePeriod, parseToDate, makeQuery, isNumber, isObject, parseToDatePeriod, parseQueryArray, fillUrlWithParams, deepEqual, concatArray, debounceSignal } from '@reforgium/internal';
|
|
3
2
|
import { HttpClient } from '@angular/common/http';
|
|
3
|
+
import { InjectionToken, inject, signal, computed, DestroyRef, effect, untracked } from '@angular/core';
|
|
4
4
|
import { Subject, filter, timer, merge, map } from 'rxjs';
|
|
5
5
|
import { debounce, tap, throttle, finalize } from 'rxjs/operators';
|
|
6
6
|
|
|
@@ -311,8 +311,6 @@ class Serializer {
|
|
|
311
311
|
}
|
|
312
312
|
}
|
|
313
313
|
|
|
314
|
-
const SERIALIZER_CONFIG = new InjectionToken('SERIALIZER_CONFIG');
|
|
315
|
-
|
|
316
314
|
class LruCache {
|
|
317
315
|
limit;
|
|
318
316
|
map = new Map();
|
|
@@ -368,23 +366,31 @@ class LruCache {
|
|
|
368
366
|
}
|
|
369
367
|
|
|
370
368
|
class LocalStorage {
|
|
369
|
+
prefix;
|
|
370
|
+
constructor(prefix = 're') {
|
|
371
|
+
this.prefix = prefix;
|
|
372
|
+
}
|
|
371
373
|
get length() {
|
|
372
|
-
return localStorage.length;
|
|
374
|
+
return Object.keys(localStorage).filter((key) => key.startsWith(this.prefix || '')).length;
|
|
373
375
|
}
|
|
374
376
|
get(key) {
|
|
375
|
-
const raw = localStorage.getItem(
|
|
377
|
+
const raw = localStorage.getItem(this.getSafePrefix(key));
|
|
376
378
|
const parsed = JSON.parse(raw ?? 'null');
|
|
377
379
|
return parsed ?? null;
|
|
378
380
|
}
|
|
379
381
|
set(key, value) {
|
|
380
382
|
const str = JSON.stringify(value);
|
|
381
|
-
localStorage.setItem(
|
|
383
|
+
localStorage.setItem(this.getSafePrefix(key), str);
|
|
382
384
|
}
|
|
383
385
|
remove(key) {
|
|
384
|
-
return localStorage.removeItem(
|
|
386
|
+
return localStorage.removeItem(this.getSafePrefix(key));
|
|
385
387
|
}
|
|
386
388
|
clear() {
|
|
387
|
-
|
|
389
|
+
const keys = Object.keys(localStorage).filter((key) => key.startsWith(this.prefix || ''));
|
|
390
|
+
keys.forEach((key) => localStorage.removeItem(key));
|
|
391
|
+
}
|
|
392
|
+
getSafePrefix(key) {
|
|
393
|
+
return this.prefix ? `${this.prefix}:${key}` : String(key);
|
|
388
394
|
}
|
|
389
395
|
}
|
|
390
396
|
|
|
@@ -408,23 +414,31 @@ class MemoryStorage {
|
|
|
408
414
|
}
|
|
409
415
|
|
|
410
416
|
class SessionStorage {
|
|
417
|
+
prefix;
|
|
418
|
+
constructor(prefix = 're') {
|
|
419
|
+
this.prefix = prefix;
|
|
420
|
+
}
|
|
411
421
|
get length() {
|
|
412
|
-
return sessionStorage.length;
|
|
422
|
+
return Object.keys(sessionStorage).filter((key) => key.startsWith(this.prefix || '')).length;
|
|
413
423
|
}
|
|
414
424
|
get(key) {
|
|
415
|
-
const raw = sessionStorage.getItem(
|
|
425
|
+
const raw = sessionStorage.getItem(this.getSafePrefix(key));
|
|
416
426
|
const parsed = JSON.parse(raw ?? 'null');
|
|
417
427
|
return parsed ?? null;
|
|
418
428
|
}
|
|
419
429
|
set(key, value) {
|
|
420
430
|
const str = JSON.stringify(value);
|
|
421
|
-
sessionStorage.setItem(
|
|
431
|
+
sessionStorage.setItem(this.getSafePrefix(key), str);
|
|
422
432
|
}
|
|
423
433
|
remove(key) {
|
|
424
|
-
return sessionStorage.removeItem(
|
|
434
|
+
return sessionStorage.removeItem(this.getSafePrefix(key));
|
|
425
435
|
}
|
|
426
436
|
clear() {
|
|
427
|
-
|
|
437
|
+
const keys = Object.keys(sessionStorage).filter((key) => key.startsWith(this.prefix || ''));
|
|
438
|
+
keys.forEach((key) => sessionStorage.removeItem(key));
|
|
439
|
+
}
|
|
440
|
+
getSafePrefix(key) {
|
|
441
|
+
return this.prefix ? `${this.prefix}:${key}` : String(key);
|
|
428
442
|
}
|
|
429
443
|
}
|
|
430
444
|
|
|
@@ -453,6 +467,9 @@ const storageStrategy = (strategy) => {
|
|
|
453
467
|
return fabrics[strategy]();
|
|
454
468
|
};
|
|
455
469
|
|
|
470
|
+
// noinspection ES6PreferShortImport
|
|
471
|
+
const STATUM_CONFIG = new InjectionToken('RE_STATUM_CONFIG');
|
|
472
|
+
|
|
456
473
|
/**
|
|
457
474
|
* Error thrown when requested data is missing in the cache.
|
|
458
475
|
*
|
|
@@ -501,8 +518,14 @@ function isAbort(e) {
|
|
|
501
518
|
function joinUrl(base, path) {
|
|
502
519
|
return base ? `${base.replace(/\/$/, '')}/${path.replace(/^\//, '')}` : path;
|
|
503
520
|
}
|
|
504
|
-
function buildKey(method, path) {
|
|
505
|
-
|
|
521
|
+
function buildKey(method, path, args) {
|
|
522
|
+
const params = Object.entries(args.params || {})
|
|
523
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
524
|
+
.join('&');
|
|
525
|
+
const query = Object.entries(args.query || {})
|
|
526
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
527
|
+
.join('&');
|
|
528
|
+
return `${method}|${path}|${params}|${query}`;
|
|
506
529
|
}
|
|
507
530
|
|
|
508
531
|
/**
|
|
@@ -641,10 +664,11 @@ class KeyedScheduler {
|
|
|
641
664
|
*/
|
|
642
665
|
class ResourceStore {
|
|
643
666
|
http = inject(HttpClient);
|
|
644
|
-
serializer = new Serializer(inject(
|
|
667
|
+
serializer = new Serializer(inject(STATUM_CONFIG, { optional: true })?.serializer ?? {});
|
|
645
668
|
#value = signal(null, ...(ngDevMode ? [{ debugName: "#value" }] : []));
|
|
646
669
|
#status = signal('idle', ...(ngDevMode ? [{ debugName: "#status" }] : []));
|
|
647
670
|
#error = signal(null, ...(ngDevMode ? [{ debugName: "#error" }] : []));
|
|
671
|
+
#activeRequests = signal(0, ...(ngDevMode ? [{ debugName: "#activeRequests" }] : []));
|
|
648
672
|
/**
|
|
649
673
|
* Current resource value.
|
|
650
674
|
* Returns `null` if no data yet or the request failed.
|
|
@@ -662,7 +686,7 @@ class ResourceStore {
|
|
|
662
686
|
* Convenience loading flag: `true` when `loading` or `stale`.
|
|
663
687
|
* Useful for spinners and disabling buttons.
|
|
664
688
|
*/
|
|
665
|
-
loading = computed(() =>
|
|
689
|
+
loading = computed(() => this.#activeRequests() > 0 || this.#status() === 'stale', ...(ngDevMode ? [{ debugName: "loading" }] : []));
|
|
666
690
|
routes;
|
|
667
691
|
opts;
|
|
668
692
|
entries = new Map();
|
|
@@ -687,7 +711,7 @@ class ResourceStore {
|
|
|
687
711
|
*/
|
|
688
712
|
async get(args, cfg = {}) {
|
|
689
713
|
const url = this.buildUrl('GET', args);
|
|
690
|
-
const key = buildKey('GET', url);
|
|
714
|
+
const key = buildKey('GET', url, args);
|
|
691
715
|
const query = this.prepareQuery(args);
|
|
692
716
|
const entry = this.ensureEntry(key);
|
|
693
717
|
const strategy = cfg.strategy ?? 'network-first';
|
|
@@ -709,7 +733,8 @@ class ResourceStore {
|
|
|
709
733
|
}
|
|
710
734
|
const delay = cfg.delay ?? this.opts.delay ?? 0;
|
|
711
735
|
const mode = cfg.delayMode ?? this.opts.delayMode ?? 'debounce';
|
|
712
|
-
|
|
736
|
+
const isSWR = strategy === 'cache-first' && entry.data != null;
|
|
737
|
+
entry.status = isSWR ? 'stale' : 'loading';
|
|
713
738
|
this.promoteCurrent(entry, cfg.promote);
|
|
714
739
|
const task = this.scheduler.schedule(key, mode, delay, this.exec$({
|
|
715
740
|
req$: this.http.get(url, { params: query }),
|
|
@@ -774,7 +799,7 @@ class ResourceStore {
|
|
|
774
799
|
*/
|
|
775
800
|
abort(method, args, reason) {
|
|
776
801
|
const url = this.buildUrl(method, args);
|
|
777
|
-
const key = buildKey(method, url);
|
|
802
|
+
const key = buildKey(method, url, args);
|
|
778
803
|
this.scheduler.cancel?.(key, reason);
|
|
779
804
|
}
|
|
780
805
|
/**
|
|
@@ -795,7 +820,7 @@ class ResourceStore {
|
|
|
795
820
|
}
|
|
796
821
|
async callApi(method, args, config = {}) {
|
|
797
822
|
const url = this.buildUrl(method, args);
|
|
798
|
-
const key = buildKey(method, url);
|
|
823
|
+
const key = buildKey(method, url, args);
|
|
799
824
|
const query = this.prepareQuery(args);
|
|
800
825
|
const payload = { ...(this.opts.presetPayload || {}), ...(args.payload || {}) };
|
|
801
826
|
const serializedPayload = this.serializer.serialize(payload);
|
|
@@ -836,23 +861,27 @@ class ResourceStore {
|
|
|
836
861
|
const mergedQuery = { ...(this.opts.presetQueries || {}), ...(args.query || {}) };
|
|
837
862
|
return this.serializer.serialize(mergedQuery);
|
|
838
863
|
}
|
|
839
|
-
exec$ = ({ req$, entry, promote = true, parseFn }) => () =>
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
864
|
+
exec$ = ({ req$, entry, promote = true, parseFn }) => () => {
|
|
865
|
+
promote && this.#activeRequests.update((n) => n + 1);
|
|
866
|
+
return req$.pipe(map((data) => (parseFn ? parseFn(data) : data)), tap({
|
|
867
|
+
next: (data) => {
|
|
868
|
+
promote && (entry.data = data);
|
|
869
|
+
entry.status = 'success';
|
|
870
|
+
entry.updatedAt = Date.now();
|
|
871
|
+
entry.error = null;
|
|
872
|
+
this.promoteCurrent(entry, promote);
|
|
873
|
+
},
|
|
874
|
+
error: (err) => {
|
|
875
|
+
entry.error = err;
|
|
876
|
+
entry.status = 'error';
|
|
877
|
+
this.promoteCurrent(entry, promote);
|
|
878
|
+
},
|
|
879
|
+
}), finalize(() => {
|
|
880
|
+
entry.inflight = undefined;
|
|
881
|
+
promote && this.#activeRequests.update((n) => Math.max(0, n - 1));
|
|
850
882
|
this.promoteCurrent(entry, promote);
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
entry.inflight = undefined;
|
|
854
|
-
this.promoteCurrent(entry, promote);
|
|
855
|
-
}));
|
|
883
|
+
}));
|
|
884
|
+
};
|
|
856
885
|
promoteCurrent(entry, promote = true) {
|
|
857
886
|
if (!promote) {
|
|
858
887
|
return;
|
|
@@ -863,8 +892,6 @@ class ResourceStore {
|
|
|
863
892
|
}
|
|
864
893
|
}
|
|
865
894
|
|
|
866
|
-
const PDS_CONFIG = new InjectionToken('RE_PDS_CONFIG');
|
|
867
|
-
|
|
868
895
|
// noinspection ES6PreferShortImport
|
|
869
896
|
/**
|
|
870
897
|
* Store for paginated data (tables/lists) with per-page cache and unified requests.
|
|
@@ -885,7 +912,7 @@ const PDS_CONFIG = new InjectionToken('RE_PDS_CONFIG');
|
|
|
885
912
|
class PaginatedDataStore {
|
|
886
913
|
route;
|
|
887
914
|
config;
|
|
888
|
-
defaultConfig = inject(
|
|
915
|
+
defaultConfig = inject(STATUM_CONFIG, { optional: true })?.paginatedData || {};
|
|
889
916
|
#transport;
|
|
890
917
|
#cache;
|
|
891
918
|
/** Current page data (reactive). */
|
|
@@ -904,6 +931,7 @@ class PaginatedDataStore {
|
|
|
904
931
|
pageSize = 20;
|
|
905
932
|
/** Total number of elements reported by the server. */
|
|
906
933
|
totalElements = 0;
|
|
934
|
+
routeParams = {};
|
|
907
935
|
/**
|
|
908
936
|
* @param route Resource URL pattern (e.g., `'/users'`)
|
|
909
937
|
* @param config Store behavior: method, cache, request/response parsers, debounce, presets, etc.
|
|
@@ -985,6 +1013,46 @@ class PaginatedDataStore {
|
|
|
985
1013
|
this.cached.set([]);
|
|
986
1014
|
this.initTransport();
|
|
987
1015
|
};
|
|
1016
|
+
/**
|
|
1017
|
+
* Set route parameters (path variables) for the resource URL.
|
|
1018
|
+
* Does not trigger loading automatically — call `refresh()` or `updatePage()` after.
|
|
1019
|
+
*
|
|
1020
|
+
* @param params Dictionary of route parameters (e.g., `{ id: '123' }`)
|
|
1021
|
+
* @param opts Options object
|
|
1022
|
+
* @param opts.reset If `true` (default), resets page to 0, clears cache, total elements count, and items
|
|
1023
|
+
* @param opts.abort If `true`, aborts all active transport requests and sets loading to false
|
|
1024
|
+
*
|
|
1025
|
+
* @example
|
|
1026
|
+
* ```ts
|
|
1027
|
+
* store.setRouteParams({ userId: '42' });
|
|
1028
|
+
* await store.refresh(); // fetch with new params
|
|
1029
|
+
*
|
|
1030
|
+
* // Or with options:
|
|
1031
|
+
* store.setRouteParams({ userId: '42' }, { reset: false, abort: true });
|
|
1032
|
+
* ```
|
|
1033
|
+
*/
|
|
1034
|
+
setRouteParams = (params = {}, opts = {}) => {
|
|
1035
|
+
const isChanged = !deepEqual(this.routeParams, params);
|
|
1036
|
+
if (!isChanged) {
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
this.routeParams = params;
|
|
1040
|
+
if (opts.reset) {
|
|
1041
|
+
this.page = 0;
|
|
1042
|
+
this.totalElements = 0;
|
|
1043
|
+
this.#cache.clear();
|
|
1044
|
+
this.cached.set([]);
|
|
1045
|
+
this.items.set([]);
|
|
1046
|
+
}
|
|
1047
|
+
if (opts?.abort) {
|
|
1048
|
+
try {
|
|
1049
|
+
this.#transport?.abortAll?.('Route params changed');
|
|
1050
|
+
}
|
|
1051
|
+
finally {
|
|
1052
|
+
this.loading.set(false);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
};
|
|
988
1056
|
/**
|
|
989
1057
|
* Update store config on the fly (without re-creation).
|
|
990
1058
|
* Does not trigger loading automatically.
|
|
@@ -1046,12 +1114,13 @@ class PaginatedDataStore {
|
|
|
1046
1114
|
});
|
|
1047
1115
|
}
|
|
1048
1116
|
async runTransport(payload, query) {
|
|
1117
|
+
const params = this.routeParams;
|
|
1049
1118
|
if (this.config.method === 'GET') {
|
|
1050
|
-
return this.#transport.get({ query }, { promote: false, dedupe: true });
|
|
1119
|
+
return this.#transport.get({ query, params }, { promote: false, dedupe: true });
|
|
1051
1120
|
}
|
|
1052
1121
|
const method = this.config.method?.toLowerCase() || 'post';
|
|
1053
1122
|
// @ts-ignore
|
|
1054
|
-
return this.#transport[method]({ query, payload }, { promote: false, dedupe: true });
|
|
1123
|
+
return this.#transport[method]({ query, payload, params }, { promote: false, dedupe: true });
|
|
1055
1124
|
}
|
|
1056
1125
|
parseQuery({ page = 0, size, sort, filters }) {
|
|
1057
1126
|
const method = this.config.method || 'GET';
|
|
@@ -1128,6 +1197,12 @@ class DictStore {
|
|
|
1128
1197
|
apiUrl;
|
|
1129
1198
|
storageKey;
|
|
1130
1199
|
static MAX_CACHE_SIZE = 400;
|
|
1200
|
+
/**
|
|
1201
|
+
* Default dictionary configuration resolved from {@link STATUM_CONFIG}.
|
|
1202
|
+
*
|
|
1203
|
+
* Used as a global fallback when local configuration does not explicitly
|
|
1204
|
+
*/
|
|
1205
|
+
defaultConfig = inject(STATUM_CONFIG, { optional: true })?.dict || {};
|
|
1131
1206
|
#helper;
|
|
1132
1207
|
#lru = new LruCache(_a.MAX_CACHE_SIZE);
|
|
1133
1208
|
#effectRefs = [];
|
|
@@ -1141,6 +1216,7 @@ class DictStore {
|
|
|
1141
1216
|
* With `fixed: true` filters the local cache; with `fixed: false` triggers server search.
|
|
1142
1217
|
*/
|
|
1143
1218
|
searchText = signal('', ...(ngDevMode ? [{ debugName: "searchText" }] : []));
|
|
1219
|
+
debouncedSearchText = debounceSignal(this.searchText, 300);
|
|
1144
1220
|
/**
|
|
1145
1221
|
* Additional filters for server request (or presets).
|
|
1146
1222
|
*/
|
|
@@ -1153,7 +1229,7 @@ class DictStore {
|
|
|
1153
1229
|
items = computed(() => {
|
|
1154
1230
|
const cached = this.cachedItems();
|
|
1155
1231
|
if (!this.fixed) {
|
|
1156
|
-
return this.
|
|
1232
|
+
return this.debouncedSearchText() || !cached.length ? this.#helper.items() : cached;
|
|
1157
1233
|
}
|
|
1158
1234
|
return cached.length ? this.filterLocal() : this.#helper.items();
|
|
1159
1235
|
}, ...(ngDevMode ? [{ debugName: "items" }] : []));
|
|
@@ -1173,7 +1249,7 @@ class DictStore {
|
|
|
1173
1249
|
* @param storageKey key for saving cache in the selected strategy
|
|
1174
1250
|
* @param cfg behavior (fixed/search, parsers, label/value keys, cache strategy, etc.)
|
|
1175
1251
|
*/
|
|
1176
|
-
constructor(apiUrl, storageKey, {
|
|
1252
|
+
constructor(apiUrl, storageKey, { autoLoad = true, method = this.defaultConfig.defaultRestMethod, presetFilters = this.defaultConfig.defaultPresetFilters, parseResponse, parseRequest, debounceTime = this.defaultConfig.defaultDebounceTime, fixed = true, maxOptionsSize = this.defaultConfig.defaultMaxOptionsSize, labelKey = this.defaultConfig.defaultLabelKey || 'name', valueKey = this.defaultConfig.defaultValueKey || 'code', keyPrefix = this.defaultConfig.defaultPrefix || 're', cacheStrategy = this.defaultConfig.defaultCacheStrategy || 'persist', }) {
|
|
1177
1253
|
this.apiUrl = apiUrl;
|
|
1178
1254
|
this.storageKey = storageKey;
|
|
1179
1255
|
this.#helper = new PaginatedDataStore(this.apiUrl, {
|
|
@@ -1188,10 +1264,11 @@ class DictStore {
|
|
|
1188
1264
|
this.labelKey = labelKey;
|
|
1189
1265
|
this.valueKey = valueKey;
|
|
1190
1266
|
this.maxOptionsSize = maxOptionsSize;
|
|
1191
|
-
this._armed.set(autoLoad);
|
|
1192
1267
|
if (cacheStrategy !== 'memory') {
|
|
1193
1268
|
this.storage = storageStrategy(cacheStrategy);
|
|
1269
|
+
this.storage.prefix = keyPrefix;
|
|
1194
1270
|
}
|
|
1271
|
+
this.setAutoload(autoLoad);
|
|
1195
1272
|
this.restoreFromStorage();
|
|
1196
1273
|
this.#effectRefs.push(effect(() => {
|
|
1197
1274
|
const incoming = this.#helper.items();
|
|
@@ -1206,11 +1283,15 @@ class DictStore {
|
|
|
1206
1283
|
}
|
|
1207
1284
|
const rest = this.filters();
|
|
1208
1285
|
if (!this.fixed) {
|
|
1209
|
-
const query = this.
|
|
1210
|
-
|
|
1286
|
+
const query = this.debouncedSearchText().trim();
|
|
1287
|
+
untracked(() => {
|
|
1288
|
+
this._lastPromise = this.#helper.updateFilters({ name: query, ...rest });
|
|
1289
|
+
});
|
|
1211
1290
|
}
|
|
1212
1291
|
else if (!this.cachedItems().length) {
|
|
1213
|
-
|
|
1292
|
+
untracked(() => {
|
|
1293
|
+
this._lastPromise = this.#helper.updateFilters({ name: '', ...rest });
|
|
1294
|
+
});
|
|
1214
1295
|
}
|
|
1215
1296
|
}));
|
|
1216
1297
|
}
|
|
@@ -1310,7 +1391,7 @@ class DictStore {
|
|
|
1310
1391
|
}
|
|
1311
1392
|
filterLocal() {
|
|
1312
1393
|
const list = this.cachedItems();
|
|
1313
|
-
const query = this.searchText().toLowerCase();
|
|
1394
|
+
const query = (this.fixed ? this.searchText() : this.debouncedSearchText()).toLowerCase();
|
|
1314
1395
|
if (!query) {
|
|
1315
1396
|
return list;
|
|
1316
1397
|
}
|
|
@@ -1329,15 +1410,41 @@ class DictStore {
|
|
|
1329
1410
|
}
|
|
1330
1411
|
return typeof raw === 'string' ? raw : String(raw);
|
|
1331
1412
|
}
|
|
1413
|
+
setAutoload(autoLoad) {
|
|
1414
|
+
if (autoLoad === 'whenEmpty') {
|
|
1415
|
+
const isEmpty = !this.storage?.get(this.storageKey)?.length;
|
|
1416
|
+
this._armed.set(isEmpty);
|
|
1417
|
+
}
|
|
1418
|
+
else {
|
|
1419
|
+
this._armed.set(autoLoad);
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1332
1422
|
}
|
|
1333
1423
|
_a = DictStore;
|
|
1334
1424
|
|
|
1335
1425
|
class DictLocalStore {
|
|
1426
|
+
/**
|
|
1427
|
+
* Default dictionary configuration resolved from {@link STATUM_CONFIG}.
|
|
1428
|
+
*
|
|
1429
|
+
* Used as a global fallback when local configuration does not explicitly
|
|
1430
|
+
* define dictionary keys.
|
|
1431
|
+
*/
|
|
1432
|
+
defaultConfig = inject(STATUM_CONFIG, { optional: true })?.dict || {};
|
|
1433
|
+
/**
|
|
1434
|
+
* Source dictionary items.
|
|
1435
|
+
*
|
|
1436
|
+
* Represents the full, unfiltered list of dictionary entries.
|
|
1437
|
+
* Used as the base data set for search and option generation.
|
|
1438
|
+
*/
|
|
1336
1439
|
items = signal([], ...(ngDevMode ? [{ debugName: "items" }] : []));
|
|
1337
1440
|
#draftItems = signal([], ...(ngDevMode ? [{ debugName: "#draftItems" }] : []));
|
|
1338
1441
|
/**
|
|
1339
|
-
*
|
|
1340
|
-
*
|
|
1442
|
+
* Computed list of options in `{ label, value }` format.
|
|
1443
|
+
*
|
|
1444
|
+
* Based on the current draft items (after search/filtering).
|
|
1445
|
+
* Respects `maxOptionsSize` to limit the number of generated options.
|
|
1446
|
+
*
|
|
1447
|
+
* Intended for direct use in dropdowns and select-like components.
|
|
1341
1448
|
*/
|
|
1342
1449
|
options = computed(() => {
|
|
1343
1450
|
const options = this.#draftItems().map((it) => ({
|
|
@@ -1350,19 +1457,30 @@ class DictLocalStore {
|
|
|
1350
1457
|
valueKey;
|
|
1351
1458
|
maxOptionsSize;
|
|
1352
1459
|
constructor(items, config) {
|
|
1353
|
-
this.labelKey = config?.labelKey ?? '
|
|
1354
|
-
this.valueKey = config?.valueKey ?? '
|
|
1460
|
+
this.labelKey = config?.labelKey ?? (this.defaultConfig.defaultLabelKey || 'name');
|
|
1461
|
+
this.valueKey = config?.valueKey ?? (this.defaultConfig.defaultValueKey || 'code');
|
|
1355
1462
|
this.maxOptionsSize = config?.maxOptionsSize ?? 1000;
|
|
1356
1463
|
this.items.set(items);
|
|
1357
1464
|
this.#draftItems.set(items);
|
|
1358
1465
|
}
|
|
1466
|
+
// noinspection JSUnusedGlobalSymbols
|
|
1359
1467
|
/**
|
|
1360
|
-
*
|
|
1468
|
+
* Restore cached dictionary state.
|
|
1469
|
+
*
|
|
1470
|
+
* This implementation is a no-op and exists only for API compatibility
|
|
1471
|
+
* with {@link DictStore}.
|
|
1361
1472
|
*/
|
|
1362
1473
|
restoreCache() { }
|
|
1363
1474
|
/**
|
|
1364
|
-
*
|
|
1365
|
-
*
|
|
1475
|
+
* Resolve display label by value.
|
|
1476
|
+
*
|
|
1477
|
+
* Performs a linear search over dictionary items and returns
|
|
1478
|
+
* the corresponding label for the given value.
|
|
1479
|
+
*
|
|
1480
|
+
* Commonly used for reverse binding (value → label).
|
|
1481
|
+
*
|
|
1482
|
+
* @param value Dictionary value to look up
|
|
1483
|
+
* @returns Resolved label string, or `undefined` if not found
|
|
1366
1484
|
*/
|
|
1367
1485
|
findLabel = (value) => {
|
|
1368
1486
|
for (const item of this.items()) {
|
|
@@ -1372,6 +1490,16 @@ class DictLocalStore {
|
|
|
1372
1490
|
}
|
|
1373
1491
|
return undefined;
|
|
1374
1492
|
};
|
|
1493
|
+
/**
|
|
1494
|
+
* Filter dictionary items by label.
|
|
1495
|
+
*
|
|
1496
|
+
* Applies a case-insensitive substring match against the label field
|
|
1497
|
+
* and updates the internal draft items used for option generation.
|
|
1498
|
+
*
|
|
1499
|
+
* Passing an empty string resets the filter and restores all items.
|
|
1500
|
+
*
|
|
1501
|
+
* @param name Search query string
|
|
1502
|
+
*/
|
|
1375
1503
|
search = (name = '') => {
|
|
1376
1504
|
const items = this.items();
|
|
1377
1505
|
if (name?.length) {
|
|
@@ -1381,6 +1509,18 @@ class DictLocalStore {
|
|
|
1381
1509
|
this.#draftItems.set(items);
|
|
1382
1510
|
}
|
|
1383
1511
|
};
|
|
1512
|
+
/**
|
|
1513
|
+
* Preload additional dictionary items.
|
|
1514
|
+
*
|
|
1515
|
+
* Can either replace the current items completely or append
|
|
1516
|
+
* new entries to the existing list.
|
|
1517
|
+
*
|
|
1518
|
+
* Updates both the source items and the current draft items.
|
|
1519
|
+
*
|
|
1520
|
+
* @param items Items to preload
|
|
1521
|
+
* @param opts Preload options
|
|
1522
|
+
* @param opts.replace When `true`, replaces existing items instead of appending
|
|
1523
|
+
*/
|
|
1384
1524
|
preload = (items, opts) => {
|
|
1385
1525
|
if (opts?.replace) {
|
|
1386
1526
|
this.items.set(items);
|
|
@@ -1398,5 +1538,5 @@ class DictLocalStore {
|
|
|
1398
1538
|
* Generated bundle index. Do not edit.
|
|
1399
1539
|
*/
|
|
1400
1540
|
|
|
1401
|
-
export { AbortError, CacheMissError, DictLocalStore, DictStore, LruCache,
|
|
1541
|
+
export { AbortError, CacheMissError, DictLocalStore, DictStore, LruCache, PaginatedDataStore, ResourceStore, STATUM_CONFIG, Serializer, isAbort, storageStrategy };
|
|
1402
1542
|
//# sourceMappingURL=reforgium-statum.mjs.map
|