@sumaris-net/ngx-components 21.3.2 → 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`)
@@ -25934,14 +25934,14 @@ class AccountService extends BaseGraphqlService {
25934
25934
  console.error('[account] Error while saving account locally: ' + ((err && err.message) || err), err);
25935
25935
  }
25936
25936
  }
25937
- async saveLocalSettingsRemotely(source) {
25937
+ async saveLocalSettingsRemotely(source, force) {
25938
25938
  if (!source || !this.isLogin() || source.accountInheritance === false)
25939
25939
  return; // Skip
25940
25940
  const account = this.account;
25941
25941
  // Merge local settings into account's settings
25942
25942
  const settings = UserSettings.fromObject(account.settings) || new UserSettings();
25943
25943
  const changed = settings.merge(source, this._remoteLocalSettingsKeys);
25944
- if (!changed)
25944
+ if (!changed && !force)
25945
25945
  return; // Skip if unchanged
25946
25946
  // Save remotely
25947
25947
  this.account.settings = settings;
@@ -33898,11 +33898,11 @@ class AboutModal {
33898
33898
  }
33899
33899
  }
33900
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 });
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)\"\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 });
33902
33902
  }
33903
33903
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: AboutModal, decorators: [{
33904
33904
  type: Component,
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)\"\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"] }]
33906
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: [{
33907
33907
  type: Optional
33908
33908
  }, {
@@ -41914,6 +41914,7 @@ class AppTable {
41914
41914
  propagateRowError = false;
41915
41915
  permanentSelectionAllowed = false; // Allow keep selected rows over page change or sort change
41916
41916
  openRowThrottleTime = 800; // Prevent double click - see issue sumaris-app#864
41917
+ loadingDebounceTime = 250; // Debounce time for loading spinner
41917
41918
  defaultSortBy;
41918
41919
  defaultSortDirection;
41919
41920
  defaultPageSize = DEFAULT_PAGE_SIZE;
@@ -42952,9 +42953,12 @@ class AppTable {
42952
42953
  return true; // Already the edited row
42953
42954
  if (event?.defaultPrevented)
42954
42955
  return false;
42955
- const confirmed = this.confirmEditCreate();
42956
- if (!confirmed)
42957
- 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
+ }
42958
42962
  if (!row.editing && !this.loading) {
42959
42963
  this.focusColumn = opts?.focusColumn ?? this.focusColumn;
42960
42964
  this._dataSource.startEdit(row);
@@ -43517,7 +43521,7 @@ class AppTable {
43517
43521
  this.checkIfRowPristine(row);
43518
43522
  // Restore editing state
43519
43523
  if (keepEditing) {
43520
- this.editRow(undefined, row);
43524
+ this.editRow(undefined, row, { canceling: true });
43521
43525
  }
43522
43526
  }
43523
43527
  async checkIfRowPristine(row, opts) {
@@ -43567,7 +43571,7 @@ class AppTable {
43567
43571
  // If changed to True: propagate as soon as possible
43568
43572
  tap$1((loading) => loading && this.setLoading(true)),
43569
43573
  // If changed to False: wait 250ms before propagate (to make sure the spinner has been displayed)
43570
- debounceTime(250), tap$1((loading) => !loading && this.setLoading(false)))
43574
+ debounceTime(this.loadingDebounceTime), tap$1((loading) => !loading && this.setLoading(false)))
43571
43575
  .subscribe();
43572
43576
  this.registerSubscription(this._dataSourceLoadingSubscription);
43573
43577
  }
@@ -43620,7 +43624,7 @@ class AppTable {
43620
43624
  }
43621
43625
  }
43622
43626
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: AppTable, deps: "invalid", target: i0.ɵɵFactoryTarget.Directive });
43623
- 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 });
43624
43628
  }
43625
43629
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: AppTable, decorators: [{
43626
43630
  type: Directive
@@ -43660,6 +43664,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
43660
43664
  type: Input
43661
43665
  }], openRowThrottleTime: [{
43662
43666
  type: Input
43667
+ }], loadingDebounceTime: [{
43668
+ type: Input
43663
43669
  }], defaultSortBy: [{
43664
43670
  type: Input
43665
43671
  }], defaultSortDirection: [{
@@ -44787,6 +44793,7 @@ class AppAsyncTable {
44787
44793
  propagateRowError = false;
44788
44794
  permanentSelectionAllowed = false; // Allow keep selected rows over page change or sort change
44789
44795
  openRowThrottleTime = 800; // Prevent double click - see issue sumaris-app#864
44796
+ loadingDebounceTime = 250; // Debounce time for loading spinner
44790
44797
  defaultSortBy;
44791
44798
  defaultSortDirection;
44792
44799
  defaultPageSize = DEFAULT_PAGE_SIZE;
@@ -45395,6 +45402,14 @@ class AppAsyncTable {
45395
45402
  const row = await this.addRowToTable(insertAt, opts);
45396
45403
  return !!row;
45397
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
+ */
45398
45413
  async save(opts) {
45399
45414
  opts = {
45400
45415
  keepEditing: this.keepEditedRowOnSave,
@@ -45404,15 +45419,22 @@ class AppAsyncTable {
45404
45419
  throw { code: ErrorCodes.TABLE_READ_ONLY, message: 'ERROR.TABLE_READ_ONLY' };
45405
45420
  }
45406
45421
  this.resetError();
45407
- // Keep edited row id (should be done BEFORE confirmEditCreate() )
45422
+ // Keep edited row id (should be done BEFORE confirmEditCreate())
45408
45423
  const editedRow = this.dataSource.getSingleEditingRow();
45409
- 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;
45410
45425
  const previouslyEditedRowId = this.previouslyEditedRowId;
45411
45426
  const previouslyEditedData = opts.keepEditing
45412
- ? (isNotNil(previouslyEditedRowId) && editedRow?.currentData) || this.singleSelectedRow?.currentData
45427
+ ? ((isNotNil(previouslyEditedRowId) && editedRow?.currentData) ?? this.singleSelectedRow?.currentData)
45413
45428
  : undefined;
45414
- if (!(await this.confirmEditCreate())) {
45415
- 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
+ }
45416
45438
  }
45417
45439
  // Mark as saving
45418
45440
  this.markAsSaving();
@@ -45435,8 +45457,15 @@ class AppAsyncTable {
45435
45457
  if (isNotNil(previouslyEditedRowId)) {
45436
45458
  await this.selectRowByIdOrData(previouslyEditedRowId, previouslyEditedData);
45437
45459
  }
45460
+ else if (isNotEmptyArray(previouslySelectedRowIds)) {
45461
+ await this.selectRowsByIds(previouslySelectedRowIds);
45462
+ }
45438
45463
  }
45439
45464
  }
45465
+ async selectRowsByIds(rowIds) {
45466
+ await this.waitIdle();
45467
+ this.selection.setSelection(...(this.dataSource?.getRows() ?? []).filter((row) => rowIds.includes(row.id)));
45468
+ }
45440
45469
  async cancel(event, opts) {
45441
45470
  // Check confirmation
45442
45471
  if ((!opts || opts.interactive !== false) && this.dirty && (this.confirmBeforeCancel || this.onBeforeCancelRows.observed)) {
@@ -45817,8 +45846,11 @@ class AppAsyncTable {
45817
45846
  return true; // Already the edited row
45818
45847
  if (event?.defaultPrevented)
45819
45848
  return false;
45820
- if (!(await this.confirmEditCreate())) {
45821
- return false;
45849
+ // Don't call confirmEditCreate() if canceling
45850
+ if (!toBoolean(opts?.canceling, false)) {
45851
+ if (!(await this.confirmEditCreate())) {
45852
+ return false;
45853
+ }
45822
45854
  }
45823
45855
  if (!row.editing && !this.loading) {
45824
45856
  this.focusColumn = opts?.focusColumn ?? this.focusColumn;
@@ -46359,6 +46391,7 @@ class AppAsyncTable {
46359
46391
  // Make sure row will be cancelled, and NOT deleted
46360
46392
  if (row.id === -1 || !row.editing)
46361
46393
  throw new Error('Row cannot be canceling, but only deleting');
46394
+ // TODO invert this condition to call canCancelRows
46362
46395
  const confirmed = !opts || opts.interactive !== false;
46363
46396
  // Ask user confirmation, if cancel
46364
46397
  if (!confirmed && row.dirty && (this.confirmBeforeCancel || this.onBeforeCancelRows.observed)) {
@@ -46374,7 +46407,7 @@ class AppAsyncTable {
46374
46407
  await this.checkIfRowPristine(row);
46375
46408
  // Restore editing state
46376
46409
  if (keepEditing) {
46377
- await this.editRow(undefined, row);
46410
+ await this.editRow(undefined, row, { canceling: true });
46378
46411
  }
46379
46412
  }
46380
46413
  async checkIfRowPristine(row, opts) {
@@ -46424,7 +46457,7 @@ class AppAsyncTable {
46424
46457
  // If changed to True: propagate as soon as possible
46425
46458
  tap$1((loading) => loading && this.setLoading(true)),
46426
46459
  // If changed to False: wait 250ms before propagate (to make sure the spinner has been displayed)
46427
- debounceTime(250), tap$1((loading) => !loading && this.setLoading(false)))
46460
+ debounceTime(this.loadingDebounceTime), tap$1((loading) => !loading && this.setLoading(false)))
46428
46461
  .subscribe();
46429
46462
  this.registerSubscription(this._dataSourceLoadingSubscription);
46430
46463
  }
@@ -46476,7 +46509,7 @@ class AppAsyncTable {
46476
46509
  }
46477
46510
  }
46478
46511
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: AppAsyncTable, deps: "invalid", target: i0.ɵɵFactoryTarget.Directive });
46479
- 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 });
46480
46513
  }
46481
46514
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: AppAsyncTable, decorators: [{
46482
46515
  type: Directive
@@ -46516,6 +46549,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
46516
46549
  type: Input
46517
46550
  }], openRowThrottleTime: [{
46518
46551
  type: Input
46552
+ }], loadingDebounceTime: [{
46553
+ type: Input
46519
46554
  }], defaultSortBy: [{
46520
46555
  type: Input
46521
46556
  }], defaultSortDirection: [{