Aller au contenu

App > Guide de développement

Ce guide présente les conventions de code de l'App SUMARiS (Angular 21, Ionic standalone, Angular Material), pour un développeur qui rejoint le projet.

Il est rédigé à partir des règles des agents IA du dépôt sumaris-app (.agents/rules/), qui font foi : chaque section indique sa règle source, sur GitLab et dans sa copie synchronisée sur ce site. Pour mettre à jour ces copies et savoir quelles sections relire : npm run site:sync-rules.

Voir aussi : installation de l'environnement, API du Pod.

Règles communes

  • Commentaires et logs (console.*) en anglais ; tests unitaires avec Jasmine quand c'est possible.
  • Les fichiers sont suffixés selon leur rôle, jamais par .component.ts :

    Suffixe Rôle Classe de base
    .page.ts page d'édition d'une entité (éditeur) AppEntityEditor et dérivées
    .form.ts formulaire réactif réutilisable AppForm et dérivées
    .table.ts tableau réutilisable AppTable / AppSyncTable
    .service.ts service
    .validator.ts construction du FormGroup et de ses validateurs AppValidatorService et dérivées
    .class.ts, .utils.ts, .config.ts classe abstraite, fonctions utilitaires, options de configuration
  • Composants Ionic en priorité (sinon Angular Material), icônes <ion-icon> (ou mat-icon, plus ancien).

  • ChangeDetectionStrategy.OnPush pour tous les composants.
  • Conventions Angular 21 : blocs @if / @for plutôt que *ngIf / *ngFor, injection par inject() en champ de classe (déclaré au-dessus du constructeur) plutôt que par paramètre de constructeur.

Source : common.md (copie)

Composants

  • Template HTML dans un fichier séparé.
  • Ionic est utilisé en mode standalone (provideIonicAngular()), jamais via IonicModule :
    • les composants Ionic sont importés via AppIonicModule (@app/shared/ionic.module, déjà exporté par AppSharedModule) ;
    • toute nouvelle balise <ion-*> doit être ajoutée à APP_IONIC_COMPONENTS (src/app/shared/ionic.module.ts), sinon elle reste inerte ;
    • contrôleurs et types (ModalController, AlertController...) s'importent depuis @ionic/angular/standalone ;
    • une <ion-icon> au nom statique doit être enregistrée dans APP_ICONS (src/app/app.icons.ts).
  • Dates : classe Moment.
  • État du composant avec RxState : providers: [RxState], une interface MonComposantState déclarée juste au-dessus de la classe, et les attributs dynamiques exposés par décorateurs :

    @RxStateProperty() @Input() label: string;      // getter/setter adossés au state
    @RxStateSelect() label$: Observable<string>;    // observable de la propriété
    
  • Modales : un composant dédié (jamais de modale inline dans le template), ouvert par le composant appelant via ModalController.

  • Libellés : ngx-translate, avec le TranslatePipe standalone (plus de TranslateModule.forChild()) et des clés préfixées par le nom du composant (MON_COMPOSANT.).
  • Code généré (par une IA ou autre) : ajouter en tête de classe un commentaire indiquant la licence GPL v3.

Source : component.md (copie)

Entités TypeScript

  • Classe annotée @EntityClass({typename: 'MaClasseVO'}), qui étend Entity<T, ID> (id et updateDate sont hérités).
  • Paramètres de type <T, ID = number, AO = EntityAsObjectOptions, FO = any>, une méthode statique fromObject() et un constructeur.
  • Sérialisation dans asObject() / fromObject() :
    • dates : toDateISOString() / fromDateISOString() (objets Moment) ;
    • entités liées : leur propre asObject() / MonEntite.fromObject().

Source : entities.md (copie)

Services

  • Service CRUD complet : étendre BaseEntityService<T, F, ID>, qui fournit load(), loadAll(), watchAll(), save(), saveAll(), deleteAll() et listenChanges(). Service plus simple : BaseGraphqlService.
  • Requêtes et mutations GraphQL déclarées en constantes (gql) au-dessus de la classe, avec des fragments partagés, et passées au constructeur parent.
  • Ne jamais redéclarer GraphqlService ni PlatformService dans le constructeur, ni faire inject(GraphqlService) : ils sont fournis par la classe de base (this.graphql, this.platform). Ailleurs, injecter le jeton APP_GRAPHQL_SERVICE. Sinon : erreur NG0201: No provider found for GraphqlService à l'exécution.
  • Les autres dépendances via inject() en champ de classe ; constructeur réduit à super(...) et à l'initialisation :

    @Injectable({ providedIn: 'root' })
    export class MyEntityService extends BaseEntityService<MyEntity, MyEntityFilter> {
      protected accountService = inject(AccountService);
    
      constructor() {
        super(MyEntity, MyEntityFilter, { queries: Queries, mutations: Mutations });
        this._logPrefix = '[my-entity-service] ';
        this._debug = !environment.production;
      }
    }
    
  • Erreurs : codes ErrorCodes et clés i18n ; logs de debug conditionnés par this._debug.

  • Exemples : src/app/referential/services/parameter.service.ts, src/app/data/position/device/device-position.service.ts.

Source : service.md (copie)

Données hors ligne

Les services des données racines utilisables sur le terrain (marées, sorties...) étendent RootDataSynchroService :

  • stockage local via this.entities (EntityStorage) : save(), loadAll(), watchAll(), corbeille ;
  • identifiants négatifs pour les données locales, positifs pour les données du serveur (EntityUtils.isLocalId()) ;
  • champ synchronizationStatus : DIRTY (local, non synchronisé), READY_TO_SYNC, SYNC ;
  • save() délègue à saveLocally() pour une donnée locale ; synchronize() envoie la donnée au serveur puis nettoie le stockage local ;
  • import des référentiels et données pour le mode hors ligne : runImport() / getImportJobs().

Source : service-offline.md (copie)

Qualité des données

Cycle de vie d'une donnée : contrôle → terminaison → validation (→ qualification).

  • IDataEntityQualityService (control(), qualify(), canUserWrite()) pour toute donnée, IRootDataEntityQualityService (terminate(), validate(), unvalidate()) pour les données racines ;
  • le contrôle d'une donnée racine délègue aux données filles (marée → opérations → lots), avec un suivi de progression partagé ;
  • erreurs stockées dans qualificationComments (DataEntityUtils.markAsInvalid()), après traduction ;
  • la validation exige une donnée enregistrée sur le serveur, déjà contrôlée (controlDate) et non validée.

Source : service-quality.md (copie)

Formulaires, validateurs et autocomplétion

  • Un formulaire (.form.ts) étend AppForm<T> (ou MeasurementValuesForm pour des mesures pilotées par les PSFM du programme). Il ne charge ni n'enregistre l'entité : c'est le rôle de l'éditeur parent.
  • Le FormGroup n'est jamais construit dans le composant : il vient du validateur (.validator.ts), passé à super().
  • Dans le template, utiliser les champs du projet : mat-autocomplete-field, mat-date-field, mat-date-time-field, mat-boolean-field, mat-latlong-field, mat-chips-field...
  • Attendre await this.ready() avant de lire ou d'écrire des valeurs.

Source : form.md (copie)

Validateurs :

  • hiérarchie AppValidatorService → BaseValidatorService → DataEntityValidatorService → DataRootEntityValidatorService → DataRootVesselEntityValidatorService ;
  • UntypedFormBuilder / UntypedFormGroup ; options typées (withObservers, withMeasurements...) ;
  • getFormGroupConfig() pour les contrôles de base, getFormGroup() pour les sous-formulaires conditionnels (form.addControl()), updateFormGroup() pour les adapter à l'exécution ; AppFormArray pour les listes ;
  • validateurs partagés : SharedValidators.entity, SharedValidators.dateRangeEnd()...

Source : validator.md (copie)

Champs d'autocomplétion : déclarés dans ngOnInit() par registerAutocompleteField(), alimentés par un service (service + filter), une liste statique (items) ou une fonction (suggestFn), puis affichés selon attributes, displayWith...

Source : autocomplete.md (copie)

Tableaux

  • Un tableau étend AppTable<T, F>, avec les providers AppValidatorService (validateur de la ligne) et AppTable (forwardRef).
  • Colonnes passées au constructeur entre RESERVED_START_COLUMNS et RESERVED_END_COLUMNS, avec un EntitiesTableDataSource.
  • Options usuelles : inlineEdition, i18nColumnPrefix, confirmBeforeDelete, saveBeforeSort, saveBeforeFilter.
  • Template mat-table : une ng-container matColumnDef par colonne, <app-actions-column> pour les actions de ligne, mat-paginator sur ordinateur, ion-infinite-scroll sur mobile.
  • Exemple complet : ngx-sumaris-components/src/app/core/table/testing/.

Source : table.md (copie)

Éditeurs (pages)

Un éditeur (.page.ts) charge, affiche et enregistre une entité, en orchestrant des formulaires et tableaux enfants. Choisir la classe la plus spécialisée qui convient :

Classe de base Pour
AppEditor éditeur générique (cycle chargement/enregistrement, formulaires enfants)
AppTabEditor ... avec des onglets
AppEntityEditor ... adossé à un service d'entité
AppDataEntityEditor donnée : résolution du programme, de la stratégie et des PSFM (ex : opération)
AppRootDataEntityEditor donnée racine : qualité, observateurs, synchronisation hors ligne (ex : marée, sortie)
  • À implémenter : registerForms(), get form(), setValue(), computeTitle(), getFirstInvalidTabIndex() ; points d'extension : onNewEntity(), onEntityLoaded(), getValue(), getJsonValueToSave(), canUserWrite().
  • Enchaînement : route → load() → onNewEntity() / onEntityLoaded() → setValue() ; à l'enregistrement : getValue() → getJsonValueToSave() → service.save() → onEntitySaved().
  • Exemples : trip.page.ts, observed-location.page.ts, landing.page.ts, operation.page.ts.

Source : editor.md (copie)

Configuration

  • Options de configuration serveur (*_CONFIG_OPTIONS) et préférences locales (*_LOCAL_SETTINGS_OPTIONS) : objets figés de FormFieldDefinition, déclarés dans le .config.ts du module et enregistrés dans app.module.ts (APP_CONFIG_OPTIONS, APP_LOCAL_SETTINGS_OPTIONS).
  • Le ConfigService charge la configuration du Pod et permet d'activer des fonctionnalités à l'exécution.
  • Le même fichier déclare les politiques de cache Apollo (*_GRAPHQL_TYPE_POLICIES) et de stockage local (*_STORAGE_TYPE_POLICIES).

Source : configuration.md (copie)

Traductions

  • Fichiers src/assets/i18n/fr.json et en.json : toute nouvelle clé est ajoutée dans les deux.
  • Clés en SNAKE_CASE, préfixées par le domaine (TRIP.SAMPLE.EDIT.) ; boutons BTN_*, erreurs ERROR.*.

Source : translations.md (copie)

  • Routes principales : /trips/:tripId (marées : onglets Marée, Engins, Opérations), /observations/:observedLocationId (sorties : l'éditeur des débarquements dépend de l'option de programme sumaris.landing.editor).
  • Une donnée validée s'ouvre en lecture seule.

Source : navigation.md (copie)

  • Tests de bout en bout avec Playwright (npx playwright test, --headed, --ui), sur l'App servie avec ng serve --configuration test et un Pod local : chaque test est connecté automatiquement avec un profil (admin, supervisor ou observer, déclarés dans e2e/.environments.json).
  • Importer test et expect depuis e2e/fixtures.ts et utiliser ses fixtures (nav, table, editor...) ; naviguer comme un utilisateur ; ne jamais utiliser page.waitForTimeout() ; un test qui crée des données doit les supprimer, même en cas d'échec.

Source : testing.md (copie)

Sujets spécifiques

Sujet Règle
Ichtyomètres (codes-barres, Bluetooth) ichthyometer.md (copie)
Données FishBase / SeaLifeBase fishbase.md (copie)
Usage de GitLab (tickets, branches, MR) gitlab.md (copie)