- TypeScript 87.4%
- HTML 6.9%
- SCSS 5.3%
- JavaScript 0.3%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| .devcontainer | ||
| .docker | ||
| .github/workflows | ||
| .husky | ||
| .readme | ||
| dev | ||
| e2e | ||
| scripts/pipeline | ||
| src | ||
| .dockerignore | ||
| .editorconfig | ||
| .gitattributes | ||
| .gitignore | ||
| .gitlab-ci.yml | ||
| .lintstagedrc | ||
| .nvmrc | ||
| .prettierignore | ||
| .prettierrc.json | ||
| angular.json | ||
| docker-compose.ktzh.yml | ||
| Dockerfile | ||
| eslint.config.mjs | ||
| knip.config.ts | ||
| LICENSE.txt | ||
| package-lock.json | ||
| package.json | ||
| playwright.config.ts | ||
| prepare.mjs | ||
| README.md | ||
| remove-generated-api-version-references.js | ||
| renovate.json | ||
| sonar-project.properties | ||
| swagger.yaml | ||
| tsconfig.app.json | ||
| tsconfig.json | ||
| tsconfig.spec.json | ||
| update-version.js | ||
| vitest.config.ts | ||
GB3 Frontend
This project was generated with Angular CLI version 14.2.3.
Table of Contents
Node version
We strive to use the most recent LTS version. Whenever an update is due, make sure you adjust the following:
Dockerfile./dev/Dockerfile.dev.github/workflows/node*.yml(Warning! There are cases where the pipeline does not yet have the newest node version; in that case, leave it as before and ignore the pipeline warnings).nvmrcpackage.json, update the@typed/nodepackage to the matching version; runnpm installafterwards to freshly generate thepackage-lock.jsonrenovate.jsonupdate both"matchManagers": ["dockerfile"]and"matchManagers": ["npm"]to the matching version
You should point it towards the latest minor update (e.g. 20.x), such that we can control potentially larger updates.
If you're using nvm on a Unix-based environment, you can conveniently use nvm use in
the root directory and it will automatically set the node version to the correct one.
Development server
Run ng serve for a dev server. Navigate to http://localhost:4200/. The application will automatically reload if you
change any of the source files.
Code scaffolding
Run ng generate component component-name to generate a new component. You can also
use ng generate directive|pipe|service|class|guard|interface|enum|module.
Build
Run ng build to build the project. The build artifacts will be stored in the dist/browser/ directory.
Running unit tests
Run ng test to execute the unit tests via Karma.
Warning
Starting with
@arcgis/core4.32, memory demands have increased significantly due to more types, which requires more work for checking types. As such, karma may run into memory overflows. If this happens, you can increase the memory limit for the node process by setting theNODE_OPTIONSenvironment variable to--max_old_space_size=80192(or any other value that suits your needs).
Running end-to-end tests
Installing browsers
To install all necessary browsers, execute:
npx playwright install --with-deps
This will install all dependencies and browsers necessary.
Running the tests
ng e2e
or
npm run e2e
Running tests with HAR mocks
HAR stands for Http ARchive. Playwright is able to use HAR files to play back API responses and make tests more deterministic while allowing for adaptions to test data without having to deploy any environment first.
HAR mocks get written per test file. To update the current ones or create new ones, we need to specify a username and password for any tests that need a logged in user. To specify user and password and tell Playwright to write new HAR files, we execute ng e2e with three environment variables:
WRITE_HAR=1 TEST_EIAM_USERNAME=[some user] TEST_EIAM_PASSWORD=[some password] ng e2e
Keep in mind that you need to replace [some user] and [some password] with a valid test user. Any credentials passed here are automatically redacted from the HAR files afterwards. Additionally, we use a mocked login page when HAR files are used, since we can't 100% replicate the backend flow.
Debugging
To enable "debug mode", do the following steps in order:
- Go to playwright.config.ts and comment in the debugging part (i.e. everything below
// DEBUGGING) - Execute
PWDEBUG=1 ng e2e
This will now start one window per specified .spec.ts file. An additional window is Playwrights own debugger window. From there, you can follow the official docs. If you want to execute a single specific test, you can adjust pass the --files parameter.
If you also want to capture browser console out, you can use the CAPTURE_CONSOLE env var: CAPTURE_CONSOLE=1 ng e2e.
Further help
To get more help on the Angular CLI use ng help or go check out
the Angular CLI Overview and Command Reference page.
Docker
Building the image
Before the first start create volume for log files. It has to be done only once
docker volume create nginx-logs
The docker image has to be built for each environment separately, since we cannot use runtime environment configurations.
In order to build the docker image use the following command (adjust tag as needed):
docker build --no-cache --build-arg TARGET_ENVIRONMENT={target_environment} -t gb3-frontend:latest .
- gb3-frontend is the name of the image
- latest is the tag used to mark the version of this image
- target_environment is the target build environment, which is one of the following:
local: localhost developmentlocal-gb2: localhost development with locally deployed GB2 backenddev-ebp: production deployment for EBP environmentstaging: production deployment for KTZH staging environmentstaging-using-productive-gb2-backend: production deployment for KTZH staging environment which uses the productive GB2 backend infrastructure.uat: production deployment for KTZH UAT environmentproduction: default if this variable is missing; production deployment for KTZH production (internet & intranet) environment
- APP_VERSION: see below (optional)
- APP_RELEASE: see below (optional)
The target_environment is used to create environment specific build outputs so as to not divulge sensitive information
such as internal domains. This is mainly reflected in the runtime configuration mechanism described below.
Overriding app version and release number
During build, the Dockerfile will run npm run update-version. Per default, it tries to extract the last git commit
hash as app version number and the last tag as release number. This only works if the build command is run within the
context of a repository-checkout; if it fails, it will display UNKNOWN_VERSION.
If you're building the image outside of a repository context (i.e. within a pipeline), you can specify the version and release number explicitly using the following build-args:
APP_VERSION: The version number, usually the last git commit hash in--shortformAPP_RELEASE: The release number, usually in the form of "Release-xx"
An example command would look like this:
docker build --no-cache --build-arg APP_VERSION=MyCustomAppVersion --build-arg APP_RELEASE=MyCustomRelease -t gb3-frontend:latest .
Irrespective of how the versions are extracted, they overwrite the /src/version.ts file, which is in turn exposed via
Angular's environment configs.
Run the image
This image exposes port 8080 and can be run like this:
docker run -p 80:8080 -v nginx-logs:/var/log/nginx --name gb3-frontend gb3-frontend:latest
- 80:8080 maps the internal port 8080 to the external port 80; the later can be chosen freely
- gb3-frontend is the name of the image
- latest is the version tag for this image
- -v nginxlogs:/var/log/nginx volume folder with log files for filebeat
Naming conventions
Generally, we are orientating ourselves at the default Typescript naming conventions.
WIP - add more naming conventions :)
Branchname and commit message
Whenever possible, a Jira ticket should be referenced in both branchname and commit message:
- Branches:
[feature|hotfix|bugfix]/[gb3|ghub]-[xxx]-[name-of-branch], wherexxxrefers to a Jira ticket and thename-of-branchis a short summary of the feature/hotfix/bugfix.` - Commits:
[GB3|GHUB]-[xxx]: Your commit message, , wherexxxrefers to a Jira ticket
Our githooks check for both the branch name and the commit message. They will output a warning and reject the commmit if they don't
match. In case you might want to deviate from these rules having no Jira ticket, use answer-to-life Jira ticket GHUB-42.
ES Lint is enabled in the precommit hook. This means that every commit will be checked for linting errors. If there are any, the commit will be rejected.
Either fix the error (unused variable or import, etc.) or disable the rule for the specific line or file (any in .spec.ts files).
In case of the latter, consider adding a comment explaining why the rule was disabled. Example for such a case:
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Allow "any" in test file
const someVariable = {} as any;
The rules are defined in the .eslintrc.config.mjs file.
Code documentation
- The
ActiveMapItemclass- Spatial Reference System(s)
- State
- Runtime configurations
- (S)CSS structure
- Custom icons
- Transformation from GB2 backend API to GB3 interfaces
- Error handling
- Application Initialization based on share link
- Adding new NPM packages
- Feature flags
- Handling of date objects
- Injection tokens
The ActiveMapItem class
The heart of the application is the ActiveMapItem class. All data the user can add to the map (and reorder, toggle
visibility, interact with in terms of settings, etc.) is an instance of the abstract ActiveMapItem class.
All ActiveMapItems are available in the ActiveMapItemState. In order for the map to correctly render the state,
the MapService implementation has to be synchronized with the state: If e.g. an item is added, the MapService has to
handle this accordingly: Create a new framework-dependent instance of the layer, add it to the map, handle ordering,
etc.
Usage
The ActiveMapItem is an abstract class; the actual implementation is delegated to subclasses within
the implementations subfolder, representing different
types of layers that can be added to a map, such as Gb2WmsActiveMapItem. Since configuration for these layers differs,
the ActiveMapItem has a property settings which is a discriminated union type ActiveMapItemSettings,
holding all layer settings. This allows for a flexible combination of ActiveMapItems and their settings.
As a variation of the visitor pattern, the ActiveMapItem also has
an abstract method addToMap which the subclasses need to imlement - this method is responsible for adding a given
instance of ActiveMapItem to the map by using the appropriate method on the AddToMapVisitor interface.
In order to avoid the Array.filter(m => m instanceof x).map(m => m as x) pattern, the isActiveMapItemOfType
typeguard can be used: Array.filter(isActiveMapItemOfType(x)).
Selectors
To get a list of all currently active map items, the selectItems selector from the class active-map-item.selector.ts can be used. This selector returns all non-temporary ActiveMapItems.
If you want to get all active map items including the temporary items, you can use the selectAllItems selector instead.
Spatial Reference System(s)
Because we're using different datasources, we cannot always determin what SRS our GeoJSON objects have. In order to
specify the SRS of a given GeoJSON object, use the wrapper classes defined
in src/app/shared/interfaces/geojson-types-with-srs.interface.ts which also specify the SRS.
All supported SRS in our app are defined as SupportedSrs type.
Using these helper interfaces and types allows us to properly leverage Esri's built-in transformation services without
relying on the implicit conversion of coordinates. As such, whenever possible, we should not rely on the geojson
package itself, but rather on its wrapper classes.
State
All application-wide state is handled by NGRX.
TODO: Explain our state in detail :)
Mutating nested state
As of now, we still have (deeply) nested state and for as long as we do not normalize our state, this will pose some challenges, namely that mutating the state in reducers requires a deep copy of the current state object. Luckily, there's a nifty package called immer that helps working with mutable objects. All it does is leverage Proxy objects to create "clever" deep copies. Inside its lifecycle, you can mutate the object as if it were mutable, yet it becomes immutable once it is returned. It also is more performant than e.g. structuredClone because it keeps track of what has changed and only changes (deeply) nested objects if they actually did change.
Apart from performance considerations (which could be further optimized by restructuring our state and/or our reducers),
the main reason for introducting immer was that our typings broke with rxjs > 7.8.x in that using structuredClone
broke type recognition in Angular's compiler, leading to an inconsistent state between IDE and tooling. So it was either
using a proper library (as recommended by ngrx (!)) or adding xxx as xyz typecasts after each structuredClone.
Example
Let's take our ActiveMapItemState as an example, which is the state that is most deeply nested:
export interface ActiveMapItemState {
activeMapItems: ActiveMapItem[];
}
A simple reducer for updating the visibility on a given ActiveMapItem would look like this:
on(ActiveMapItemActions.setVisibility, (state, {visible, activeMapItem}): ActiveMapItemState => {
const activeMapItems = state.activeMapItems.map((mapItem) => {
if (mapItem.id === activeMapItem.id) {
const newActiveMapItem = structuredClone(mapItem);
newActiveMapItem.visible = visible;
return newActiveMapItem;
}
return mapItem;
});
return {...state, activeMapItems: [...activeMapItems]};
});
Not only does it copy the whole object, it also requires a verbose Array.map operation. With immer, we can inject
its produce function directly in the on() parameters, like so:
on(
ActiveMapItemActions.setVisibility,
produce((draft, {visible, activeMapItem}) => {
draft.activeMapItems.forEach((mapItem) => {
if (mapItem.id === activeMapItem.id) {
mapItem.visible = visible;
}
});
}),
);
This yields the same result as the first example, but it is much more readable and more in a functional programming
style. All it does is creating a proxy object of our state (clarified by using draft and not state), which can then
be directly modified using Array.forEach - and then,
the result is returned automatically.
Using immer with ES6 classes
The above workflow works for all basic Object types as well as interfaces. As soon as ES6 classes are involved, they
need to be marked as such. For convenience, we have a IsImmerable
interface available that encapsulates this behaviour. Note that if you fail to do so, you will
get Cannot assign to read only property xxxx of object errors, because these class instances are not immerized.
Conventions
Some conventions exist and should be adhered to when dealing with immutable state mutations:
- Always use the
immerapproach - do not use things likelodashorstructuredClone. - In reducers, always try to add the
producecall at the top-most level to avoid nasty sideeffects. - Most likely, you can add the call at the
onparameter level as in the example above.- If so, name the state variable
draftto signal to the user that this is mutable.
- If so, name the state variable
- Only use
immerwhen it is actually needed - if you don't modify deeply nested states, you're most likely not going to need it.
Runtime configurations
The app supports multiple environments with different endpoints. Because the production deployment has different endpoints depending on whether it is access via internet or intranet, these URLs need to be added during runtime, so Angular's environment files do not work.
As a workaround, the ConfigService can be used. This service will check the current hostname and return the given API
configurations. The replacements are done (similar to the environment configurations) as part of the angular.json
build file replacements.
The configurations are found in src/app/shared/configs/runtime.config.ts and configured via their environment
replacement files.
API config types
Within the runtime configurations there are API configurations used to configure any endpoint URLs. There are currently three types of API runtime configurations:
ApiBaseUrlConfig
The most basic one. Only contains one property: thebaseUrl.MockedApiBaseUrlConfig
A relict from older times where the APIs weren't as stable and we regularly needed mock data. This is a child class fromApiBaseUrlConfigand has one more flaguseMockDatato indicate whether to use mock data instead of the real service.VersionedApiBaseUrlConfig
Some of the backend APIs have changing versions to separate breaking changes. This child class fromApiBaseUrlConfigcontains a propertyversionwhere this comes into play. Each environment can use its own versioned API endpoints.
Available URL configurations
| Stagename | Subdomain | Verwendung | GB2 Backend | WMS Backend | Geolion | Bemerkung |
|---|---|---|---|---|---|---|
| DEV | dev.geo.zh.ch | EBP | ||||
| PROD | geo.zh.ch | öffentlich | maps.zh.ch | wms.zh.ch | geolion.zh.ch | |
| PROD | geo.ktzh.ch | Verwaltung | web.maps.zh.ch | web.wms.zh.ch | geolion.ktzh.ch | |
| UAT | uat.geo.ktzh.ch | Verwaltungsinterne Tests | uatmaps.kt.ktzh.ch | uatwms.kt.ktzh.ch | uatgeolion.kt.ktzh.ch | |
| STAGING | staging.geo.ktzh.ch | Produktionsvorbereitung | testmaps.kt.ktzh.ch | testwms.kt.ktzh.ch | testgeolion.kt.ktzh.ch |
(S)CSS structure
BEM - structured CSS
We are using BEM to structure our (S)CSS: https://getbem.com/introduction/
Basically there are three important elements to keep track of:
- blocks
Standalone entity that is meaningful on its own.
Example:active-map-item-header
How to use:active-map-item-header(no change) - Element
A part of a block that has no standalone meaning and is semantically tied to its block.
Example:header-title
How to use:active-map-item-header__title(connect to a block using two underscores) - Modifier
A flag on a block or element. Use them to change appearance or behavior.
Example:disabled
How to use:active-map-item-header--disabled(connect to a block or element using two dashes)
Global functions / variables / mixins / overrides
Each component is responsible for its own styling. However, to prevent too much code duplications we have some global
helper files in our \styles folder:
- functions/... contains some helper functions to calculate e.g. the RGBA value of a hex value.
- mixins/... contains mixin files divided into categories used to style specific sections of the application. These are the styles that can be shared between different components.
- overrides/... contains a couple of style files used to globally override certain elements. Use with caution.
- variables/_ktzh-design-variables.scss contains all important variables used within the GB3 application. Most notable the color palettes that are used everywhere. Try to avoid hard-coded color values inside some local SCSS file.
- variables/_z-index-variables.scss contains all z-indices ordered by the highest value first. This is used to keep track of which element should be on top of which element in one place.
- _headings.scss contains all heading styles used in the application. This is used to keep track of all heading styles in one place. They are based on the design system of the Canton of Zurich (see Figma).
To use those global styles within a local SCSS file use the following syntax (or part of it):
@use 'functions/helper.function' as functions;
@use 'mixins/helpers.mixin' as mixins;
@use 'mixins/material.mixin' as mat-mixins;
@use 'variables/ktzh-design-variables' as ktzh-variables;
@use 'variables/z-index-variables' as z-index-variables;
Example of a potential usage:
.button {
background-color: functions.get-color-from-palette('secondary-accent');
}
The get-color-from-palette function optionally accepts a hue value. A list of all available palettes:
'primary''primary-light''secondary-accent''accent''warn'
The setup itself distinguishes between intranet and internet and switches the colours automatically.
Custom icons
All custom icons are handled via the IconsService and the iconsConfig. Add the identifier and the path to the URL (
relative or absolute) and the service adds the icons. Use them as follows:
<mat-icon svgIcon="icon_id_from_config"></mat-icon>
In order to have the SVG adjust itself to the color (e.g. disabled state), replace all fill="color" occurrences in the
SVG which should be assigned the font color with fill="currentColor". In some cases, the color might also be
within stroke or other attributes.
Transformation from GB2 backend API to GB3 interfaces
To separate the GB2 API interfaces from the internal ones we use transformation methods inside the corresponding
GB3-services. Usually it's used to adjust some minor issues like naming (gb2_url => gb2Url).
There are a few exceptions where more logic is used to transform values:
Topics endpoint
There are many mayor changes during the transformation of the topics.json from the GB2 API.
First of all the naming is different:
| GB2 API interface name | GB3 interface name | Example |
|---|---|---|
| category | topic | Luft und Klima |
| topic | map | Lichtemissionen |
| layer | layer | August 2018 |
Another noticeable change is the order of layers. WMS 1.3 describes the order as follows:
A WMS shall render the requested layers by drawing the leftmost in the list bottommost, the next one over that, and so on.
Meaning that the layer with the lowest index has the lowest visibility. However, in GB3 the order is inverted to that as the item with the lowest index has the highest visibility. Therefore, the order of the GB2 API layers get inverted to tackle that problem.
Error handling
The global error handler is located in the error-handling module. All errors are caught by Angular and delegated to
this handler which then handles the errors according to their type. Additionally, while in develop mode, the errors are
logged to the console.
The application itself defines errors that extend the native Error object, which allows for easier
handling and runtime error checks. These abstract errors are defined within app/shared/errors/abstract.errors.ts. The
abstract base class of all custom errors is Gb3RuntimeError. It defines an (optional) property originalError
of unknown type which can be used to wrap any caught error. The error handler will then check whether this property is
set and log the original message as well.
All errors should extend from the following abstract classes, extending Gb3RuntimeError. They have different behaviour
in the error handler:
FatalError: This error will raise an error that prevents the current screen from being used by redirecting to our fatal error page.RecoverableError: This error will pop an error notification, but will not prevent the app from being used.SilentError: This error will do nothing. Useful for errors that should not be communicated to the user.
Of course, all other errors that might be thrown in the code and that are not caught (e.g.
simple throw new Error('Fail!')) will be handled as well; and currently, they are treated as FatalError because we
cannot reliably determin whether an error is critical or not.
Implementing custom error classes
Implementing a custom error class is as simple as extending from one of the mentioned base classes. In most cases, you
should add a public override message: string = 'Your Error Message' to the class, and you can, of course, add custom
logic.
If no constructor is specified, the constructor of Gb3RuntimeError is taken that can be supplied (optionally) with
the actually thrown error.
Handling errors
In general, throwing an error is straight-forward: Just throw it.
In practice, there are situations where this is not as simple: In situations where we have an API call within an effect
and also use a loadingState, we cannot directly use catchError in the service API call's pipe chain, because this
would only show the error message without updating the loading state. For these cases, you should add a
dedicated setError action which sets the loading state through the reducer, and then have another effect that listens
to this action and then raises the appropriate error. In order to also have the originally thrown error, the
helper errorProps() can be used as ActionProp so you can pass along the original error for usage within the effect.
For examples of this, see e.g. the LegendEffect.
Importantly, if you throw exceptions within the constructor of a service, make sure to inject the ErrorHandler
interface and throw it explicitly using the handleError method. Otherwise, depending on the order of Angular's DI, the
error handler might not yet be registered and throw the exception outside of the Angular error handler.
Map page Initialization
When the /maps page is opened without any further parameters, the initial center and scale of the map is calculated based on the viewport.
This is required because all the UI-Elements are placed above the map page and can potentially cover parts of the Canton of Zurich.
Thus, the map cannot be centered on the entire page but its center needs to calculated dynamically.
For this, a number of constant are defined in the map-page.constants.ts file.
This includes the bounding box of the Canton of Zurich and the paddings to make sure nothing is covered by the UI-Elements.
There are different constants for regular and tablet view and mobile view. These constants describe the number of pixels the center needs to be shifted in each direction.
These numbers are derived from the UI-Elements and need to be updated whenever any of these elements are changed.
The following list explains how each value is derived and on which value it is based:
-
For Regular and Tablet View:
- Left (474px):
$map-overlay-width(450px) &$map-overlay-width-adjustment(12px) (_map-layout-variables.scss)- standard padding (12px, not globally defined)
- Right (180px):
.basemap-selector__active-class (width: 96px,basemap-selector.component.scss)--mdc-icon-button-state-layer-size(48px, Angular Material).map-controls-class (gap: 12px,map-controls.component.scss).map-container__map-controls-class (right: 12px,map-page.component.scss)- standard padding (12px, not globally defined)
- Top (88px):
- Height of the search-bar (64px/100%,
search-bar.component.scss) .map-container__search-window-class (top: 12px,map-page.component.scss)- standard padding (12px, not globally defined)
- Height of the search-bar (64px/100%,
- Bottom (88px):
.coordinate-scale-inputs-class (48px: height: 42px + padding 3px (top and bottom),coordinate-scale-inputs.component.scss).map-controls__inputs-class (gap: 12px,map-controls.component.scss).map-container__map-controls-class (bottom: 28px,map-page.component.scss)
- Left (474px):
-
For Mobile View:
- Left (12px):
- standard padding (12px, not globally defined)
- Right (12px):
- standard padding (12px, not globally defined)
- Top (84px):
@mixin tool-button-mobile(60px_helpers.mixin.scss).map-container__map-tools--mobile-class (top: 12px,map-page.component.scss)- standard padding (12px, not globally defined)
- Bottom (100px):
search-window__searchbar--mobile-class (height: 60px,search-window.component.scss).map-container__search-window--mobile-class (bottom: 28px,map-page.component.scss)- standard padding (12px, not globally defined)
- Left (12px):
Additionally, there is the NAV_BAR_HEIGHT-Constant in the map.constant.ts file (72px).
This needs to be changed if $navbar-height is changed in the _map-layout-variables.scss file.
Application Initialization based on share link
Loading and initializing of the application based on a previously shared link ID is completely done within
the share-link.state.
There is the whole initializeApplication and validation part where the application gets initialized.
This part is basically a big state machine used to control the initialization flow. It's taking care of all potential
side effects like
invalid share link item contents or topics that are not getting loaded.
The basic flow based on actions and effects goes like this:
┌─────────────────────────────────────────────────┐
│ ShareLinkActions.initializeApplicationBasedOnId │
└─────────────────────┬───────────────────────────┘
waitForAuthenticationStatusToBeLoaded$ │
┌─────────────────────▼──────────────────────────────────┐
│ ShareLinkActions.completedAuthenticationInitialization │
└───────┬─────────────┬─────────────────┬────────────────┘
initializeApplicationByLoadingShareLinkItem$ │ │ │ initializeApplicationByLoadingTopics$
┌───────────────▼───────────┐ │ ┌───────────────▼──────────────────────┐
│ ShareLinkActions.loadItem │ │ │ LayerCatalogActions.loadLayerCatalog │
└───────────────┬───────────┘ │ └───────────────┬──────────────────────┘
└───────────► │◄────────────────┘
initializeApplicationByVerifyingSharedItem$ │
┌────────────────▼──────────────┐
│ ShareLinkActions.validateItem │
└────────────────┬──────────────┘
validateShareLinkItem$ │
┌───────────────────▼─────────────────┐
│ ShareLinkActions.completeValidation │
└──────┬────────────┬──────────┬──────┘
setMapConfigAfterValidation$ │ │ │ setActiveMapItemsAfterValidation$
┌───────────────────────────▼──────────┐ │ ┌────────▼────────────────────────────────┐
│ MapConfigActions.setInitialMapConfig │ │ │ ActiveMapItemActions.addInitialMapItems │
└──────────────────────────────────────┘ │ └────────┬────────────────────────────────┘
│ ◄────────┘
completeInitialization$ │
┌─────────────────────────▼──────────────────────────┐
│ ShareLinkActions.completeApplicationInitialization │
└────────────────────────────────────────────────────┘
Note that error actions/effects are not visible on this diagram
Adding new NPM packages
Usually there are two ways to add new NPM packages. Either calling
npm install <package> --save
To install the package and add it to thepackage.json(andpackage-lock.json) in the section dependencies.npm install <package> --save-dev
To install the package and add it to thepackage.json(andpackage-lock.json) in the section devDependencies.
The later (devDependencies) is usually reserved for all dependencies that are not necessary to run the code in a productive environment (e.g. a unit test framework like Jasmine). And usually also stuff like @angular/compiler or @types/....
However, this code gets build inside a docker container using the command npm ci --ignore-scripts --omit=dev. --omit=dev ignores all packages in the devDependencies section. Afterward, a build is triggered. This build of course needs build tools like @angular/compiler which are not available if they're not located inside the dependencies section.
This has the consequence that a lot of packages are located within the dependencies section that would usually be in the devDepencies section.
To test if a new package has to be added to the dependencies or the devDependencies section it's easy to test by running the following commands:
npm ci --ignore-scripts --omit=dev
npm run build-production
Feature flags
Feature flags can be used to toggle features throughout the application. They work as follows:
- Available featureflags are defined in the
FeatureFlagsinterface. - Default values for all features flags need to be defined in the
feature-flags.config.tsfile. - Each runtime configuration might specify overrides for the default values; they are then injected via the
ConfigService. - The
FeatureFlagsServiceand itsgetFeatureFlagmethod is used to access the feature flags. - For convenience, the
FeatureFlagDirectivecan be used to toggle elements based on a feature flag.
Handling of date objects
Currently, we are using dayjs to handle date objects. In order to have a high degree of abstraction and to be able to easily replace the library (i.e. using native Javascript features like Intl),
all date handlings are done via the TimeService interface, which is implemented as e.g. the DayjsService. Currently, the actual implementation is injected via the timeServiceFactory; and as a convenience, this is also
added in test.ts so it does not have to be provided for each and every test.
Injection tokens
Starting with Angular 20, using inject() is preferred over constructor-based injection. This also simplifies the management of abstract classes, since super calls are no longer necessary, because the injection can be fully defined in the parent (abstract) class.
However, moving injection to the parents had the important consequence that tokens can lead to circular dependencies because all tokens were defined in app.module.ts. This happens because a concrete implementation inherits from an abstract representation, which loaded the token from app.module.ts - because we're now having an early instantation, this led to a circular dependency, because the token itself triggers an app module load which in turn triggers the service instantiation again.
The solution was to move all tokens to a dedicated app.tokens.ts file, where they are loaded by both the app module as well as the abstract classes, thus removing the circular dependency.
Git conventions
Branching strategy
Our repository is mainly using the standard Git flow branching model.
There are the following branches:
- main
The production branch. Every commit has to be stable and tested as it is used as released code. Therefore, every commit that was released is marked by a tagrelease-XXXwhereXXXis the release number. It's entirely possible to have multiple release tags on the same commit if this repository wasn't updated since the last releases. - develop
The main development branch. Every commit has to be stable as it will be deployed automatically to the dev-environment server. - feature/* , bugfix/*
Individual feature/bugfix branches. They don't have to be stable as they are connected to one person working on it. They are based on the develop branch and finally get merged into that branch again.
ℹ See also naming conventions for branch naming and commit message format: Branchname and commit message - hotfix/*
This is reserved to fix bugs that occur in a productive environment and need to be fixed ASAP. They're based on the main branch (and not the develop) because it is entirely possible that there are already new features on the develop branch that should not be released. As soon as this branch is finished it needs to be merged back to main (via PR). After that it's very important to create a second PR to merge this branch into the develop branch as well.
Naming conventions
Release management
This process describes how to tag a release so the team at ARE-GEO-GIS can release the correct version of the application. A more detailed internal version can be found here.
- Create a new PR called
Releasefromdevelopintomainfor all repos that have had changes since the last release. - After merging, create a new tag for the release. This can be done by clicking on
Releaseson the overview page on GitHub and then clicking onDraft a new release. - Under
Choose a tagcreate a new tag on publish with the formatRelease-XXwhereXXis the release number. This number should be incremented by 1 from the last release. - Set the target to
main - Press
Generate release notesto automatically generate the release notes based on the PRs that have been merged since the last release. Clean up if needed. - Click on
Publish releaseto create the tag, set it as the latest release and publish the release notes. - In Teams, create a new announcement with the release notes and mention which tag is relevant for which repo.
Contributors
The project was developed for the Amt für Raumentwicklung - Abteilung Geoinformation of the Canton of Zurich.
It has been initially developed by EBP Schweiz AG as a closed-source project and has been made open-source in 2024 after the first production release. EBP is still actively contributing and maintaining the project.
Individual contributors
The following people have contributed to this project: