@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 hasStaticLists = (cfg.DETAILS_STATIC_LISTS?.length ?? 0) > 0;
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(cfg.DETAILS_STATIC_LISTS);
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
- // Update succeeded in place — refresh the change-history so the new
2456
- // entry shows without a manual reload.
2457
- this.loadAuditHistory();
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'.