@ecodev/natural 56.0.4 → 57.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.
@@ -1,18 +1,18 @@
1
1
  import * as i0 from '@angular/core';
2
2
  import { Directive, Component, Inject, Injectable, HostBinding, HostListener, inject, InjectionToken, TemplateRef, ViewEncapsulation, ViewChild, Injector, Optional, Input, Host, Self, EventEmitter, Output, Pipe, LOCALE_ID, APP_INITIALIZER, ContentChild, createEnvironmentInjector, createComponent, runInInjectionContext, PLATFORM_ID, ErrorHandler, importProvidersFrom } from '@angular/core';
3
- import { Subject, BehaviorSubject, of, timer, switchMap as switchMap$1, endWith, last, EMPTY, merge as merge$1, Observable, first as first$1, combineLatest, catchError, from, ReplaySubject, debounceTime as debounceTime$1, raceWith, take as take$1, mergeMap, shareReplay as shareReplay$1, forkJoin, map as map$1, tap as tap$1, asyncScheduler } from 'rxjs';
3
+ import { Subject, Observable, takeUntil, BehaviorSubject, of, timer, switchMap as switchMap$1, tap, endWith, last, EMPTY, finalize, merge as merge$1, first as first$1, take, map as map$1, ReplaySubject, debounceTime, raceWith, mergeMap, shareReplay, catchError, forkJoin, combineLatest, from, startWith as startWith$1, filter as filter$1, asyncScheduler } from 'rxjs';
4
4
  import * as i2$2 from '@angular/forms';
5
5
  import { FormGroup, FormArray, Validators, UntypedFormGroup, UntypedFormArray, FormControl, FormsModule, ReactiveFormsModule, UntypedFormControl, FormControlDirective, FormControlName } from '@angular/forms';
6
6
  import * as i2$3 from '@angular/router';
7
- import { Router, ActivatedRoute, NavigationStart, NavigationEnd, PRIMARY_OUTLET, RouterLink, NavigationError, DefaultUrlSerializer } from '@angular/router';
8
- import { merge, isArray, pickBy, isEmpty, cloneDeep, uniq, groupBy, mergeWith, defaultsDeep, omit, kebabCase, clone, pick, isEqual, defaults, isObject, intersection, flatten, differenceWith } from 'lodash-es';
7
+ import { ActivatedRoute, Router, NavigationStart, NavigationEnd, PRIMARY_OUTLET, RouterLink, NavigationError, DefaultUrlSerializer } from '@angular/router';
8
+ import { isArray, pickBy, cloneDeep, uniq, groupBy, mergeWith, defaultsDeep, omit, isEqual, kebabCase, merge, clone, pick, defaults, isEmpty, isObject, intersection, flatten, differenceWith } from 'lodash-es';
9
9
  import * as i1 from '@angular/material/dialog';
10
10
  import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
11
11
  import * as i3 from '@angular/material/button';
12
12
  import { MatButtonModule } from '@angular/material/button';
13
13
  import * as i2 from '@angular/material/snack-bar';
14
14
  import { MatSnackBarModule } from '@angular/material/snack-bar';
15
- import { switchMap, first, map, filter, finalize, takeUntil, take, tap, takeWhile, debounceTime, shareReplay, startWith, distinctUntilChanged, throttleTime } from 'rxjs/operators';
15
+ import { switchMap, first, map, filter, takeUntil as takeUntil$1, takeWhile, debounceTime as debounceTime$1, tap as tap$1, shareReplay as shareReplay$1, startWith, distinctUntilChanged, finalize as finalize$1, throttleTime } from 'rxjs/operators';
16
16
  import * as i4$3 from '@angular/material/table';
17
17
  import { MatTableDataSource, MatTableModule } from '@angular/material/table';
18
18
  import { DataSource, SelectionModel } from '@angular/cdk/collections';
@@ -40,7 +40,7 @@ import extractFiles from 'extract-files/extractFiles.mjs';
40
40
  import isExtractableFile from 'extract-files/isExtractableFile.mjs';
41
41
  import { Kind, OperationTypeNode } from 'graphql/language';
42
42
  import * as i1$4 from 'apollo-angular';
43
- import { gql } from 'apollo-angular';
43
+ import { Apollo, gql } from 'apollo-angular';
44
44
  import * as i1$6 from '@angular/cdk/layout';
45
45
  import { Breakpoints } from '@angular/cdk/layout';
46
46
  import * as i7$1 from '@angular/material/tooltip';
@@ -203,7 +203,22 @@ class NaturalAbstractPanel extends NaturalAbstractController {
203
203
  this.panelData = panelData;
204
204
  this.isPanel = true;
205
205
  if (this.panelData?.data) {
206
- merge(this.data, this.panelData.data);
206
+ if (this.panelData.data.model instanceof Observable) {
207
+ // Subscribe to model to know when Apollo cache is changed, so we can reflect it into `data.model`
208
+ this.panelData.data.model.pipe(takeUntil(this.ngUnsubscribe)).subscribe(model => {
209
+ this.data = {
210
+ ...this.data,
211
+ ...this.panelData?.data,
212
+ model: model,
213
+ };
214
+ });
215
+ }
216
+ else {
217
+ this.data = {
218
+ ...this.data,
219
+ ...this.panelData.data,
220
+ };
221
+ }
207
222
  }
208
223
  }
209
224
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.2.2", ngImport: i0, type: NaturalAbstractPanel, deps: null, target: i0.ɵɵFactoryTarget.Directive }); }
@@ -288,24 +303,6 @@ function relationsToIds(object) {
288
303
  function hasId(value) {
289
304
  return !!value && typeof value === 'object' && 'id' in value && !!value.id;
290
305
  }
291
- /**
292
- * Remove from source object the attributes with same value as modified
293
- * Does not consider arrays
294
- */
295
- function cleanSameValues(source, modified) {
296
- Object.keys(source).forEach(key => {
297
- if (source[key] instanceof Object) {
298
- cleanSameValues(source[key], modified[key]);
299
- if (isEmpty(source[key])) {
300
- delete source[key];
301
- }
302
- }
303
- else if (modified && source[key] === modified[key]) {
304
- delete source[key];
305
- }
306
- });
307
- return source;
308
- }
309
306
  /**
310
307
  * Returns the plural form of the given name
311
308
  *
@@ -329,12 +326,6 @@ function makePlural(name) {
329
326
  function upperCaseFirstLetter(term) {
330
327
  return term.charAt(0).toUpperCase() + term.slice(1);
331
328
  }
332
- /**
333
- * Returns the string with the first letter as lower case
334
- */
335
- function lowerCaseFirstLetter(term) {
336
- return term.charAt(0).toLowerCase() + term.slice(1);
337
- }
338
329
  /**
339
330
  * Replace all attributes of first object with the ones provided by the second, but keeps the reference
340
331
  */
@@ -2308,20 +2299,84 @@ function money(control) {
2308
2299
  return twoDecimals(control) ? { money: true } : null;
2309
2300
  }
2310
2301
 
2302
+ /**
2303
+ * Cumulate all changes made to an object over time
2304
+ */
2305
+ class CumulativeChanges {
2306
+ #original = {};
2307
+ #diff = {};
2308
+ /**
2309
+ * Initialize the original values, should be called exactly one time per instance
2310
+ */
2311
+ initialize(originalValues) {
2312
+ this.#original = cloneDeep(originalValues);
2313
+ this.#diff = {};
2314
+ }
2315
+ /**
2316
+ * Returns a literal that contains only the keys whose values have been changed by this call or any previous calls.
2317
+ *
2318
+ * Eg:
2319
+ *
2320
+ * ```ts
2321
+ * changes.initialize({a: 1, b: 2});
2322
+ * changes.differences({a: 1, b: 3}); // => {b: 3}
2323
+ * ```
2324
+ */
2325
+ differences(newValues) {
2326
+ Object.keys(newValues).forEach(key => {
2327
+ if (key in this.#diff ||
2328
+ (newValues[key] !== undefined &&
2329
+ (!(key in this.#original) || !isEqual(this.#original[key], newValues[key])))) {
2330
+ this.#diff[key] = newValues[key];
2331
+ }
2332
+ });
2333
+ return Object.keys(this.#diff).length ? this.#diff : null;
2334
+ }
2335
+ /**
2336
+ * Commit the given new values, so they are not treated as differences anymore.
2337
+ */
2338
+ commit(newValues) {
2339
+ this.#original = {
2340
+ ...this.#original,
2341
+ ...cloneDeep(newValues),
2342
+ };
2343
+ Object.keys(newValues).forEach(key => {
2344
+ if (key in this.#diff && isEqual(this.#diff[key], newValues[key])) {
2345
+ delete this.#diff[key];
2346
+ }
2347
+ });
2348
+ }
2349
+ }
2350
+
2351
+ function isNaturalDialogTriggerProvidedData(dialogData) {
2352
+ return (!!dialogData &&
2353
+ typeof dialogData === 'object' &&
2354
+ 'activatedRoute' in dialogData &&
2355
+ dialogData.activatedRoute instanceof ActivatedRoute);
2356
+ }
2311
2357
  // @dynamic
2312
2358
  class NaturalAbstractDetail extends NaturalAbstractPanel {
2359
+ #dialogData;
2313
2360
  /**
2314
2361
  * Once set, this must not change anymore, especially not right after the creation mutation,
2315
2362
  * so the form does not switch from creation mode to update mode without an actual reload of
2316
2363
  * model from DB (by navigating to update page).
2317
2364
  */
2318
2365
  #isUpdatePage;
2366
+ #changes;
2319
2367
  constructor(key, service) {
2320
2368
  super();
2321
2369
  this.key = key;
2322
2370
  this.service = service;
2323
2371
  /**
2324
- * Empty placeholder for data retrieved by the server
2372
+ * Data retrieved by the server via route resolvers.
2373
+ *
2374
+ * The key `model` is special. It is readonly and represents the model being updated
2375
+ * as it exists on server. The value is kept up to date when Apollo mutates it on server.
2376
+ *
2377
+ * The only time when `model` is not readonly is during creation. Only then can we modify the model values directly.
2378
+ *
2379
+ * Other keys, if present, are whatever is returned from route resolvers as-is.
2325
2380
  */
2326
2381
  this.data = {
2327
2382
  model: this.service.getDefaultForServer(),
@@ -2347,28 +2402,50 @@ class NaturalAbstractDetail extends NaturalAbstractPanel {
2347
2402
  * Injected service
2348
2403
  */
2349
2404
  this.route = inject(ActivatedRoute);
2405
+ this.#dialogData = inject(MAT_DIALOG_DATA, { optional: true });
2350
2406
  /**
2351
2407
  * Once set, this must not change anymore, especially not right after the creation mutation,
2352
2408
  * so the form does not switch from creation mode to update mode without an actual reload of
2353
2409
  * model from DB (by navigating to update page).
2354
2410
  */
2355
2411
  this.#isUpdatePage = false;
2412
+ this.#changes = new CumulativeChanges();
2356
2413
  }
2357
2414
  ngOnInit() {
2358
- if (!this.isPanel) {
2359
- this.route.data.subscribe(data => {
2360
- this.data = merge({ model: this.service.getDefaultForServer() }, data[this.key]);
2361
- this.data = merge(this.data, omit(data, [this.key]));
2362
- this.initForm();
2363
- });
2415
+ if (this.isPanel) {
2416
+ this.initForm();
2364
2417
  }
2365
2418
  else {
2366
- this.initForm();
2419
+ const route = isNaturalDialogTriggerProvidedData(this.#dialogData)
2420
+ ? this.#dialogData.activatedRoute
2421
+ : this.route;
2422
+ this.#subscribeToModelFromResolvedData(route);
2367
2423
  }
2368
2424
  }
2369
2425
  changeTab(index) {
2370
2426
  this.showFabButton = index === 0;
2371
2427
  }
2428
+ #subscribeToModelFromResolvedData(route) {
2429
+ let firstTime = true;
2430
+ route.data
2431
+ .pipe(switchMap$1(data => {
2432
+ if (!(data.model instanceof Observable)) {
2433
+ throw new Error('Resolved data must include the key `model`, and it must be an observable (usually one from Apollo).');
2434
+ }
2435
+ // Subscribe to model to know when Apollo cache is changed, so we can reflect it into `data.model`
2436
+ return data.model.pipe(tap((model) => {
2437
+ this.data = {
2438
+ ...data,
2439
+ model: model,
2440
+ };
2441
+ if (firstTime) {
2442
+ firstTime = false;
2443
+ this.initForm();
2444
+ }
2445
+ }));
2446
+ }))
2447
+ .subscribe();
2448
+ }
2372
2449
  /**
2373
2450
  * Returns whether `data.model` was fetched from DB, so we are on an update page, or if it is a new object
2374
2451
  * with (only) default values, so we are on a creation page.
@@ -2378,24 +2455,33 @@ class NaturalAbstractDetail extends NaturalAbstractPanel {
2378
2455
  isUpdatePage() {
2379
2456
  return this.#isUpdatePage;
2380
2457
  }
2381
- update(now = false) {
2458
+ /**
2459
+ * Update the object on the server with the values from the form fields that were modified since
2460
+ * the initialization, or since the previous successful update.
2461
+ *
2462
+ * Form fields that are never modified are **not** sent to the server, unless if you specify `submitAllFields`.
2463
+ */
2464
+ update(now = false, submitAllFields = false) {
2382
2465
  if (!this.isUpdatePage()) {
2383
2466
  return;
2384
2467
  }
2385
2468
  validateAllFormControls(this.form);
2386
2469
  ifValid(this.form).subscribe(() => {
2387
- this.formToData();
2388
- const postUpdate = (model) => {
2470
+ const newValues = this.form.getRawValue();
2471
+ if (submitAllFields) {
2472
+ this.#changes.initialize({});
2473
+ }
2474
+ const toSubmit = {
2475
+ id: this.data.model.id,
2476
+ ...this.#changes.differences(newValues),
2477
+ };
2478
+ const update = now ? this.service.updateNow(toSubmit) : this.service.update(toSubmit);
2479
+ update.subscribe(model => {
2480
+ this.#changes.commit(newValues);
2389
2481
  this.alertService.info($localize `Mis à jour`);
2390
2482
  this.form.patchValue(model);
2391
2483
  this.postUpdate(model);
2392
- };
2393
- if (now) {
2394
- this.service.updateNow(this.data.model).subscribe(postUpdate);
2395
- }
2396
- else {
2397
- this.service.update(this.data.model).subscribe(postUpdate);
2398
- }
2484
+ });
2399
2485
  });
2400
2486
  }
2401
2487
  create(redirect = true) {
@@ -2403,10 +2489,10 @@ class NaturalAbstractDetail extends NaturalAbstractPanel {
2403
2489
  if (!this.form.valid) {
2404
2490
  return;
2405
2491
  }
2406
- this.formToData();
2492
+ const newValues = this.form.getRawValue();
2407
2493
  this.form.disable();
2408
2494
  this.service
2409
- .create(this.data.model)
2495
+ .create(newValues)
2410
2496
  .pipe(switchMap$1(model => {
2411
2497
  this.alertService.info($localize `Créé`);
2412
2498
  this.form.patchValue(model);
@@ -2435,7 +2521,7 @@ class NaturalAbstractDetail extends NaturalAbstractPanel {
2435
2521
  (confirmer ??
2436
2522
  this.alertService.confirm($localize `Suppression`, $localize `Voulez-vous supprimer définitivement cet élément ?`, $localize `Supprimer définitivement`))
2437
2523
  .pipe(switchMap$1(confirmed => {
2438
- if (!confirmed) {
2524
+ if (!confirmed || !this.isUpdatePage()) {
2439
2525
  return EMPTY;
2440
2526
  }
2441
2527
  this.preDelete(this.data.model);
@@ -2474,12 +2560,7 @@ class NaturalAbstractDetail extends NaturalAbstractPanel {
2474
2560
  initForm() {
2475
2561
  this.#isUpdatePage = !!this.data.model.id;
2476
2562
  this.form = this.service.getFormGroup(this.data.model);
2477
- }
2478
- /**
2479
- * Merge values of form into `this.data.model`.
2480
- */
2481
- formToData() {
2482
- mergeWith(this.data.model, this.form.value, mergeOverrideArray);
2563
+ this.#changes.initialize(this.form.getRawValue());
2483
2564
  }
2484
2565
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.2.2", ngImport: i0, type: NaturalAbstractDetail, deps: "invalid", target: i0.ɵɵFactoryTarget.Directive }); }
2485
2566
  static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "17.2.2", type: NaturalAbstractDetail, usesInheritance: true, ngImport: i0 }); }
@@ -2929,7 +3010,7 @@ class NaturalDropdownService {
2929
3010
  // When click on backdrop, validate result.. ?
2930
3011
  overlayRef
2931
3012
  .backdropClick()
2932
- .pipe(takeUntil(dropdownContainer.closed))
3013
+ .pipe(takeUntil$1(dropdownContainer.closed))
2933
3014
  .subscribe(() => dropdownContainer.close());
2934
3015
  return dropdownRef;
2935
3016
  }
@@ -3605,7 +3686,7 @@ class NaturalDataSource extends DataSource {
3605
3686
  this.ngUnsubscribe = new Subject();
3606
3687
  if (value instanceof Observable) {
3607
3688
  this.internalData = new BehaviorSubject(null);
3608
- value.pipe(takeUntil(this.ngUnsubscribe)).subscribe(res => (this.data = res));
3689
+ value.pipe(takeUntil$1(this.ngUnsubscribe)).subscribe(res => (this.data = res));
3609
3690
  }
3610
3691
  else {
3611
3692
  this.internalData = new BehaviorSubject(value);
@@ -3624,7 +3705,7 @@ class NaturalDataSource extends DataSource {
3624
3705
  this.internalData.next(data);
3625
3706
  }
3626
3707
  connect() {
3627
- return this.internalData.pipe(takeUntil(this.ngUnsubscribe), map(data => (data ? data.items : [])));
3708
+ return this.internalData.pipe(takeUntil$1(this.ngUnsubscribe), map(data => (data ? data.items : [])));
3628
3709
  }
3629
3710
  disconnect() {
3630
3711
  this.ngUnsubscribe.next(); // unsubscribe everybody
@@ -3757,12 +3838,12 @@ class NaturalAbstractList extends NaturalAbstractPanel {
3757
3838
  // But we need parameters from url after NavigationEnd. So proceed in two steps with a flag.
3758
3839
  let isPopState = false;
3759
3840
  this.router.events
3760
- .pipe(takeUntil(this.ngUnsubscribe), filter(event => event instanceof NavigationStart && event.navigationTrigger === 'popstate'))
3841
+ .pipe(takeUntil$1(this.ngUnsubscribe), filter(event => event instanceof NavigationStart && event.navigationTrigger === 'popstate'))
3761
3842
  .subscribe(() => {
3762
3843
  isPopState = true;
3763
3844
  });
3764
3845
  this.router.events
3765
- .pipe(takeUntil(this.ngUnsubscribe), filter(event => event instanceof NavigationEnd && isPopState))
3846
+ .pipe(takeUntil$1(this.ngUnsubscribe), filter(event => event instanceof NavigationEnd && isPopState))
3766
3847
  .subscribe(() => {
3767
3848
  isPopState = false; // reset flag
3768
3849
  this.naturalSearchSelections = fromUrl(this.persistenceService.getFromUrl('ns', this.route));
@@ -3973,7 +4054,7 @@ class NaturalAbstractList extends NaturalAbstractPanel {
3973
4054
  // the casting and resolve things in a better way, but that's too much work for now
3974
4055
  return this.service
3975
4056
  .watchAll(this.variablesManager)
3976
- .pipe(takeUntil(this.ngUnsubscribe));
4057
+ .pipe(takeUntil$1(this.ngUnsubscribe));
3977
4058
  }
3978
4059
  initFromPersisted() {
3979
4060
  if (!this.persistSearch || this.isPanel) {
@@ -4130,7 +4211,7 @@ class NaturalAbstractNavigableList extends NaturalAbstractList {
4130
4211
  super.ngOnInit();
4131
4212
  }
4132
4213
  getDataObservable() {
4133
- return this.service.watchAll(this.variablesManager).pipe(takeUntil(this.ngUnsubscribe), map(result => {
4214
+ return this.service.watchAll(this.variablesManager).pipe(takeUntil$1(this.ngUnsubscribe), map(result => {
4134
4215
  // On each data arriving, we query children count to show/hide chevron
4135
4216
  const navigableItems = result.items.map(item => {
4136
4217
  const navigableItem = {
@@ -4259,7 +4340,7 @@ function createHttpLink(httpLink, httpBatchLink, options) {
4259
4340
  *
4260
4341
  * This is typically useful to replace setTimeout() in components where the callback
4261
4342
  * would crash if executed after the component destruction. That can easily happen
4262
- * when the user navigate quickly between pages.
4343
+ * when the user navigates quickly between pages.
4263
4344
  *
4264
4345
  * Typical usage in a component would be:
4265
4346
  *
@@ -4283,7 +4364,7 @@ function createHttpLink(httpLink, httpBatchLink, options) {
4283
4364
  * ```
4284
4365
  */
4285
4366
  function cancellableTimeout(canceller, milliSeconds = 0) {
4286
- return timer(milliSeconds).pipe(take(1), takeUntil(canceller), map(() => {
4367
+ return timer(milliSeconds).pipe(take(1), takeUntil(canceller), map$1(() => {
4287
4368
  return;
4288
4369
  }));
4289
4370
  }
@@ -4301,10 +4382,156 @@ function debug(debugName) {
4301
4382
  });
4302
4383
  }
4303
4384
 
4385
+ /**
4386
+ * Debounce subscriptions to update mutations, with the possibility to cancel one, flush one, or flush all of them.
4387
+ *
4388
+ * `modelService` is also used to separate objects by their types. So User with ID 1 is not confused with Product with ID 1.
4389
+ *
4390
+ * `id` must be the ID of the object that will be updated.
4391
+ */
4392
+ class NaturalDebounceService {
4393
+ constructor() {
4394
+ /**
4395
+ * Stores the debounced update function
4396
+ */
4397
+ this.allDebouncedUpdateCache = new Map();
4398
+ }
4399
+ /**
4400
+ * Debounce the `modelService.updateNow()` mutation for a short time. If called multiple times with the same
4401
+ * modelService and id, it will postpone the subscription to the mutation.
4402
+ *
4403
+ * All input variables for the same object (same service and ID) will be cumulated over time. So it is possible
4404
+ * to update `field1`, then `field2`, and they will be batched into a single XHR including `field1` and `field2`.
4405
+ *
4406
+ * But it will always keep the same debouncing timeline.
4407
+ */
4408
+ debounce(modelService, id, object) {
4409
+ const debouncedUpdateCache = this.getMap(modelService);
4410
+ let debounced = debouncedUpdateCache.get(id);
4411
+ if (debounced) {
4412
+ debounced.object = {
4413
+ ...debounced.object,
4414
+ ...object,
4415
+ };
4416
+ }
4417
+ else {
4418
+ const debouncer = new ReplaySubject(1);
4419
+ let wasCancelled = false;
4420
+ const canceller = new Subject();
4421
+ canceller.subscribe(() => {
4422
+ wasCancelled = true;
4423
+ debouncer.complete();
4424
+ canceller.complete();
4425
+ this.delete(modelService, id);
4426
+ });
4427
+ const flusher = new Subject();
4428
+ debounced = {
4429
+ object,
4430
+ debouncer,
4431
+ canceller,
4432
+ flusher,
4433
+ modelService: modelService,
4434
+ result: debouncer.pipe(debounceTime(2000), // Wait 2 seconds...
4435
+ raceWith(flusher), // ...unless flusher is triggered
4436
+ take(1), mergeMap(() => {
4437
+ this.delete(modelService, id);
4438
+ if (wasCancelled || !debounced) {
4439
+ return EMPTY;
4440
+ }
4441
+ return modelService.updateNow(debounced.object);
4442
+ }), shareReplay()),
4443
+ };
4444
+ debouncedUpdateCache.set(id, debounced);
4445
+ }
4446
+ // Notify our debounced update each time we ask to update
4447
+ debounced.debouncer.next();
4448
+ // Return and observable that is updated when mutation is done
4449
+ return debounced.result;
4450
+ }
4451
+ cancelOne(modelService, id) {
4452
+ const debounced = this.allDebouncedUpdateCache.get(modelService)?.get(id);
4453
+ debounced?.canceller.next();
4454
+ }
4455
+ /**
4456
+ * Immediately execute the pending update, if any.
4457
+ *
4458
+ * It should typically be called before resolving the object, to mutate it before re-fetching it from server.
4459
+ *
4460
+ * The returned observable will complete when the update completes, even if it errors.
4461
+ */
4462
+ flushOne(modelService, id) {
4463
+ const debounced = this.allDebouncedUpdateCache.get(modelService)?.get(id);
4464
+ return this.internalFlush(debounced ? [debounced] : []);
4465
+ }
4466
+ /**
4467
+ * Immediately execute all pending updates.
4468
+ *
4469
+ * It should typically be called before login out.
4470
+ *
4471
+ * The returned observable will complete when all updates complete, even if some of them error.
4472
+ */
4473
+ flush() {
4474
+ const all = [];
4475
+ this.allDebouncedUpdateCache.forEach(map => map.forEach(debounced => all.push(debounced)));
4476
+ return this.internalFlush(all);
4477
+ }
4478
+ internalFlush(debounceds) {
4479
+ const all = [];
4480
+ const allFlusher = [];
4481
+ debounceds.forEach(debounced => {
4482
+ all.push(debounced.result.pipe(catchError(() => of(undefined))));
4483
+ allFlusher.push(debounced.flusher);
4484
+ });
4485
+ if (!all.length) {
4486
+ all.push(of(undefined));
4487
+ }
4488
+ return new Observable(subscriber => {
4489
+ const subscription = forkJoin(all)
4490
+ .pipe(map$1(() => undefined))
4491
+ .subscribe(subscriber);
4492
+ // Flush only after subscription process is finished
4493
+ allFlusher.forEach(flusher => flusher.next());
4494
+ return subscription;
4495
+ });
4496
+ }
4497
+ /**
4498
+ * Count of pending updates
4499
+ */
4500
+ get count() {
4501
+ let count = 0;
4502
+ this.allDebouncedUpdateCache.forEach(map => (count += map.size));
4503
+ return count;
4504
+ }
4505
+ getMap(modelService) {
4506
+ let debouncedUpdateCache = this.allDebouncedUpdateCache.get(modelService);
4507
+ if (!debouncedUpdateCache) {
4508
+ debouncedUpdateCache = new Map();
4509
+ this.allDebouncedUpdateCache.set(modelService, debouncedUpdateCache);
4510
+ }
4511
+ return debouncedUpdateCache;
4512
+ }
4513
+ delete(modelService, id) {
4514
+ const map = this.allDebouncedUpdateCache.get(modelService);
4515
+ if (!map) {
4516
+ return;
4517
+ }
4518
+ map.delete(id);
4519
+ if (!map.size) {
4520
+ this.allDebouncedUpdateCache.delete(modelService);
4521
+ }
4522
+ }
4523
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.2.2", ngImport: i0, type: NaturalDebounceService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
4524
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.2.2", ngImport: i0, type: NaturalDebounceService, providedIn: 'root' }); }
4525
+ }
4526
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.2.2", ngImport: i0, type: NaturalDebounceService, decorators: [{
4527
+ type: Injectable,
4528
+ args: [{
4529
+ providedIn: 'root',
4530
+ }]
4531
+ }] });
4532
+
4304
4533
  class NaturalAbstractModelService {
4305
- constructor(apollo, naturalDebounceService, name, oneQuery, allQuery, createMutation, updateMutation, deleteMutation) {
4306
- this.apollo = apollo;
4307
- this.naturalDebounceService = naturalDebounceService;
4534
+ constructor(name, oneQuery, allQuery, createMutation, updateMutation, deleteMutation) {
4308
4535
  this.name = name;
4309
4536
  this.oneQuery = oneQuery;
4310
4537
  this.allQuery = allQuery;
@@ -4315,6 +4542,8 @@ class NaturalAbstractModelService {
4315
4542
  * Store the creation mutations that are pending
4316
4543
  */
4317
4544
  this.creatingCache = new Map();
4545
+ this.apollo = inject(Apollo);
4546
+ this.naturalDebounceService = inject(NaturalDebounceService);
4318
4547
  }
4319
4548
  /**
4320
4549
  * List of individual fields validators
@@ -4395,7 +4624,7 @@ class NaturalAbstractModelService {
4395
4624
  * `getFormGroupValidators`, `getFormGroupAsyncValidators` might be.
4396
4625
  */
4397
4626
  getFormGroup(model) {
4398
- const formConfig = this.getFormConfig(model);
4627
+ const formConfig = this.getFormConfig(deepClone(model));
4399
4628
  return new UntypedFormGroup(formConfig, {
4400
4629
  validators: this.getFormGroupValidators(model),
4401
4630
  asyncValidators: this.getFormGroupAsyncValidators(model),
@@ -4405,9 +4634,27 @@ class NaturalAbstractModelService {
4405
4634
  * Get a single object
4406
4635
  *
4407
4636
  * If available it will emit object from cache immediately, then it
4408
- * will **always** fetch from network and then the observable will be completed
4637
+ * will **always** fetch from network and then the observable will be completed.
4638
+ *
4639
+ * You must subscribe to start getting results (and fetch from network).
4640
+ */
4641
+ getOne(id) {
4642
+ return this.#prepareOneQuery(id, 'cache-and-network').pipe(takeWhile(result => result.networkStatus !== NetworkStatus.ready, true), map(result => result.data[this.name]));
4643
+ }
4644
+ /**
4645
+ * Watch a single object
4646
+ *
4647
+ * If available it will emit object from cache immediately, then it
4648
+ * will **always** fetch from network, and then keep watching the cache forever.
4649
+ *
4650
+ * You must subscribe to start getting results (and fetch from network).
4651
+ *
4652
+ * You **MUST** unsubscribe.
4409
4653
  */
4410
- getOne(id, fetchPolicy = 'cache-and-network') {
4654
+ watchOne(id, fetchPolicy = 'cache-and-network') {
4655
+ return this.#prepareOneQuery(id, fetchPolicy).pipe(map(result => result.data[this.name]));
4656
+ }
4657
+ #prepareOneQuery(id, fetchPolicy) {
4411
4658
  this.throwIfObservable(id);
4412
4659
  this.throwIfNotQuery(this.oneQuery);
4413
4660
  return this.getVariablesForOne(id).pipe(switchMap(variables => {
@@ -4418,7 +4665,7 @@ class NaturalAbstractModelService {
4418
4665
  fetchPolicy: fetchPolicy,
4419
4666
  nextFetchPolicy: 'cache-only',
4420
4667
  }).valueChanges;
4421
- }), filter(result => !!result.data), takeWhile(result => result.networkStatus !== NetworkStatus.ready, true), map(result => result.data[this.name]));
4668
+ }), filter(result => !!result.data));
4422
4669
  }
4423
4670
  /**
4424
4671
  * Get a collection of objects
@@ -4444,18 +4691,19 @@ class NaturalAbstractModelService {
4444
4691
  * Get a collection of objects
4445
4692
  *
4446
4693
  * Every time the observable variables change, and they are not undefined,
4447
- * it will return result from cache, then it will **always** fetch from network.
4694
+ * it will return result from cache, then it will **always** fetch from network,
4695
+ * and then keep watching the cache forever.
4448
4696
  *
4449
4697
  * You must subscribe to start getting results (and fetch from network).
4450
4698
  *
4451
- * The observable result will only complete when unsubscribing. That means you **must** unsubscribe.
4699
+ * You **MUST** unsubscribe.
4452
4700
  */
4453
4701
  watchAll(queryVariablesManager, fetchPolicy = 'cache-and-network') {
4454
4702
  this.throwIfNotQuery(this.allQuery);
4455
4703
  return combineLatest({
4456
4704
  variables: queryVariablesManager.variables.pipe(
4457
4705
  // Ignore very fast variable changes
4458
- debounceTime(20),
4706
+ debounceTime$1(20),
4459
4707
  // Wait for variables to be defined to prevent duplicate query: with and without variables
4460
4708
  // Null is accepted value for "no variables"
4461
4709
  filter(variables => typeof variables !== 'undefined')),
@@ -4504,12 +4752,12 @@ class NaturalAbstractModelService {
4504
4752
  }
4505
4753
  }
4506
4754
  // If object was not saving, and has no ID, create it
4507
- const creation = this.create(object).pipe(tap(() => {
4755
+ const creation = this.create(object).pipe(tap$1(() => {
4508
4756
  this.creatingCache.delete(object); // remove from cache
4509
4757
  }));
4510
4758
  // stores creating observable in a cache replayable version of the observable,
4511
4759
  // so several update() can subscribe to the same creation
4512
- this.creatingCache.set(object, creation.pipe(shareReplay()));
4760
+ this.creatingCache.set(object, creation.pipe(shareReplay$1()));
4513
4761
  return creation;
4514
4762
  }
4515
4763
  /**
@@ -4518,7 +4766,7 @@ class NaturalAbstractModelService {
4518
4766
  create(object) {
4519
4767
  this.throwIfObservable(object);
4520
4768
  this.throwIfNotQuery(this.createMutation);
4521
- const variables = merge({}, { input: this.getInput(object) }, this.getPartialVariablesForCreation(object));
4769
+ const variables = merge({}, { input: this.getInput(object, true) }, this.getPartialVariablesForCreation(object));
4522
4770
  return this.apollo
4523
4771
  .mutate({
4524
4772
  mutation: this.createMutation,
@@ -4543,7 +4791,7 @@ class NaturalAbstractModelService {
4543
4791
  this.throwIfNotQuery(this.updateMutation);
4544
4792
  // Keep a single instance of the debounced update function
4545
4793
  const id = object.id;
4546
- return this.naturalDebounceService.debounce(this, id, this.updateNow(object));
4794
+ return this.naturalDebounceService.debounce(this, id, object);
4547
4795
  }
4548
4796
  /**
4549
4797
  * Update an object immediately when subscribing
@@ -4553,7 +4801,7 @@ class NaturalAbstractModelService {
4553
4801
  this.throwIfNotQuery(this.updateMutation);
4554
4802
  const variables = merge({
4555
4803
  id: object.id,
4556
- input: this.getInput(object),
4804
+ input: this.getInput(object, false),
4557
4805
  }, this.getPartialVariablesForUpdate(object));
4558
4806
  return this.apollo
4559
4807
  .mutate({
@@ -4566,27 +4814,6 @@ class NaturalAbstractModelService {
4566
4814
  return mergeWith(object, mappedResult, mergeOverrideArray);
4567
4815
  }));
4568
4816
  }
4569
- /**
4570
- * Update an object but without automatically injecting values coming
4571
- * from `getDefaultForServer()`.
4572
- */
4573
- updatePartially(object) {
4574
- this.throwIfObservable(object);
4575
- this.throwIfNotQuery(this.updateMutation);
4576
- const variables = {
4577
- id: object.id,
4578
- input: omit(relationsToIds(object), 'id'),
4579
- };
4580
- return this.apollo
4581
- .mutate({
4582
- mutation: this.updateMutation,
4583
- variables: variables,
4584
- })
4585
- .pipe(map(result => {
4586
- this.apollo.client.reFetchObservableQueries();
4587
- return this.mapUpdate(result);
4588
- }));
4589
- }
4590
4817
  /**
4591
4818
  * Delete objects and then refetch the list of objects
4592
4819
  */
@@ -4614,28 +4841,32 @@ class NaturalAbstractModelService {
4614
4841
  }));
4615
4842
  }
4616
4843
  /**
4617
- * Resolve model from server (never from cache) and items related to the model, if the id is provided, in order to show a form
4844
+ * If the id is provided, resolves an observable model. The observable model will only be emitted after we are sure
4845
+ * that Apollo cache is fresh and warm. Then the component can subscribe to the observable model to get the model
4846
+ * immediately from Apollo cache and any subsequents future mutations that may happen to Apollo cache.
4847
+ *
4848
+ * Without id, returns default values, in order to show a creation form.
4618
4849
  */
4619
4850
  resolve(id) {
4620
- // Load model if id is given
4621
- let observable;
4622
4851
  if (id) {
4623
- observable = this.naturalDebounceService
4624
- .flushOne(this, id)
4625
- .pipe(switchMap(() => this.getOne(id, 'network-only')));
4852
+ const onlyNetwork = this.watchOne(id, 'network-only').pipe(first$1());
4853
+ const onlyCache = this.watchOne(id, 'cache-first');
4854
+ // In theory, we can rely on Apollo Cache to return a result instantly. It is very fast indeed,
4855
+ // but it is still asynchronous, so there may be a very short time when we don't have the model
4856
+ // available. To fix that, we can rely on RxJS, which is able to emit synchronously the value we just
4857
+ // got from server. Once Apollo Client moves to RxJS (https://github.com/apollographql/apollo-feature-requests/issues/375),
4858
+ // we could try to remove `startWith()`.
4859
+ return onlyNetwork.pipe(map(firstValue => onlyCache.pipe(startWith(firstValue))));
4626
4860
  }
4627
4861
  else {
4628
- observable = of(this.getDefaultForServer());
4862
+ return of(of(this.getDefaultForServer()));
4629
4863
  }
4630
- return observable.pipe(map(result => {
4631
- return { model: result };
4632
- }));
4633
4864
  }
4634
4865
  /**
4635
4866
  * Return an object that match the GraphQL input type.
4636
4867
  * It creates an object with manually filled data and add uncompleted data (like required attributes that can be empty strings)
4637
4868
  */
4638
- getInput(object) {
4869
+ getInput(object, forCreation) {
4639
4870
  // Convert relations to their IDs for mutation
4640
4871
  object = relationsToIds(object);
4641
4872
  // Pick only attributes that we can find in the empty object
@@ -4643,7 +4874,9 @@ class NaturalAbstractModelService {
4643
4874
  const emptyObject = this.getDefaultForServer();
4644
4875
  let input = pick(object, Object.keys(emptyObject));
4645
4876
  // Complete a potentially uncompleted object with default values
4646
- input = defaults(input, emptyObject);
4877
+ if (forCreation) {
4878
+ input = defaults(input, emptyObject);
4879
+ }
4647
4880
  return input;
4648
4881
  }
4649
4882
  /**
@@ -4791,150 +5024,6 @@ class NaturalAbstractModelService {
4791
5024
  }
4792
5025
  }
4793
5026
 
4794
- /**
4795
- * Debounce subscriptions to observable, with possibility to cancel one, or flush all of them. Typically,
4796
- * observables are object updates, so `NaturalAbstractModelService.updateNow()`.
4797
- *
4798
- * `key` must be an instance of `NaturalAbstractModelService` to separate objects by their types. So User with ID 1 is
4799
- * not confused with Product with ID 1. It has no other purpose.
4800
- *
4801
- * `id` should be the ID of the object that will be updated.
4802
- */
4803
- class NaturalDebounceService {
4804
- constructor() {
4805
- /**
4806
- * Stores the debounced update function
4807
- */
4808
- this.allDebouncedUpdateCache = new Map();
4809
- }
4810
- /**
4811
- * Debounce the given source observable for a short time. If called multiple times with the same key and id,
4812
- * it will postpone the subscription to the source observable.
4813
- *
4814
- * Giving the same key and id but a different source observable will replace the original observable, but
4815
- * keep the same debouncing timeline.
4816
- */
4817
- debounce(key, id, source) {
4818
- const debouncedUpdateCache = this.getMap(key);
4819
- let debounced = debouncedUpdateCache.get(id);
4820
- if (debounced) {
4821
- debounced.source = source;
4822
- }
4823
- else {
4824
- const debouncer = new ReplaySubject(1);
4825
- let wasCancelled = false;
4826
- const canceller = new Subject();
4827
- canceller.subscribe(() => {
4828
- wasCancelled = true;
4829
- debouncer.complete();
4830
- canceller.complete();
4831
- this.delete(key, id);
4832
- });
4833
- const flusher = new Subject();
4834
- debounced = {
4835
- debouncer,
4836
- canceller,
4837
- flusher,
4838
- source,
4839
- result: debouncer.pipe(debounceTime$1(2000), // Wait 2 seconds...
4840
- raceWith(flusher), // ...unless flusher is triggered
4841
- take$1(1), mergeMap(() => {
4842
- this.delete(key, id);
4843
- if (wasCancelled || !debounced) {
4844
- return EMPTY;
4845
- }
4846
- return debounced.source;
4847
- }), shareReplay$1()),
4848
- };
4849
- debouncedUpdateCache.set(id, debounced);
4850
- }
4851
- // Notify our debounced update each time we ask to update
4852
- debounced.debouncer.next();
4853
- // Return and observable that is updated when mutation is done
4854
- return debounced.result;
4855
- }
4856
- cancelOne(key, id) {
4857
- const debounced = this.allDebouncedUpdateCache.get(key)?.get(id);
4858
- debounced?.canceller.next();
4859
- }
4860
- /**
4861
- * Immediately execute the pending update, if any.
4862
- *
4863
- * It should typically be called before resolving the object, to mutate it before re-fetching it from server.
4864
- *
4865
- * The returned observable will complete when the update completes, even if it errors.
4866
- */
4867
- flushOne(key, id) {
4868
- const debounced = this.allDebouncedUpdateCache.get(key)?.get(id);
4869
- return this.internalFlush(debounced ? [debounced] : []);
4870
- }
4871
- /**
4872
- * Immediately execute all pending updates.
4873
- *
4874
- * It should typically be called before login out.
4875
- *
4876
- * The returned observable will complete when all updates complete, even if some of them error.
4877
- */
4878
- flush() {
4879
- const all = [];
4880
- this.allDebouncedUpdateCache.forEach(map => map.forEach(debounced => all.push(debounced)));
4881
- return this.internalFlush(all);
4882
- }
4883
- internalFlush(debounceds) {
4884
- const all = [];
4885
- const allFlusher = [];
4886
- debounceds.forEach(debounced => {
4887
- all.push(debounced.result.pipe(catchError(() => of(undefined))));
4888
- allFlusher.push(debounced.flusher);
4889
- });
4890
- if (!all.length) {
4891
- all.push(of(undefined));
4892
- }
4893
- return new Observable(subscriber => {
4894
- const subscription = forkJoin(all)
4895
- .pipe(map$1(() => undefined))
4896
- .subscribe(subscriber);
4897
- // Flush only after subscription process is finished
4898
- allFlusher.forEach(flusher => flusher.next());
4899
- return subscription;
4900
- });
4901
- }
4902
- /**
4903
- * Count of pending updates
4904
- */
4905
- get count() {
4906
- let count = 0;
4907
- this.allDebouncedUpdateCache.forEach(map => (count += map.size));
4908
- return count;
4909
- }
4910
- getMap(key) {
4911
- let debouncedUpdateCache = this.allDebouncedUpdateCache.get(key);
4912
- if (!debouncedUpdateCache) {
4913
- debouncedUpdateCache = new Map();
4914
- this.allDebouncedUpdateCache.set(key, debouncedUpdateCache);
4915
- }
4916
- return debouncedUpdateCache;
4917
- }
4918
- delete(key, id) {
4919
- const map = this.allDebouncedUpdateCache.get(key);
4920
- if (!map) {
4921
- return;
4922
- }
4923
- map.delete(id);
4924
- if (!map.size) {
4925
- this.allDebouncedUpdateCache.delete(key);
4926
- }
4927
- }
4928
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.2.2", ngImport: i0, type: NaturalDebounceService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
4929
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.2.2", ngImport: i0, type: NaturalDebounceService, providedIn: 'root' }); }
4930
- }
4931
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.2.2", ngImport: i0, type: NaturalDebounceService, decorators: [{
4932
- type: Injectable,
4933
- args: [{
4934
- providedIn: 'root',
4935
- }]
4936
- }] });
4937
-
4938
5027
  const enumTypeQuery = gql `
4939
5028
  query EnumType($name: String!) {
4940
5029
  __type(name: $name) {
@@ -5457,7 +5546,7 @@ class NaturalLinkableTabDirective extends NaturalAbstractController {
5457
5546
  return;
5458
5547
  }
5459
5548
  // When url params change, update the mat-tab-group selected tab
5460
- this.route.fragment.pipe(takeUntil(this.ngUnsubscribe)).subscribe(fragment => {
5549
+ this.route.fragment.pipe(takeUntil$1(this.ngUnsubscribe)).subscribe(fragment => {
5461
5550
  // Get index of tab that matches wanted name
5462
5551
  const tabIndex = this.getTabIndex(fragment);
5463
5552
  // If tab index is valid (>= 0) go to given fragment
@@ -5467,7 +5556,7 @@ class NaturalLinkableTabDirective extends NaturalAbstractController {
5467
5556
  }
5468
5557
  });
5469
5558
  // When mat-tab-groups selected tab change, update url
5470
- this.component.selectedTabChange.pipe(takeUntil(this.ngUnsubscribe)).subscribe(event => {
5559
+ this.component.selectedTabChange.pipe(takeUntil$1(this.ngUnsubscribe)).subscribe(event => {
5471
5560
  const activatedTabName = getTabId(event.tab);
5472
5561
  const segments = this.route.snapshot.url;
5473
5562
  if (!segments.length) {
@@ -5870,18 +5959,22 @@ class NaturalSeoService {
5870
5959
  applicationName: '',
5871
5960
  };
5872
5961
  combineLatest({
5873
- config: configToken instanceof Observable ? configToken.pipe(startWith(this.config)) : of(configToken),
5874
- navigationEnd: this.router.events.pipe(filter(event => event instanceof NavigationEnd)),
5875
- }).subscribe(({ config }) => {
5962
+ config: configToken instanceof Observable ? configToken.pipe(startWith$1(this.config)) : of(configToken),
5963
+ navigationEnd: this.router.events.pipe(filter$1(event => event instanceof NavigationEnd)),
5964
+ })
5965
+ .pipe(takeUntilDestroyed(), switchMap$1(({ config }) => {
5876
5966
  this.config = config;
5877
5967
  const root = this.router.routerState.root.snapshot;
5878
5968
  this.routeData = this.getRouteData(root);
5879
5969
  const seo = this.routeData.seo ?? { title: '' };
5880
5970
  const dialogRouteData = this.getDialogRouteData(root);
5881
5971
  const dialogSeo = dialogRouteData?.seo;
5882
- let basic = this.toBasic(seo, this.routeData);
5883
- if (dialogRouteData && dialogSeo) {
5884
- const dialogBasic = this.toBasic(dialogSeo, dialogRouteData);
5972
+ const basic = this.toBasic(seo, this.routeData);
5973
+ const dialogBasic = dialogRouteData && dialogSeo ? this.toBasic(dialogSeo, dialogRouteData) : of(null);
5974
+ return combineLatest({ basic, dialogBasic });
5975
+ }))
5976
+ .subscribe(({ basic, dialogBasic }) => {
5977
+ if (dialogBasic) {
5885
5978
  basic = {
5886
5979
  ...dialogBasic,
5887
5980
  title: this.join([dialogBasic.title, basic.title]),
@@ -5931,7 +6024,7 @@ class NaturalSeoService {
5931
6024
  const urlTree = this.router.parseUrl(this.router.url);
5932
6025
  // need better like something recursive ?
5933
6026
  if (urlTree.root.hasChildren()) {
5934
- const segments = urlTree.root.children.primary.segments;
6027
+ const segments = urlTree.root.children.primary?.segments;
5935
6028
  if (segments && segments.length > 0) {
5936
6029
  url += '/' + segments.map(segment => segment.path).join('/');
5937
6030
  }
@@ -6034,20 +6127,23 @@ class NaturalSeoService {
6034
6127
  }
6035
6128
  toBasic(seo, routeData) {
6036
6129
  if (typeof seo === 'function') {
6037
- return seo(routeData);
6130
+ const result = seo(routeData);
6131
+ return result instanceof Observable ? result : of(result);
6038
6132
  }
6039
- else if ('resolveKey' in seo) {
6040
- const data = routeData[seo.resolveKey];
6041
- if (!data) {
6042
- throw new Error('Could not find resolved data for SEO service with key: ' + seo.resolveKey);
6133
+ else if ('resolve' in seo) {
6134
+ if (!('model' in routeData)) {
6135
+ throw new Error('Could not find resolved data `model` for SEO service');
6043
6136
  }
6044
- return {
6045
- title: data.model?.fullName ?? data.model?.name ?? '',
6046
- description: data.model?.description,
6047
- robots: seo.robots,
6048
- };
6137
+ const model = routeData.model;
6138
+ return (model instanceof Observable ? model : of(model)).pipe(map$1(value => {
6139
+ return {
6140
+ title: value?.fullName ?? value?.name ?? '',
6141
+ description: value?.description,
6142
+ robots: seo.robots,
6143
+ };
6144
+ }));
6049
6145
  }
6050
- return seo;
6146
+ return of(seo);
6051
6147
  }
6052
6148
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.2.2", ngImport: i0, type: NaturalSeoService, deps: [{ token: NATURAL_SEO_CONFIG }, { token: i2$3.Router }, { token: i2$4.Title }, { token: i2$4.Meta }, { token: DOCUMENT }, { token: LOCALE_ID }], target: i0.ɵɵFactoryTarget.Injectable }); }
6053
6149
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.2.2", ngImport: i0, type: NaturalSeoService, providedIn: 'root' }); }
@@ -6257,7 +6353,7 @@ class TypeSelectComponent extends NaturalAbstractController {
6257
6353
  const items$ = Array.isArray(this.configuration.items)
6258
6354
  ? of(this.configuration.items)
6259
6355
  : this.configuration.items;
6260
- return items$.pipe(takeUntil(this.ngUnsubscribe), map(items => {
6356
+ return items$.pipe(takeUntil$1(this.ngUnsubscribe), map(items => {
6261
6357
  this.items = items;
6262
6358
  // Reload selection, according to possible values from configuration
6263
6359
  const possibleIds = this.items.map(item => this.getId(item));
@@ -6707,7 +6803,7 @@ class NaturalSelectComponent extends AbstractSelect {
6707
6803
  }
6708
6804
  ngAfterViewInit() {
6709
6805
  this.internalCtrl.valueChanges
6710
- .pipe(takeUntil(this.ngUnsubscribe), distinctUntilChanged(), debounceTime(300))
6806
+ .pipe(takeUntil$1(this.ngUnsubscribe), distinctUntilChanged(), debounceTime$1(300))
6711
6807
  .subscribe(val => this.search(val));
6712
6808
  }
6713
6809
  onInternalFormChange() {
@@ -6766,7 +6862,7 @@ class NaturalSelectComponent extends AbstractSelect {
6766
6862
  return;
6767
6863
  }
6768
6864
  // Init query, and when query results arrive, finish loading, and count items
6769
- this.items = this.service.watchAll(this.variablesManager).pipe(takeUntil(this.ngUnsubscribe), finalize(() => (this.loading = false)), map(data => {
6865
+ this.items = this.service.watchAll(this.variablesManager).pipe(takeUntil$1(this.ngUnsubscribe), finalize$1(() => (this.loading = false)), map(data => {
6770
6866
  this.loading = false;
6771
6867
  const nbTotal = data.length;
6772
6868
  const nbListed = Math.min(data.length, this.pageSize);
@@ -7131,7 +7227,7 @@ class NaturalHierarchicSelectorService {
7131
7227
  }
7132
7228
  flatNode.loading = true;
7133
7229
  this.getList(flatNode, contextFilter)
7134
- .pipe(finalize(() => (flatNode.loading = false)))
7230
+ .pipe(finalize$1(() => (flatNode.loading = false)))
7135
7231
  .subscribe(items => {
7136
7232
  flatNode.node.childrenChange.next(items);
7137
7233
  this.dataChange.next(this.dataChange.value);
@@ -7916,7 +8012,7 @@ class NaturalHierarchicSelectorComponent extends NaturalAbstractController {
7916
8012
  // Update dataSource when receiving new list -> we assign the whole tree
7917
8013
  // The treeControl and treeFlattener will generate the displayed tree
7918
8014
  this.hierarchicSelectorService.dataChange
7919
- .pipe(takeUntil(this.ngUnsubscribe))
8015
+ .pipe(takeUntil$1(this.ngUnsubscribe))
7920
8016
  .subscribe(data => (this.dataSource.data = data));
7921
8017
  // Prevent empty screen on first load and init NaturalHierarchicSelectorService with inputted configuration
7922
8018
  let variables;
@@ -8060,7 +8156,7 @@ class NaturalHierarchicSelectorComponent extends NaturalAbstractController {
8060
8156
  this.flatNodeMap = new Map();
8061
8157
  this.hierarchicSelectorService
8062
8158
  .init(this.config, this.filters, searchVariables || null)
8063
- .pipe(finalize(() => (this.loading = false)))
8159
+ .pipe(finalize$1(() => (this.loading = false)))
8064
8160
  .subscribe();
8065
8161
  }
8066
8162
  /**
@@ -8762,7 +8858,7 @@ class NaturalAbstractFile extends NaturalAbstractController {
8762
8858
  valid: [],
8763
8859
  invalid: [],
8764
8860
  };
8765
- forkJoin(files.map(file => this.validate(file).pipe(tap$1(error => {
8861
+ forkJoin(files.map(file => this.validate(file).pipe(tap(error => {
8766
8862
  if (error) {
8767
8863
  selection.invalid.push({
8768
8864
  file: file,
@@ -8901,7 +8997,7 @@ class NaturalFileDropDirective extends NaturalAbstractFile {
8901
8997
  // It's not absolutely perfect and if dragging slowly and precisely we can
8902
8998
  // still see flicker, but it should be better for most normal usages.
8903
8999
  this.rawFileOver
8904
- .pipe(takeUntil(this.ngUnsubscribe), throttleTime(200, asyncScheduler, {
9000
+ .pipe(takeUntil$1(this.ngUnsubscribe), throttleTime(200, asyncScheduler, {
8905
9001
  leading: true,
8906
9002
  trailing: true,
8907
9003
  }))
@@ -9048,7 +9144,7 @@ class NaturalFileComponent {
9048
9144
  if (this.formCtrl) {
9049
9145
  this.formCtrl.setValue(this.model);
9050
9146
  }
9051
- const observable = this.uploader?.(file).pipe(tap$1(() => this.alertService.info($localize `Mis à jour`))) ?? of(this.model);
9147
+ const observable = this.uploader?.(file).pipe(tap(() => this.alertService.info($localize `Mis à jour`))) ?? of(this.model);
9052
9148
  observable.subscribe(result => {
9053
9149
  this.model = result;
9054
9150
  if (this.formCtrl) {
@@ -9598,7 +9694,7 @@ class NaturalPanelsService {
9598
9694
  openPanels(newItemsConfig, fullConfig) {
9599
9695
  const subject = new Subject();
9600
9696
  // Resolve everything before opening a single panel
9601
- const resolves = newItemsConfig.map((conf) => this.getResolvedData(conf));
9697
+ const resolves = newItemsConfig.map(conf => this.getResolvedData(conf));
9602
9698
  // ForkJoin emits when all promises are executed;
9603
9699
  forkJoin(resolves).subscribe(resolvedResult => {
9604
9700
  // For each new config entry, open a new panel
@@ -9641,9 +9737,7 @@ class NaturalPanelsService {
9641
9737
  resolvedData[key] = runInInjectionContext(injector, () => config.resolve[key](config));
9642
9738
  });
9643
9739
  }
9644
- return forkJoin(resolvedData).pipe(map(result => {
9645
- return result.model || result;
9646
- }));
9740
+ return forkJoin(resolvedData);
9647
9741
  }
9648
9742
  openPanel(componentOrTemplateRef, data) {
9649
9743
  const conf = {
@@ -9826,7 +9920,7 @@ class NaturalRelationsComponent extends NaturalAbstractController {
9826
9920
  this.removing.add(relation);
9827
9921
  this.linkMutationService
9828
9922
  .unlink(this.main, relation, this.otherName)
9829
- .pipe(finalize(() => this.removing.delete(relation)))
9923
+ .pipe(finalize$1(() => this.removing.delete(relation)))
9830
9924
  .subscribe(() => this.dataSource?.remove(relation));
9831
9925
  }
9832
9926
  /**
@@ -9892,7 +9986,7 @@ class NaturalRelationsComponent extends NaturalAbstractController {
9892
9986
  return;
9893
9987
  }
9894
9988
  this.loading = true;
9895
- const queryRef = this.service.watchAll(this.variablesManager).pipe(takeUntil(this.ngUnsubscribe), tap$1({
9989
+ const queryRef = this.service.watchAll(this.variablesManager).pipe(takeUntil$1(this.ngUnsubscribe), tap({
9896
9990
  next: () => (this.loading = false),
9897
9991
  complete: () => (this.loading = false),
9898
9992
  error: () => (this.loading = false),
@@ -10190,7 +10284,7 @@ function assert(value) {
10190
10284
  }
10191
10285
  }
10192
10286
  /**
10193
- * @TODO : Fix nav minimize and maximize resize
10287
+ * TODO: Fix nav minimize and maximize resize
10194
10288
  * Since Material 2 beta 10, when nav is resized the body is not resized
10195
10289
  * https://github.com/angular/material2/issues/6743
10196
10290
  * Maybe the better is to wait next release
@@ -10262,7 +10356,7 @@ class NaturalSidenavService extends NaturalAbstractController {
10262
10356
  let oldIsBig = null;
10263
10357
  this.mediaObserver
10264
10358
  .asObservable()
10265
- .pipe(takeUntil(this.ngUnsubscribe))
10359
+ .pipe(takeUntil$1(this.ngUnsubscribe))
10266
10360
  .subscribe(() => {
10267
10361
  const isBig = !this.isMobileView();
10268
10362
  this.mode = isBig ? this.modes[0] : this.modes[1];
@@ -10282,7 +10376,7 @@ class NaturalSidenavService extends NaturalAbstractController {
10282
10376
  });
10283
10377
  if (autoClose) {
10284
10378
  this.router.events
10285
- .pipe(takeUntil(this.ngUnsubscribe), filter(e => e instanceof NavigationEnd))
10379
+ .pipe(takeUntil$1(this.ngUnsubscribe), filter(e => e instanceof NavigationEnd))
10286
10380
  .subscribe(() => {
10287
10381
  this.navItemClicked();
10288
10382
  });
@@ -11453,5 +11547,5 @@ function graphqlQuerySigner(key) {
11453
11547
  * Generated bundle index. Do not edit.
11454
11548
  */
11455
11549
 
11456
- export { AvatarService, InvalidWithValueStateMatcher$1 as InvalidWithValueStateMatcher, LOCAL_STORAGE, NATURAL_DROPDOWN_DATA, NATURAL_ICONS_CONFIG, NATURAL_PERSISTENCE_VALIDATOR, NATURAL_SEO_CONFIG, NaturalAbstractController, NaturalAbstractDetail, NaturalAbstractEditableList, NaturalAbstractList, NaturalAbstractModelService, NaturalAbstractNavigableList, NaturalAbstractPanel, NaturalAlertService, NaturalAvatarComponent, NaturalCapitalizePipe, NaturalColumnsPickerComponent, NaturalConfirmComponent, NaturalDataSource, NaturalDebounceService, NaturalDetailHeaderComponent, NaturalDialogTriggerComponent, NaturalDropdownRef, NaturalEllipsisPipe, NaturalEnumPipe, NaturalEnumService, NaturalErrorHandler, NaturalFileComponent, NaturalFileDropDirective, NaturalFileSelectDirective, NaturalFileService, NaturalFixedButtonComponent, NaturalFixedButtonDetailComponent, NaturalHierarchicSelectorComponent, NaturalHierarchicSelectorDialogComponent, NaturalHierarchicSelectorDialogService, NaturalHierarchicSelectorService, NaturalHttpPrefixDirective, NaturalIconDirective, NaturalLinkMutationService, NaturalLinkableTabDirective, NaturalLoggerConfigExtra, NaturalLoggerConfigUrl, NaturalMatomoService, NaturalMemoryStorage, NaturalPanelsComponent, NaturalPanelsService, NaturalPersistenceService, NaturalQueryVariablesManager, NaturalRelationsComponent, NaturalSearchComponent, NaturalSelectComponent, NaturalSelectEnumComponent, NaturalSelectHierarchicComponent, NaturalSeoService, NaturalSidenavComponent, NaturalSidenavContainerComponent, NaturalSidenavContentComponent, NaturalSidenavService, NaturalSidenavStackService, NaturalSrcDensityDirective, NaturalStampComponent, NaturalSwissDatePipe, NaturalSwissParsingDateAdapter, NaturalTableButtonComponent, NaturalTimeAgoPipe, PanelsHooksConfig, SESSION_STORAGE, SortingOrder, TypeBooleanComponent, TypeDateComponent, TypeDateRangeComponent, TypeHierarchicSelectorComponent, TypeNaturalSelectComponent, TypeNumberComponent, TypeOptionsComponent, TypeSelectComponent, TypeTextComponent, available, cancellableTimeout, cleanSameValues, collectErrors, copyToClipboard, createHttpLink, debug, decimal, deepFreeze, deliverableEmail, ensureHttpPrefix, fallbackIfNoOpenedPanels, formatIsoDate, formatIsoDateTime, fromUrl, getForegroundColor, graphqlQuerySigner, ifValid, integer, localStorageFactory, localStorageProvider, lowerCaseFirstLetter, makePlural, memoryLocalStorageProvider, memorySessionStorageProvider, mergeOverrideArray, money, naturalPanelsUrlMatcher, naturalProviders, possibleComparableOperators, provideErrorHandler, provideIcons, providePanels, provideSeo, relationsToIds, replaceObjectKeepingReference, replaceOperatorByField, replaceOperatorByName, sessionStorageFactory, sessionStorageProvider, toGraphQLDoctrineFilter, toNavigationParameters, toUrl, unique, upperCaseFirstLetter, urlValidator, validTlds, validateAllFormControls, validateColumns, validatePagination, validateSorting, wrapLike, wrapPrefix, wrapSuffix };
11550
+ export { AvatarService, InvalidWithValueStateMatcher$1 as InvalidWithValueStateMatcher, LOCAL_STORAGE, NATURAL_DROPDOWN_DATA, NATURAL_ICONS_CONFIG, NATURAL_PERSISTENCE_VALIDATOR, NATURAL_SEO_CONFIG, NaturalAbstractController, NaturalAbstractDetail, NaturalAbstractEditableList, NaturalAbstractList, NaturalAbstractModelService, NaturalAbstractNavigableList, NaturalAbstractPanel, NaturalAlertService, NaturalAvatarComponent, NaturalCapitalizePipe, NaturalColumnsPickerComponent, NaturalConfirmComponent, NaturalDataSource, NaturalDebounceService, NaturalDetailHeaderComponent, NaturalDialogTriggerComponent, NaturalDropdownRef, NaturalEllipsisPipe, NaturalEnumPipe, NaturalEnumService, NaturalErrorHandler, NaturalFileComponent, NaturalFileDropDirective, NaturalFileSelectDirective, NaturalFileService, NaturalFixedButtonComponent, NaturalFixedButtonDetailComponent, NaturalHierarchicSelectorComponent, NaturalHierarchicSelectorDialogComponent, NaturalHierarchicSelectorDialogService, NaturalHierarchicSelectorService, NaturalHttpPrefixDirective, NaturalIconDirective, NaturalLinkMutationService, NaturalLinkableTabDirective, NaturalLoggerConfigExtra, NaturalLoggerConfigUrl, NaturalMatomoService, NaturalMemoryStorage, NaturalPanelsComponent, NaturalPanelsService, NaturalPersistenceService, NaturalQueryVariablesManager, NaturalRelationsComponent, NaturalSearchComponent, NaturalSelectComponent, NaturalSelectEnumComponent, NaturalSelectHierarchicComponent, NaturalSeoService, NaturalSidenavComponent, NaturalSidenavContainerComponent, NaturalSidenavContentComponent, NaturalSidenavService, NaturalSidenavStackService, NaturalSrcDensityDirective, NaturalStampComponent, NaturalSwissDatePipe, NaturalSwissParsingDateAdapter, NaturalTableButtonComponent, NaturalTimeAgoPipe, PanelsHooksConfig, SESSION_STORAGE, SortingOrder, TypeBooleanComponent, TypeDateComponent, TypeDateRangeComponent, TypeHierarchicSelectorComponent, TypeNaturalSelectComponent, TypeNumberComponent, TypeOptionsComponent, TypeSelectComponent, TypeTextComponent, available, cancellableTimeout, collectErrors, copyToClipboard, createHttpLink, debug, decimal, deepFreeze, deliverableEmail, ensureHttpPrefix, fallbackIfNoOpenedPanels, formatIsoDate, formatIsoDateTime, fromUrl, getForegroundColor, graphqlQuerySigner, ifValid, integer, localStorageFactory, localStorageProvider, makePlural, memoryLocalStorageProvider, memorySessionStorageProvider, mergeOverrideArray, money, naturalPanelsUrlMatcher, naturalProviders, possibleComparableOperators, provideErrorHandler, provideIcons, providePanels, provideSeo, relationsToIds, replaceObjectKeepingReference, replaceOperatorByField, replaceOperatorByName, sessionStorageFactory, sessionStorageProvider, toGraphQLDoctrineFilter, toNavigationParameters, toUrl, unique, upperCaseFirstLetter, urlValidator, validTlds, validateAllFormControls, validateColumns, validatePagination, validateSorting, wrapLike, wrapPrefix, wrapSuffix };
11457
11551
  //# sourceMappingURL=ecodev-natural.mjs.map