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.tsentity edition page (editor) AppEntityEditorand subclasses.form.tsreusable reactive form AppFormand subclasses.table.tsreusable table AppTable/AppSyncTable.service.tsservice .validator.tsbuilds the FormGroupand its validatorsAppValidatorServiceand subclasses.class.ts,.utils.ts,.config.tsabstract class, helper functions, configuration options -
Ionic components first (otherwise Angular Material),
<ion-icon>icons (ormat-icon, older). ChangeDetectionStrategy.OnPushfor every component.- Angular 21 conventions:
@if/@forblocks rather than*ngIf/*ngFor, injection withinject()as a class field (declared above the constructor) rather than constructor parameters.
Components¶
- HTML template in a separate file.
- Ionic is used in standalone mode (
provideIonicAngular()), never throughIonicModule:- Ionic components are imported through
AppIonicModule(@app/shared/ionic.module, already exported byAppSharedModule); - any new
<ion-*>tag must be added toAPP_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 inAPP_ICONS(src/app/app.icons.ts).
- Ionic components are imported through
- Dates:
Momentclass. -
Component state with RxState:
providers: [RxState], aMyComponentStateinterface 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 standaloneTranslatePipe(no moreTranslateModule.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'}), extendingEntity<T, ID>(idandupdateDateare inherited). - Type parameters
<T, ID = number, AO = EntityAsObjectOptions, FO = any>, a staticfromObject()method and a constructor. - Serialization in
asObject()/fromObject():- dates:
toDateISOString()/fromDateISOString()(Momentobjects); - related entities: their own
asObject()/MyEntity.fromObject().
- dates:
Source:
entities.md(copy)
Services¶
- Full CRUD service: extend
BaseEntityService<T, F, ID>, which providesload(),loadAll(),watchAll(),save(),saveAll(),deleteAll()andlistenChanges(). 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
GraphqlServiceorPlatformServicein the constructor, nor callinject(GraphqlService): they are provided by the base class (this.graphql,this.platform). Elsewhere, inject theAPP_GRAPHQL_SERVICEtoken. Otherwise:NG0201: No provider found for GraphqlServiceat runtime. -
Other dependencies with
inject()as class fields; the constructor only callssuper(...)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:
ErrorCodesand i18n keys; debug logs guarded bythis._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()); synchronizationStatusfield:DIRTY(local, not synchronized),READY_TO_SYNC,SYNC;save()delegates tosaveLocally()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) extendsAppForm<T>(orMeasurementValuesFormfor measurements driven by the program PMFMs). It neither loads nor saves the entity: that is the job of the parent editor. - The
FormGroupis never built in the component: it comes from the validator (.validator.ts), passed tosuper(). - 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.
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;AppFormArrayfor 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 providersAppValidatorService(row validator) andAppTable(forwardRef). - Columns passed to the constructor between
RESERVED_START_COLUMNSandRESERVED_END_COLUMNS, with anEntitiesTableDataSource. - Usual options:
inlineEdition,i18nColumnPrefix,confirmBeforeDelete,saveBeforeSort,saveBeforeFilter. mat-tabletemplate: oneng-container matColumnDefper column,<app-actions-column>for row actions,mat-paginatoron desktop,ion-infinite-scrollon mobile.- Complete example:
ngx-sumaris-components/src/app/core/table/testing/.
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.
Configuration¶
- Server configuration options (
*_CONFIG_OPTIONS) and local settings (*_LOCAL_SETTINGS_OPTIONS): frozen objects ofFormFieldDefinition, declared in the module.config.tsand registered inapp.module.ts(APP_CONFIG_OPTIONS,APP_LOCAL_SETTINGS_OPTIONS). - The
ConfigServiceloads 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.jsonanden.json: every new key is added to both. SNAKE_CASEkeys, prefixed by the domain (TRIP.SAMPLE.EDIT.); buttonsBTN_*, errorsERROR.*.
Source:
translations.md(copy)
Navigation and tests¶
- Main routes:
/trips/:tripId(trips: Trip, Gears, Operations tabs),/observations/:observedLocationId(observed locations: the landing editor depends on the program optionsumaris.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 withng serve --configuration testand a local Pod: each test is logged in automatically with a profile (admin,supervisororobserver, declared ine2e/.environments.json). - Import
testandexpectfrome2e/fixtures.tsand use its fixtures (nav,table,editor...); navigate like a user; never usepage.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) |