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 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>;
}
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 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
.
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);
}
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);
});
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:
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);
}
});
}
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. |