Skip to content

Commit

Permalink
Merge pull request #9 from gisktzh/develop
Browse files Browse the repository at this point in the history
Release
  • Loading branch information
TIL-EBP authored Oct 16, 2024
2 parents e77f556 + e6e730d commit bd6c64b
Show file tree
Hide file tree
Showing 70 changed files with 1,846 additions and 1,336 deletions.
1 change: 1 addition & 0 deletions .github/workflows/node-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ jobs:
run: |
npm ci
npm run build-dev-ebp
cp ./dist/browser/index.html ./dist/browser/404.html
- name: Create CNAME file for custom domain
run: echo 'dev.geo.zh.ch' > ./dist/browser/CNAME
Expand Down
Binary file added .readme/are.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .readme/ebp.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 3 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,6 @@ COPY ./.docker/nginx.conf /etc/nginx/nginx.conf

COPY --from=build-app /app/dist/browser /usr/share/nginx/html

ENV PORT 8080
EXPOSE 8080
CMD sh -c "rm -f /var/log/nginx/* && envsubst '\$PORT' < /etc/nginx/conf.d/configfile.template > /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;'"
ENV PORT=8080
EXPOSE $PORT
CMD ["sh", "-c", "rm -f /var/log/nginx/* && envsubst '\\$PORT' < /etc/nginx/conf.d/configfile.template > /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;'"]
46 changes: 19 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ This project was generated with [Angular CLI](https://github.com/angular/angular
> 6. [Code documentation](#code-documentation)
> 7. [Git conventions](#git-conventions)
> 8. [Release management](#release-management)
> 9. [Contributors](#contributors)
## Node version

Expand Down Expand Up @@ -170,6 +171,7 @@ match. This is because there are times when you _might_ want to deviate from the
> 9. [Application Initialization based on share link](#application-initialization-based-on-share-link)
> 10. [Adding new NPM packages](#adding-new-npm-packages)
> 11. [Feature flags](#feature-flags)
> 12. [Handling of date objects](#handling-of-date-objects)
### The `ActiveMapItem` class

Expand Down Expand Up @@ -600,6 +602,12 @@ Feature flags can be used to toggle features throughout the application. They wo
- The `FeatureFlagsService` and its `getFeatureFlag` method is used to access the feature flags.
- For convenience, the `FeatureFlagDirective` can be used to toggle elements based on a feature flag.

### Handling of date objects

Currently, we are using [dayjs](https://day.js.org/) 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.

## Git conventions

### Branching strategy
Expand All @@ -626,38 +634,22 @@ There are the following branches:
## Release management

This section gives a brief overview of the release process and how to publish it.

### Azure Devops

#### 1. Create a changelog file anywhere

It will be used later within a Teams announcement.
This is still WIP after our move from Azure DevOps to GitHub.

#### 2. Frontend
## Contributors

1. Create a pull request in `gb3-frontend` from `develop` to `main` with the title **_Release_**
2. Click on **Add commit message** and remove any line that does **not** start with `Merged PR XXXXX:` \
This has the effect that only a few lines remain containing a good summary of the release.
3. Remove the `Merged PR XXXXX:` from the beginning of each line - only the PR titles should remain. Add them to the changelog.
4. Finish creating the PR by clicking on **Create** - no need to add any reviewers.
5. Wait until the required checks succeed and then merge the PR.
<img src="./.readme/are.png" width="250" />

#### 3. All other repositories
The project was developed for the [Amt für Raumentwicklung - Abteilung Geoinformation](https://gis.zh.ch) of the Canton of Zurich.

1. Check if all required features are merged back to `main`
2. Copy all PR titles and add them to the changelog.
<img src="./.readme/ebp.png" width="150"/>

#### 4. Create a release using the release pipeline '_Code Mirroring_'
It has been initially developed by [EBP Schweiz AG](https://ebp.ch) 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.

1. Click on **Create release**
2. Add the changelog from above to the `Release description` field.
3. Start the release by clicking **Create**
### Individual contributors

#### 5. Publish the changelog
The following people have contributed to this project:

1. Open Teams and go to the GB3 **Allgemein** channel
2. Click on **Einen Beitrag starten**
3. Change the type of article by choosing **Ankündigung** (left of **Veröffentlichen**)
4. Enter the release title as _Überschrift_. E.g. `Release 42`
5. Add the changelog from above as text and slightly format it. Use previous changelogs as styleguide.
<a href="https://github.com/gisktzh/gb3-web_ui/graphs/contributors">
<img src="https://contrib.rocks/image?repo=gisktzh/gb3-web_ui" />
</a>
4 changes: 2 additions & 2 deletions sonar-project.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
sonar.projectKey=222262_GB3_frontend
sonar.projectName=222262_GB3_frontend
sonar.projectKey=gisktzh_gb3-web_ui
sonar.organization=gisktzh

# root path which is relative to the sonar-project.properties file location.
sonar.sources=.
Expand Down
4 changes: 4 additions & 0 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ import {EsriMapLoaderService} from './map/services/esri-services/esri-map-loader
import {MapLoaderService} from './map/interfaces/map-loader.service';
import {DevModeBannerComponent} from './shared/components/dev-mode-banner/dev-mode-banner.component';
import {SkipLinkComponent} from './shared/components/skip-link/skip-link.component';
import {TimeService} from './shared/interfaces/time-service.interface';
import {timeServiceFactory} from './shared/factories/time-service.factory';

// necessary for the locale 'de-CH' to work
// see https://stackoverflow.com/questions/46419026/missing-locale-data-for-the-locale-xxx-with-angular
Expand All @@ -41,6 +43,7 @@ export const MAP_SERVICE = new InjectionToken<MapService>('MapService');
export const MAP_LOADER_SERVICE = new InjectionToken<MapLoaderService>('MapLoaderService');
export const NEWS_SERVICE = new InjectionToken<NewsService>('NewsService');
export const GRAV_CMS_SERVICE = new InjectionToken<GravCmsService>('GravCmsService');
export const TIME_SERVICE = new InjectionToken<TimeService>('TimeService');

@NgModule({
declarations: [AppComponent],
Expand All @@ -62,6 +65,7 @@ export const GRAV_CMS_SERVICE = new InjectionToken<GravCmsService>('GravCmsServi
{provide: ErrorHandler, deps: [Router, ErrorHandlerService, EmbeddedErrorHandlerService], useFactory: errorHandlerServiceFactory},
{provide: MAP_SERVICE, useClass: EsriMapService},
{provide: MAP_LOADER_SERVICE, useClass: EsriMapLoaderService},
{provide: TIME_SERVICE, useFactory: timeServiceFactory},
{provide: NEWS_SERVICE, deps: [KTZHNewsService, KTZHNewsServiceMock, ConfigService], useFactory: newsFactory},
{provide: GRAV_CMS_SERVICE, deps: [GravCmsService, GravCmsServiceMock, ConfigService], useFactory: gravCmsFactory},
{provide: LOCALE_ID, useValue: 'de-CH'},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<span class="data-display__item__value">
@switch (element.type) {
@case ('text') {
<span [innerHTML]="element.value ?? '-'"></span>
<span [innerHTML]="element.value | textOrPlaceholder"></span>
}
@case ('url') {
@if (element.value) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import {Component, Input} from '@angular/core';
import {DataDisplayElement} from '../../types/data-display-element.type';
import {NgForOf} from '@angular/common';
import {TextOrPlaceholderPipe} from '../../../shared/pipes/text-or-placeholder.pipe';

@Component({
selector: 'data-display',
standalone: true,
imports: [NgForOf],
imports: [NgForOf, TextOrPlaceholderPipe],
templateUrl: './data-display.component.html',
styleUrls: ['./data-display.component.scss'],
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@
</tr>
@for (attribute of attributes; track attribute.name) {
<tr class="dataset-element-table__table__row">
<td class="dataset-element-table__table__row__column">{{ attribute.name }}</td>
<td class="dataset-element-table__table__row__column" [innerHTML]="attribute.description | formatLineBreaks"></td>
<td class="dataset-element-table__table__row__column">{{ attribute.type }}</td>
<td class="dataset-element-table__table__row__column">{{ attribute.unit }}</td>
<td class="dataset-element-table__table__row__column">{{ attribute.name | textOrPlaceholder }}</td>
<td
class="dataset-element-table__table__row__column"
[innerHTML]="attribute.description | textOrPlaceholder | formatLineBreaks"
></td>
<td class="dataset-element-table__table__row__column">{{ attribute.type | textOrPlaceholder }}</td>
<td class="dataset-element-table__table__row__column">{{ attribute.unit | textOrPlaceholder }}</td>
</tr>
}
</table>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import {Component, Input} from '@angular/core';
import {LayerAttributes} from '../../../../shared/interfaces/layer-attributes.interface';
import {NgForOf} from '@angular/common';
import {FormatLineBreaksPipe} from '../../../../shared/pipes/format-line-breaks.pipe';
import {TextOrPlaceholderPipe} from '../../../../shared/pipes/text-or-placeholder.pipe';

@Component({
selector: 'dataset-element-table',
standalone: true,
imports: [NgForOf, FormatLineBreaksPipe],
imports: [NgForOf, FormatLineBreaksPipe, TextOrPlaceholderPipe],
templateUrl: './dataset-element-table.component.html',
styleUrl: './dataset-element-table.component.scss',
})
Expand Down
4 changes: 2 additions & 2 deletions src/app/data-catalogue/utils/data-extraction.utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ describe('DataExtractionUtils', () => {
zipCode: 2222,
poBox: null,
section: null,
url: 'https://www.example.com',
url: {href: 'https://www.example.com'},
};

const result = DataExtractionUtils.extractContactElements(mockContact);
Expand All @@ -36,7 +36,7 @@ describe('DataExtractionUtils', () => {
{title: 'Tel', value: mockContact.phone, type: 'text'},
{title: 'Tel direkt', value: mockContact.phoneDirect, type: 'text'},
{title: 'E-Mail', value: mockContact.email, type: 'url'},
{title: 'www', value: {href: mockContact.url}, type: 'url'},
{title: 'www', value: mockContact.url, type: 'url'},
];
expect(result).toEqual(expected);
});
Expand Down
2 changes: 1 addition & 1 deletion src/app/data-catalogue/utils/data-extraction.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export class DataExtractionUtils {
{title: 'Tel', value: contact.phone, type: 'text'},
{title: 'Tel direkt', value: contact.phoneDirect, type: 'text'},
{title: 'E-Mail', value: contact.email, type: 'url'},
{title: 'www', value: {href: contact.url}, type: 'url'},
{title: 'www', value: contact.url, type: 'url'},
];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<data-input>
<input
(change)="setMapCenterAndDrawHighlight($event)"
(keydown.enter)="setMapCenterAndDrawHighlight($event)"
[(ngModel)]="mapCenterInput"
class="coordinate-scale-inputs__input"
type="text"
Expand Down
60 changes: 26 additions & 34 deletions src/app/map/components/time-slider/time-slider.component.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
import {Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges} from '@angular/core';
import {Component, EventEmitter, Inject, Input, OnChanges, OnInit, Output, SimpleChanges} from '@angular/core';
import {TimeExtent} from '../../interfaces/time-extent.interface';
import {TimeSliderConfiguration, TimeSliderLayerSource} from '../../../shared/interfaces/topic.interface';
import dayjs, {ManipulateType} from 'dayjs';
import {TimeSliderService} from '../../services/time-slider.service';
import {TimeExtentUtils} from '../../../shared/utils/time-extent.utils';
import duration from 'dayjs/plugin/duration';
import {MatDatepicker} from '@angular/material/datepicker';

dayjs.extend(duration);
import {TIME_SERVICE} from '../../../app.module';
import {TimeService} from '../../../shared/interfaces/time-service.interface';
import {DateUnit} from '../../../shared/types/date-unit.type';

// There is an array (`allowedDatePickerManipulationUnits`) and a new union type (`DatePickerManipulationUnits`) for two reasons:
// To be able to extract a union type subset of `ManipulateType` AND to have an array used to check if a given value is in said union type.
// => more infos: https://stackoverflow.com/questions/50085494/how-to-check-if-a-given-value-is-in-a-union-type-array
const allowedDatePickerManipulationUnits = ['years', 'months', 'days'] as const; // TS3.4 syntax
type DatePickerManipulationUnits = Extract<ManipulateType, (typeof allowedDatePickerManipulationUnits)[number]>;
type DatePickerManipulationUnits = Extract<DateUnit, (typeof allowedDatePickerManipulationUnits)[number]>;
type DatePickerStartView = 'month' | 'year' | 'multi-year';

@Component({
Expand Down Expand Up @@ -47,7 +45,10 @@ export class TimeSliderComponent implements OnInit, OnChanges {
public datePickerStartView: DatePickerStartView = 'month';
private datePickerUnit: DatePickerManipulationUnits = 'days';

constructor(private readonly timeSliderService: TimeSliderService) {}
constructor(
private readonly timeSliderService: TimeSliderService,
@Inject(TIME_SERVICE) private readonly timeService: TimeService,
) {}

public ngOnInit() {
this.availableDates = this.timeSliderService.createStops(this.timeSliderConfiguration);
Expand All @@ -57,7 +58,7 @@ export class TimeSliderComponent implements OnInit, OnChanges {
this.timeExtentDisplay = {start: this.initialTimeExtent.start, end: this.initialTimeExtent.end};
this.firstSliderPosition = this.findPositionOfDate(this.timeExtent.start) ?? 0;
this.secondSliderPosition = this.timeSliderConfiguration.range ? undefined : this.findPositionOfDate(this.timeExtent.end);
this.hasSimpleCurrentValue = this.isRangeExactlyOneOfSingleTimeUnit(this.timeSliderConfiguration.range);
this.hasSimpleCurrentValue = this.isStringSingleTimeUnitRange(this.timeSliderConfiguration.range);

// date picker
this.hasDatePicker = this.isRangeContinuousWithinAllowedTimeUnits(this.timeSliderConfiguration);
Expand Down Expand Up @@ -101,19 +102,19 @@ export class TimeSliderComponent implements OnInit, OnChanges {

// correct the thumb that was modified with the calculated time extent if necessary (e.g. enforcing a minimal range)
const hasStartTimeBeenCorrected =
TimeExtentUtils.calculateDifferenceBetweenDates(newValidatedTimeExtent.start, newTimeExtent.start) > 0;
this.timeService.calculateDifferenceBetweenDates(newValidatedTimeExtent.start, newTimeExtent.start) > 0;
if (hasStartTimeBeenCorrected) {
this.firstSliderPosition = this.findPositionOfDate(newValidatedTimeExtent.start) ?? 0;
}
const hasEndTimeBeenCorrected = TimeExtentUtils.calculateDifferenceBetweenDates(newValidatedTimeExtent.end, newTimeExtent.end) > 0;
const hasEndTimeBeenCorrected = this.timeService.calculateDifferenceBetweenDates(newValidatedTimeExtent.end, newTimeExtent.end) > 0;
if (!this.timeSliderConfiguration.range && hasEndTimeBeenCorrected) {
this.secondSliderPosition = this.findPositionOfDate(newValidatedTimeExtent.end) ?? 0;
}

// overwrite the current time extent and trigger the corresponding event if the new validated time extent is different from the previous one
if (
TimeExtentUtils.calculateDifferenceBetweenDates(this.timeExtent.start, newValidatedTimeExtent.start) > 0 ||
TimeExtentUtils.calculateDifferenceBetweenDates(this.timeExtent.end, newValidatedTimeExtent.end) > 0
this.timeService.calculateDifferenceBetweenDates(this.timeExtent.start, newValidatedTimeExtent.start) > 0 ||
this.timeService.calculateDifferenceBetweenDates(this.timeExtent.end, newValidatedTimeExtent.end) > 0
) {
this.timeExtent = newValidatedTimeExtent;
this.changeTimeExtentEvent.emit(this.timeExtent);
Expand Down Expand Up @@ -145,8 +146,12 @@ export class TimeSliderComponent implements OnInit, OnChanges {
if (eventDate === null) {
return;
}

// format the given event date to the configured time format and back to ensure that it is a valid date within the current available dates
const date = dayjs(dayjs(eventDate).format(this.timeSliderConfiguration.dateFormat), this.timeSliderConfiguration.dateFormat).toDate();
const date = this.timeService.createDateFromString(
this.timeService.getDateAsFormattedString(eventDate, this.timeSliderConfiguration.dateFormat),
this.timeSliderConfiguration.dateFormat,
);
const position = this.findPositionOfDate(date);
if (position !== undefined) {
if (changedMinimumDate) {
Expand All @@ -158,23 +163,8 @@ export class TimeSliderComponent implements OnInit, OnChanges {
}
}

/**
* Returns `true` if the given range is defined and is exactly one of a single time unit (year, month, ...).
* If the optional parameter `allowedTimeUnits` is given then only the units in there are allowed; all other return `false` as well.
* @param range The range (in ISO-8601 time span format) to be evaluated
*
* @example
* `P1Y1M` is a duration of one year AND one month which is more than one time unit; therefore is the result `false`
* `P2Y` is a duration of two years which is more than one of a single time unit; therefore is the result `false`
* `P1D` is a duration of one day which is exactly one of a single time unit; therefore the result is `true`
*/
private isRangeExactlyOneOfSingleTimeUnit(range: string | null | undefined): boolean {
if (range) {
const rangeDuration = dayjs.duration(range);
const unit = TimeExtentUtils.extractUniqueUnitFromDuration(rangeDuration);
return unit !== undefined && TimeExtentUtils.getDurationAsNumber(rangeDuration, unit) === 1;
}
return false;
private isStringSingleTimeUnitRange(range: string | null | undefined): boolean {
return range ? this.timeService.isStringSingleTimeUnitRange(range) : false;
}

/**
Expand All @@ -201,23 +191,25 @@ export class TimeSliderComponent implements OnInit, OnChanges {
}

private extractUniqueDatePickerUnitFromDateFormat(dateFormat: string): DatePickerManipulationUnits | undefined {
const unit = TimeExtentUtils.extractUniqueUnitFromDateFormat(dateFormat);
const unit = this.timeSliderService.extractUniqueUnitFromDateFormat(dateFormat);
if (unit !== undefined && allowedDatePickerManipulationUnits.some((allowedUnit) => allowedUnit === unit)) {
return unit as DatePickerManipulationUnits;
}
return undefined;
}

private isLayerSourceContinuous(layerSource: TimeSliderLayerSource, unit: DatePickerManipulationUnits): boolean {
const dateAsAscendingSortedNumbers = layerSource.layers.map((layer) => dayjs(layer.date, unit).get(unit)).sort((a, b) => a - b);
const dateAsAscendingSortedNumbers = layerSource.layers
.map((layer) => this.timeService.createPartialFromString(layer.date, unit))
.sort((a, b) => a - b);
// all date numbers must be part of a continuous and strictly monotonously rising series each with exactly
// one step between them: `date[0] = x` => `date[n] = x + n`
return !dateAsAscendingSortedNumbers.some((dateAsNumber, index) => dateAsNumber !== dateAsAscendingSortedNumbers[0] + index);
}

private findPositionOfDate(date: Date): number | undefined {
const index = this.availableDates.findIndex(
(availableDate) => TimeExtentUtils.calculateDifferenceBetweenDates(availableDate, date) === 0,
(availableDate) => this.timeService.calculateDifferenceBetweenDates(availableDate, date) === 0,
);
return index === -1 ? undefined : index;
}
Expand Down
Loading

0 comments on commit bd6c64b

Please sign in to comment.