diff --git a/projects/laji/src/app/+project-form/form/named-place/named-place-wrapper/named-place-wrapper.component.ts b/projects/laji/src/app/+project-form/form/named-place/named-place-wrapper/named-place-wrapper.component.ts index 9e43ca99b..8ffeb54b3 100644 --- a/projects/laji/src/app/+project-form/form/named-place/named-place-wrapper/named-place-wrapper.component.ts +++ b/projects/laji/src/app/+project-form/form/named-place/named-place-wrapper/named-place-wrapper.component.ts @@ -10,6 +10,8 @@ import { take } from 'rxjs/operators'; + [activeNP]="data.activeNP?.id" + [filterBy]="data.filterBy" + [tab]="data.tab" + (activePlaceChange)="onActivePlaceChange($event)" + (filterChange)="onFilterChange($event)" + (tabChange)="onTabChange($event)"> (); @Output() tagsChange = new EventEmitter(); @Output() activeIdChange = new EventEmitter(); + @Output() filterChange = new EventEmitter(); + @Output() tabChange = new EventEmitter(); @Output() use = new EventEmitter(); @Output() edit = new EventEmitter(); @Output() create = new EventEmitter(); @@ -89,6 +101,8 @@ export class NamedPlaceComponent implements OnInit, OnDestroy { private updateFromInput!: Subscription; private documentForm$ = new BehaviorSubject(undefined); private activeNP$ = new BehaviorSubject(undefined); + private filterBy$ = new BehaviorSubject(undefined); + private tab$ = new BehaviorSubject(undefined); private municipality$ = new BehaviorSubject(undefined); private birdAssociationArea$ = new BehaviorSubject(undefined); private tags$ = new BehaviorSubject(undefined); @@ -164,7 +178,19 @@ export class NamedPlaceComponent implements OnInit, OnDestroy { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const formRights$ = this.documentForm$.pipe(switchMap(documentForm => this.formPermissionService.getRights(documentForm!))); - this.vm$ = combineLatest(this.documentForm$, placeForm$, this.municipality$, this.birdAssociationArea$, this.tags$, activeNP$, namedPlaces$, user$, formRights$).pipe( + this.vm$ = combineLatest( + this.documentForm$, + placeForm$, + this.municipality$, + this.birdAssociationArea$, + this.tags$, + activeNP$, + this.filterBy$, + this.tab$, + namedPlaces$, + user$, + formRights$ + ).pipe( map(([ documentForm, placeForm, @@ -172,6 +198,8 @@ export class NamedPlaceComponent implements OnInit, OnDestroy { birdAssociationArea, tags, activeNP, + filterBy, + tab, namedPlaces, user, formRights @@ -186,6 +214,8 @@ export class NamedPlaceComponent implements OnInit, OnDestroy { municipality, tags, activeNP, + filterBy, + tab, description: documentForm.options?.namedPlaceOptions?.chooseDescription ?? 'np.defaultDescription', allowEdit: (documentForm?.options?.namedPlaceOptions?.allowAddingPublic || formRights.admin) && !this.readonly, mapOptionsData: NamedPlaceComponent.getMapOptions(documentForm), @@ -271,6 +301,14 @@ export class NamedPlaceComponent implements OnInit, OnDestroy { }); } + onFilterChange(filterBy: string) { + this.filterChange.emit(filterBy); + } + + onTabChange(tab: string) { + this.tabChange.emit(tab); + } + onEdit() { this.activeNP$.pipe(take(1)).subscribe(id => this.edit.emit(id)); } diff --git a/projects/laji/src/app/+project-form/form/named-place/np-choose/np-choose.component.css b/projects/laji/src/app/+project-form/form/named-place/np-choose/np-choose.component.css index d005d92a2..2e6d193e7 100644 --- a/projects/laji/src/app/+project-form/form/named-place/np-choose/np-choose.component.css +++ b/projects/laji/src/app/+project-form/form/named-place/np-choose/np-choose.component.css @@ -20,3 +20,7 @@ .nav a { cursor: pointer; } + +.filterBy { + padding: 15px 0; +} diff --git a/projects/laji/src/app/+project-form/form/named-place/np-choose/np-choose.component.html b/projects/laji/src/app/+project-form/form/named-place/np-choose/np-choose.component.html index afb89e99e..b29c05faa 100644 --- a/projects/laji/src/app/+project-form/form/named-place/np-choose/np-choose.component.html +++ b/projects/laji/src/app/+project-form/form/named-place/np-choose/np-choose.component.html @@ -1,4 +1,15 @@
+
+
+
+ +
+
+
-
+
- > + (activePlaceChange)="onActivePlaceChange($event)" + (filteredIDs)="onFilteredIDsChange($event)">
diff --git a/projects/laji/src/app/+project-form/form/named-place/np-choose/np-choose.component.ts b/projects/laji/src/app/+project-form/form/named-place/np-choose/np-choose.component.ts index d42e236ac..d1b0318fc 100644 --- a/projects/laji/src/app/+project-form/form/named-place/np-choose/np-choose.component.ts +++ b/projects/laji/src/app/+project-form/form/named-place/np-choose/np-choose.component.ts @@ -26,6 +26,7 @@ import { formOptionToClassName } from '../../../../shared/directive/project-form export class NpChooseComponent implements OnInit, OnChanges { activeIndex = 0; loadedTabs = new LoadedElementsStore(['list', 'map']); + filteredIDs: string[] = []; height = '600px'; _namedPlaces: ExtendedNamedPlace[] = []; @@ -37,14 +38,17 @@ export class NpChooseComponent implements OnInit, OnChanges { @Input() formRights?: Rights; @Output() activePlaceChange = new EventEmitter(); + @Output() filterChange = new EventEmitter(); @Output() tabChange = new EventEmitter(); sent = this.isSent.bind(this); private seasonStart?: Date; private seasonEnd?: Date; + private queryParamTab?: string; _activeNP?: string; + _filterBy?: string; formOptionToClassName = formOptionToClassName; @@ -59,6 +63,10 @@ export class NpChooseComponent implements OnInit, OnChanges { ngOnChanges(changes: SimpleChanges) { if (changes['documentForm']) { + if (this.queryParamTab !== undefined) { + return; + } + // use named place options if query params do not specify tab if (this._documentForm?.options?.namedPlaceOptions?.startWithMap) { this.activeIndex = this.loadedTabs.getIdxFromName('map'); this.loadedTabs.load(this.activeIndex); @@ -97,7 +105,7 @@ export class NpChooseComponent implements OnInit, OnChanges { updateHeight() { if (this.platformService.isBrowser) { - this.height = Math.min(window.innerHeight - 70, 490) + 'px'; + this.height = Math.min(window.innerHeight - 70, 600) + 'px'; } } @@ -111,8 +119,24 @@ export class NpChooseComponent implements OnInit, OnChanges { this._activeNP = id; } + @Input() set filterBy(id: string|undefined) { + this._filterBy = id; + } + + @Input() set tab(tab: string|undefined) { + this.queryParamTab = tab; + + if (tab) { + const idx = this.loadedTabs.getIdxFromName(tab); + if (idx !== -1) { + this.activeIndex = idx; + this.loadedTabs.load(idx); + } + } + } + private findNPIdByIndex(idx: number) { - return this._namedPlaces[idx].id; + return this._namedPlaces[idx]?.id; } findNPIndexById(id?: string) { @@ -127,6 +151,19 @@ export class NpChooseComponent implements OnInit, OnChanges { this.activePlaceChange.emit(this._activeNP); } + updateFilter(event: any) { + this._filterBy = event.target.value; + this.filterChange.emit(this._filterBy); + } + + onFilteredIDsChange(ids: string[]) { + this.filteredIDs = ids; + if (this._activeNP && !this.filteredIDs.includes(this._activeNP)) { + this._activeNP = undefined; + this.onActivePlaceChange(-1); + } + } + isSent(np: NamedPlace) { if (this.seasonStart && this.seasonEnd && np?.prepopulatedDocument?.gatheringEvent?.dateBegin) { const dateBegin = new Date(np.prepopulatedDocument.gatheringEvent.dateBegin); diff --git a/projects/laji/src/app/+project-form/form/named-place/np-choose/np-list/np-list.component.css b/projects/laji/src/app/+project-form/form/named-place/np-choose/np-list/np-list.component.css index 440bae8fe..3a610a519 100644 --- a/projects/laji/src/app/+project-form/form/named-place/np-choose/np-list/np-list.component.css +++ b/projects/laji/src/app/+project-form/form/named-place/np-choose/np-list/np-list.component.css @@ -27,7 +27,3 @@ padding: 0; margin: 0; } - -.filterBy { - padding: 15px 0; -} diff --git a/projects/laji/src/app/+project-form/form/named-place/np-choose/np-list/np-list.component.html b/projects/laji/src/app/+project-form/form/named-place/np-choose/np-list/np-list.component.html index ae911eb3d..06ad306d7 100644 --- a/projects/laji/src/app/+project-form/form/named-place/np-choose/np-list/np-list.component.html +++ b/projects/laji/src/app/+project-form/form/named-place/np-choose/np-list/np-list.component.html @@ -1,13 +1,3 @@ -
-
-
- -
-
-
@@ -15,6 +5,7 @@ #dataTable class="observation-table" (rowSelect)="changeActivePlace($event)" + (filteredIDs)="changeFilteredIDs($event)" [getRowClass]="getRowClass" [virtualScrolling]="true" [clientSideSorting]="true" @@ -30,6 +21,7 @@ [columns]="columns!" [selectionType]="selectionType.single" [preselectedRowIndex]="activeNP!" + [visible]="_visible" >
diff --git a/projects/laji/src/app/+project-form/form/named-place/np-choose/np-list/np-list.component.ts b/projects/laji/src/app/+project-form/form/named-place/np-choose/np-list/np-list.component.ts index 12a437c9b..782f76814 100644 --- a/projects/laji/src/app/+project-form/form/named-place/np-choose/np-list/np-list.component.ts +++ b/projects/laji/src/app/+project-form/form/named-place/np-choose/np-list/np-list.component.ts @@ -10,8 +10,8 @@ import { } from '@angular/core'; import { NamedPlace } from '../../../../../shared/model/NamedPlace'; import { Util } from '../../../../../shared/service/util.service'; -import { map, take } from 'rxjs/operators'; -import { forkJoin } from 'rxjs'; +import { tap } from 'rxjs/operators'; +import { forkJoin, Observable } from 'rxjs'; import { AreaNamePipe } from '../../../../../shared/pipe/area-name.pipe'; import { BoolToStringPipe } from '../../../../../shared/pipe/bool-to-string.pipe'; import Timeout = NodeJS.Timeout; @@ -22,6 +22,7 @@ import { DatatableColumn } from '../../../../../shared-modules/datatable/model/d import { DatatableComponent } from '../../../../../shared-modules/datatable/datatable/datatable.component'; import { SelectionType, SortType } from '@achimha/ngx-datatable'; import { NpInfoComponent } from '../../np-info/np-info.component'; +import { DatatableUtil } from '../../../../../shared-modules/datatable/service/datatable-util.service'; @Component({ selector: 'laji-np-list', @@ -40,7 +41,6 @@ export class NpListComponent implements OnDestroy { selectionType = SelectionType; showLegendList? = false; multisort? = false; - filterBy?: string; legendList = [ {label: 'Vapaa', color: '#ffffff'}, {label: 'Varattu', color: '#d1c400'}, @@ -48,7 +48,7 @@ export class NpListComponent implements OnDestroy { {label: 'Ilmoitettu', color: '#00aa00'} ]; columnsMetaData: {[columnName: string]: DatatableColumn}; - private _visible?: boolean; + _visible = true; private _visibleTimeout?: Timeout; private _formRights?: Rights; private _documentForm!: Form.SchemaForm; @@ -61,13 +61,16 @@ export class NpListComponent implements OnDestroy { @ViewChild('dataTable', { static: true }) public datatable!: DatatableComponent; @Output() activePlaceChange = new EventEmitter(); + @Output() filteredIDs = new EventEmitter(); @Input() activeNP?: number|null; + @Input() filterBy?: string; @Input({ required: true }) height!: string; @Input() listColumnNameMapping?: { [key: string]: string}; constructor(private cd: ChangeDetectorRef, - private areaNamePipe: AreaNamePipe + private areaNamePipe: AreaNamePipe, + private datatableUtil: DatatableUtil ) { this.columnsMetaData = { '$.alternativeIDs[0]': { @@ -110,6 +113,16 @@ export class NpListComponent implements OnDestroy { '$.locality': { label: 'np.locality' }, + '$.alternativeIDs': { + label: 'np.archipelago.alternativeIDs', + cellTemplate: 'labelIDTpl', + width: 100 + }, + '$.tags': { + label: 'np.archipelago.tags', + cellTemplate: 'labelIDTpl', + width: 100 + }, '$.prepopulatedDocument.gatherings[0].invasiveControlOpen': { label: 'np.invasiveControlOpen', cellTemplate: 'boolToStrTpl' @@ -134,6 +147,10 @@ export class NpListComponent implements OnDestroy { this.activePlaceChange.emit(this.data.indexOf(event.row)); } + changeFilteredIDs(event: string[]) { + this.filteredIDs.emit(event); + } + getRowClass(row: any) { const status = row['$._status']; if (status !== 'free') { return status; } @@ -206,47 +223,43 @@ export class NpListComponent implements OnDestroy { this._visibleTimeout = setTimeout(() => { this.datatable.showActiveRow(); this._visibleTimeout = undefined; - }, 10); + }, 100); } this._visible = visibility; } - updateFilter(event: any) { - this.filterBy = event.target.value; - } - private initData() { if (!this._fields || !this._namedPlaces) { return; } + const results: any[] = []; - const municipalities$ = []; + const observables: Observable[] = []; + for (const namedPlace of this._namedPlaces) { const row: any = {}; + for (const path of this._fields) { - let value = Util.parseJSONPath(namedPlace, path); - if (value && value.length && ( - path === '$.prepopulatedDocument.gatheringEvent.dateBegin' - || path === '$.prepopulatedDocument.gatheringEvent.dateEnd' - )) { - value = value.split('.').reverse().join('-'); - } + const value = Util.parseJSONPath(namedPlace, path); row[path] = value; + const observable = this.datatableUtil.getVisibleValue(value, namedPlace, 'label'); + observables.push( + observable.pipe( + tap(visibleValue => { + if (visibleValue && visibleValue.length > 0) { + row[path] = visibleValue; + } + }) + ) + ); } - const municipality = row['$.municipality']; - if (municipality && municipality.length) { - municipalities$.push(forkJoin( - ...municipality.map((_muni: string) => this.areaNamePipe.updateValue(_muni) - .pipe(take(1))) - ).pipe(map(areaLabel => [row, areaLabel]))); - } + + row['$.id'] = namedPlace.id; results.push(row); } - if (municipalities$.length) { - forkJoin(...municipalities$).subscribe((municipalityTuples) => { - municipalityTuples.forEach(([row, municipalityLabel]: [any, string[]]) => { - row['$.municipality'] = municipalityLabel.join(', '); - }); + + if (observables.length > 0) { + forkJoin(observables).subscribe(() => { this.data = results; this.cd.markForCheck(); }); diff --git a/projects/laji/src/app/+project-form/form/named-place/np-choose/np-map/np-map.component.ts b/projects/laji/src/app/+project-form/form/named-place/np-choose/np-map/np-map.component.ts index dfe4aa31f..87e2c5555 100644 --- a/projects/laji/src/app/+project-form/form/named-place/np-choose/np-map/np-map.component.ts +++ b/projects/laji/src/app/+project-form/form/named-place/np-choose/np-map/np-map.component.ts @@ -39,6 +39,8 @@ export class NpMapComponent implements OnInit, OnChanges { @Input() reservable?: boolean; @Input() placeForm: any; @Input({ required: true }) documentForm!: Form.SchemaForm; + @Input() filterBy?: string; + @Input() filteredIDs: string[] = []; @Output() activePlaceChange = new EventEmitter(); visualization?: LajiMapVisualization; @@ -49,6 +51,7 @@ export class NpMapComponent implements OnInit, OnChanges { private _popupCallback?: (elemOrString: HTMLElement | string) => void; private _zoomOnNextTick = false; private _lastVisibleActiveNP?: number|null; + private _pendingFilteredIdx?: number; private placeColor = '#5294cc'; private placeActiveColor = '#375577'; @@ -70,7 +73,7 @@ export class NpMapComponent implements OnInit, OnChanges { } ngOnChanges(changes: SimpleChanges) { - if (changes['namedPlaces']) { + if (changes['namedPlaces'] || changes['filteredIDs']) { this.initMapData(); } if (changes['visible'] && changes['visible'].currentValue === true && this._lastVisibleActiveNP !== this.activeNP) { @@ -80,7 +83,14 @@ export class NpMapComponent implements OnInit, OnChanges { if (this.visible) { this._lastVisibleActiveNP = this.activeNP; } - this.setNewActivePlace(changes['activeNP'].currentValue); + const fullIdx = changes['activeNP'].currentValue; + const filteredIdx = this.getFilteredIdx(fullIdx); + // Apply immediately if map is ready, or store as pending if not + if (this.lajiMap.map) { + this.setNewActivePlace(filteredIdx); + } else { + this._pendingFilteredIdx = filteredIdx; + } } } @@ -89,6 +99,12 @@ export class NpMapComponent implements OnInit, OnChanges { if (popup && this._popupCallback) { this._popupCallback(popup); } + + if (this._pendingFilteredIdx !== undefined) { + this.setNewActivePlace(this._pendingFilteredIdx); + this._pendingFilteredIdx = undefined; + } + if (this._zoomOnNextTick) { this._zoomOnNextTick = false; if (this.activeNP !== undefined && this.activeNP !== -1) { @@ -103,6 +119,18 @@ export class NpMapComponent implements OnInit, OnChanges { } } + private getFilteredIdx(fullIdx: any): number { + // Transform selected place's index in all places list to index in filtered places list + if (fullIdx !== undefined && fullIdx !== null && fullIdx !== -1) { + const namedPlace = this.namedPlaces![fullIdx]; + const filteredPlaces = this.namedPlaces!.filter(np => !this.filterBy || this.filteredIDs.includes(np.id)); + const filteredIdx = filteredPlaces.findIndex(np => np.id === namedPlace.id); + return filteredIdx; + } else { + return -1; + } + } + private setNewActivePlace(newActive: number) { if (!this.lajiMap.map) { return; } @@ -184,24 +212,32 @@ export class NpMapComponent implements OnInit, OnChanges { onChange: (events: any) => { events.forEach((e: any) => { if (e.type === 'active') { - this.zone.run(() => { - this.activePlaceChange.emit(e.idx); - }); + // Transform selected place's index in filtered places list to index in all places list before emitting + const filteredPlaces = this.namedPlaces!.filter(np => !this.filterBy || this.filteredIDs.includes(np.id)); + const selectedPlace = filteredPlaces[e.idx]; + if (selectedPlace) { + const fullIdx = this.namedPlaces!.findIndex(np => np.id === selectedPlace.id); + this.zone.run(() => { + this.activePlaceChange.emit(fullIdx); + }); + } } }); }, featureCollection: { type: 'FeatureCollection', - features: this.namedPlaces.map(np => ({ - type: 'Feature', - geometry: np.geometry, - properties: { - reserved: np._status, - name: np.name, - municipality: np.municipality, - taxon: np.taxonIDs - } - })) + features: this.namedPlaces + .filter(np => !this.filterBy || this.filteredIDs.includes(np.id)) + .map(np => ({ + type: 'Feature', + geometry: np.geometry, + properties: { + reserved: np._status, + name: np.name, + municipality: np.municipality, + taxon: np.taxonIDs + } + })) }, getPopup: ({featureIdx, feature}: {featureIdx: number; feature: string}, cb: (elem: string | HTMLElement) => void) => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion @@ -209,7 +245,7 @@ export class NpMapComponent implements OnInit, OnChanges { this._popupCallback = cb; this.cdr.markForCheck(); }, - activeIdx: this.activeNP, + activeIdx: this.getFilteredIdx(this.activeNP), cluster: mapCluster }; } catch (e) { } diff --git a/projects/laji/src/app/shared-modules/datatable/datatable/datatable.component.ts b/projects/laji/src/app/shared-modules/datatable/datatable/datatable.component.ts index facb9b459..95fe2492a 100644 --- a/projects/laji/src/app/shared-modules/datatable/datatable/datatable.component.ts +++ b/projects/laji/src/app/shared-modules/datatable/datatable/datatable.component.ts @@ -55,12 +55,15 @@ export class DatatableComponent implements AfterViewInit, OnInit, OnChanges, OnD _filterBy?: FilterByType; _height = '100%'; _isFixedHeight = false; + _visible = true; + _pendingScroll = false; @Output() pageChange = new EventEmitter(); @Output() sortChange = new EventEmitter(); @Output() reorder = new EventEmitter(); @Output() datatableSelect = new EventEmitter(); @Output() rowSelect = new EventEmitter(); + @Output() filteredIDs = new EventEmitter(); filterByChange?: Subscription; @@ -201,8 +204,19 @@ export class DatatableComponent implements AfterViewInit, OnInit, OnChanges, OnD if (!this.selected.length) { return; } - if (this.initialized) { + setTimeout(() => { this.showActiveRow(); + }, 100); + } + + @Input() set visible(visible: boolean) { + this._visible = visible; + // If the table becomes visible and there is a pending scroll to active row, do the scroll + if (this._visible && this._pendingScroll) { + this._pendingScroll = false; + setTimeout(() => { + this.showActiveRow(); + }, 100); } } @@ -210,6 +224,11 @@ export class DatatableComponent implements AfterViewInit, OnInit, OnChanges, OnD if (!this.initialized || this._preselectedRowIndex === -1 || !this.datatable || !this.datatable._internalRows) { return; } + // If the table is not visible, postpone scrolling by setting a pending scroll + if (!this._visible) { + this._pendingScroll = true; + return; + } const postSortIndex = (this._rows || []).findIndex((element) => element.preSortIndex === this._preselectedRowIndex); // Calculate relative position of selected row and scroll to it const rowHeight = this.datatable.bodyComponent.rowHeight as number; @@ -239,6 +258,10 @@ export class DatatableComponent implements AfterViewInit, OnInit, OnChanges, OnD this.changeDetectorRef.markForCheck(); }); + if (this._filterBy && this._originalRows) { + this.updateFilteredRows(); + this.changeDetectorRef.markForCheck(); + } } ngAfterViewInit() { @@ -250,7 +273,7 @@ export class DatatableComponent implements AfterViewInit, OnInit, OnChanges, OnD this.initialized = true; // Make sure that preselected row index setter is called after initialization this.showActiveRow(); - }, 10); + }, 100); } } @@ -345,6 +368,7 @@ export class DatatableComponent implements AfterViewInit, OnInit, OnChanges, OnD this._page = 1; this.scrollTo(); this.sortRows(this.sorts); + this.filteredIDs.emit(this._rows?.map(r => r['$.id'])); } private scrollTo(offsetY: number = 0) { diff --git a/projects/laji/src/app/shared/service/filter.service.ts b/projects/laji/src/app/shared/service/filter.service.ts index 121d616d8..8f8dd43bc 100644 --- a/projects/laji/src/app/shared/service/filter.service.ts +++ b/projects/laji/src/app/shared/service/filter.service.ts @@ -70,7 +70,7 @@ export class FilterService { HaystackKey extends string, HaystackValue extends FilterBaseType >( - needle: HaystackValue, haystack: HaystackValue | HaystackObj, properties: HaystackKey[] + needle: HaystackValue, haystack: HaystackValue | HaystackObj, properties?: HaystackKey[] ) { switch (typeof haystack) { case 'string': @@ -93,7 +93,7 @@ export class FilterService { if (typeof obj[i] === 'undefined') { continue; } - if (this.contains(needle, obj[i], properties)) { + if (this.contains(needle, obj[i])) { return true; } } diff --git a/projects/laji/src/app/shared/service/project-form.service.ts b/projects/laji/src/app/shared/service/project-form.service.ts index 482299b28..d9c6cc142 100644 --- a/projects/laji/src/app/shared/service/project-form.service.ts +++ b/projects/laji/src/app/shared/service/project-form.service.ts @@ -19,6 +19,8 @@ export interface NamedPlacesQuery { birdAssociationArea?: string; municipality?: string; activeNP?: string; + filterBy?: string; + tab?: string; } export interface NamedPlacesQueryModel { tags?: string[]; @@ -30,6 +32,8 @@ export interface NamedPlacesQueryModel { export interface NamedPlacesRouteData extends NamedPlacesQueryModel { documentForm: Form.SchemaForm; namedPlace?: NamedPlace; + filterBy?: string; + tab?: string; } export interface ExcelFormOptions { diff --git a/projects/laji/src/i18n/fi.json b/projects/laji/src/i18n/fi.json index 1f5bf30ef..e6b8b212e 100644 --- a/projects/laji/src/i18n/fi.json +++ b/projects/laji/src/i18n/fi.json @@ -1093,6 +1093,8 @@ "notification.markAll": "Merkitse nähdyiksi", "notification.title": "Ilmoitukset", "noYear": "vuosi puuttuu", + "np.archipelago.alternativeIDs": "Reitin nimi", + "np.archipelago.tags": "Saaristotyyppi", "np.biogeographicalProvince": "Eliömaakunta", "np.chooseLabel": "Valitse alla olevasta luettelosta", "np.copyAddress": "Kopioi linkki",