App Sync service

Introduction

DcpSync is a collection of services that gives DCP Applications option for communication. The implementation was created to make communication between Top Navigation, Side Menu and SAW applications easy. The problem is that the apps need to communicate in realtime. Chaining communication between them as previously used by Angular applications is not working. There Angular applications like Login, Administration, MVDA, etc. are using localStorageSyncReducer to reload data that previous application save in localStorage.

DCP Sync Model

DCP Sync Model is situated in DcShared module.

export interface DcpSyncModel {
  userSettings: DcpSyncEl<{
      timeFormat: string;
      dateFormat: string;
  }>;
  siteContext: DcpSyncEl<number>;
  siteTimeZoneName: DcpSyncEl<string>;
  activeModules: DcpSyncEl<ActiveModulesModel[]>;
  accessToken: DcpSyncEl<string>;
  userName: DcpSyncEl<string>;
  userDisplayName: DcpSyncEl<string>;
  userPermissions: DcpSyncEl<UserPermissionModel[]>;
  userUid: DcpSyncEl<string>;
  isGlobalAdmin: DcpSyncEl<boolean>;
  forceCustomModal: DcpSyncEl<{
    show: false,
    data: {}
  }>;
  language: DcpSyncEl<string>;
  showAlert: DcpSyncEl<{type: MessagesTypeEnum, text: string}>;
  alertOnAppChange: DcpSyncEl<boolean>;
  siteSelectionEnable: DcpSyncEl<boolean>;
  addLoadingKey: DcpSyncEl<string>;
  removeLoadingKey: DcpSyncEl<string>;
  appStatus: DcpSyncEl<{ appName: string; isValidated: boolean }>;
  gxpScopeData: DcpSyncEl<(boolean | undefined)[]>;
  currentUrl: DcpSyncEl<string>;
  userSitesList: DcpSyncEl<UserSiteModel[]>;
}

export interface DcpSyncEl<T> {
    update(value: T): void;
    getCurrent(): T;
    data(): Observable<T>;
}

Base Application

Implementation starts in Base application where we create object with AppSyncService as a value for attributes attached to window.dcpSync

classDiagram
class AppSyncService {
    -_bs: BehaviorSubject 
    -data$ Observable
    +data(): Observable
    +update(data: any): void
    +getCurrent(): any
    -hasChange(): boolean
}
window.dcpSync = {
    userSettings: new AppSyncService({
        timeFormat: 'hh:mm:ss a',
        dateFormat: 'MMM dd, yyyy'
    }),
    accessToken: new AppSyncService(''),
    userName: new AppSyncService(''),
    userDisplayName: new AppSyncService(''),
    // .....
}

DCP Sync Base Class

DCP Sync Base Class is a typescript class created to be extended by Service created specifically for Angular or Vue.

classDiagram
class DcpSyncBase {
    +dcpSync: DcpSyncModel;
    +clearSubscriptions(): void;
    +_subscribeTo...(calback): void; 
    +_update...(value): void; 
}

Inside you can find DcpSyncBase.dcpSync which will gives you direct access to window.dcpSync.

Also, there is some ready to use _subscribeTo.. and _update... methods like _updateCurrentUrl or _subscribeToAccessToken.

Angular and VueJS DcpSyncService

Both Angular and VueJS have implementation of DcpSynService that extend DcpSyncBase Class.

classDiagram

direction LR

class DcpSyncBase {
    +dcpSync: DcpSyncModel;
    +clearSubscriptions(): void;
    +_subscribeTo...(calback): void; 
    +_update...(value): void; 
}


class DcpSyncService {
    +subscribeTo...(): void; 
    +update...(value): void; 
    +get...(): void; 
}

DcpSyncBase --|> DcpSyncService

The differences in Angular and Vue implementation of DcpSyncService is based on use cases and Framework specifics.

Inside Angular service we need to run ngZone in some cases, so ngZone to be aware of the change that we make.

subscribeToSiteContext() {
    this._subscribeToSiteContext(siteContext => {
        this.ngZone.run(() => {
            this.store.dispatch(new ChangeSiteContextSuccess(siteContext)); 
        });
    });
}

subscribeToShowAlert() {
    this._subscribeToShowAlert(showAlert => {
        if (showAlert.type && showAlert.text) {
            this.ngZone.run(() => {
                this.store.dispatch(new SetMessage(showAlert.type, showAlert.text));
            });
            this._updateShowAlert({type: MessagesTypeEnum.Success, text: ''});
        }
    });
}

Vue

subscribeToAccessToken(callback: (token: string) => void) {
    this._subscribeToAccessToken(token => {
        callback(token);
    });
}
updateAppStatus() {
    const appStatus = {
        appName: process.env.VUE_APP_IS_APP_NAME,
        isValidated: process.env.VUE_APP_IS_APP_VALIDATED === 'true'
    };
    this._updateAppStatus(appStatus);
}

Example Usage

Access Token and User Information

Top Navigation app.component.ts

constructor(
    private readonly dcpSync: DcpSyncService
) {}

this.subscription.add(
    this.store.pipe(select(state => state.user.current)).subscribe(user => {
        this.dcpSync.updateAccessToken(user.access_token);
        this.dcpSync.dcpSync.userPermissions.update(user.permissions);
    })
);

SAW App.vue

private readonly syncService = new DcpSyncService();
userStore = useUserStore();
permissions = usePermissions();

this.syncService.subscribeToAccessToken((token: string) => {
  this.userStore.updateToken(token);
});

this.syncService.dcpSync.userPermissions.data().subscribe(permissions => {
  this.permissions.setPermissions(permissions);
});

URL Change

This is needed because on the screen we have 3 applications and if you change the Routing with one of them it is use same application Router. This way the change stays inside the application scope and other loaded applications are not aware of that.

Example flow:

  1. You navigate to SAW Analyses with Side Menu button it will load the correct SAW page.
  2. Then you use SAW button to open some element in edit mode.
  3. Finally, you try to return to SAW Analyses page by again clicking on Side Menu button.

On the last step nothing happens, because last information that Angular Router in Side Menu have, is the same url.

Side Menu and Top Navigation implementation

app.component.ts

constructor(
    private readonly dcpSync: DcpSyncService,
    private readonly router: Router,
) {
    router.events.subscribe((event: RouterEvent) => {
        this.navigationInterceptor(event);
    });
}

this.dcpSync.subscribeToCurrentUrl();

navigationInterceptor(event: RouterEvent): void {
    if (event instanceof NavigationEnd) {
        if (event.urlAfterRedirects !== this.dcpSync.dcpSync.currentUrl.getCurrent()) {
            this.dcpSync.updateCurrentUrl(event.urlAfterRedirects);
        }
    }
}

dcp-sync.service.ts

subscribeToCurrentUrl() {
    this._subscribeToCurrentUrl(url => {
        if (url && url !== this.route.snapshot._routerState?.url) {
            this.router.navigate([location.pathname]);
        }
    });
}

updateCurrentUrl(url: string) {
    this._updateCurrentUrl(url);
}

SAW App.vue

private readonly syncService = new DcpSyncService();

watch: {
    $route(to, from) {
        if (to.fullPath !== from.fullPath && to.fullPath !== this.syncService.dcpSync.currentUrl.getCurrent()) {
          this.syncService.updateCurrentUrl(to.fullPath);
        }
    }
}

subscribeToCurrentUrlChange() {
    this.syncService.subscribeToCurrentUrl(url => {
        if (url && this.$route.path !== url) {
          this.$router.push(url);
        }
    });
}

List of implemented AppSyncServices

Attribute Data type Description
accessToken string Current login Access Token
activeModules ActiveModulesModel[] List of Active DCP modules for currently selected Site
addLoadingKey string Add loading key to loading object. More information here
appStatus { appName: string; isValidated: boolean } Give Top Navigation information if loaded main application (e.g. SAW) is validated or not. This is used to calculate correct visualization for Gxp/NonGxp label. More information here
currentUrl string Currently loaded url.
isGlobalAdmin boolean Is current user Global admin or not
gxpScopeData (boolean or undefined)[] Collection of GXP BE states, that are used to calculate correct visualization for Gxp/NonGxp label. More information here
language string Selected DCP language - en/de
removeLoadingKey string Remove loading key from loading object. More information here
siteContext number Currently selected Site ID
siteSelectionEnable boolean Enable and Disable Site Select in Top Navigation
siteTimeZoneName string Selected site TimeZone name. Example: 'Europe/Berlin'
showAlert {type: MessagesTypeEnum, text: string} Used to trigger show allert message in Top Navigation from other applications like SAW. Example: dcpSyncService.updateShowAlert({type: MessagesTypeEnum.Success, text: t('SHARED_LINK_IS_COPIED')});
userSettings {timeFormat: string, dateFormat: string} Current user Date and Time format settings
userName string Current user userName
userDisplayName string Current user full name
userPermissions UserPermissionModel[] List of current user Permissions
userUid string Current user UID
userSitesList UserSiteModel[] List of Current User availbale Sites.
This page was last edited on 03 May 2024, 07:57 (UTC).