Example 1: MAP + Angular library
Use a CLI command to generate a plugin component with name map.
eo g plugin map
Use a CLI command to install maps package (takes some time).
npm install -P @agm/core
Import AgmCoreModule module to CustomPluginsModule.
custom-plugins.module.tsimport {NgModule} from '@angular/core'; import {CommonModule} from '@angular/common'; import {EoFrameworkModule} from '@eo-sdk/client'; import {PluginsModule} from '@eo-sdk/client'; import {EoPlugin} from '@eo-sdk/client'; import {links} from '../custom-states/custom-states.module'; import {MapComponent} from './map/map.component'; import {AgmCoreModule} from '@agm/core'; export const entryComponents: EoPlugin[] = [ MapComponent, ]; @NgModule({ imports: [ CommonModule, EoFrameworkModule, AgmCoreModule.forRoot(), PluginsModule.forRoot(entryComponents, links) ], declarations: [MapComponent], exports: [PluginsModule] }) export class CustomPluginsModule { }
Update map.component.ts to load current position from navigator, and modify matchType property if necessary.
map.component.tsimport { Component, OnInit } from '@angular/core'; @Component({ selector: 'eo-map', templateUrl: './map.component.html', styleUrls: ['./map.component.scss'] }) export class MapComponent implements OnInit { static id = 'eo.custom.plugin.map'; static matchType = new RegExp ('object-details-tab.*'); currentPosition; secureOriginIssue = false; constructor() { } private getCurrentPosition(): void { navigator.geolocation.getCurrentPosition((position) => { this.currentPosition = position; }, failure => { if (failure.message.indexOf('Only secure origins are allowed') === 0) { this.secureOriginIssue = true; } }); } ngOnInit() { this.getCurrentPosition(); } }
Update map.component.html to include a map component with the latitude and longitude based on current position.
map.component.html<agm-map *ngIf="currentPosition && !secureOriginIssue" [latitude]="currentPosition.coords.latitude" [longitude]="currentPosition.coords.longitude" [style.height.%]="90"> <agm-marker [latitude]="currentPosition.coords.latitude" [longitude]="currentPosition.coords.longitude"></agm-marker> </agm-map> <section *ngIf="!currentPosition && secureOriginIssue" class="secure-origin__issue"> <eo-icon class="no-file" [iconSrc]="'assets/_default/svg/ic_not_listed_location.svg'"></eo-icon> <span translate>eo.custom.plugin.map.secure.origin</span> </section>
To make it look nice we add some scss
map.component.scss.secure-origin__issue { height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; text-transform: capitalize; border: 0; .no-file { width: 128px; height: 128px; opacity: .06; } }
Use a CLI command to generate labels/translations
eo g label eo.custom.plugin.map --en Map --de Stadtplan
eo g label eo.custom.plugin.map.secure.origin --en "Please, change to a secure connection (https)" --de "Bitte wechseln sie zu einer sicheren verbindung (https)"
Example 2a: MAP via iframe + core services
Use a CLI command to generate a plugin component.
eo g plugin map-frame
Update map-frame.component.ts to load current address from dms object, and modify matchType property if necessary. Please check normalize function if it match your scheme properties!
map-frame.component.tsimport { Component, AfterViewInit, ViewChild, ElementRef, Renderer2 } from '@angular/core'; import {DmsService, DmsObject, EventService, EnaioEvent, Event} from '@eo-sdk/core'; import {SelectionService, UnsubscribeOnDestroy} from '@eo-sdk/client'; import {takeUntil} from 'rxjs/operators'; @Component({ selector: 'eo-map-frame', templateUrl: './map-frame.component.html', styleUrls: ['./map-frame.component.scss'] }) export class MapFrameComponent extends UnsubscribeOnDestroy implements AfterViewInit { static id = 'eo.custom.plugin.map-frame'; static matchType = new RegExp('object-details-tab.*'); context; @ViewChild('mapFrame') mapFrame: ElementRef; constructor(private selectionService: SelectionService, private dmsService: DmsService, private renderer: Renderer2, private eventService: EventService) { super(); } /** * normalize Address data - map your data based on scheme properties * @param data * @returns */ private normalize(data: any = {}): any { return { streethw: data.strassehw, townhw: data.orthw, countryhw: data.landhw, ...data }; } /** * Process dmsObject to get the url for map frame * @param dmsObj */ setupMap(dmsObj: DmsObject) { if (dmsObj) { const {streethw, townhw, countryhw} = this.normalize(dmsObj.data); const url = `https://www.google.com/maps/embed/v1/place?key=AIzaSyDX8znfh-d4u3spGhC1GvCjq6EA1pjPovQ&q=${streethw}+${townhw}+${countryhw}`; this.renderer.setAttribute(this.mapFrame.nativeElement, 'src', url); } } /** * Load & update current context/dmsObject * @param event */ eventHandler(event: Event) { if (event.type === EnaioEvent.DMS_OBJECT_LOADED || (this.context && this.context.id === event.data.id)) { this.context = event.data; this.setupMap(event.data); } } ngAfterViewInit() { this.eventService .on(EnaioEvent.DMS_OBJECT_LOADED, EnaioEvent.DMS_OBJECT_UPDATED) .pipe( takeUntil(this.componentDestroyed$) ) .subscribe(e => this.eventHandler(e)); } }
Update map-frame.component.html to include a google maps iframe based on address of the selected dms object.
map-frame.component.html<p> map-frame works! </p> <iframe #mapFrame width="100%" height="90%" frameborder="0" style="border:0" allowfullscreen> </iframe>
Use a CLI command to generate labels/translations
eo g label eo.custom.plugin.map-frame --en GoogleMaps --de GoogleMaps
Example 2b: MAP with Address and Geocoord via iframe + core services
- We start by extending the "MAP via iframe + core service"
For readability, we move most of our functionality to a service
Terminalng g s eo-custom-client/services/location --spec false
To keep the code organized we introduce LocationType enum.
Terminalng g enum eo-custom-client/enum/LocationType
Update locationType.ts
locationTypes.rsexport enum LocationType { COORDS = 'coords', ADDRESS = 'address', EMPTY = 'empty' }
To keep the code organized we introduce a Interfaces for the Locations.
Terminalng g interface eo-custom-client/interfaces/Location
Update location.interface.ts
location.interface.tsexport interface LocationByAddress { type: string; streethw: string; townhw: string; countryhw: string } export interface LocationByCoords { type: string; photogpsla: number; photogpslo: number; } export interface NoLocation { type: string; }
Final location.service.ts. Check that the normalize function matches your scheme properties!
location.service.tsimport {Injectable} from '@angular/core'; import {LocationType} from '../enums/location-type.enum'; import {Observable, of, throwError} from 'rxjs'; import {LocationByAddress, LocationByCoords, NoLocation} from '../interfaces/location'; @Injectable({ providedIn: 'root' }) export class LocationService { private readonly apiKey = `AIzaSyDX8znfh-d4u3spGhC1GvCjq6EA1pjPovQ`; private readonly mapUrl = `https://www.google.com/maps/embed/v1/place`; /** * normalize Address data - map your data based on scheme properties * @param data * @returns */ private normalize(data: any = {}): any { return { streethw: data.strassehw, townhw: data.orthw, countryhw: data.landhw, photogpsla: data.photogpsla, photogpslo: data.photogpslo, ...data }; } /** * retrieve Address from data object * @param data * @returns */ private locationbDataWithAddress(data): LocationByAddress | NoLocation { const {streethw, townhw, countryhw} = data; return (townhw && countryhw) ? {type: LocationType.ADDRESS, streethw, townhw, countryhw} : {type: LocationType.EMPTY}; } /** * retrieve Geo Coordinates from data object * @param data * @returns */ private locationbDataWithCoords(data): LocationByCoords | NoLocation { const {photogpsla, photogpslo} = data; return (photogpsla && photogpslo) ? {type: LocationType.COORDS, photogpsla, photogpslo} : {type: LocationType.EMPTY}; } /** * Geo Coordinates are assumed to be only present in photo Objects. * We check the objects type to either retrieve Geo Coordinates or Address Data. * If neither is available we return empty type. * * @param typeName * @param data * @returns */ locationbData(typeName: string, data: Object): LocationByAddress | LocationByCoords | NoLocation { let normalizedData = this.normalize(data); return typeName === 'albumphoto' ? this.locationbDataWithCoords(normalizedData) : this.locationbDataWithAddress(normalizedData); } /** * Depending on the Location type we build a different URL. * If we have no Location type we return an error. * * @param location * @returns */ mapsUrl(location): Observable<string> { let params: string; if (location.type === LocationType.ADDRESS) { const {streethw, townhw, countryhw} = location; params = `${streethw || ''}+${townhw}+${countryhw}`; } else if (location.type === LocationType.COORDS) { const {photogpsla, photogpslo} = location; params = `${photogpsla},${photogpslo}`; } else { return throwError(true) } return of(`${this.mapUrl}?key=${this.apiKey}&q=${params}`); } }
Since we want to hide the Map when we don't have any Location information, we need to update the map.component.html.
map-frame.component.html<iframe class="map" [hidden]="mapAvailable" #mapFrame frameborder="0" allowfullscreen> </iframe> <section class="map__not-available" [hidden]="!mapAvailable"> <eo-icon class="no-file" [iconSrc]="'assets/_default/svg/ic_no-file.svg'"></eo-icon> </section>
As a last step in the TS files we need to update map-frame.component.ts
map-frame.component.tsimport { Component, AfterViewInit, ViewChild, ElementRef, Renderer2 } from '@angular/core'; import {DmsService, DmsObject, EventService, EnaioEvent, Event} from '@eo-sdk/core'; import {SelectionService, UnsubscribeOnDestroy} from '@eo-sdk/client'; import {takeUntil} from 'rxjs/operators'; import {LocationService} from '../../services/location.service'; @Component({ selector: 'eo-map-frame', templateUrl: './map-frame.component.html', styleUrls: ['./map-frame.component.scss'] }) export class MapFrameComponent extends UnsubscribeOnDestroy implements AfterViewInit { static id = 'eo.custom.plugin.map-frame'; static matchType = new RegExp('object-details-tab.*'); context; mapAvailable; @ViewChild('mapFrame') mapFrame: ElementRef; constructor(private selectionService: SelectionService, private dmsService: DmsService, private renderer: Renderer2, private eventService: EventService, private locationService: LocationService) { super(); } /** * Process dmsObject to get the url for map frame * @param dmsObj */ setupMap(dmsObj: DmsObject) { if (dmsObj) { const {typeName, data} = dmsObj; const location = this.locationService.locationbData(typeName, data); this.locationService .mapsUrl(location) .pipe( takeUntil(this.componentDestroyed$) ) .subscribe(url => { this.mapAvailable = false; this.renderer.setAttribute(this.mapFrame.nativeElement, 'src', url); }, error => this.mapAvailable = error); } } /** * Load & update current context/dmsObject * @param event */ eventHandler(event: Event) { if (event.type === EnaioEvent.DMS_OBJECT_LOADED || (this.context && this.context.id === event.data.id)) { this.context = event.data; this.setupMap(event.data); } } ngAfterViewInit() { this.eventService .on(EnaioEvent.DMS_OBJECT_LOADED, EnaioEvent.DMS_OBJECT_UPDATED) .pipe( takeUntil(this.componentDestroyed$) ) .subscribe(e => this.eventHandler(e)); } }
To finish this plugin, add some styling.
map-frame.component.scss.map{ width: 100%; height: 90%; &__not-available{ height: 100%; display: flex; justify-content: center; align-items: center; text-transform: capitalize; border: 0; .no-file{ width: 128px; height: 128px; opacity: .06; } } }
The results (left: address data, right: coordinates, click on the image enlarges it)
Example 3: Personal Cover Sheet for object state
Use a CLI command to generate a plugin component with name personal-cover.
eo g plugin personal-cover
Update personal-cover.component.ts to load ID picture and all leave applications, and check if employee is on holidays.
personal-cover.component.tsimport {Component, OnInit, Inject} from '@angular/core'; import {ObjectStateDetailsComponent} from '@eo-sdk/client'; import {BackendService, SystemService, DmsService, DmsObject, Utils} from '@eo-sdk/core'; import {of as observableOf} from 'rxjs'; import {catchError, flatMap} from 'rxjs/operators'; @Component({ selector: 'eo-personal-cover', templateUrl: './personal-cover.component.html', styleUrls: ['./personal-cover.component.scss'] }) export class PersonalCoverComponent implements OnInit { static id = 'eo.custom.plugin.personal-cover'; static matchType = new RegExp('object-state-details-tab.*'); context: DmsObject; picture: DmsObject; holiday: any; constructor(@Inject(ObjectStateDetailsComponent) private parent: ObjectStateDetailsComponent, private backend: BackendService, private system: SystemService, private dms: DmsService) { } /** * normalize Result data - map your data based on scheme properties * @param data * @returns */ private normalize(data: any = {}): any { return { pictureType: 'passfoto', holidayType: 'urlaub', holidayFrom: 'urlaub.holidayfrom', holidayTo: 'urlaub.holidayuntil', ...data }; } selectHoliday() { this.parent.selectFrontPageDoc(this.holiday); } loadByQuery(context: DmsObject, filter) { let postData = { contextid: context.id, contexttype: context.typeName, mode: 'result', filter, timezone: Utils.getTimezoneOffset() }; return this.backend .post('/contextview', postData, this.backend.getContextBase()) .pipe(catchError(error => observableOf(error))); } ngOnInit() { const data = this.normalize({}); this.context = this.parent.context; // load ID picture result & related DmsObject which contains the picture this.loadByQuery(this.parent.context, [`Types[${data.pictureType}]`]).pipe( flatMap(photos => photos.hitcount ? this.dms.getDmsObjectByParams(photos.dms[0]) : observableOf(null)) ).subscribe(photo => this.picture = photo); // load Leave Application results & check if employee is on holidays this.loadByQuery(this.parent.context, [`Types[${data.holidayType}]`]) .subscribe((holidays: any) => { const now = new Date().toISOString().slice(0,10); this.holiday = holidays.dms.find(dms => dms[data.holidayFrom] <= now && now <= dms[data.holidayTo]); if (this.holiday) { this.holiday.objectType = this.system.getObjectType(this.holiday.type); } }); } }
Update personal-cover.component.html to include a media component & holiday icon.
personal-cover.component.html<div class="eo-head"> <header class="eo-header"> <eo-icon class="eo-header-icon white" [objectType]="context.type" [iconTitle]="context.type.label"></eo-icon> <div class="eo-header-info eo-header__content"> <h2 class="eo-header-title">{{context.title}}</h2> <h3 class="eo-header-subtitle"> <span translate>eo.state.object.frontpage.created</span> <span class="date"> {{context.created.on | localeDate}}</span> </h3> </div> <div class="eo-header-actions"> <eo-icon *ngIf="holiday" [objectType]="holiday.objectType" [iconTitle]="holiday.title" (click)="selectHoliday()"></eo-icon> </div> </header> </div> <div class="eo-body"> <eo-media *ngIf="picture" [dmsObject]="picture" #viewer></eo-media> <div [hidden]="!!picture" class="empty-container"> <eo-icon class="nofile" [iconSrc]="'assets/_default/svg/ic_no-file.svg'"></eo-icon> </div> </div>
To make it look nice we add some styles to indicate active holidays.
personal-cover.component.scss:host { eo-icon.eo-header-icon { padding: 0; } .eo-header-actions { eo-icon { color: var(--color-accent); cursor: pointer; padding: 0; } } .empty-container { position: absolute; top: 0; right: 0; bottom: 0; left: 0; display: flex; justify-content: center; align-items: center; .nofile { width: 128px; height: 128px; opacity: .06; } } }
Use a CLI command to generate labels/translations
eo g label eo.custom.plugin.personal-cover --en "Cover sheet" --de "Aktendeckel"