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.tspage d'édition d'une entité (éditeur) AppEntityEditoret dérivées.form.tsformulaire réactif réutilisable AppFormet dérivées.table.tstableau réutilisable AppTable/AppSyncTable.service.tsservice .validator.tsconstruction du FormGroupet de ses validateursAppValidatorServiceet dérivées.class.ts,.utils.ts,.config.tsclasse abstraite, fonctions utilitaires, options de configuration -
Composants Ionic en priorité (sinon Angular Material), icônes
<ion-icon>(oumat-icon, plus ancien). ChangeDetectionStrategy.OnPushpour tous les composants.- Conventions Angular 21 : blocs
@if/@forplutôt que*ngIf/*ngFor, injection parinject()en champ de classe (déclaré au-dessus du constructeur) plutôt que par paramètre de constructeur.
Composants¶
- Template HTML dans un fichier séparé.
- Ionic est utilisé en mode standalone (
provideIonicAngular()), jamais viaIonicModule:- les composants Ionic sont importés via
AppIonicModule(@app/shared/ionic.module, déjà exporté parAppSharedModule) ; - 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 dansAPP_ICONS(src/app/app.icons.ts).
- les composants Ionic sont importés via
- Dates : classe
Moment. -
État du composant avec RxState :
providers: [RxState], une interfaceMonComposantStatedé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 leTranslatePipestandalone (plus deTranslateModule.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 étendEntity<T, ID>(idetupdateDatesont hérités). - Paramètres de type
<T, ID = number, AO = EntityAsObjectOptions, FO = any>, une méthode statiquefromObject()et un constructeur. - Sérialisation dans
asObject()/fromObject():- dates :
toDateISOString()/fromDateISOString()(objetsMoment) ; - entités liées : leur propre
asObject()/MonEntite.fromObject().
- dates :
Source :
entities.md(copie)
Services¶
- Service CRUD complet : étendre
BaseEntityService<T, F, ID>, qui fournitload(),loadAll(),watchAll(),save(),saveAll(),deleteAll()etlistenChanges(). 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
GraphqlServiceniPlatformServicedans le constructeur, ni faireinject(GraphqlService): ils sont fournis par la classe de base (this.graphql,this.platform). Ailleurs, injecter le jetonAPP_GRAPHQL_SERVICE. Sinon : erreurNG0201: 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
ErrorCodeset clés i18n ; logs de debug conditionnés parthis._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) étendAppForm<T>(ouMeasurementValuesFormpour 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
FormGroupn'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.
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 ;AppFormArraypour 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 providersAppValidatorService(validateur de la ligne) etAppTable(forwardRef). - Colonnes passées au constructeur entre
RESERVED_START_COLUMNSetRESERVED_END_COLUMNS, avec unEntitiesTableDataSource. - Options usuelles :
inlineEdition,i18nColumnPrefix,confirmBeforeDelete,saveBeforeSort,saveBeforeFilter. - Template
mat-table: uneng-container matColumnDefpar colonne,<app-actions-column>pour les actions de ligne,mat-paginatorsur ordinateur,ion-infinite-scrollsur mobile. - Exemple complet :
ngx-sumaris-components/src/app/core/table/testing/.
É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.
Configuration¶
- Options de configuration serveur (
*_CONFIG_OPTIONS) et préférences locales (*_LOCAL_SETTINGS_OPTIONS) : objets figés deFormFieldDefinition, déclarés dans le.config.tsdu module et enregistrés dansapp.module.ts(APP_CONFIG_OPTIONS,APP_LOCAL_SETTINGS_OPTIONS). - Le
ConfigServicecharge 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.jsoneten.json: toute nouvelle clé est ajoutée dans les deux. - Clés en
SNAKE_CASE, préfixées par le domaine (TRIP.SAMPLE.EDIT.) ; boutonsBTN_*, erreursERROR.*.
Source :
translations.md(copie)
Navigation et tests¶
- 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 programmesumaris.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 avecng serve --configuration testet un Pod local : chaque test est connecté automatiquement avec un profil (admin,supervisorouobserver, déclarés danse2e/.environments.json). - Importer
testetexpectdepuise2e/fixtures.tset utiliser ses fixtures (nav,table,editor...) ; naviguer comme un utilisateur ; ne jamais utiliserpage.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) |