/
Custom Plug-in via Tab Container

Custom Plug-in via Tab Container

The CLI component is only working for versions before 9.10. It must be upgraded to Angular 15 as well and will come later.

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.


Implementation

  1. Use a CLI command to generate a plug-in component with name map.

    eo g plugin map
  2. Use a CLI command to install the maps package (takes some time).

    npm install -P @agm/core
  3. Import AgmCoreModule module to CustomPluginsModule.

    custom-plugins.module.ts
    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 the current position from navigator, and modify the matchType property if necessary.

    map.component.ts
    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 the 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>
  6. 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;
      }
    }
  7. 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

Description

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


Implementation

  1. Use a CLI command to generate a plug-in component.

    eo g plugin map-frame
  2. Update map-frame.component.ts to load the current address from the dms object, and modify matchType property if necessary. Make sure that the normalize function matches your scheme properties!

    map-frame.component.ts
    import {
      Component,
      AfterViewInit,
      ViewChild,
      ElementRef,
      Renderer2
    } from '@angular/core';
    import {DmsService, DmsObject, EventService, EnaioEvent, Event} from '@eo-sdk/core';
    // In the following line ', UnsubscribeOnDestroy' has to be removed when updating to version 9.10 or younger:
    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']
    })
    // In the following line 'extends UnsubscribeOnDestroy' has to be removed when updating to version 9.10 or younger:
    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(              // since version 9.10 this pipe has to be removed
            takeUntil(this.componentDestroyed$)
          )
          .subscribe(e => this.eventHandler(e));
      }
    }
    
    
  3. Update map-frame.component.html to include a Google maps iframe based on the address of the selected dms object.

    map-frame.component.html
    <iframe
      #mapFrame
      width="100%"
      height="90%"
      frameborder="0" style="border:0" allowfullscreen>
    </iframe>
  4. 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

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.

(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

    Terminal
    ng g s eo-custom-client/services/location --spec false
  3. To keep the code organized, we introduce LocationType enum.

    Terminal
    ng g enum eo-custom-client/enum/LocationType
  4. Update locationType.ts

    locationTypes.rs
    export enum LocationType {
      COORDS = 'coords',
      ADDRESS = 'address',
      EMPTY = 'empty'
    }
  5. To keep the code organized, we introduce an Interfaces for the Locations.

    Terminal
    ng g interface eo-custom-client/interfaces/Location
  6. Update location.interface.ts

    location.interface.ts
    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. Make sure the normalize function matches your scheme properties!

    location.service.ts
    import {Injectable} from '@angular/core';
    import {LocationType} from '../enum/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.imagegpsla,
          photogpslo: data.imagegpslo,
          ...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 data 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 {
        const normalizedData = this.normalize(data);
        return normalizedData.photogpsla ? 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.

    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>
  9. As a last step in the TS files we need to update map-frame.component.ts

    map-frame.component.ts
    import {
      Component,
      AfterViewInit,
      ViewChild,
      ElementRef,
      Renderer2
    } from '@angular/core';
    import {DmsService, DmsObject, EventService, EnaioEvent, Event} from '@eo-sdk/core';
    // In the following line ', UnsubscribeOnDestroy' has to be removed when updating to version 9.10 or younger:
    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']
    })
    // In the following line 'extends UnsubscribeOnDestroy' has to be removed when updating to version 9.10 or younger:
    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(               // since version 9.10 this pipe has to be removed
              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(              // since version 9.10 this pipe has to be removed
            takeUntil(this.componentDestroyed$)
          )
          .subscribe(e => this.eventHandler(e));
      }
    }
    
    
  10. To finish this plug-in, add some styling. In addition, we are able to display the map frame plugin only per specific types that contain Geo Coordinates or Address Data. 

    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;
    	}
      }
    }
    
    // Display map frame plugin only per specific types that contain Geo Coordinates or Address Data 
    ::ng-deep {
      // by default hide map-frame plugin (tab content & tab label)
      eo-object-details [id="eo.custom.plugin.map-frame"],
      eo-object-details [id="eo.custom.plugin.map-frame-label"] {
        display: none;
      }
    
      // display map frame only per specific types
      eo-object-details[data-type=personalakte] [id="eo.custom.plugin.map-frame"],
      eo-object-details[data-type=personalakte] [id="eo.custom.plugin.map-frame-label"],
    
      eo-object-details[data-type=photobasic] [id="eo.custom.plugin.map-frame"],
      eo-object-details[data-type=photobasic] [id="eo.custom.plugin.map-frame-label"],
    
      eo-object-details[data-type=foto] [id="eo.custom.plugin.map-frame"],
      eo-object-details[data-type=foto] [id="eo.custom.plugin.map-frame-label"],
    
      eo-object-details[data-type=albumphoto] [id="eo.custom.plugin.map-frame"],
      eo-object-details[data-type=albumphoto] [id="eo.custom.plugin.map-frame-label"] {
        display: block;
      }
    }
    

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.


Implementation

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

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

    personal-cover.component.ts
    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 and leave 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">&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 that the user is on leave.

    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;
        }
      }
    }
    
    
  5. Use a CLI command to generate labels/translations

    eo g label eo.custom.plugin.personal-cover --en "Cover sheet" --de "Aktendeckel"

Related content

Custom State via Sidebar and Router
Custom State via Sidebar and Router
More like this
Developing your Individual Client
Developing your Individual Client
Read with this
Custom Link via Sidebar
Custom Link via Sidebar
More like this
Customizing the Client
Customizing the Client
More like this
Custom Action via Object Actions Menu
Custom Action via Object Actions Menu
More like this
Client Plug-in API
Client Plug-in API
More like this