@elasticias/screens 1.0.49 → 1.0.51
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.
|
@@ -4,7 +4,7 @@ import { PersistedState, StorageUtils, AppUtils, CsvUtils } from '@elasticias/ut
|
|
|
4
4
|
import { Subject, combineLatest, take, from, merge, map, distinctUntilChanged } from 'rxjs';
|
|
5
5
|
import { Permissions } from '@elasticias/types';
|
|
6
6
|
import { Router, ActivatedRoute } from '@angular/router';
|
|
7
|
-
import { CacheService, ToastService, ConfirmDialogService, EfShortcutService } from '@elasticias/core';
|
|
7
|
+
import { CacheService, ToastService, ConfirmDialogService, EfShortcutService, isServerReported, extractProblem } from '@elasticias/core';
|
|
8
8
|
import { Location } from '@angular/common';
|
|
9
9
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
10
10
|
import { TranslateService } from '@ngx-translate/core';
|
|
@@ -1575,6 +1575,7 @@ class AbstractSearchScreenV2 extends AbstractScreenComponent {
|
|
|
1575
1575
|
this.totalCount.set(result?.totalCount ?? 0);
|
|
1576
1576
|
this.cacheService.setCache(this.screenStateKey, this.criteria());
|
|
1577
1577
|
this.loading.set(false);
|
|
1578
|
+
this.afterSearch(result);
|
|
1578
1579
|
},
|
|
1579
1580
|
error: (err) => {
|
|
1580
1581
|
if (seq !== this.searchSeq)
|
|
@@ -1585,6 +1586,15 @@ class AbstractSearchScreenV2 extends AbstractScreenComponent {
|
|
|
1585
1586
|
},
|
|
1586
1587
|
});
|
|
1587
1588
|
}
|
|
1589
|
+
/**
|
|
1590
|
+
* Hook for a screen whose search returns more than a page of rows (counts,
|
|
1591
|
+
* facets, a summary): called with the raw response after `items` and
|
|
1592
|
+
* `totalCount` are set, for the latest issued search only. A stale or
|
|
1593
|
+
* failed search never reaches it.
|
|
1594
|
+
*/
|
|
1595
|
+
afterSearch(_result) {
|
|
1596
|
+
/* no-op by default */
|
|
1597
|
+
}
|
|
1588
1598
|
/* ── Convenience criteria mutators ───────────────────────────── */
|
|
1589
1599
|
setSearchText(text) {
|
|
1590
1600
|
// Templates bind this to ef-smart-bar's (search), and that binding has
|
|
@@ -2045,6 +2055,22 @@ class AbstractDetailScreenV2 extends AbstractScreenComponent {
|
|
|
2045
2055
|
* `<ef-change-history [entries]="auditEntries()" />`.
|
|
2046
2056
|
*/
|
|
2047
2057
|
auditEntries = signal([], ...(ngDevMode ? [{ debugName: "auditEntries" }] : /* istanbul ignore next */ []));
|
|
2058
|
+
/**
|
|
2059
|
+
* Field path to reference items for `<ef-change-history [valueLists]>`,
|
|
2060
|
+
* built from `ScreenConfig.AUDIT_VALUE_LISTS` and the already-loaded static
|
|
2061
|
+
* lists. Empty (raw values) when the config does not declare any.
|
|
2062
|
+
*/
|
|
2063
|
+
auditValueLists = computed(() => {
|
|
2064
|
+
this.grantsVersion();
|
|
2065
|
+
const map = this.getConfig()?.AUDIT_VALUE_LISTS;
|
|
2066
|
+
const out = {};
|
|
2067
|
+
if (!map || !this.context)
|
|
2068
|
+
return out;
|
|
2069
|
+
for (const [field, list] of Object.entries(map)) {
|
|
2070
|
+
out[field] = this.context.ref.get(list)() ?? [];
|
|
2071
|
+
}
|
|
2072
|
+
return out;
|
|
2073
|
+
}, ...(ngDevMode ? [{ debugName: "auditValueLists" }] : /* istanbul ignore next */ []));
|
|
2048
2074
|
/** Loaded entity. Empty object when on `/details` (create mode). */
|
|
2049
2075
|
entity = signal({}, ...(ngDevMode ? [{ debugName: "entity" }] : /* istanbul ignore next */ []));
|
|
2050
2076
|
/** PK from the route param, or `null` on `/details` (new). */
|
|
@@ -2135,11 +2161,12 @@ class AbstractDetailScreenV2 extends AbstractScreenComponent {
|
|
|
2135
2161
|
const cfg = this.getConfig();
|
|
2136
2162
|
if (cfg) {
|
|
2137
2163
|
const hasDynamicRefs = (cfg.DETAILS_REFERENTIALS_KEYS?.length ?? 0) > 0;
|
|
2138
|
-
const
|
|
2164
|
+
const staticLists = Array.from(new Set([...(cfg.DETAILS_STATIC_LISTS ?? []), ...Object.values(cfg.AUDIT_VALUE_LISTS ?? {})]));
|
|
2165
|
+
const hasStaticLists = staticLists.length > 0;
|
|
2139
2166
|
if (hasDynamicRefs)
|
|
2140
2167
|
this.initializeReferenceKeys(cfg.DETAILS_REFERENTIALS_KEYS);
|
|
2141
2168
|
if (hasStaticLists)
|
|
2142
|
-
this.initializeStaticLists(
|
|
2169
|
+
this.initializeStaticLists(staticLists);
|
|
2143
2170
|
if (hasDynamicRefs || hasStaticLists)
|
|
2144
2171
|
this.loadReferenceData(cfg.REF_DATA_OPTIONS);
|
|
2145
2172
|
}
|
|
@@ -2224,14 +2251,7 @@ class AbstractDetailScreenV2 extends AbstractScreenComponent {
|
|
|
2224
2251
|
}
|
|
2225
2252
|
this.loadSub?.unsubscribe();
|
|
2226
2253
|
this.loadSub = this.serviceInstance.get(id).subscribe({
|
|
2227
|
-
next: (result) =>
|
|
2228
|
-
const vm = new ViewModelEntity(result);
|
|
2229
|
-
vm.id = id;
|
|
2230
|
-
this.entity.set(vm);
|
|
2231
|
-
this.loading.set(false);
|
|
2232
|
-
this.afterLoad();
|
|
2233
|
-
this.loadAuditHistory();
|
|
2234
|
-
},
|
|
2254
|
+
next: (result) => this.applyLoaded(result, id),
|
|
2235
2255
|
error: (err) => {
|
|
2236
2256
|
console.error('AbstractDetailScreenV2.loadData failed', err);
|
|
2237
2257
|
this.errorMsg.set(err?.message ?? 'Erreur de chargement');
|
|
@@ -2246,6 +2266,15 @@ class AbstractDetailScreenV2 extends AbstractScreenComponent {
|
|
|
2246
2266
|
},
|
|
2247
2267
|
});
|
|
2248
2268
|
}
|
|
2269
|
+
/** Install a fetched (or save-returned) record as the current entity. */
|
|
2270
|
+
applyLoaded(result, id) {
|
|
2271
|
+
const vm = new ViewModelEntity(result);
|
|
2272
|
+
vm.id = id;
|
|
2273
|
+
this.entity.set(vm);
|
|
2274
|
+
this.loading.set(false);
|
|
2275
|
+
this.afterLoad();
|
|
2276
|
+
this.loadAuditHistory();
|
|
2277
|
+
}
|
|
2249
2278
|
/**
|
|
2250
2279
|
* Persist the entity. Routes to `service.create()` when in new
|
|
2251
2280
|
* or duplicate mode, `service.update(id, entity)` when editing.
|
|
@@ -2287,9 +2316,44 @@ class AbstractDetailScreenV2 extends AbstractScreenComponent {
|
|
|
2287
2316
|
this.setFormErrors(response.errors);
|
|
2288
2317
|
this.onSaveError(response.errors);
|
|
2289
2318
|
}
|
|
2319
|
+
if (!isCreate && this.isStaleSave(response))
|
|
2320
|
+
this.offerStaleReload();
|
|
2290
2321
|
},
|
|
2291
2322
|
});
|
|
2292
2323
|
}
|
|
2324
|
+
/**
|
|
2325
|
+
* True when a failed save means the record changed since it was loaded:
|
|
2326
|
+
* a refusal whose `ruleCode` is in the config's `STALE_RULE_CODES`. A bare
|
|
2327
|
+
* 409 is a duplicate (e.g. a slug already taken), not a stale record, so
|
|
2328
|
+
* it never offers a reload. Only a refusal
|
|
2329
|
+
* the app's interceptor already toasted counts, so the user is never
|
|
2330
|
+
* told twice.
|
|
2331
|
+
*/
|
|
2332
|
+
isStaleSave(error) {
|
|
2333
|
+
if (!isServerReported(error))
|
|
2334
|
+
return false;
|
|
2335
|
+
const problem = extractProblem(error);
|
|
2336
|
+
if (!problem)
|
|
2337
|
+
return false;
|
|
2338
|
+
const codes = this.getConfig()?.STALE_RULE_CODES ?? [];
|
|
2339
|
+
return !!problem.ruleCode && codes.includes(problem.ruleCode);
|
|
2340
|
+
}
|
|
2341
|
+
/**
|
|
2342
|
+
* Asks, in an accessible confirm dialog (focus trapped, Escape cancels),
|
|
2343
|
+
* whether to reload the record. Accepting discards the unsaved edits,
|
|
2344
|
+
* and the message says so. The refusal itself was already toasted.
|
|
2345
|
+
* App keys: `ef_stale_reload` (button), `ef_stale_reload_confirm`
|
|
2346
|
+
* (message) and optionally `ef_stale_reload_header`.
|
|
2347
|
+
*/
|
|
2348
|
+
offerStaleReload() {
|
|
2349
|
+
this.confirmDialogService.confirm({
|
|
2350
|
+
messageKey: 'ef_stale_reload_confirm',
|
|
2351
|
+
headerKey: 'ef_stale_reload_header',
|
|
2352
|
+
acceptLabelKey: 'ef_stale_reload',
|
|
2353
|
+
icon: 'pi pi-refresh',
|
|
2354
|
+
accept: () => this.loadData(),
|
|
2355
|
+
});
|
|
2356
|
+
}
|
|
2293
2357
|
/**
|
|
2294
2358
|
* Soft-delete-then-navigate-back. Confirms with the user first.
|
|
2295
2359
|
*/
|
|
@@ -2452,9 +2516,24 @@ class AbstractDetailScreenV2 extends AbstractScreenComponent {
|
|
|
2452
2516
|
this.router.navigate([this.detailsBaseUrl(), newId]);
|
|
2453
2517
|
}
|
|
2454
2518
|
else {
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2519
|
+
if (cfg?.RELOAD_AFTER_SAVE) {
|
|
2520
|
+
// The record carries server-owned state (a concurrency
|
|
2521
|
+
// revision): take the fresh copy. An update response that is
|
|
2522
|
+
// the saved record itself (same id) is used as is, anything
|
|
2523
|
+
// else (void, id, flag) triggers one GET. Both refresh history.
|
|
2524
|
+
const id = this.entityId();
|
|
2525
|
+
if (result && typeof result === 'object' && String(result.id) === String(id)) {
|
|
2526
|
+
this.applyLoaded(result, id);
|
|
2527
|
+
}
|
|
2528
|
+
else {
|
|
2529
|
+
this.loadData();
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
else {
|
|
2533
|
+
// Update succeeded in place — refresh the change-history so the new
|
|
2534
|
+
// entry shows without a manual reload.
|
|
2535
|
+
this.loadAuditHistory();
|
|
2536
|
+
}
|
|
2458
2537
|
}
|
|
2459
2538
|
}
|
|
2460
2539
|
/** Override to surface form-level validation errors after a 4xx
|
|
@@ -2811,6 +2890,34 @@ class ScreenConfig {
|
|
|
2811
2890
|
* to opt out. Must match the backend IAuditable.AuditEntityType (e.g. 'Client').
|
|
2812
2891
|
*/
|
|
2813
2892
|
AUDIT_ENTITY_TYPE;
|
|
2893
|
+
/**
|
|
2894
|
+
* Detail screens: translate raw values in the change-history box. Maps an
|
|
2895
|
+
* audited field path to the static reference list holding its codes, e.g.
|
|
2896
|
+
* `{ status: 'boutique_order_status' }` (key `status` also covers the
|
|
2897
|
+
* from/to of status-change entries). The lists are loaded with the screen
|
|
2898
|
+
* (no need to repeat them in DETAILS_STATIC_LISTS); bind
|
|
2899
|
+
* `<ef-change-history [entries]="auditEntries()" [valueLists]="auditValueLists()" />`.
|
|
2900
|
+
* Unset keeps raw values.
|
|
2901
|
+
*/
|
|
2902
|
+
AUDIT_VALUE_LISTS;
|
|
2903
|
+
/**
|
|
2904
|
+
* Detail screens: business-rule codes (422 `ruleCode`) meaning "the record
|
|
2905
|
+
* changed since you loaded it", e.g. ERP `['BR_SO_0123']`. A failed update
|
|
2906
|
+
* with one of them offers a "reload" confirm; accepting
|
|
2907
|
+
* reloads the record and drops the unsaved edits. The app must define the
|
|
2908
|
+
* i18n keys `ef_stale_reload` (button), `ef_stale_reload_confirm` (message,
|
|
2909
|
+
* say that unsaved edits are lost) and `ef_stale_reload_header`.
|
|
2910
|
+
*/
|
|
2911
|
+
STALE_RULE_CODES;
|
|
2912
|
+
/**
|
|
2913
|
+
* Detail screens: after a successful in-place update (not a create or
|
|
2914
|
+
* duplicate) refresh the record, so server-owned fields such as a
|
|
2915
|
+
* concurrency revision are current for the next save. Uses the update
|
|
2916
|
+
* response when it is the saved record (an object whose `id` is the
|
|
2917
|
+
* screen's entity id), otherwise one `get(id)`. Change history reloads
|
|
2918
|
+
* either way. Default false: the record is left as edited.
|
|
2919
|
+
*/
|
|
2920
|
+
RELOAD_AFTER_SAVE;
|
|
2814
2921
|
/**
|
|
2815
2922
|
* Report screens (AbstractReportScreenV2): starting period preset for the
|
|
2816
2923
|
* screen's ef-datepicker-advanced. Unset → 'last_30_days'.
|