@elasticias/screens 1.0.49 → 1.0.50

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
@@ -2224,14 +2234,7 @@ class AbstractDetailScreenV2 extends AbstractScreenComponent {
2224
2234
  }
2225
2235
  this.loadSub?.unsubscribe();
2226
2236
  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
- },
2237
+ next: (result) => this.applyLoaded(result, id),
2235
2238
  error: (err) => {
2236
2239
  console.error('AbstractDetailScreenV2.loadData failed', err);
2237
2240
  this.errorMsg.set(err?.message ?? 'Erreur de chargement');
@@ -2246,6 +2249,15 @@ class AbstractDetailScreenV2 extends AbstractScreenComponent {
2246
2249
  },
2247
2250
  });
2248
2251
  }
2252
+ /** Install a fetched (or save-returned) record as the current entity. */
2253
+ applyLoaded(result, id) {
2254
+ const vm = new ViewModelEntity(result);
2255
+ vm.id = id;
2256
+ this.entity.set(vm);
2257
+ this.loading.set(false);
2258
+ this.afterLoad();
2259
+ this.loadAuditHistory();
2260
+ }
2249
2261
  /**
2250
2262
  * Persist the entity. Routes to `service.create()` when in new
2251
2263
  * or duplicate mode, `service.update(id, entity)` when editing.
@@ -2287,9 +2299,44 @@ class AbstractDetailScreenV2 extends AbstractScreenComponent {
2287
2299
  this.setFormErrors(response.errors);
2288
2300
  this.onSaveError(response.errors);
2289
2301
  }
2302
+ if (!isCreate && this.isStaleSave(response))
2303
+ this.offerStaleReload();
2290
2304
  },
2291
2305
  });
2292
2306
  }
2307
+ /**
2308
+ * True when a failed save means the record changed since it was loaded:
2309
+ * a refusal whose `ruleCode` is in the config's `STALE_RULE_CODES`. A bare
2310
+ * 409 is a duplicate (e.g. a slug already taken), not a stale record, so
2311
+ * it never offers a reload. Only a refusal
2312
+ * the app's interceptor already toasted counts, so the user is never
2313
+ * told twice.
2314
+ */
2315
+ isStaleSave(error) {
2316
+ if (!isServerReported(error))
2317
+ return false;
2318
+ const problem = extractProblem(error);
2319
+ if (!problem)
2320
+ return false;
2321
+ const codes = this.getConfig()?.STALE_RULE_CODES ?? [];
2322
+ return !!problem.ruleCode && codes.includes(problem.ruleCode);
2323
+ }
2324
+ /**
2325
+ * Asks, in an accessible confirm dialog (focus trapped, Escape cancels),
2326
+ * whether to reload the record. Accepting discards the unsaved edits,
2327
+ * and the message says so. The refusal itself was already toasted.
2328
+ * App keys: `ef_stale_reload` (button), `ef_stale_reload_confirm`
2329
+ * (message) and optionally `ef_stale_reload_header`.
2330
+ */
2331
+ offerStaleReload() {
2332
+ this.confirmDialogService.confirm({
2333
+ messageKey: 'ef_stale_reload_confirm',
2334
+ headerKey: 'ef_stale_reload_header',
2335
+ acceptLabelKey: 'ef_stale_reload',
2336
+ icon: 'pi pi-refresh',
2337
+ accept: () => this.loadData(),
2338
+ });
2339
+ }
2293
2340
  /**
2294
2341
  * Soft-delete-then-navigate-back. Confirms with the user first.
2295
2342
  */
@@ -2452,9 +2499,24 @@ class AbstractDetailScreenV2 extends AbstractScreenComponent {
2452
2499
  this.router.navigate([this.detailsBaseUrl(), newId]);
2453
2500
  }
2454
2501
  else {
2455
- // Update succeeded in place — refresh the change-history so the new
2456
- // entry shows without a manual reload.
2457
- this.loadAuditHistory();
2502
+ if (cfg?.RELOAD_AFTER_SAVE) {
2503
+ // The record carries server-owned state (a concurrency
2504
+ // revision): take the fresh copy. An update response that is
2505
+ // the saved record itself (same id) is used as is, anything
2506
+ // else (void, id, flag) triggers one GET. Both refresh history.
2507
+ const id = this.entityId();
2508
+ if (result && typeof result === 'object' && String(result.id) === String(id)) {
2509
+ this.applyLoaded(result, id);
2510
+ }
2511
+ else {
2512
+ this.loadData();
2513
+ }
2514
+ }
2515
+ else {
2516
+ // Update succeeded in place — refresh the change-history so the new
2517
+ // entry shows without a manual reload.
2518
+ this.loadAuditHistory();
2519
+ }
2458
2520
  }
2459
2521
  }
2460
2522
  /** Override to surface form-level validation errors after a 4xx
@@ -2811,6 +2873,24 @@ class ScreenConfig {
2811
2873
  * to opt out. Must match the backend IAuditable.AuditEntityType (e.g. 'Client').
2812
2874
  */
2813
2875
  AUDIT_ENTITY_TYPE;
2876
+ /**
2877
+ * Detail screens: business-rule codes (422 `ruleCode`) meaning "the record
2878
+ * changed since you loaded it", e.g. ERP `['BR_SO_0123']`. A failed update
2879
+ * with one of them offers a "reload" confirm; accepting
2880
+ * reloads the record and drops the unsaved edits. The app must define the
2881
+ * i18n keys `ef_stale_reload` (button), `ef_stale_reload_confirm` (message,
2882
+ * say that unsaved edits are lost) and `ef_stale_reload_header`.
2883
+ */
2884
+ STALE_RULE_CODES;
2885
+ /**
2886
+ * Detail screens: after a successful in-place update (not a create or
2887
+ * duplicate) refresh the record, so server-owned fields such as a
2888
+ * concurrency revision are current for the next save. Uses the update
2889
+ * response when it is the saved record (an object whose `id` is the
2890
+ * screen's entity id), otherwise one `get(id)`. Change history reloads
2891
+ * either way. Default false: the record is left as edited.
2892
+ */
2893
+ RELOAD_AFTER_SAVE;
2814
2894
  /**
2815
2895
  * Report screens (AbstractReportScreenV2): starting period preset for the
2816
2896
  * screen's ef-datepicker-advanced. Unset → 'last_30_days'.