Skip to content

App > Developer guide

This guide presents the coding conventions of the SUMARiS App (Angular 21, standalone Ionic, Angular Material), for a developer joining the project.

It is written from the AI agent rules of the sumaris-app repository (.agents/rules/), which are the reference: each section gives its source rule, on GitLab and as a synchronized copy on this site. To update these copies and find the sections to review: npm run site:sync-rules.

See also: environment setup, Pod API.

Common rules

  • Comments and logs (console.*) in English; unit tests with Jasmine when possible.
  • Files are suffixed by their role, never with .component.ts:

    Suffix Role Base class
    .page.ts entity edition page (editor) AppEntityEditor and subclasses
    .form.ts reusable reactive form AppForm and subclasses
    .table.ts reusable table AppTable / AppSyncTable
    .service.ts service
    .validator.ts builds the FormGroup and its validators AppValidatorService and subclasses
    .class.ts, .utils.ts, .config.ts abstract class, helper functions, configuration options
  • Ionic components first (otherwise Angular Material), <ion-icon> icons (or mat-icon, older).

  • ChangeDetectionStrategy.OnPush for every component.
  • Angular 21 conventions: @if / @for blocks rather than *ngIf / *ngFor, injection with inject() as a class field (declared above the constructor) rather than constructor parameters.

Source: common.md (copy)

Components

  • HTML template in a separate file.
  • Ionic is used in standalone mode (provideIonicAngular()), never through IonicModule:
    • Ionic components are imported through AppIonicModule (@app/shared/ionic.module, already exported by AppSharedModule);
    • any new <ion-*> tag must be added to APP_IONIC_COMPONENTS (src/app/shared/ionic.module.ts), otherwise it stays inert;
    • controllers and types (ModalController, AlertController...) are imported from @ionic/angular/standalone;
    • an <ion-icon> with a static name must be registered in APP_ICONS (src/app/app.icons.ts).
  • Dates: Moment class.
  • Component state with RxState: providers: [RxState], a MyComponentState interface declared right above the class, and dynamic attributes exposed with decorators:

    @RxStateProperty() @Input() label: string;      // getter/setter backed by the state
    @RxStateSelect() label$: Observable<string>;    // observable of the property
    
  • Modals: a dedicated component (never an inline modal in the template), opened by the caller through ModalController.

  • Labels: ngx-translate, with the standalone TranslatePipe (no more TranslateModule.forChild()) and keys prefixed by the component name (MY_COMPONENT.).
  • Generated code (by an AI or otherwise): add a comment at the top of the class stating the GPL v3 license.

Source: component.md (copy)

TypeScript entities

  • Class annotated with @EntityClass({typename: 'MyClassVO'}), extending Entity<T, ID> (id and updateDate are inherited).
  • Type parameters <T, ID = number, AO = EntityAsObjectOptions, FO = any>, a static fromObject() method and a constructor.
  • Serialization in asObject() / fromObject():
    • dates: toDateISOString() / fromDateISOString() (Moment objects);
    • related entities: their own asObject() / MyEntity.fromObject().

Source: entities.md (copy)

Services

  • Full CRUD service: extend BaseEntityService<T, F, ID>, which provides load(), loadAll(), watchAll(), save(), saveAll(), deleteAll() and listenChanges(). Simpler service: BaseGraphqlService.
  • GraphQL queries and mutations declared as constants (gql) above the class, with shared fragments, and passed to the parent constructor.
  • Never redeclare GraphqlService or PlatformService in the constructor, nor call inject(GraphqlService): they are provided by the base class (this.graphql, this.platform). Elsewhere, inject the APP_GRAPHQL_SERVICE token. Otherwise: NG0201: No provider found for GraphqlService at runtime.
  • Other dependencies with inject() as class fields; the constructor only calls super(...) and does the setup:

    @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;
      }
    }
    
  • Errors: ErrorCodes and i18n keys; debug logs guarded by this._debug.

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

Source: service.md (copy)

Offline data

Services of root data usable in the field (trips, observed locations...) extend RootDataSynchroService:

  • local storage through this.entities (EntityStorage): save(), loadAll(), watchAll(), trash;
  • negative ids for local data, positive ids for server data (EntityUtils.isLocalId());
  • synchronizationStatus field: DIRTY (local, not synchronized), READY_TO_SYNC, SYNC;
  • save() delegates to saveLocally() for local data; synchronize() pushes the data to the server, then cleans the local storage;
  • import of referentials and data for offline mode: runImport() / getImportJobs().

Source: service-offline.md (copy)

Data quality

Data lifecycle: control → termination → validation (→ qualification).

  • IDataEntityQualityService (control(), qualify(), canUserWrite()) for any data, IRootDataEntityQualityService (terminate(), validate(), unvalidate()) for root data;
  • controlling root data delegates to child data (trip → operations → batches), with a shared progression;
  • errors are stored in qualificationComments (DataEntityUtils.markAsInvalid()), once translated;
  • validation requires data saved on the server, already controlled (controlDate) and not yet validated.

Source: service-quality.md (copy)

Forms, validators and autocomplete

  • A form (.form.ts) extends AppForm<T> (or MeasurementValuesForm for measurements driven by the program PMFMs). It neither loads nor saves the entity: that is the job of the parent editor.
  • The FormGroup is never built in the component: it comes from the validator (.validator.ts), passed to super().
  • In the template, use the project fields: mat-autocomplete-field, mat-date-field, mat-date-time-field, mat-boolean-field, mat-latlong-field, mat-chips-field...
  • Wait for await this.ready() before reading or writing values.

Source: form.md (copy)

Validators:

  • hierarchy AppValidatorService → BaseValidatorService → DataEntityValidatorService → DataRootEntityValidatorService → DataRootVesselEntityValidatorService;
  • UntypedFormBuilder / UntypedFormGroup; typed options (withObservers, withMeasurements...);
  • getFormGroupConfig() for the base controls, getFormGroup() for conditional sub-forms (form.addControl()), updateFormGroup() to adapt them at runtime; AppFormArray for lists;
  • shared validators: SharedValidators.entity, SharedValidators.dateRangeEnd()...

Source: validator.md (copy)

Autocomplete fields: declared in ngOnInit() with registerAutocompleteField(), fed by a service (service + filter), a static list (items) or a function (suggestFn), then displayed according to attributes, displayWith...

Source: autocomplete.md (copy)

Tables

  • A table extends AppTable<T, F>, with the providers AppValidatorService (row validator) and AppTable (forwardRef).
  • Columns passed to the constructor between RESERVED_START_COLUMNS and RESERVED_END_COLUMNS, with an EntitiesTableDataSource.
  • Usual options: inlineEdition, i18nColumnPrefix, confirmBeforeDelete, saveBeforeSort, saveBeforeFilter.
  • mat-table template: one ng-container matColumnDef per column, <app-actions-column> for row actions, mat-paginator on desktop, ion-infinite-scroll on mobile.
  • Complete example: ngx-sumaris-components/src/app/core/table/testing/.

Source: table.md (copy)

Editors (pages)

An editor (.page.ts) loads, displays and saves one entity, orchestrating child forms and tables. Pick the most specialized class that fits:

Base class For
AppEditor generic editor (load/save lifecycle, child forms)
AppTabEditor ... with tabs
AppEntityEditor ... backed by an entity service
AppDataEntityEditor data: program, strategy and PMFM resolution (e.g. operation)
AppRootDataEntityEditor root data: quality, observers, offline synchronization (e.g. trip, observed location)
  • To implement: registerForms(), get form(), setValue(), computeTitle(), getFirstInvalidTabIndex(); hooks: onNewEntity(), onEntityLoaded(), getValue(), getJsonValueToSave(), canUserWrite().
  • Flow: route → load() → onNewEntity() / onEntityLoaded() → setValue(); on save: getValue() → getJsonValueToSave() → service.save() → onEntitySaved().
  • Examples: trip.page.ts, observed-location.page.ts, landing.page.ts, operation.page.ts.

Source: editor.md (copy)

Configuration

  • Server configuration options (*_CONFIG_OPTIONS) and local settings (*_LOCAL_SETTINGS_OPTIONS): frozen objects of FormFieldDefinition, declared in the module .config.ts and registered in app.module.ts (APP_CONFIG_OPTIONS, APP_LOCAL_SETTINGS_OPTIONS).
  • The ConfigService loads the Pod configuration, to enable features at runtime.
  • The same file declares the Apollo cache policies (*_GRAPHQL_TYPE_POLICIES) and local storage policies (*_STORAGE_TYPE_POLICIES).

Source: configuration.md (copy)

Translations

  • Files src/assets/i18n/fr.json and en.json: every new key is added to both.
  • SNAKE_CASE keys, prefixed by the domain (TRIP.SAMPLE.EDIT.); buttons BTN_*, errors ERROR.*.

Source: translations.md (copy)

  • Main routes: /trips/:tripId (trips: Trip, Gears, Operations tabs), /observations/:observedLocationId (observed locations: the landing editor depends on the program option sumaris.landing.editor).
  • Validated data opens in read-only mode.

Source: navigation.md (copy)

  • End-to-end tests with Playwright (npx playwright test, --headed, --ui), on the App served with ng serve --configuration test and a local Pod: each test is logged in automatically with a profile (admin, supervisor or observer, declared in e2e/.environments.json).
  • Import test and expect from e2e/fixtures.ts and use its fixtures (nav, table, editor...); navigate like a user; never use page.waitForTimeout(); a test that creates data must delete it, even when it fails.

Source: testing.md (copy)

Specific topics

Topic Rule
Ichthyometers (barcodes, Bluetooth) ichthyometer.md (copy)
FishBase / SeaLifeBase data fishbase.md (copy)
GitLab usage (issues, branches, MRs) gitlab.md (copy)