Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

...

...

Table of Contents

Example 1: MAP + Angular library

Use a CLI command to generate a plugin component with name map.

Code Block
languagebash
linenumberstrue
eo g plugin map

...

Table of Contents
include^Example.*

Example 1: MAP + Angular library

Description

In this example we will create a new tab in the object area, which will show a map with our current location.

Image Added


Implementation

  1. Use a CLI command to generate a plugin component with name map.

    Code Block
    languagebash
    linenumberstrue
    eo g plugin map


  2. Use a CLI command to install maps package (takes some time).

    Code Block
    languagebash
    linenumberstrue
    npm install -P @agm/core


  3. Import AgmCoreModule module to CustomPluginsModule.

    Code Block
    languagejs
    titlecustom-plugins.module.ts
    linenumberstrue
    import {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 {
    }
    
    


  4. Update map.component.ts to load current position from navigator, and modify matchType property if necessary.

    Code Block
    languagejs
    titlemap.component.ts
    linenumberstrue
    import { 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();
      }
    
    }
    
    


  5. Update map.component.html to include a map component with the latitude and longitude based on current position.

    Code Block
    languagexml
    titlemap.component.html
    linenumberstrue
    <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>


  6. To make it look nice we add some scss

    Code Block
    languagecss
    titlemap.component.scss
    linenumberstrue
    .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;
      }
    }


  7. Use a CLI command to generate labels/translations

    Code Block
    languagebash
    linenumberstrue
    eo g label eo.custom.plugin.map --en Map --de Stadtplan


    Code Block
    languagebash
    linenumberstrue
    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

Description

In this example we will create a new tab in the object area, which will show a map pointing to the objects address.

Image Added


Implementation

  1. Use a CLI command to generate a plugin component.

    Code Block
    languagebash
    linenumberstrue
    eo g plugin map-frame


  2. 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!

    Code Block
    languagejs
    titlemap-frame.component.ts
    linenumberstrue
    import {
      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));
      }
    }
    
    


  3. Update map-frame.component.html to include a google maps iframe based on address of the selected dms object.

    <p> map-frame works! </p>
    Code Block
    languagexml
    titlemap-frame.component.html
    linenumberstrue
    true
    <iframe
      #mapFrame
      width="100%"
      height="90%"
      frameborder="0" style="border:0" allowfullscreen>
    </iframe>


  4. Use a CLI command to generate labels/translations

    Code Block
    languagebash
    linenumberstrue
    eo g label eo.custom.plugin.map-frame --en GoogleMaps --de GoogleMaps


Example 2b: MAP with Address and Geocoord via iframe + core services

Description

In this example we extend Example 2a to also be able to show a location in the map provided by coordinates. If we don't have a location, the map will be hidden.

Image AddedImage Added

(left: address data, right: coordinates)



Implementation

  1. We start by extending the "MAP via iframe + core service"
  2. For readability, we move most of our functionality to a service

    Code Block
    languagebash
    titleTerminal
    ng g s eo-custom-client/services/location --spec false


  3. To keep the code organized we introduce LocationType enum.

    Code Block
    languagebash
    titleTerminal
    ng g enum eo-custom-client/enum/LocationType


  4. Update locationType.ts

    Code Block
    languagejs
    titlelocationTypes.rs
    linenumberstrue
    export enum LocationType {
      COORDS = 'coords',
      ADDRESS = 'address',
      EMPTY = 'empty'
    }


  5. To keep the code organized we introduce a Interfaces for the Locations.

    Code Block
    languagebash
    titleTerminal
    ng g interface eo-custom-client/interfaces/Location


  6. Update location.interface.ts

    Code Block
    languagejs
    titlelocation.interface.ts
    linenumberstrue
    export interface LocationByAddress {
      type: string;
      streethw: string;
      townhw: string;
      countryhw: string
    }
    
    export interface LocationByCoords {
      type: string;
      photogpsla: number;
      photogpslo: number;
    }
    
    export interface NoLocation {
      type: string;
    }


  7. Final location.service.ts. Check that the normalize function matches your scheme properties!

    Code Block
    languagejs
    titlelocation.service.ts
    linenumberstrue
    import {Injectable} from '@angular/core';
    import {LocationType} from '../enumsenum/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}`);
      }
    }
    
    


  8. Since we want to hide the Map when we don't have any Location information, we need to update the map.component.html.

    Code Block
    languagexml
    titlemap-frame.component.html
    linenumberstrue
    <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>


  9. As a last step in the TS files we need to update map-frame.component.ts

    Code Block
    languagejs
    titlemap-frame.component.ts
    linenumberstrue
    import {
      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));
      }
    }
    
    


  10. To finish this plugin, add some styling.

    Code Block
    languagecss
    titlemap-frame.component.scss
    linenumberstrue
    .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)
    Image RemovedImage Removed
    
    	}
      }
    }
    
    


Example 3: Personal Cover Sheet for object state

Description

This example creates a button that will show custom information in the information area. We will be able to see the ID picture of the selected person, as well as an icon if the person is currently on leave. Clicking the icon will select the leave application in the structure tree.

Image AddedImage Added


Implementation

  1. Use a CLI command to generate a plugin component with name personal-cover.

    Code Block
    languagebash
    linenumberstrue
    eo g plugin personal-cover


  2. Update personal-cover.component.ts to load ID picture and all leave applications, and check if employee is on holidays.

    Code Block
    languagejs
    titlepersonal-cover.component.ts
    linenumberstrue
    import {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);
            }
          });
      }
    
    }
    
    


  3. Update personal-cover.component.html to include a media component & holiday icon.

    Code Block
    languagexml
    titlepersonal-cover.component.html
    linenumberstrue
    <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">&nbsp;{{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>
    
    


  4. To make it look nice we add some styles to indicate active holidays.

    Code Block
    languagecss
    titlepersonal-cover.component.scss
    linenumberstrue
    :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;
        }
      }
    }
    
    


  5. Use a CLI command to generate labels/translations

    Code Block
    languagebash
    linenumberstrue
    eo g label eo.custom.plugin.personal-cover --en "Cover sheet" --de "Aktendeckel"