@sumaris-net/ngx-components 21.3.1 → 21.3.3-rc1

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/AGENTS.md ADDED
@@ -0,0 +1,78 @@
1
+ # Developer Guide
2
+
3
+ ## Project
4
+
5
+ `@sumaris-net/ngx-components` est une **bibliothèque Angular** (publiée sur npm) fournissant des composants de base réutilisables pour des applications Angular/Ionic. Elle est actuellement migrée vers Angular 21.
6
+
7
+ - Dépôt GitLab : https://gitlab.ifremer.fr/sih-public/sumaris/ngx-sumaris-components
8
+ - Branche principale : `master`
9
+ - Version actuelle : alignée sur la version majeure d'Angular (ex. `21.x.x` pour Angular 21)
10
+
11
+ ## General
12
+
13
+ - Ask me if not possible, or instructions is not clear
14
+ - Use Angular 21, Angular Material 21 and Ionic 8
15
+ - Agent configuration (rules, skills, MCP) lives in `.agents/` - see [.agents/README.md](.agents/README.md)
16
+
17
+ ## Stack
18
+
19
+ | Technologie | Version | Usage |
20
+ |-----------------------|----------|----------------------------------------------------------|
21
+ | Angular | ~21.2.x | Framework principal |
22
+ | Angular Material | ~21.2.x | Composants UI Material Design |
23
+ | Ionic | ~8.8.x | Composants mobiles et navigation |
24
+ | Capacitor | ~7.4.x | Accès aux APIs natives (Android / iOS) |
25
+ | RxState (@rx-angular) | ~21.x | Gestion d'état réactif dans les composants |
26
+ | Apollo / GraphQL | ~4.x | Communication avec le back-end via GraphQL |
27
+ | ngx-translate | ~16.x | Internationalisation (i18n) |
28
+ | Moment.js | ~2.30.x | Manipulation des dates |
29
+ | Playwright | ~1.58.x | Tests end-to-end |
30
+ | Jasmine / Karma | ~5.x | Tests unitaires |
31
+ | ng-packagr | ~21.2.x | Build et publication de la bibliothèque |
32
+
33
+ ## Commands
34
+
35
+ ```bash
36
+ npm run start # Démarre l'app de démo sur http://localhost:4201
37
+ npm run build # Build de production de l'app de démo
38
+ npm run packagr # Build de la bibliothèque (dist/)
39
+ npm run publish # Build + publication npm
40
+ npm test # Tests unitaires (Jasmine + Karma)
41
+ npm run lint # Vérification ESLint
42
+ npm run lint:fix # Correction automatique ESLint
43
+ npm run e2e # Tests end-to-end (Playwright)
44
+ npm run e2e:install # Installation du navigateur Playwright (Chromium)
45
+ ```
46
+
47
+ > L'app de démo/test tourne sur **http://localhost:4201** (voir `testing.md` pour les tests Playwright).
48
+
49
+ ## Key directories
50
+
51
+ | Répertoire | Description |
52
+ |----------------------|---------------------------------------------------------------------|
53
+ | `src/app/core/` | Module principal : entités de base, services, composants génériques |
54
+ | `src/app/shared/` | Module partagé : formulaires, utilitaires, directives |
55
+ | `src/app/social/` | Module social (commentaires, notifications) |
56
+ | `src/app/admin/` | Module administration (utilisateurs, permissions) |
57
+ | `src/app/testing/` | Composants et pages pour les tests et la démo |
58
+ | `src/theme/` | Styles globaux Ionic / Material / SCSS custom |
59
+ | `src/assets/i18n/` | Fichiers de traduction (en.json, fr.json…) |
60
+
61
+ ## Rules
62
+
63
+ Ce tableau référence l'ensemble des règles de développement du projet. Chaque règle est détaillée dans un fichier dédié du dossier `.agents/rules/`.
64
+
65
+ | Fichier | Description |
66
+ |--------------------------------------------------------|-----------------------------------------------------------------------------------------------|
67
+ | [common.md](.agents/rules/common.md) | Règles communes : conventions de nommage, suffixes de fichiers, frameworks |
68
+ | [component.md](.agents/rules/component.md) | Écriture d'un composant Angular : RxState, template HTML, modals, ngx-translate |
69
+ | [entities.md](.agents/rules/entities.md) | Modèle d'entité TypeScript : `@EntityClass`, `asObject()`, `fromObject()` |
70
+ | [service.md](.agents/rules/service.md) | Écriture d'un service : `BaseEntityService`, GraphQL, CRUD, pattern de sauvegarde |
71
+ | [service-offline.md](.agents/rules/service-offline.md) | Gestion offline : `RootDataSynchroService`, `EntityStorage`, synchronisation |
72
+ | [service-quality.md](.agents/rules/service-quality.md) | Qualité des données : control, terminaison, validation, délégation aux services fils |
73
+ | [validator.md](.agents/rules/validator.md) | Écriture d'un validateur : `AppValidatorService`, Reactive Forms, options typées, FormArray |
74
+ | [table.md](.agents/rules/table.md) | Écriture d'un tableau : `AppTable`, validateur, colonnes, template HTML |
75
+ | [gitlab.md](.agents/rules/gitlab.md) | Workflow GitLab : branches, merge requests, revues de code, conventions |
76
+ | [configuration.md](.agents/rules/configuration.md) | Options de configuration par module : `FormFieldDefinition`, `ConfigService`, settings locaux |
77
+ | [testing.md](.agents/rules/testing.md) | Tests de l'application : unit test (Jasmine), reproduction de bugs, tests e2e via Playwright |
78
+ | [theming.md](.agents/rules/theming.md) | Thème et styles : rôle de chaque fichier de `src/theme`, SCSS/CSS (Ionic / Material) |
package/CLAUDE.md ADDED
@@ -0,0 +1 @@
1
+ @AGENTS.md
package/doc/changelog.md CHANGED
@@ -2291,9 +2291,50 @@ enh: Environment: add useHash property to configure Angular router to use hash U
2291
2291
  - fix(polyfills): 'npm run utils:compat' now add all core-js compat imports for old browsers defined in the file '.browserslistrc' (eg. Chrome 61+)
2292
2292
  - fix(platform) Fix keyboard issue on Android before 14 (fix issue sumaris-app#2076)
2293
2293
 
2294
- ## 18.26.16
2294
+ ## 18.27.0
2295
2295
  - enh(about) Add changelog link (Notes de version) to About modal, using ConfigurationService and markdown modal
2296
2296
 
2297
+ ## 18.27.2
2298
+ - fix(about) Replace version placeholder `{version}` in changelog link
2299
+
2300
+ ## 18.27.3
2301
+ - fix(about) Generate changelog from the gitlab release API. Now we can URL like :
2302
+ * https://gitlab.ifremer.fr/api/v4/projects/sih-public%2Fsumaris%2Fsumaris-app/releases
2303
+ * or https://gitlab.ifremer.fr/api/v4/projects/sih-public%2Fsumaris%2Fsumaris-app/releases/2.16.2
2304
+ - enh(upload-file): File size validation error message
2305
+ - enh(upload-file): Check for excluded extensions
2306
+ - enh(alerts): implement options, including `backdropDismiss` and `keyboardClose`
2307
+
2308
+ ## 18.27.4
2309
+ - enh(alerts): expose new options to callers
2310
+
2311
+ ## 18.27.5
2312
+ - fix(build) Fix build error on quadrige3-app (Angular 20)
2313
+
2314
+ ## 18.27.7
2315
+ - fix(animation) Fix warning in the JS console, on expansionAnimation
2316
+
2317
+ ## 18.27.8
2318
+ - fix(settings-page) Local settings was always overridden by account settings - Fix issue sumaris-app#1755
2319
+
2320
+ ## 18.27.9
2321
+ - fix(image): increase default max dimensions to 2048x2048
2322
+
2323
+ ## 18.27.10
2324
+ - fix(table): Don't call `confirmEditCreate()` on canceling row edition
2325
+
2326
+ ## 18.27.11
2327
+ - enh(account): Add option to force account settings to be saved remotely
2328
+
2329
+ ## 18.27.12
2330
+ - enh(table): Add option to set loading debounce time
2331
+
2332
+ ## 18.27.14
2333
+ - enh(async-table): Add new options to save(): `keepSelection`, `confirmRow`, `throwIfNotConfirmed`
2334
+
2335
+ ## 18.27.15
2336
+ - enh(about-modal): reorganise about modal (Mantis #69903)
2337
+
2297
2338
  ## 21.0.0
2298
2339
  - enh(core) Migration to Angular 21 and Ionic 8
2299
2340
  - enh(e2e) Allow to run e2e on many peer/login, using file `e2e/.environments.json` or `.environments.json` (or `.local/environments.json`)
@@ -2383,4 +2424,7 @@ The library now uses the Ionic **standalone** build (`@ionic/angular/standalone`
2383
2424
 
2384
2425
  ## 21.3.1
2385
2426
  - enh(image-gallery): add `slideshowActionsTemplate` input to `AppImageGalleryComponent` (and `actionsTemplate` to `AppImageGallerySlideshowComponent`)
2386
- - a `TemplateRef` slot rendered in the slideshow footer's middle column
2427
+ - a `TemplateRef` slot rendered in the slideshow footer's middle column
2428
+
2429
+ ## 21.3.2
2430
+ - fix(latlong-field): remove duplicated subscript reserved space under lat/long fields
@@ -14662,6 +14662,11 @@ class MatLatLongField {
14662
14662
  get readonly() {
14663
14663
  return this._readonly;
14664
14664
  }
14665
+ /**
14666
+ * Subscript sizing of the HOST wrapper (the one rendering projected hints/errors).
14667
+ * NOTE: the inner <mat-form-field> is always forced to 'dynamic' (its subscript is always
14668
+ * empty) — changing this input only affects the space reserved around the host wrapper.
14669
+ */
14665
14670
  set subscriptSizing(value) {
14666
14671
  this._subscriptSizing = value;
14667
14672
  }
@@ -14764,7 +14769,7 @@ class MatLatLongField {
14764
14769
  multi: true,
14765
14770
  useExisting: forwardRef(() => MatLatLongField),
14766
14771
  },
14767
- ], viewQueries: [{ propertyName: "input", first: true, predicate: ["input"], descendants: true }], ngImport: i0, template: "<mat-form-field\n #formField\n [floatLabel]=\"floatLabel | asFloatLabelType\"\n [appearance]=\"appearance\"\n [subscriptSizing]=\"subscriptSizing\"\n [class.mdc-text-field--invalid]=\"formControl.touched && formControl.invalid\"\n class=\"mat-latlong-field mat-latlong-field-{{ type }} {{ classList }}\"\n>\n <div matPrefix>\n <ng-container *ngTemplateOutlet=\"matPrefixTemplate\"></ng-container>\n </div>\n\n @if (floatLabel !== 'never' && placeholder | isNotNilOrBlank) {\n <mat-label>{{ placeholder }}</mat-label>\n }\n\n <mat-latlong-input\n #input\n [formControl]=\"formControl\"\n [required]=\"required\"\n [disabled]=\"disabled\"\n [type]=\"type\"\n [latLongPattern]=\"pattern\"\n [defaultSign]=\"defaultSign\"\n [maxDecimals]=\"maxDecimals\"\n [readonly]=\"readonly\"\n ></mat-latlong-input>\n\n <div matSuffix>\n <ng-container *ngTemplateOutlet=\"matSuffixTemplate\"></ng-container>\n </div>\n</mat-form-field>\n\n<!-- Do the same as MatFromField since angular 15 (see component source)-->\n<div\n class=\"mat-mdc-form-field-subscript-wrapper\"\n [class.mat-mdc-form-field-bottom-align]=\"subscriptSizing === 'fixed'\"\n [class.mat-mdc-form-field-subscript-dynamic-size]=\"subscriptSizing === 'dynamic'\"\n>\n @if (formControl.touched && formControl.invalid) {\n <!-- errors -->\n <div class=\"mat-mdc-form-field-error-wrapper\">\n @if (formControl.errors | mapKeys | arrayFirst; as errorKey) {\n <mat-error>\n {{ i18nErrorKeys | mapGet: errorKey | translate: (formControl.errors | mapGet: errorKey) }}\n </mat-error>\n } @else {\n <ng-container *ngTemplateOutlet=\"matErrorTemplate\"></ng-container>\n }\n </div>\n } @else {\n <!-- hints -->\n <mat-hint>\n <ng-container *ngTemplateOutlet=\"matHintTemplate\"></ng-container>\n </mat-hint>\n }\n</div>\n\n<ng-template #matPrefixTemplate>\n <ng-content select=\"[matPrefix]\"></ng-content>\n</ng-template>\n\n<ng-template #matSuffixTemplate>\n @if (clearable && !readonly) {\n <button\n matSuffix\n mat-icon-button\n tabindex=\"-1\"\n type=\"button\"\n (click)=\"clearValue($event)\"\n [hidden]=\"formControl.value | isNil\"\n >\n <mat-icon>close</mat-icon>\n </button>\n }\n\n <ng-content select=\"[matSuffix]\"></ng-content>\n</ng-template>\n\n<ng-template #matErrorTemplate>\n <ng-content select=\"mat-error,[matError]\"></ng-content>\n</ng-template>\n\n<ng-template #matHintTemplate>\n <div class=\"mat-mdc-form-field-hint-wrapper\">\n <ng-content select=\"mat-hint:not([align='end']),[matHint]\"></ng-content>\n <div class=\"mat-mdc-form-field-hint-spacer\"></div>\n <ng-content select=\"mat-hint[align='end']\"></ng-content>\n </div>\n</ng-template>\n", styles: [""], dependencies: [{ kind: "directive", type: i4$1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i1$2.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "component", type: i3.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3.MatLabel, selector: "mat-label" }, { kind: "directive", type: i3.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: i3.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "directive", type: i3.MatPrefix, selector: "[matPrefix], [matIconPrefix], [matTextPrefix]", inputs: ["matTextPrefix"] }, { kind: "directive", type: i3.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "component", type: i9.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i6$2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: MatLatLongFieldInput, selector: "mat-latlong-input", inputs: ["aria-describedby", "required", "disabled", "value", "defaultSign", "type", "latLongPattern", "maxDecimals", "readonly"] }, { kind: "pipe", type: ArrayFirstPipe, name: "arrayFirst" }, { kind: "pipe", type: MapGetPipe, name: "mapGet" }, { kind: "pipe", type: MapKeysPipe, name: "mapKeys" }, { kind: "pipe", type: IsNotNilOrBlankPipe, name: "isNotNilOrBlank" }, { kind: "pipe", type: IsNilPipe, name: "isNil" }, { kind: "pipe", type: AsFloatLabelTypePipe, name: "asFloatLabelType" }, { kind: "pipe", type: i2.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
14772
+ ], viewQueries: [{ propertyName: "input", first: true, predicate: ["input"], descendants: true }], ngImport: i0, template: "<!-- Errors and hints are rendered by this component's own subscript wrapper (below the form-field),\n so force 'dynamic' here: with the default 'fixed' sizing, the inner form-field would reserve\n an extra ~22px subscript line, making rows with lat/long fields taller than other rows\n (e.g. ObsMer operation form). -->\n<mat-form-field\n #formField\n [floatLabel]=\"floatLabel | asFloatLabelType\"\n [appearance]=\"appearance\"\n [subscriptSizing]=\"'dynamic'\"\n [class.mdc-text-field--invalid]=\"formControl.touched && formControl.invalid\"\n class=\"mat-latlong-field mat-latlong-field-{{ type }} {{ classList }}\"\n>\n <div matPrefix>\n <ng-container *ngTemplateOutlet=\"matPrefixTemplate\"></ng-container>\n </div>\n\n @if (floatLabel !== 'never' && placeholder | isNotNilOrBlank) {\n <mat-label>{{ placeholder }}</mat-label>\n }\n\n <mat-latlong-input\n #input\n [formControl]=\"formControl\"\n [required]=\"required\"\n [disabled]=\"disabled\"\n [type]=\"type\"\n [latLongPattern]=\"pattern\"\n [defaultSign]=\"defaultSign\"\n [maxDecimals]=\"maxDecimals\"\n [readonly]=\"readonly\"\n ></mat-latlong-input>\n\n <div matSuffix>\n <ng-container *ngTemplateOutlet=\"matSuffixTemplate\"></ng-container>\n </div>\n</mat-form-field>\n\n<!-- Do the same as MatFromField since angular 15 (see component source)-->\n<div\n class=\"mat-mdc-form-field-subscript-wrapper\"\n [class.mat-mdc-form-field-bottom-align]=\"subscriptSizing === 'fixed'\"\n [class.mat-mdc-form-field-subscript-dynamic-size]=\"subscriptSizing === 'dynamic'\"\n>\n @if (formControl.touched && formControl.invalid) {\n <!-- errors -->\n <div class=\"mat-mdc-form-field-error-wrapper\">\n @if (formControl.errors | mapKeys | arrayFirst; as errorKey) {\n <mat-error>\n {{ i18nErrorKeys | mapGet: errorKey | translate: (formControl.errors | mapGet: errorKey) }}\n </mat-error>\n } @else {\n <ng-container *ngTemplateOutlet=\"matErrorTemplate\"></ng-container>\n }\n </div>\n } @else {\n <!-- hints -->\n <mat-hint>\n <ng-container *ngTemplateOutlet=\"matHintTemplate\"></ng-container>\n </mat-hint>\n }\n</div>\n\n<ng-template #matPrefixTemplate>\n <ng-content select=\"[matPrefix]\"></ng-content>\n</ng-template>\n\n<ng-template #matSuffixTemplate>\n @if (clearable && !readonly) {\n <button\n matSuffix\n mat-icon-button\n tabindex=\"-1\"\n type=\"button\"\n (click)=\"clearValue($event)\"\n [hidden]=\"formControl.value | isNil\"\n >\n <mat-icon>close</mat-icon>\n </button>\n }\n\n <ng-content select=\"[matSuffix]\"></ng-content>\n</ng-template>\n\n<ng-template #matErrorTemplate>\n <ng-content select=\"mat-error,[matError]\"></ng-content>\n</ng-template>\n\n<ng-template #matHintTemplate>\n <div class=\"mat-mdc-form-field-hint-wrapper\">\n <ng-content select=\"mat-hint:not([align='end']),[matHint]\"></ng-content>\n <div class=\"mat-mdc-form-field-hint-spacer\"></div>\n <ng-content select=\"mat-hint[align='end']\"></ng-content>\n </div>\n</ng-template>\n", styles: [":host{display:inline-block;width:100%;position:relative}\n"], dependencies: [{ kind: "directive", type: i4$1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i1$2.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "component", type: i3.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3.MatLabel, selector: "mat-label" }, { kind: "directive", type: i3.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: i3.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "directive", type: i3.MatPrefix, selector: "[matPrefix], [matIconPrefix], [matTextPrefix]", inputs: ["matTextPrefix"] }, { kind: "directive", type: i3.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "component", type: i9.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i6$2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: MatLatLongFieldInput, selector: "mat-latlong-input", inputs: ["aria-describedby", "required", "disabled", "value", "defaultSign", "type", "latLongPattern", "maxDecimals", "readonly"] }, { kind: "pipe", type: ArrayFirstPipe, name: "arrayFirst" }, { kind: "pipe", type: MapGetPipe, name: "mapGet" }, { kind: "pipe", type: MapKeysPipe, name: "mapKeys" }, { kind: "pipe", type: IsNotNilOrBlankPipe, name: "isNotNilOrBlank" }, { kind: "pipe", type: IsNilPipe, name: "isNil" }, { kind: "pipe", type: AsFloatLabelTypePipe, name: "asFloatLabelType" }, { kind: "pipe", type: i2.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
14768
14773
  }
14769
14774
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MatLatLongField, decorators: [{
14770
14775
  type: Component,
@@ -14774,7 +14779,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
14774
14779
  multi: true,
14775
14780
  useExisting: forwardRef(() => MatLatLongField),
14776
14781
  },
14777
- ], changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<mat-form-field\n #formField\n [floatLabel]=\"floatLabel | asFloatLabelType\"\n [appearance]=\"appearance\"\n [subscriptSizing]=\"subscriptSizing\"\n [class.mdc-text-field--invalid]=\"formControl.touched && formControl.invalid\"\n class=\"mat-latlong-field mat-latlong-field-{{ type }} {{ classList }}\"\n>\n <div matPrefix>\n <ng-container *ngTemplateOutlet=\"matPrefixTemplate\"></ng-container>\n </div>\n\n @if (floatLabel !== 'never' && placeholder | isNotNilOrBlank) {\n <mat-label>{{ placeholder }}</mat-label>\n }\n\n <mat-latlong-input\n #input\n [formControl]=\"formControl\"\n [required]=\"required\"\n [disabled]=\"disabled\"\n [type]=\"type\"\n [latLongPattern]=\"pattern\"\n [defaultSign]=\"defaultSign\"\n [maxDecimals]=\"maxDecimals\"\n [readonly]=\"readonly\"\n ></mat-latlong-input>\n\n <div matSuffix>\n <ng-container *ngTemplateOutlet=\"matSuffixTemplate\"></ng-container>\n </div>\n</mat-form-field>\n\n<!-- Do the same as MatFromField since angular 15 (see component source)-->\n<div\n class=\"mat-mdc-form-field-subscript-wrapper\"\n [class.mat-mdc-form-field-bottom-align]=\"subscriptSizing === 'fixed'\"\n [class.mat-mdc-form-field-subscript-dynamic-size]=\"subscriptSizing === 'dynamic'\"\n>\n @if (formControl.touched && formControl.invalid) {\n <!-- errors -->\n <div class=\"mat-mdc-form-field-error-wrapper\">\n @if (formControl.errors | mapKeys | arrayFirst; as errorKey) {\n <mat-error>\n {{ i18nErrorKeys | mapGet: errorKey | translate: (formControl.errors | mapGet: errorKey) }}\n </mat-error>\n } @else {\n <ng-container *ngTemplateOutlet=\"matErrorTemplate\"></ng-container>\n }\n </div>\n } @else {\n <!-- hints -->\n <mat-hint>\n <ng-container *ngTemplateOutlet=\"matHintTemplate\"></ng-container>\n </mat-hint>\n }\n</div>\n\n<ng-template #matPrefixTemplate>\n <ng-content select=\"[matPrefix]\"></ng-content>\n</ng-template>\n\n<ng-template #matSuffixTemplate>\n @if (clearable && !readonly) {\n <button\n matSuffix\n mat-icon-button\n tabindex=\"-1\"\n type=\"button\"\n (click)=\"clearValue($event)\"\n [hidden]=\"formControl.value | isNil\"\n >\n <mat-icon>close</mat-icon>\n </button>\n }\n\n <ng-content select=\"[matSuffix]\"></ng-content>\n</ng-template>\n\n<ng-template #matErrorTemplate>\n <ng-content select=\"mat-error,[matError]\"></ng-content>\n</ng-template>\n\n<ng-template #matHintTemplate>\n <div class=\"mat-mdc-form-field-hint-wrapper\">\n <ng-content select=\"mat-hint:not([align='end']),[matHint]\"></ng-content>\n <div class=\"mat-mdc-form-field-hint-spacer\"></div>\n <ng-content select=\"mat-hint[align='end']\"></ng-content>\n </div>\n</ng-template>\n" }]
14782
+ ], changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<!-- Errors and hints are rendered by this component's own subscript wrapper (below the form-field),\n so force 'dynamic' here: with the default 'fixed' sizing, the inner form-field would reserve\n an extra ~22px subscript line, making rows with lat/long fields taller than other rows\n (e.g. ObsMer operation form). -->\n<mat-form-field\n #formField\n [floatLabel]=\"floatLabel | asFloatLabelType\"\n [appearance]=\"appearance\"\n [subscriptSizing]=\"'dynamic'\"\n [class.mdc-text-field--invalid]=\"formControl.touched && formControl.invalid\"\n class=\"mat-latlong-field mat-latlong-field-{{ type }} {{ classList }}\"\n>\n <div matPrefix>\n <ng-container *ngTemplateOutlet=\"matPrefixTemplate\"></ng-container>\n </div>\n\n @if (floatLabel !== 'never' && placeholder | isNotNilOrBlank) {\n <mat-label>{{ placeholder }}</mat-label>\n }\n\n <mat-latlong-input\n #input\n [formControl]=\"formControl\"\n [required]=\"required\"\n [disabled]=\"disabled\"\n [type]=\"type\"\n [latLongPattern]=\"pattern\"\n [defaultSign]=\"defaultSign\"\n [maxDecimals]=\"maxDecimals\"\n [readonly]=\"readonly\"\n ></mat-latlong-input>\n\n <div matSuffix>\n <ng-container *ngTemplateOutlet=\"matSuffixTemplate\"></ng-container>\n </div>\n</mat-form-field>\n\n<!-- Do the same as MatFromField since angular 15 (see component source)-->\n<div\n class=\"mat-mdc-form-field-subscript-wrapper\"\n [class.mat-mdc-form-field-bottom-align]=\"subscriptSizing === 'fixed'\"\n [class.mat-mdc-form-field-subscript-dynamic-size]=\"subscriptSizing === 'dynamic'\"\n>\n @if (formControl.touched && formControl.invalid) {\n <!-- errors -->\n <div class=\"mat-mdc-form-field-error-wrapper\">\n @if (formControl.errors | mapKeys | arrayFirst; as errorKey) {\n <mat-error>\n {{ i18nErrorKeys | mapGet: errorKey | translate: (formControl.errors | mapGet: errorKey) }}\n </mat-error>\n } @else {\n <ng-container *ngTemplateOutlet=\"matErrorTemplate\"></ng-container>\n }\n </div>\n } @else {\n <!-- hints -->\n <mat-hint>\n <ng-container *ngTemplateOutlet=\"matHintTemplate\"></ng-container>\n </mat-hint>\n }\n</div>\n\n<ng-template #matPrefixTemplate>\n <ng-content select=\"[matPrefix]\"></ng-content>\n</ng-template>\n\n<ng-template #matSuffixTemplate>\n @if (clearable && !readonly) {\n <button\n matSuffix\n mat-icon-button\n tabindex=\"-1\"\n type=\"button\"\n (click)=\"clearValue($event)\"\n [hidden]=\"formControl.value | isNil\"\n >\n <mat-icon>close</mat-icon>\n </button>\n }\n\n <ng-content select=\"[matSuffix]\"></ng-content>\n</ng-template>\n\n<ng-template #matErrorTemplate>\n <ng-content select=\"mat-error,[matError]\"></ng-content>\n</ng-template>\n\n<ng-template #matHintTemplate>\n <div class=\"mat-mdc-form-field-hint-wrapper\">\n <ng-content select=\"mat-hint:not([align='end']),[matHint]\"></ng-content>\n <div class=\"mat-mdc-form-field-hint-spacer\"></div>\n <ng-content select=\"mat-hint[align='end']\"></ng-content>\n </div>\n</ng-template>\n", styles: [":host{display:inline-block;width:100%;position:relative}\n"] }]
14778
14783
  }], ctorParameters: () => [{ type: i0.ChangeDetectorRef }, { type: i1$2.FormGroupDirective, decorators: [{
14779
14784
  type: Optional
14780
14785
  }] }, { type: undefined, decorators: [{
@@ -25929,14 +25934,14 @@ class AccountService extends BaseGraphqlService {
25929
25934
  console.error('[account] Error while saving account locally: ' + ((err && err.message) || err), err);
25930
25935
  }
25931
25936
  }
25932
- async saveLocalSettingsRemotely(source) {
25937
+ async saveLocalSettingsRemotely(source, force) {
25933
25938
  if (!source || !this.isLogin() || source.accountInheritance === false)
25934
25939
  return; // Skip
25935
25940
  const account = this.account;
25936
25941
  // Merge local settings into account's settings
25937
25942
  const settings = UserSettings.fromObject(account.settings) || new UserSettings();
25938
25943
  const changed = settings.merge(source, this._remoteLocalSettingsKeys);
25939
- if (!changed)
25944
+ if (!changed && !force)
25940
25945
  return; // Skip if unchanged
25941
25946
  // Save remotely
25942
25947
  this.account.settings = settings;
@@ -33893,11 +33898,11 @@ class AboutModal {
33893
33898
  }
33894
33899
  }
33895
33900
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: AboutModal, deps: [{ token: i2.TranslateService }, { token: i2$2.ModalController }, { token: ConfigService }, { token: NetworkService }, { token: PlatformService }, { token: i2$4.HttpClient }, { token: i0.ChangeDetectorRef }, { token: ENVIRONMENT, optional: true }, { token: APP_ABOUT_DEVELOPERS, optional: true }, { token: APP_ABOUT_PARTNERS, optional: true }], target: i0.ɵɵFactoryTarget.Component });
33896
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: AboutModal, isStandalone: false, selector: "app-about-modal", ngImport: i0, template: "<ion-header>\n <ion-toolbar color=\"light\">\n <ion-title>\n {{ 'ABOUT.TITLE' | translate }}\n </ion-title>\n\n <ion-buttons slot=\"end\">\n <ion-button (click)=\"close()\" visible-xs visible-sm visible-mobile>\n {{ 'COMMON.BTN_CLOSE' | translate }}\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n</ion-header>\n\n<ion-content class=\"ion-padding\">\n <ion-list lines=\"none\">\n <!-- Peer info -->\n @if (config$ | async; as config) {\n <ion-item>\n <span slot=\"start\">&nbsp;</span>\n <ion-label class=\"ion-text-wrap\">\n <h2>\n <b>{{ config.label | capitalize }}</b>\n @if (config.name) {\n -\n <span [innerHTML]=\"config.name\"></span>\n }\n </h2>\n\n @if (config.description) {\n <p>\n <markdown [data]=\"config.description\" emoji></markdown>\n </p>\n }\n </ion-label>\n </ion-item>\n }\n\n <!-- Version info -->\n <ion-item>\n <span slot=\"start\">&nbsp;</span>\n <ion-label class=\"ion-text-wrap\">\n <h2>\n @if (name) {\n <small>{{ 'ABOUT.POWERED_BY' | translate }}</small>\n {{ name }}\n }\n @if (version) {\n <span [innerHTML]=\"'ABOUT.VERSION' | translate: { version: version }\"></span>\n }\n @if (allowVersionDetails) {\n &nbsp;\n <small>\n <a (click)=\"showVersionDetails = !showVersionDetails\" tappable>\n {{ 'COMMON.BTN_SHOW_MORE' | translate }}\n <ion-icon [name]=\"!showVersionDetails ? 'chevron-down' : 'chevron-up'\"></ion-icon>\n </a>\n @if (changelogUrl) {\n &nbsp;\n <a (click)=\"openChangelog($event)\"\n [href]=\"changelogUrl\" tappable>{{ 'ABOUT.BTN_CHANGELOG' | translate}}</a>\n }\n </small>\n }\n </h2>\n\n <!-- version details -->\n @if (showVersionDetails) {\n <p class=\"version-details\">\n <!-- App version -->\n @if (buildDate) {\n <small>\n <ion-icon name=\"logo-angular\"></ion-icon>\n <span [innerHTML]=\"'ABOUT.APP_VERSION' | translate: { version: version }\"></span>\n &nbsp;\n <ion-icon name=\"time-outline\"></ion-icon>\n {{ buildDate | dateFormat: { time: true, seconds: false } }}\n </small>\n <br />\n }\n <!-- Pod version -->\n @if (nodeInfo?.softwareVersion && nodeInfo.buildDate) {\n <small>\n <ion-icon name=\"server\"></ion-icon>\n <span [innerHTML]=\"'ABOUT.POD_VERSION' | translate: { version: nodeInfo.softwareVersion }\"></span>\n &nbsp;\n <ion-icon name=\"time-outline\"></ion-icon>\n {{ nodeInfo.buildDate | dateFormat: { time: true, seconds: false } }}\n </small>\n }\n </p>\n }\n\n <!-- license-->\n <p [innerHTML]=\"'ABOUT.LICENSE' | translate\"></p>\n </ion-label>\n </ion-item>\n\n <!-- Help -->\n @if (forumUrl || helpUrl) {\n <ion-item>\n <mat-icon slot=\"start\">help_outline</mat-icon>\n <ion-label>\n <p>\n {{ 'ABOUT.HELP' | translate }}\n </p>\n <p>\n <ion-button fill=\"solid\" color=\"accent\" href=\"{{ helpUrl }}\" target=\"help\" size=\"default\">\n <mat-icon slot=\"start\">description</mat-icon>\n <ion-text>{{ 'ABOUT.USER_MANUAL' | translate }}</ion-text>\n </ion-button>\n <ion-button fill=\"solid\" color=\"secondary\" href=\"{{ forumUrl }}\" target=\"forum\" size=\"default\">\n <ion-icon slot=\"start\" name=\"chatbubbles\"></ion-icon>\n <ion-text>{{ 'ABOUT.FORUM' | translate }}</ion-text>\n </ion-button>\n </p>\n </ion-label>\n </ion-item>\n }\n\n <!-- Report issue -->\n @if (reportIssueUrl) {\n <ion-item>\n <ion-icon slot=\"start\" name=\"bug\" color=\"dark\"></ion-icon>\n <ion-label>\n <p>\n {{ 'ABOUT.REPORT_ISSUE' | translate }}\n </p>\n <p>\n <a color=\"primary\" href=\"{{ reportIssueUrl }}\" target=\"_system\">{{ 'ABOUT.BTN_REPORT_ISSUE' | translate }}</a>\n </p>\n </ion-label>\n </ion-item>\n }\n\n <!-- Source code -->\n @if (sourceUrl) {\n <ion-item>\n <ion-icon slot=\"start\" name=\"code\" color=\"dark\"></ion-icon>\n <ion-label>\n <p>\n {{ 'ABOUT.SOURCE_CODE' | translate }}\n </p>\n <p>\n <a color=\"primary\" href=\"{{ sourceUrl }}\" target=\"_system\">{{ sourceUrl }}</a>\n </p>\n </ion-label>\n </ion-item>\n }\n\n <!-- Developers -->\n @if (developers | isNotEmptyArray) {\n <ion-item>\n <ion-icon slot=\"start\" name=\"people\" color=\"dark\"></ion-icon>\n <ion-label>\n <p>\n {{ 'ABOUT.DEVELOPED_BY' | translate }}\n </p>\n <ion-list lines=\"none\">\n @for (item of developers; track item) {\n <ion-item class=\"ion-no-padding\">\n <ion-label>\n <p>\n <a color=\"primary\" href=\"{{ item.siteUrl }}\" target=\"_system\">{{ item.name || item.label }}</a>\n </p>\n </ion-label>\n <ion-img slot=\"end\" [src]=\"item.logo\" />\n </ion-item>\n }\n </ion-list>\n </ion-label>\n </ion-item>\n }\n\n <!-- Partners -->\n @if (partners | isNotEmptyArray) {\n <ion-item class=\"item-partners\">\n <ion-icon slot=\"start\" name=\"megaphone\" color=\"dark\"></ion-icon>\n <ion-text>\n <p>{{ 'ABOUT.PARTNERS' | translate }}</p>\n <p>\n @for (item of partners; track item) {\n <a href=\"{{ item.siteUrl }}\" class=\"partners\" target=\"_system\">\n @if (item.logo) {\n <img [src]=\"item.logo\" [alt]=\"item.label\" />\n } @else {\n <ion-text>{{ item.label }}</ion-text>\n }\n </a>\n }\n </p>\n </ion-text>\n </ion-item>\n }\n </ion-list>\n</ion-content>\n\n<ion-footer hidden-xs hidden-sm hidden-mobile>\n <ion-toolbar>\n <ion-row class=\"ion-no-padding\" nowrap>\n <ion-col></ion-col>\n <ion-col size=\"auto\">\n <ion-button fill=\"solid\" color=\"tertiary\" (click)=\"close()\">{{ 'COMMON.BTN_CLOSE' | translate }}</ion-button>\n </ion-col>\n </ion-row>\n </ion-toolbar>\n</ion-footer>\n", styles: [".item-partners ion-text{height:auto}.item-partners ion-text p:last-child{display:flex;flex-wrap:wrap;flex-direction:row;justify-content:space-between}.item-partners ion-text p:last-child a{padding:2px}.item-partners ion-text p:last-child a img{text-align:center;display:inline-block;max-height:40px!important}\n"], dependencies: [{ kind: "component", type: i2$2.IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: i2$2.IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: i2$2.IonCol, selector: "ion-col", inputs: ["offset", "offsetLg", "offsetMd", "offsetSm", "offsetXl", "offsetXs", "pull", "pullLg", "pullMd", "pullSm", "pullXl", "pullXs", "push", "pushLg", "pushMd", "pushSm", "pushXl", "pushXs", "size", "sizeLg", "sizeMd", "sizeSm", "sizeXl", "sizeXs"] }, { kind: "component", type: i2$2.IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: i2$2.IonFooter, selector: "ion-footer", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: i2$2.IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: i2$2.IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: i2$2.IonImg, selector: "ion-img", inputs: ["alt", "src"] }, { kind: "component", type: i2$2.IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: i2$2.IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: i2$2.IonList, selector: "ion-list", inputs: ["inset", "lines", "mode"] }, { kind: "component", type: i2$2.IonRow, selector: "ion-row" }, { kind: "component", type: i2$2.IonText, selector: "ion-text", inputs: ["color", "mode"] }, { kind: "component", type: i2$2.IonTitle, selector: "ion-title", inputs: ["color", "size"] }, { kind: "component", type: i2$2.IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: i4$4.MarkdownComponent, selector: "markdown, [markdown]", inputs: ["data", "src", "disableSanitizer", "inline", "clipboard", "clipboardButtonComponent", "clipboardButtonTemplate", "emoji", "katex", "katexOptions", "mermaid", "mermaidOptions", "lineHighlight", "line", "lineOffset", "lineNumbers", "start", "commandLine", "filterOutput", "host", "prompt", "output", "user"], outputs: ["error", "load", "ready"] }, { kind: "component", type: i6$2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "pipe", type: i4$1.AsyncPipe, name: "async" }, { kind: "pipe", type: i2.TranslatePipe, name: "translate" }, { kind: "pipe", type: DateFormatPipe, name: "dateFormat" }, { kind: "pipe", type: NotEmptyArrayPipe, name: "isNotEmptyArray" }, { kind: "pipe", type: CapitalizePipe, name: "capitalize" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
33901
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: AboutModal, isStandalone: false, selector: "app-about-modal", ngImport: i0, template: "<ion-header>\n <ion-toolbar color=\"light\">\n <ion-title>\n {{ 'ABOUT.TITLE' | translate }}\n </ion-title>\n\n <ion-buttons slot=\"end\">\n <ion-button (click)=\"close()\" visible-xs visible-sm visible-mobile>\n {{ 'COMMON.BTN_CLOSE' | translate }}\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n</ion-header>\n\n<ion-content class=\"ion-padding\">\n <ion-list lines=\"none\">\n <!-- Peer info -->\n @if (config$ | async; as config) {\n <ion-item>\n <span slot=\"start\">&nbsp;</span>\n <ion-label class=\"ion-text-wrap\">\n <h2>\n <b>{{ config.label | capitalize }}</b>\n @if (config.name) {\n -\n <span [innerHTML]=\"config.name\"></span>\n }\n </h2>\n\n @if (config.description) {\n <p>\n <markdown [data]=\"config.description\" emoji></markdown>\n </p>\n }\n </ion-label>\n </ion-item>\n }\n\n <!-- Version info -->\n <ion-item>\n <span slot=\"start\">&nbsp;</span>\n <ion-label class=\"ion-text-wrap\">\n <h2>\n @if (name) {\n <small>{{ 'ABOUT.POWERED_BY' | translate }}</small>\n {{ name }}\n }\n @if (version) {\n <span [innerHTML]=\"'ABOUT.VERSION' | translate: { version: version }\"></span>\n }\n @if (allowVersionDetails) {\n &nbsp;\n <small>\n <a (click)=\"showVersionDetails = !showVersionDetails\" tappable>\n {{ 'COMMON.BTN_SHOW_MORE' | translate }}\n <ion-icon [name]=\"!showVersionDetails ? 'chevron-down' : 'chevron-up'\"></ion-icon>\n </a>\n @if (changelogUrl) {\n &nbsp;\n <a (click)=\"openChangelog($event)\" [href]=\"changelogUrl\" tappable>\n {{ 'ABOUT.BTN_CHANGELOG' | translate }}\n </a>\n }\n </small>\n }\n </h2>\n\n <!-- version details -->\n @if (showVersionDetails) {\n <p class=\"version-details\">\n <!-- App version -->\n @if (buildDate) {\n <small>\n <ion-icon name=\"logo-angular\"></ion-icon>\n <span [innerHTML]=\"'ABOUT.APP_VERSION' | translate: { version: version }\"></span>\n &nbsp;\n <ion-icon name=\"time-outline\"></ion-icon>\n {{ buildDate | dateFormat: { time: true, seconds: false } }}\n </small>\n <br />\n }\n <!-- Pod version -->\n @if (nodeInfo?.softwareVersion && nodeInfo.buildDate) {\n <small>\n <ion-icon name=\"server\"></ion-icon>\n <span [innerHTML]=\"'ABOUT.POD_VERSION' | translate: { version: nodeInfo.softwareVersion }\"></span>\n &nbsp;\n <ion-icon name=\"time-outline\"></ion-icon>\n {{ nodeInfo.buildDate | dateFormat: { time: true, seconds: false } }}\n </small>\n }\n </p>\n }\n\n <!-- license-->\n <p [innerHTML]=\"'ABOUT.LICENSE' | translate\"></p>\n </ion-label>\n </ion-item>\n\n <!-- Help -->\n @if (forumUrl || helpUrl) {\n <ion-item>\n <mat-icon slot=\"start\">help_outline</mat-icon>\n <ion-label>\n <p>\n {{ 'ABOUT.HELP' | translate }}\n </p>\n <p>\n <ion-button fill=\"solid\" color=\"accent\" href=\"{{ helpUrl }}\" target=\"help\" size=\"default\">\n <mat-icon slot=\"start\">description</mat-icon>\n <ion-text>{{ 'ABOUT.USER_MANUAL' | translate }}</ion-text>\n </ion-button>\n <ion-button fill=\"solid\" color=\"secondary\" href=\"{{ forumUrl }}\" target=\"forum\" size=\"default\">\n <ion-icon slot=\"start\" name=\"chatbubbles\"></ion-icon>\n <ion-text>{{ 'ABOUT.FORUM' | translate }}</ion-text>\n </ion-button>\n </p>\n </ion-label>\n </ion-item>\n }\n\n <!-- Report issue -->\n @if (reportIssueUrl) {\n <ion-item>\n <ion-icon slot=\"start\" name=\"bug\" color=\"dark\"></ion-icon>\n <ion-label>\n <p>\n {{ 'ABOUT.REPORT_ISSUE' | translate }}\n </p>\n <p>\n <a color=\"primary\" href=\"{{ reportIssueUrl }}\" target=\"_system\">\n {{ 'ABOUT.BTN_REPORT_ISSUE' | translate }}\n </a>\n </p>\n </ion-label>\n </ion-item>\n }\n\n <!-- Partners -->\n @if (partners | isNotEmptyArray) {\n <ion-item class=\"item-partners\">\n <ion-icon slot=\"start\" name=\"megaphone\" color=\"dark\"></ion-icon>\n <ion-text>\n <p>{{ 'ABOUT.PARTNERS' | translate }}</p>\n <p>\n @for (item of partners; track item) {\n <a href=\"{{ item.siteUrl }}\" class=\"partners\" target=\"_system\">\n @if (item.logo) {\n <img [src]=\"item.logo\" [alt]=\"item.label\" />\n } @else {\n <ion-text>{{ item.label }}</ion-text>\n }\n </a>\n }\n </p>\n </ion-text>\n </ion-item>\n }\n\n <!-- Developers -->\n @if (developers | isNotEmptyArray) {\n <ion-item>\n <ion-icon slot=\"start\" name=\"people\" color=\"dark\"></ion-icon>\n <ion-label>\n <p>\n {{ 'ABOUT.DEVELOPED_BY' | translate }}\n </p>\n <ion-list lines=\"none\">\n @for (item of developers; track item) {\n <ion-item class=\"ion-no-padding\">\n <ion-label>\n <p>\n <a color=\"primary\" href=\"{{ item.siteUrl }}\" target=\"_system\">{{ item.name || item.label }}</a>\n </p>\n </ion-label>\n <ion-img slot=\"end\" [src]=\"item.logo\" />\n </ion-item>\n }\n </ion-list>\n </ion-label>\n </ion-item>\n } @else {\n <ion-item>\n <ion-icon slot=\"start\" name=\"people\" color=\"dark\"></ion-icon>\n <ion-label>\n <p>\n {{ 'ABOUT.DEVELOPED_BY' | translate }}\n </p>\n </ion-label>\n </ion-item>\n }\n\n <!-- Source code -->\n @if (sourceUrl) {\n <ion-item>\n <ion-icon slot=\"start\" name=\"code\" color=\"dark\"></ion-icon>\n <ion-label>\n <p>\n {{ 'ABOUT.SOURCE_CODE' | translate }}\n </p>\n <p>\n <a color=\"primary\" href=\"{{ sourceUrl }}\" target=\"_system\">{{ sourceUrl }}</a>\n </p>\n </ion-label>\n </ion-item>\n }\n </ion-list>\n</ion-content>\n\n<ion-footer hidden-xs hidden-sm hidden-mobile>\n <ion-toolbar>\n <ion-row class=\"ion-no-padding\" nowrap>\n <ion-col></ion-col>\n <ion-col size=\"auto\">\n <ion-button fill=\"solid\" color=\"tertiary\" (click)=\"close()\">{{ 'COMMON.BTN_CLOSE' | translate }}</ion-button>\n </ion-col>\n </ion-row>\n </ion-toolbar>\n</ion-footer>\n", styles: [".item-partners ion-text{height:auto}.item-partners ion-text p:last-child{display:flex;flex-wrap:wrap;flex-direction:row;justify-content:space-between}.item-partners ion-text p:last-child a{padding:2px}.item-partners ion-text p:last-child a img{text-align:center;display:inline-block;max-height:40px}\n"], dependencies: [{ kind: "component", type: i2$2.IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: i2$2.IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: i2$2.IonCol, selector: "ion-col", inputs: ["offset", "offsetLg", "offsetMd", "offsetSm", "offsetXl", "offsetXs", "pull", "pullLg", "pullMd", "pullSm", "pullXl", "pullXs", "push", "pushLg", "pushMd", "pushSm", "pushXl", "pushXs", "size", "sizeLg", "sizeMd", "sizeSm", "sizeXl", "sizeXs"] }, { kind: "component", type: i2$2.IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: i2$2.IonFooter, selector: "ion-footer", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: i2$2.IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: i2$2.IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: i2$2.IonImg, selector: "ion-img", inputs: ["alt", "src"] }, { kind: "component", type: i2$2.IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: i2$2.IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: i2$2.IonList, selector: "ion-list", inputs: ["inset", "lines", "mode"] }, { kind: "component", type: i2$2.IonRow, selector: "ion-row" }, { kind: "component", type: i2$2.IonText, selector: "ion-text", inputs: ["color", "mode"] }, { kind: "component", type: i2$2.IonTitle, selector: "ion-title", inputs: ["color", "size"] }, { kind: "component", type: i2$2.IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: i4$4.MarkdownComponent, selector: "markdown, [markdown]", inputs: ["data", "src", "disableSanitizer", "inline", "clipboard", "clipboardButtonComponent", "clipboardButtonTemplate", "emoji", "katex", "katexOptions", "mermaid", "mermaidOptions", "lineHighlight", "line", "lineOffset", "lineNumbers", "start", "commandLine", "filterOutput", "host", "prompt", "output", "user"], outputs: ["error", "load", "ready"] }, { kind: "component", type: i6$2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "pipe", type: i4$1.AsyncPipe, name: "async" }, { kind: "pipe", type: i2.TranslatePipe, name: "translate" }, { kind: "pipe", type: DateFormatPipe, name: "dateFormat" }, { kind: "pipe", type: NotEmptyArrayPipe, name: "isNotEmptyArray" }, { kind: "pipe", type: CapitalizePipe, name: "capitalize" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
33897
33902
  }
33898
33903
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: AboutModal, decorators: [{
33899
33904
  type: Component,
33900
- args: [{ selector: 'app-about-modal', changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<ion-header>\n <ion-toolbar color=\"light\">\n <ion-title>\n {{ 'ABOUT.TITLE' | translate }}\n </ion-title>\n\n <ion-buttons slot=\"end\">\n <ion-button (click)=\"close()\" visible-xs visible-sm visible-mobile>\n {{ 'COMMON.BTN_CLOSE' | translate }}\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n</ion-header>\n\n<ion-content class=\"ion-padding\">\n <ion-list lines=\"none\">\n <!-- Peer info -->\n @if (config$ | async; as config) {\n <ion-item>\n <span slot=\"start\">&nbsp;</span>\n <ion-label class=\"ion-text-wrap\">\n <h2>\n <b>{{ config.label | capitalize }}</b>\n @if (config.name) {\n -\n <span [innerHTML]=\"config.name\"></span>\n }\n </h2>\n\n @if (config.description) {\n <p>\n <markdown [data]=\"config.description\" emoji></markdown>\n </p>\n }\n </ion-label>\n </ion-item>\n }\n\n <!-- Version info -->\n <ion-item>\n <span slot=\"start\">&nbsp;</span>\n <ion-label class=\"ion-text-wrap\">\n <h2>\n @if (name) {\n <small>{{ 'ABOUT.POWERED_BY' | translate }}</small>\n {{ name }}\n }\n @if (version) {\n <span [innerHTML]=\"'ABOUT.VERSION' | translate: { version: version }\"></span>\n }\n @if (allowVersionDetails) {\n &nbsp;\n <small>\n <a (click)=\"showVersionDetails = !showVersionDetails\" tappable>\n {{ 'COMMON.BTN_SHOW_MORE' | translate }}\n <ion-icon [name]=\"!showVersionDetails ? 'chevron-down' : 'chevron-up'\"></ion-icon>\n </a>\n @if (changelogUrl) {\n &nbsp;\n <a (click)=\"openChangelog($event)\"\n [href]=\"changelogUrl\" tappable>{{ 'ABOUT.BTN_CHANGELOG' | translate}}</a>\n }\n </small>\n }\n </h2>\n\n <!-- version details -->\n @if (showVersionDetails) {\n <p class=\"version-details\">\n <!-- App version -->\n @if (buildDate) {\n <small>\n <ion-icon name=\"logo-angular\"></ion-icon>\n <span [innerHTML]=\"'ABOUT.APP_VERSION' | translate: { version: version }\"></span>\n &nbsp;\n <ion-icon name=\"time-outline\"></ion-icon>\n {{ buildDate | dateFormat: { time: true, seconds: false } }}\n </small>\n <br />\n }\n <!-- Pod version -->\n @if (nodeInfo?.softwareVersion && nodeInfo.buildDate) {\n <small>\n <ion-icon name=\"server\"></ion-icon>\n <span [innerHTML]=\"'ABOUT.POD_VERSION' | translate: { version: nodeInfo.softwareVersion }\"></span>\n &nbsp;\n <ion-icon name=\"time-outline\"></ion-icon>\n {{ nodeInfo.buildDate | dateFormat: { time: true, seconds: false } }}\n </small>\n }\n </p>\n }\n\n <!-- license-->\n <p [innerHTML]=\"'ABOUT.LICENSE' | translate\"></p>\n </ion-label>\n </ion-item>\n\n <!-- Help -->\n @if (forumUrl || helpUrl) {\n <ion-item>\n <mat-icon slot=\"start\">help_outline</mat-icon>\n <ion-label>\n <p>\n {{ 'ABOUT.HELP' | translate }}\n </p>\n <p>\n <ion-button fill=\"solid\" color=\"accent\" href=\"{{ helpUrl }}\" target=\"help\" size=\"default\">\n <mat-icon slot=\"start\">description</mat-icon>\n <ion-text>{{ 'ABOUT.USER_MANUAL' | translate }}</ion-text>\n </ion-button>\n <ion-button fill=\"solid\" color=\"secondary\" href=\"{{ forumUrl }}\" target=\"forum\" size=\"default\">\n <ion-icon slot=\"start\" name=\"chatbubbles\"></ion-icon>\n <ion-text>{{ 'ABOUT.FORUM' | translate }}</ion-text>\n </ion-button>\n </p>\n </ion-label>\n </ion-item>\n }\n\n <!-- Report issue -->\n @if (reportIssueUrl) {\n <ion-item>\n <ion-icon slot=\"start\" name=\"bug\" color=\"dark\"></ion-icon>\n <ion-label>\n <p>\n {{ 'ABOUT.REPORT_ISSUE' | translate }}\n </p>\n <p>\n <a color=\"primary\" href=\"{{ reportIssueUrl }}\" target=\"_system\">{{ 'ABOUT.BTN_REPORT_ISSUE' | translate }}</a>\n </p>\n </ion-label>\n </ion-item>\n }\n\n <!-- Source code -->\n @if (sourceUrl) {\n <ion-item>\n <ion-icon slot=\"start\" name=\"code\" color=\"dark\"></ion-icon>\n <ion-label>\n <p>\n {{ 'ABOUT.SOURCE_CODE' | translate }}\n </p>\n <p>\n <a color=\"primary\" href=\"{{ sourceUrl }}\" target=\"_system\">{{ sourceUrl }}</a>\n </p>\n </ion-label>\n </ion-item>\n }\n\n <!-- Developers -->\n @if (developers | isNotEmptyArray) {\n <ion-item>\n <ion-icon slot=\"start\" name=\"people\" color=\"dark\"></ion-icon>\n <ion-label>\n <p>\n {{ 'ABOUT.DEVELOPED_BY' | translate }}\n </p>\n <ion-list lines=\"none\">\n @for (item of developers; track item) {\n <ion-item class=\"ion-no-padding\">\n <ion-label>\n <p>\n <a color=\"primary\" href=\"{{ item.siteUrl }}\" target=\"_system\">{{ item.name || item.label }}</a>\n </p>\n </ion-label>\n <ion-img slot=\"end\" [src]=\"item.logo\" />\n </ion-item>\n }\n </ion-list>\n </ion-label>\n </ion-item>\n }\n\n <!-- Partners -->\n @if (partners | isNotEmptyArray) {\n <ion-item class=\"item-partners\">\n <ion-icon slot=\"start\" name=\"megaphone\" color=\"dark\"></ion-icon>\n <ion-text>\n <p>{{ 'ABOUT.PARTNERS' | translate }}</p>\n <p>\n @for (item of partners; track item) {\n <a href=\"{{ item.siteUrl }}\" class=\"partners\" target=\"_system\">\n @if (item.logo) {\n <img [src]=\"item.logo\" [alt]=\"item.label\" />\n } @else {\n <ion-text>{{ item.label }}</ion-text>\n }\n </a>\n }\n </p>\n </ion-text>\n </ion-item>\n }\n </ion-list>\n</ion-content>\n\n<ion-footer hidden-xs hidden-sm hidden-mobile>\n <ion-toolbar>\n <ion-row class=\"ion-no-padding\" nowrap>\n <ion-col></ion-col>\n <ion-col size=\"auto\">\n <ion-button fill=\"solid\" color=\"tertiary\" (click)=\"close()\">{{ 'COMMON.BTN_CLOSE' | translate }}</ion-button>\n </ion-col>\n </ion-row>\n </ion-toolbar>\n</ion-footer>\n", styles: [".item-partners ion-text{height:auto}.item-partners ion-text p:last-child{display:flex;flex-wrap:wrap;flex-direction:row;justify-content:space-between}.item-partners ion-text p:last-child a{padding:2px}.item-partners ion-text p:last-child a img{text-align:center;display:inline-block;max-height:40px!important}\n"] }]
33905
+ args: [{ selector: 'app-about-modal', changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<ion-header>\n <ion-toolbar color=\"light\">\n <ion-title>\n {{ 'ABOUT.TITLE' | translate }}\n </ion-title>\n\n <ion-buttons slot=\"end\">\n <ion-button (click)=\"close()\" visible-xs visible-sm visible-mobile>\n {{ 'COMMON.BTN_CLOSE' | translate }}\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n</ion-header>\n\n<ion-content class=\"ion-padding\">\n <ion-list lines=\"none\">\n <!-- Peer info -->\n @if (config$ | async; as config) {\n <ion-item>\n <span slot=\"start\">&nbsp;</span>\n <ion-label class=\"ion-text-wrap\">\n <h2>\n <b>{{ config.label | capitalize }}</b>\n @if (config.name) {\n -\n <span [innerHTML]=\"config.name\"></span>\n }\n </h2>\n\n @if (config.description) {\n <p>\n <markdown [data]=\"config.description\" emoji></markdown>\n </p>\n }\n </ion-label>\n </ion-item>\n }\n\n <!-- Version info -->\n <ion-item>\n <span slot=\"start\">&nbsp;</span>\n <ion-label class=\"ion-text-wrap\">\n <h2>\n @if (name) {\n <small>{{ 'ABOUT.POWERED_BY' | translate }}</small>\n {{ name }}\n }\n @if (version) {\n <span [innerHTML]=\"'ABOUT.VERSION' | translate: { version: version }\"></span>\n }\n @if (allowVersionDetails) {\n &nbsp;\n <small>\n <a (click)=\"showVersionDetails = !showVersionDetails\" tappable>\n {{ 'COMMON.BTN_SHOW_MORE' | translate }}\n <ion-icon [name]=\"!showVersionDetails ? 'chevron-down' : 'chevron-up'\"></ion-icon>\n </a>\n @if (changelogUrl) {\n &nbsp;\n <a (click)=\"openChangelog($event)\" [href]=\"changelogUrl\" tappable>\n {{ 'ABOUT.BTN_CHANGELOG' | translate }}\n </a>\n }\n </small>\n }\n </h2>\n\n <!-- version details -->\n @if (showVersionDetails) {\n <p class=\"version-details\">\n <!-- App version -->\n @if (buildDate) {\n <small>\n <ion-icon name=\"logo-angular\"></ion-icon>\n <span [innerHTML]=\"'ABOUT.APP_VERSION' | translate: { version: version }\"></span>\n &nbsp;\n <ion-icon name=\"time-outline\"></ion-icon>\n {{ buildDate | dateFormat: { time: true, seconds: false } }}\n </small>\n <br />\n }\n <!-- Pod version -->\n @if (nodeInfo?.softwareVersion && nodeInfo.buildDate) {\n <small>\n <ion-icon name=\"server\"></ion-icon>\n <span [innerHTML]=\"'ABOUT.POD_VERSION' | translate: { version: nodeInfo.softwareVersion }\"></span>\n &nbsp;\n <ion-icon name=\"time-outline\"></ion-icon>\n {{ nodeInfo.buildDate | dateFormat: { time: true, seconds: false } }}\n </small>\n }\n </p>\n }\n\n <!-- license-->\n <p [innerHTML]=\"'ABOUT.LICENSE' | translate\"></p>\n </ion-label>\n </ion-item>\n\n <!-- Help -->\n @if (forumUrl || helpUrl) {\n <ion-item>\n <mat-icon slot=\"start\">help_outline</mat-icon>\n <ion-label>\n <p>\n {{ 'ABOUT.HELP' | translate }}\n </p>\n <p>\n <ion-button fill=\"solid\" color=\"accent\" href=\"{{ helpUrl }}\" target=\"help\" size=\"default\">\n <mat-icon slot=\"start\">description</mat-icon>\n <ion-text>{{ 'ABOUT.USER_MANUAL' | translate }}</ion-text>\n </ion-button>\n <ion-button fill=\"solid\" color=\"secondary\" href=\"{{ forumUrl }}\" target=\"forum\" size=\"default\">\n <ion-icon slot=\"start\" name=\"chatbubbles\"></ion-icon>\n <ion-text>{{ 'ABOUT.FORUM' | translate }}</ion-text>\n </ion-button>\n </p>\n </ion-label>\n </ion-item>\n }\n\n <!-- Report issue -->\n @if (reportIssueUrl) {\n <ion-item>\n <ion-icon slot=\"start\" name=\"bug\" color=\"dark\"></ion-icon>\n <ion-label>\n <p>\n {{ 'ABOUT.REPORT_ISSUE' | translate }}\n </p>\n <p>\n <a color=\"primary\" href=\"{{ reportIssueUrl }}\" target=\"_system\">\n {{ 'ABOUT.BTN_REPORT_ISSUE' | translate }}\n </a>\n </p>\n </ion-label>\n </ion-item>\n }\n\n <!-- Partners -->\n @if (partners | isNotEmptyArray) {\n <ion-item class=\"item-partners\">\n <ion-icon slot=\"start\" name=\"megaphone\" color=\"dark\"></ion-icon>\n <ion-text>\n <p>{{ 'ABOUT.PARTNERS' | translate }}</p>\n <p>\n @for (item of partners; track item) {\n <a href=\"{{ item.siteUrl }}\" class=\"partners\" target=\"_system\">\n @if (item.logo) {\n <img [src]=\"item.logo\" [alt]=\"item.label\" />\n } @else {\n <ion-text>{{ item.label }}</ion-text>\n }\n </a>\n }\n </p>\n </ion-text>\n </ion-item>\n }\n\n <!-- Developers -->\n @if (developers | isNotEmptyArray) {\n <ion-item>\n <ion-icon slot=\"start\" name=\"people\" color=\"dark\"></ion-icon>\n <ion-label>\n <p>\n {{ 'ABOUT.DEVELOPED_BY' | translate }}\n </p>\n <ion-list lines=\"none\">\n @for (item of developers; track item) {\n <ion-item class=\"ion-no-padding\">\n <ion-label>\n <p>\n <a color=\"primary\" href=\"{{ item.siteUrl }}\" target=\"_system\">{{ item.name || item.label }}</a>\n </p>\n </ion-label>\n <ion-img slot=\"end\" [src]=\"item.logo\" />\n </ion-item>\n }\n </ion-list>\n </ion-label>\n </ion-item>\n } @else {\n <ion-item>\n <ion-icon slot=\"start\" name=\"people\" color=\"dark\"></ion-icon>\n <ion-label>\n <p>\n {{ 'ABOUT.DEVELOPED_BY' | translate }}\n </p>\n </ion-label>\n </ion-item>\n }\n\n <!-- Source code -->\n @if (sourceUrl) {\n <ion-item>\n <ion-icon slot=\"start\" name=\"code\" color=\"dark\"></ion-icon>\n <ion-label>\n <p>\n {{ 'ABOUT.SOURCE_CODE' | translate }}\n </p>\n <p>\n <a color=\"primary\" href=\"{{ sourceUrl }}\" target=\"_system\">{{ sourceUrl }}</a>\n </p>\n </ion-label>\n </ion-item>\n }\n </ion-list>\n</ion-content>\n\n<ion-footer hidden-xs hidden-sm hidden-mobile>\n <ion-toolbar>\n <ion-row class=\"ion-no-padding\" nowrap>\n <ion-col></ion-col>\n <ion-col size=\"auto\">\n <ion-button fill=\"solid\" color=\"tertiary\" (click)=\"close()\">{{ 'COMMON.BTN_CLOSE' | translate }}</ion-button>\n </ion-col>\n </ion-row>\n </ion-toolbar>\n</ion-footer>\n", styles: [".item-partners ion-text{height:auto}.item-partners ion-text p:last-child{display:flex;flex-wrap:wrap;flex-direction:row;justify-content:space-between}.item-partners ion-text p:last-child a{padding:2px}.item-partners ion-text p:last-child a img{text-align:center;display:inline-block;max-height:40px}\n"] }]
33901
33906
  }], ctorParameters: () => [{ type: i2.TranslateService }, { type: i2$2.ModalController }, { type: ConfigService }, { type: NetworkService }, { type: PlatformService }, { type: i2$4.HttpClient }, { type: i0.ChangeDetectorRef }, { type: undefined, decorators: [{
33902
33907
  type: Optional
33903
33908
  }, {
@@ -41909,6 +41914,7 @@ class AppTable {
41909
41914
  propagateRowError = false;
41910
41915
  permanentSelectionAllowed = false; // Allow keep selected rows over page change or sort change
41911
41916
  openRowThrottleTime = 800; // Prevent double click - see issue sumaris-app#864
41917
+ loadingDebounceTime = 250; // Debounce time for loading spinner
41912
41918
  defaultSortBy;
41913
41919
  defaultSortDirection;
41914
41920
  defaultPageSize = DEFAULT_PAGE_SIZE;
@@ -42947,9 +42953,12 @@ class AppTable {
42947
42953
  return true; // Already the edited row
42948
42954
  if (event?.defaultPrevented)
42949
42955
  return false;
42950
- const confirmed = this.confirmEditCreate();
42951
- if (!confirmed)
42952
- return false;
42956
+ // Don't call confirmEditCreate() if canceling
42957
+ if (!toBoolean(opts?.canceling, false)) {
42958
+ const confirmed = this.confirmEditCreate();
42959
+ if (!confirmed)
42960
+ return false;
42961
+ }
42953
42962
  if (!row.editing && !this.loading) {
42954
42963
  this.focusColumn = opts?.focusColumn ?? this.focusColumn;
42955
42964
  this._dataSource.startEdit(row);
@@ -43512,7 +43521,7 @@ class AppTable {
43512
43521
  this.checkIfRowPristine(row);
43513
43522
  // Restore editing state
43514
43523
  if (keepEditing) {
43515
- this.editRow(undefined, row);
43524
+ this.editRow(undefined, row, { canceling: true });
43516
43525
  }
43517
43526
  }
43518
43527
  async checkIfRowPristine(row, opts) {
@@ -43562,7 +43571,7 @@ class AppTable {
43562
43571
  // If changed to True: propagate as soon as possible
43563
43572
  tap$1((loading) => loading && this.setLoading(true)),
43564
43573
  // If changed to False: wait 250ms before propagate (to make sure the spinner has been displayed)
43565
- debounceTime(250), tap$1((loading) => !loading && this.setLoading(false)))
43574
+ debounceTime(this.loadingDebounceTime), tap$1((loading) => !loading && this.setLoading(false)))
43566
43575
  .subscribe();
43567
43576
  this.registerSubscription(this._dataSourceLoadingSubscription);
43568
43577
  }
@@ -43615,7 +43624,7 @@ class AppTable {
43615
43624
  }
43616
43625
  }
43617
43626
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: AppTable, deps: "invalid", target: i0.ɵɵFactoryTarget.Directive });
43618
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.22", type: AppTable, isStandalone: true, inputs: { settingsId: "settingsId", debug: "debug", i18nColumnPrefix: "i18nColumnPrefix", i18nColumnSuffix: "i18nColumnSuffix", autoLoad: "autoLoad", readOnly: "readOnly", inlineEdition: "inlineEdition", focusFirstColumn: "focusFirstColumn", confirmBeforeDelete: "confirmBeforeDelete", confirmBeforeCancel: "confirmBeforeCancel", undoableDeletion: "undoableDeletion", saveBeforeDelete: "saveBeforeDelete", keepEditedRowOnSave: "keepEditedRowOnSave", saveBeforeSort: "saveBeforeSort", saveBeforeFilter: "saveBeforeFilter", propagateRowError: "propagateRowError", permanentSelectionAllowed: "permanentSelectionAllowed", openRowThrottleTime: "openRowThrottleTime", defaultSortBy: "defaultSortBy", defaultSortDirection: "defaultSortDirection", defaultPageSize: "defaultPageSize", defaultPageSizeOptions: "defaultPageSizeOptions", focusColumn: "focusColumn", dataSource: "dataSource", filter: "filter", disabled: "disabled", paginator: "paginator" }, outputs: { onRefresh: "onRefresh", onClickRow: "onClickRow", onOpenRow: "onOpenRow", onNewRow: "onNewRow", onStartEditingRow: "onStartEditingRow", onConfirmEditCreateRow: "onConfirmEditCreateRow", onCancelOrDeleteRow: "onCancelOrDeleteRow", onBeforeDeleteRows: "onBeforeDeleteRows", onBeforeCancelRows: "onBeforeCancelRows", onBeforeSave: "onBeforeSave", onAfterDeletedRows: "onAfterDeletedRows", onSort: "onSort", onDirty: "onDirty", onError: "onError" }, viewQueries: [{ propertyName: "table", first: true, predicate: MatTable, descendants: true }, { propertyName: "childPaginator", first: true, predicate: MatPaginator, descendants: true }, { propertyName: "sort", first: true, predicate: MatSort, descendants: true }], ngImport: i0 });
43627
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.22", type: AppTable, isStandalone: true, inputs: { settingsId: "settingsId", debug: "debug", i18nColumnPrefix: "i18nColumnPrefix", i18nColumnSuffix: "i18nColumnSuffix", autoLoad: "autoLoad", readOnly: "readOnly", inlineEdition: "inlineEdition", focusFirstColumn: "focusFirstColumn", confirmBeforeDelete: "confirmBeforeDelete", confirmBeforeCancel: "confirmBeforeCancel", undoableDeletion: "undoableDeletion", saveBeforeDelete: "saveBeforeDelete", keepEditedRowOnSave: "keepEditedRowOnSave", saveBeforeSort: "saveBeforeSort", saveBeforeFilter: "saveBeforeFilter", propagateRowError: "propagateRowError", permanentSelectionAllowed: "permanentSelectionAllowed", openRowThrottleTime: "openRowThrottleTime", loadingDebounceTime: "loadingDebounceTime", defaultSortBy: "defaultSortBy", defaultSortDirection: "defaultSortDirection", defaultPageSize: "defaultPageSize", defaultPageSizeOptions: "defaultPageSizeOptions", focusColumn: "focusColumn", dataSource: "dataSource", filter: "filter", disabled: "disabled", paginator: "paginator" }, outputs: { onRefresh: "onRefresh", onClickRow: "onClickRow", onOpenRow: "onOpenRow", onNewRow: "onNewRow", onStartEditingRow: "onStartEditingRow", onConfirmEditCreateRow: "onConfirmEditCreateRow", onCancelOrDeleteRow: "onCancelOrDeleteRow", onBeforeDeleteRows: "onBeforeDeleteRows", onBeforeCancelRows: "onBeforeCancelRows", onBeforeSave: "onBeforeSave", onAfterDeletedRows: "onAfterDeletedRows", onSort: "onSort", onDirty: "onDirty", onError: "onError" }, viewQueries: [{ propertyName: "table", first: true, predicate: MatTable, descendants: true }, { propertyName: "childPaginator", first: true, predicate: MatPaginator, descendants: true }, { propertyName: "sort", first: true, predicate: MatSort, descendants: true }], ngImport: i0 });
43619
43628
  }
43620
43629
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: AppTable, decorators: [{
43621
43630
  type: Directive
@@ -43655,6 +43664,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
43655
43664
  type: Input
43656
43665
  }], openRowThrottleTime: [{
43657
43666
  type: Input
43667
+ }], loadingDebounceTime: [{
43668
+ type: Input
43658
43669
  }], defaultSortBy: [{
43659
43670
  type: Input
43660
43671
  }], defaultSortDirection: [{
@@ -44782,6 +44793,7 @@ class AppAsyncTable {
44782
44793
  propagateRowError = false;
44783
44794
  permanentSelectionAllowed = false; // Allow keep selected rows over page change or sort change
44784
44795
  openRowThrottleTime = 800; // Prevent double click - see issue sumaris-app#864
44796
+ loadingDebounceTime = 250; // Debounce time for loading spinner
44785
44797
  defaultSortBy;
44786
44798
  defaultSortDirection;
44787
44799
  defaultPageSize = DEFAULT_PAGE_SIZE;
@@ -45390,6 +45402,14 @@ class AppAsyncTable {
45390
45402
  const row = await this.addRowToTable(insertAt, opts);
45391
45403
  return !!row;
45392
45404
  }
45405
+ /**
45406
+ * Save the table
45407
+ *
45408
+ * @param opts.keepEditing if true, the edited row will be kept after save (defaulting to this.keepEditedRowOnSave)
45409
+ * @param opts.keepSelection if true, the selected row will be kept after save (no effect if opts.keepEditing is true)
45410
+ * @param opts.confirmRow if true or not provided, the edited row will be confirmed before save
45411
+ * @param opts.throwIfNotConfirmed if true or not provided, an error will be thrown if the edited row is not confirmed
45412
+ */
45393
45413
  async save(opts) {
45394
45414
  opts = {
45395
45415
  keepEditing: this.keepEditedRowOnSave,
@@ -45399,15 +45419,22 @@ class AppAsyncTable {
45399
45419
  throw { code: ErrorCodes.TABLE_READ_ONLY, message: 'ERROR.TABLE_READ_ONLY' };
45400
45420
  }
45401
45421
  this.resetError();
45402
- // Keep edited row id (should be done BEFORE confirmEditCreate() )
45422
+ // Keep edited row id (should be done BEFORE confirmEditCreate())
45403
45423
  const editedRow = this.dataSource.getSingleEditingRow();
45404
- this.previouslyEditedRowId = opts.keepEditing ? (editedRow?.editing ? editedRow.id : undefined) || this.singleSelectedRow?.id : undefined;
45424
+ this.previouslyEditedRowId = opts.keepEditing ? ((editedRow?.editing ? editedRow.id : undefined) ?? this.singleSelectedRow?.id) : undefined;
45405
45425
  const previouslyEditedRowId = this.previouslyEditedRowId;
45406
45426
  const previouslyEditedData = opts.keepEditing
45407
- ? (isNotNil(previouslyEditedRowId) && editedRow?.currentData) || this.singleSelectedRow?.currentData
45427
+ ? ((isNotNil(previouslyEditedRowId) && editedRow?.currentData) ?? this.singleSelectedRow?.currentData)
45408
45428
  : undefined;
45409
- if (!(await this.confirmEditCreate())) {
45410
- throw { code: ErrorCodes.TABLE_INVALID_ROW_ERROR, message: 'ERROR.TABLE_INVALID_ROW_ERROR' };
45429
+ const previouslySelectedRowIds = !opts.keepEditing && opts.keepSelection && this.selection.hasValue() ? this.selection.selected.map((row) => row.id) : undefined;
45430
+ if (opts.confirmRow !== false && !(await this.confirmEditCreate())) {
45431
+ if (opts.throwIfNotConfirmed !== false) {
45432
+ throw { code: ErrorCodes.TABLE_INVALID_ROW_ERROR, message: 'ERROR.TABLE_INVALID_ROW_ERROR' };
45433
+ }
45434
+ else {
45435
+ // Return false if confirmRow is true and throwIfNotConfirmed is false
45436
+ return false;
45437
+ }
45411
45438
  }
45412
45439
  // Mark as saving
45413
45440
  this.markAsSaving();
@@ -45430,8 +45457,15 @@ class AppAsyncTable {
45430
45457
  if (isNotNil(previouslyEditedRowId)) {
45431
45458
  await this.selectRowByIdOrData(previouslyEditedRowId, previouslyEditedData);
45432
45459
  }
45460
+ else if (isNotEmptyArray(previouslySelectedRowIds)) {
45461
+ await this.selectRowsByIds(previouslySelectedRowIds);
45462
+ }
45433
45463
  }
45434
45464
  }
45465
+ async selectRowsByIds(rowIds) {
45466
+ await this.waitIdle();
45467
+ this.selection.setSelection(...(this.dataSource?.getRows() ?? []).filter((row) => rowIds.includes(row.id)));
45468
+ }
45435
45469
  async cancel(event, opts) {
45436
45470
  // Check confirmation
45437
45471
  if ((!opts || opts.interactive !== false) && this.dirty && (this.confirmBeforeCancel || this.onBeforeCancelRows.observed)) {
@@ -45812,8 +45846,11 @@ class AppAsyncTable {
45812
45846
  return true; // Already the edited row
45813
45847
  if (event?.defaultPrevented)
45814
45848
  return false;
45815
- if (!(await this.confirmEditCreate())) {
45816
- return false;
45849
+ // Don't call confirmEditCreate() if canceling
45850
+ if (!toBoolean(opts?.canceling, false)) {
45851
+ if (!(await this.confirmEditCreate())) {
45852
+ return false;
45853
+ }
45817
45854
  }
45818
45855
  if (!row.editing && !this.loading) {
45819
45856
  this.focusColumn = opts?.focusColumn ?? this.focusColumn;
@@ -46354,6 +46391,7 @@ class AppAsyncTable {
46354
46391
  // Make sure row will be cancelled, and NOT deleted
46355
46392
  if (row.id === -1 || !row.editing)
46356
46393
  throw new Error('Row cannot be canceling, but only deleting');
46394
+ // TODO invert this condition to call canCancelRows
46357
46395
  const confirmed = !opts || opts.interactive !== false;
46358
46396
  // Ask user confirmation, if cancel
46359
46397
  if (!confirmed && row.dirty && (this.confirmBeforeCancel || this.onBeforeCancelRows.observed)) {
@@ -46369,7 +46407,7 @@ class AppAsyncTable {
46369
46407
  await this.checkIfRowPristine(row);
46370
46408
  // Restore editing state
46371
46409
  if (keepEditing) {
46372
- await this.editRow(undefined, row);
46410
+ await this.editRow(undefined, row, { canceling: true });
46373
46411
  }
46374
46412
  }
46375
46413
  async checkIfRowPristine(row, opts) {
@@ -46419,7 +46457,7 @@ class AppAsyncTable {
46419
46457
  // If changed to True: propagate as soon as possible
46420
46458
  tap$1((loading) => loading && this.setLoading(true)),
46421
46459
  // If changed to False: wait 250ms before propagate (to make sure the spinner has been displayed)
46422
- debounceTime(250), tap$1((loading) => !loading && this.setLoading(false)))
46460
+ debounceTime(this.loadingDebounceTime), tap$1((loading) => !loading && this.setLoading(false)))
46423
46461
  .subscribe();
46424
46462
  this.registerSubscription(this._dataSourceLoadingSubscription);
46425
46463
  }
@@ -46471,7 +46509,7 @@ class AppAsyncTable {
46471
46509
  }
46472
46510
  }
46473
46511
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: AppAsyncTable, deps: "invalid", target: i0.ɵɵFactoryTarget.Directive });
46474
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.22", type: AppAsyncTable, isStandalone: true, inputs: { settingsId: "settingsId", debug: "debug", i18nColumnPrefix: "i18nColumnPrefix", i18nColumnSuffix: "i18nColumnSuffix", autoLoad: "autoLoad", readOnly: "readOnly", inlineEdition: "inlineEdition", focusFirstColumn: "focusFirstColumn", confirmBeforeDelete: "confirmBeforeDelete", confirmBeforeCancel: "confirmBeforeCancel", undoableDeletion: "undoableDeletion", saveBeforeDelete: "saveBeforeDelete", keepEditedRowOnSave: "keepEditedRowOnSave", saveBeforeSort: "saveBeforeSort", saveBeforeFilter: "saveBeforeFilter", propagateRowError: "propagateRowError", permanentSelectionAllowed: "permanentSelectionAllowed", openRowThrottleTime: "openRowThrottleTime", defaultSortBy: "defaultSortBy", defaultSortDirection: "defaultSortDirection", defaultPageSize: "defaultPageSize", defaultPageSizeOptions: "defaultPageSizeOptions", focusColumn: "focusColumn", dataSource: "dataSource", filter: "filter", disabled: "disabled", paginator: "paginator" }, outputs: { onRefresh: "onRefresh", onClickRow: "onClickRow", onOpenRow: "onOpenRow", onNewRow: "onNewRow", onStartEditingRow: "onStartEditingRow", onConfirmEditCreateRow: "onConfirmEditCreateRow", onCancelOrDeleteRow: "onCancelOrDeleteRow", onBeforeDeleteRows: "onBeforeDeleteRows", onBeforeCancelRows: "onBeforeCancelRows", onBeforeSave: "onBeforeSave", onAfterDeletedRows: "onAfterDeletedRows", onSort: "onSort", onDirty: "onDirty", onError: "onError" }, viewQueries: [{ propertyName: "table", first: true, predicate: MatTable, descendants: true }, { propertyName: "childPaginator", first: true, predicate: MatPaginator, descendants: true }, { propertyName: "sort", first: true, predicate: MatSort, descendants: true }], ngImport: i0 });
46512
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.22", type: AppAsyncTable, isStandalone: true, inputs: { settingsId: "settingsId", debug: "debug", i18nColumnPrefix: "i18nColumnPrefix", i18nColumnSuffix: "i18nColumnSuffix", autoLoad: "autoLoad", readOnly: "readOnly", inlineEdition: "inlineEdition", focusFirstColumn: "focusFirstColumn", confirmBeforeDelete: "confirmBeforeDelete", confirmBeforeCancel: "confirmBeforeCancel", undoableDeletion: "undoableDeletion", saveBeforeDelete: "saveBeforeDelete", keepEditedRowOnSave: "keepEditedRowOnSave", saveBeforeSort: "saveBeforeSort", saveBeforeFilter: "saveBeforeFilter", propagateRowError: "propagateRowError", permanentSelectionAllowed: "permanentSelectionAllowed", openRowThrottleTime: "openRowThrottleTime", loadingDebounceTime: "loadingDebounceTime", defaultSortBy: "defaultSortBy", defaultSortDirection: "defaultSortDirection", defaultPageSize: "defaultPageSize", defaultPageSizeOptions: "defaultPageSizeOptions", focusColumn: "focusColumn", dataSource: "dataSource", filter: "filter", disabled: "disabled", paginator: "paginator" }, outputs: { onRefresh: "onRefresh", onClickRow: "onClickRow", onOpenRow: "onOpenRow", onNewRow: "onNewRow", onStartEditingRow: "onStartEditingRow", onConfirmEditCreateRow: "onConfirmEditCreateRow", onCancelOrDeleteRow: "onCancelOrDeleteRow", onBeforeDeleteRows: "onBeforeDeleteRows", onBeforeCancelRows: "onBeforeCancelRows", onBeforeSave: "onBeforeSave", onAfterDeletedRows: "onAfterDeletedRows", onSort: "onSort", onDirty: "onDirty", onError: "onError" }, viewQueries: [{ propertyName: "table", first: true, predicate: MatTable, descendants: true }, { propertyName: "childPaginator", first: true, predicate: MatPaginator, descendants: true }, { propertyName: "sort", first: true, predicate: MatSort, descendants: true }], ngImport: i0 });
46475
46513
  }
46476
46514
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: AppAsyncTable, decorators: [{
46477
46515
  type: Directive
@@ -46511,6 +46549,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
46511
46549
  type: Input
46512
46550
  }], openRowThrottleTime: [{
46513
46551
  type: Input
46552
+ }], loadingDebounceTime: [{
46553
+ type: Input
46514
46554
  }], defaultSortBy: [{
46515
46555
  type: Input
46516
46556
  }], defaultSortDirection: [{