Merge remote-tracking branch 'origin/monitor-admin-library' into angular-16

This commit is contained in:
Konstantinos Triantafyllou 2023-10-24 11:57:24 +03:00
commit 8647136803
27 changed files with 5399 additions and 28 deletions

View File

@ -1,3 +1,5 @@
import {stakeholderTypes} from "../../monitor/entities/stakeholder";
export class User { export class User {
email: string; email: string;
firstname: string; firstname: string;
@ -98,23 +100,11 @@ export class Session {
} }
public static isMonitorCurator(user: User): boolean { public static isMonitorCurator(user: User): boolean {
return this.isCommunityCurator(user) || this.isProjectCurator(user) || this.isFunderCurator(user) || this.isOrganizationCurator(user); return stakeholderTypes.filter(stakeholderType => this.isTypeCurator(stakeholderType.value, user)).length > 0;
} }
public static isCommunityCurator(user: User): boolean { public static isCommunityCurator(user: User): boolean {
return this.isTypeCurator("Community", user); return this.isTypeCurator("community", user);
}
public static isFunderCurator(user: User): boolean {
return this.isTypeCurator("Funder", user);
}
public static isProjectCurator(user: User): boolean {
return this.isTypeCurator("Project", user);
}
public static isOrganizationCurator(user: User): boolean {
return this.isTypeCurator("Institution", user);
} }
private static isTypeCurator(type: string, user: User): boolean { private static isTypeCurator(type: string, user: User): boolean {
@ -122,16 +112,7 @@ export class Session {
} }
public static isCurator(type: string, user: User): boolean { public static isCurator(type: string, user: User): boolean {
if (type == 'funder') { return stakeholderTypes.find(stakeholderType => stakeholderType.value == type) && this.isTypeCurator(type, user);
return user && this.isFunderCurator(user);
} else if (type == 'ri' || type == 'community') {
return user && this.isCommunityCurator(user);
} else if (type == 'organization' || type == 'institution') {
return user && this.isOrganizationCurator(user);
} else if (type == 'project') {
return user && this.isProjectCurator(user);
}
return false;
} }
public static isPortalAdministrator(user: User): boolean { public static isPortalAdministrator(user: User): boolean {

View File

@ -0,0 +1,10 @@
.uk-border-circle {
width: 100px;
height: 100px;
position: relative;
& > img {
max-width: 64px;
max-height: 64px;
}
}

View File

@ -0,0 +1,443 @@
import {Component, Input, OnDestroy, ViewChild} from "@angular/core";
import {Stakeholder} from "../../../monitor/entities/stakeholder";
import {UntypedFormBuilder, UntypedFormGroup, Validators} from "@angular/forms";
import {StakeholderUtils} from "../../utils/indicator-utils";
import {Option} from "../../../sharedComponents/input/input.component";
import {Subscription} from "rxjs";
import {EnvProperties} from "../../../utils/properties/env-properties";
import {properties} from "src/environments/environment";
import {StakeholderService} from "../../../monitor/services/stakeholder.service";
import {UtilitiesService} from "../../../services/utilities.service";
import {Role, Session, User} from "../../../login/utils/helper.class";
import {UserManagementService} from "../../../services/user-management.service";
import {StringUtils} from "../../../utils/string-utils.class";
import {NotifyFormComponent} from "../../../notifications/notify-form/notify-form.component";
import {NotificationUtils} from "../../../notifications/notification-utils";
import {Notification} from "../../../notifications/notifications";
import {NotificationHandler} from "../../../utils/notification-handler";
import {StatsProfilesService} from "../../utils/services/stats-profiles.service";
@Component({
selector: 'edit-stakeholder',
template: `
<form *ngIf="stakeholderFb" [formGroup]="stakeholderFb">
<div class="uk-grid uk-grid-large" uk-grid>
<div class="uk-width-1-2@m">
<div input id="name" [formInput]="stakeholderFb.get('name')"
placeholder="Name"></div>
</div>
<div class="uk-width-1-2@m">
<div input [formInput]="stakeholderFb.get('alias')"
placeholder="URL Alias"></div>
</div>
<div class="uk-width-1-3@m">
<div input [formInput]="stakeholderFb.get('index_id')"
placeholder="Index ID"></div>
</div>
<div class="uk-width-1-3@m">
<div input [formInput]="stakeholderFb.get('index_name')"
placeholder="Index Name"></div>
</div>
<div class="uk-width-1-3@m">
<div input [formInput]="stakeholderFb.get('index_shortName')"
placeholder="Index Short Name"></div>
</div>
<ng-container *ngIf="isCurator">
<div class="uk-width-1-3@m">
<div *ngIf="statsProfiles" input [formInput]="stakeholderFb.get('statsProfile')" [type]="'select'"
[options]="statsProfiles"
placeholder="Stats Profile"></div>
</div>
<div class="uk-width-1-3@m">
<div input [formInput]="stakeholderFb.get('projectUpdateDate')" [type]="'date'"
placeholder="Last Project Update"></div>
</div>
</ng-container>
<div class="uk-width-1-3@m">
<div input [formInput]="stakeholderFb.get('locale')" [type]="'select'"
[options]="stakeholderUtils.locales"
placeholder="Locale"></div>
</div>
<div class="uk-width-1-1">
<div input [type]="'textarea'" placeholder="Description"
[rows]="4" [formInput]="stakeholderFb.get('description')"></div>
</div>
<div class="uk-width-1-1">
<input #file id="photo" type="file" class="uk-hidden" (change)="fileChangeEvent($event)"/>
<div *ngIf="!stakeholderFb.get('isUpload').value" class="uk-grid uk-grid-column-large" uk-grid>
<div class="uk-margin-top uk-width-auto@l uk-width-1-1">
<div class="uk-grid uk-grid-column-large uk-flex-middle" uk-grid>
<div class="uk-width-auto@l uk-width-1-1 uk-flex uk-flex-center">
<button class="uk-button uk-button-primary uk-flex uk-flex-middle uk-flex-wrap"
(click)="file.click()">
<icon name="cloud_upload" [flex]="true"></icon>
<span class="uk-margin-small-left">Upload a file</span>
</button>
</div>
<div class="uk-text-center uk-text-bold uk-width-expand">
OR
</div>
</div>
</div>
<div input class="uk-width-expand" type="logoURL" [placeholder]="'Link to the logo'"
[formInput]="stakeholderFb.get('logoUrl')"></div>
</div>
<div *ngIf="stakeholderFb.get('isUpload').value" class="uk-width-1-1 uk-flex uk-flex-middle">
<div class="uk-card uk-card-default uk-text-center uk-border-circle">
<img class="uk-position-center uk-blend-multiply" [src]="photo">
</div>
<div class="uk-margin-left">
<button (click)="remove()" class="uk-button-danger uk-icon-button uk-icon-button-small">
<icon [flex]="true" ratio="0.8" name="delete"></icon>
</button>
</div>
<div class="uk-margin-small-left">
<button class="uk-button-secondary uk-icon-button uk-icon-button-small"
(click)="file.click()">
<icon [flex]="true" ratio="0.8" name="edit"></icon>
</button>
</div>
</div>
<!-- Full width error message -->
<div *ngIf="uploadError" class="uk-text-danger uk-margin-small-top uk-width-1-1">{{uploadError}}</div>
</div>
<div [class]="canChooseTemplate ? 'uk-width-1-3@m' : 'uk-width-1-2@m'">
<div input [formInput]="stakeholderFb.get('visibility')"
[placeholder]="'Select a status'"
[options]="stakeholderUtils.statuses" type="select"></div>
</div>
<div [class]="canChooseTemplate ? 'uk-width-1-3@m' : 'uk-width-1-2@m'">
<div input [formInput]="stakeholderFb.get('type')"
[placeholder]="'Select a type'"
[options]="types" type="select"></div>
</div>
<ng-container *ngIf="canChooseTemplate">
<div class="uk-width-1-3@m">
<div [placeholder]="'Select a template'"
input [formInput]="stakeholderFb.get('defaultId')"
[options]="defaultStakeholdersOptions" type="select"></div>
</div>
</ng-container>
</div>
</form>
<div #notify [class.uk-hidden]="!stakeholderFb" notify-form
class="uk-width-1-1 uk-margin-large-top uk-margin-medium-bottom"></div>
`,
styleUrls: ['edit-stakeholder.component.less']
})
export class EditStakeholderComponent implements OnDestroy {
@Input()
public disableAlias: boolean = false;
public stakeholderFb: UntypedFormGroup;
public secure: boolean = false;
public stakeholderUtils: StakeholderUtils = new StakeholderUtils();
public defaultStakeholdersOptions: Option[];
public defaultStakeholders: Stakeholder[];
public alias: string[];
public stakeholder: Stakeholder;
public isDefault: boolean;
public isNew: boolean;
public loading: boolean = false;
public types: Option[];
public statsProfiles: string[];
public properties: EnvProperties = properties;
private subscriptions: any[] = [];
/**
* Photo upload
* */
public file: File;
public photo: string | ArrayBuffer;
public uploadError: string;
public deleteCurrentPhoto: boolean = false;
private maxsize: number = 200 * 1024;
user: User;
@ViewChild('notify', {static: true}) notify: NotifyFormComponent;
private notification: Notification;
constructor(private fb: UntypedFormBuilder,
private stakeholderService: StakeholderService,
private statsProfileService: StatsProfilesService,
private utilsService: UtilitiesService, private userManagementService: UserManagementService,) {
}
ngOnDestroy() {
this.reset();
}
public init(stakeholder: Stakeholder, alias: string[], defaultStakeholders: Stakeholder[], isDefault: boolean, isNew: boolean) {
this.reset();
this.deleteCurrentPhoto = false;
this.stakeholder = stakeholder;
this.alias = alias;
this.defaultStakeholders = defaultStakeholders;
this.isDefault = isDefault;
this.isNew = isNew;
this.subscriptions.push(this.userManagementService.getUserInfo().subscribe(user => {
this.user = user;
if (this.isCurator) {
this.subscriptions.push(this.statsProfileService.getStatsProfiles().subscribe(statsProfiles => {
this.statsProfiles = statsProfiles;
}, error => {
this.statsProfiles = [];
}));
} else {
this.statsProfiles = [];
}
this.types = this.stakeholderUtils.getTypesByUserRoles(this.user, this.stakeholder.alias);
this.stakeholderFb = this.fb.group({
_id: this.fb.control(this.stakeholder._id),
defaultId: this.fb.control(this.stakeholder.defaultId),
name: this.fb.control(this.stakeholder.name, Validators.required),
description: this.fb.control(this.stakeholder.description),
index_name: this.fb.control(this.stakeholder.index_name, Validators.required),
index_id: this.fb.control(this.stakeholder.index_id, Validators.required),
index_shortName: this.fb.control(this.stakeholder.index_shortName, Validators.required),
statsProfile: this.fb.control(this.stakeholder.statsProfile, Validators.required),
locale: this.fb.control(this.stakeholder.locale, Validators.required),
projectUpdateDate: this.fb.control(this.stakeholder.projectUpdateDate),
creationDate: this.fb.control(this.stakeholder.creationDate),
alias: this.fb.control(this.stakeholder.alias,
[
Validators.required,
this.stakeholderUtils.aliasValidatorString(
this.alias.filter(alias => alias !== this.stakeholder.alias)
)]
),
isDefault: this.fb.control((this.isDefault)),
visibility: this.fb.control(this.stakeholder.visibility, Validators.required),
type: this.fb.control(this.stakeholder.type, Validators.required),
topics: this.fb.control(this.stakeholder.topics),
isUpload: this.fb.control(this.stakeholder.isUpload),
logoUrl: this.fb.control(this.stakeholder.logoUrl),
});
if (this.stakeholder.isUpload) {
this.stakeholderFb.get('logoUrl').clearValidators();
this.stakeholderFb.get('logoUrl').updateValueAndValidity();
} else {
this.stakeholderFb.get('logoUrl').setValidators([StringUtils.urlValidator()]);
this.stakeholderFb.get('logoUrl').updateValueAndValidity();
}
this.subscriptions.push(this.stakeholderFb.get('isUpload').valueChanges.subscribe(value => {
if (value == true) {
this.stakeholderFb.get('logoUrl').clearValidators();
this.stakeholderFb.updateValueAndValidity();
} else {
this.stakeholderFb.get('logoUrl').setValidators([StringUtils.urlValidator()]);
this.stakeholderFb.updateValueAndValidity();
}
}));
this.secure = (!this.stakeholderFb.get('logoUrl').value || this.stakeholderFb.get('logoUrl').value.includes('https://'));
this.subscriptions.push(this.stakeholderFb.get('logoUrl').valueChanges.subscribe(value => {
this.secure = (!value || value.includes('https://'));
}));
this.initPhoto();
this.subscriptions.push(this.stakeholderFb.get('type').valueChanges.subscribe(value => {
this.onTypeChange(value, defaultStakeholders);
}));
this.stakeholderFb.setControl('defaultId', this.fb.control(stakeholder.defaultId, (this.isDefault && !this.isNew)?[]:Validators.required));
if (!this.isNew) {
this.notification = NotificationUtils.editStakeholder(this.user.firstname + ' ' + this.user.lastname, this.stakeholder.name);
this.notify.reset(this.notification.message);
if (this.isAdmin) {
if (this.disableAlias) {
setTimeout(() => {
this.stakeholderFb.get('alias').disable();
}, 0);
}
} else {
if (!this.isCurator) {
setTimeout(() => {
this.stakeholderFb.get('statsProfile').disable();
}, 0);
}
setTimeout(() => {
this.stakeholderFb.get('alias').disable();
this.stakeholderFb.get('index_id').disable();
this.stakeholderFb.get('index_name').disable();
this.stakeholderFb.get('index_shortName').disable();
}, 0);
}
setTimeout(() => {
this.stakeholderFb.get('type').disable();
}, 0);
} else {
this.notification = NotificationUtils.createStakeholder(this.user.firstname + ' ' + this.user.lastname);
this.notify.reset(this.notification.message);
setTimeout(() => {
this.stakeholderFb.get('type').enable();
}, 0);
}
}));
}
public get isAdmin() {
return Session.isPortalAdministrator(this.user);
}
public get isCurator() {
return this.stakeholder && (this.isAdmin || Session.isCurator(this.stakeholder.type, this.user));
}
public get disabled(): boolean {
return (this.stakeholderFb && this.stakeholderFb.invalid) ||
(this.stakeholderFb && this.stakeholderFb.pristine && !this.isNew && !this.file) ||
(this.uploadError && this.uploadError.length > 0);
}
public get dirty(): boolean {
return this.stakeholderFb && this.stakeholderFb.dirty;
}
public get canChooseTemplate(): boolean {
return this.isNew && this.stakeholderFb.get('type').valid && !!this.defaultStakeholdersOptions;
}
reset() {
this.uploadError = null;
this.stakeholderFb = null;
this.subscriptions.forEach(subscription => {
if (subscription instanceof Subscription) {
subscription.unsubscribe();
}
});
}
onTypeChange(value, defaultStakeholders: Stakeholder[]) {
this.stakeholderFb.setControl('defaultId', this.fb.control(this.stakeholder.defaultId, (this.isDefault && !this.isNew)?[]:Validators.required));
this.defaultStakeholdersOptions = [{
label: 'New blank profile',
value: '-1'
}];
defaultStakeholders.filter(stakeholder => stakeholder.type === value).forEach(stakeholder => {
this.defaultStakeholdersOptions.push({
label: 'Use ' + stakeholder.name + ' profile',
value: stakeholder._id
})
});
}
public save(callback: Function, errorCallback: Function = null) {
this.loading = true;
if (this.file) {
this.subscriptions.push(this.utilsService.uploadPhoto(this.properties.utilsService + "/upload/" + encodeURIComponent(this.stakeholderFb.getRawValue().type) + "/" + encodeURIComponent(this.stakeholderFb.getRawValue().alias), this.file).subscribe(res => {
this.deletePhoto();
this.stakeholderFb.get('logoUrl').setValue(res.filename);
this.removePhoto();
this.saveStakeholder(callback, errorCallback);
}, error => {
this.uploadError = "An error has been occurred during upload your image. Try again later";
this.saveStakeholder(callback, errorCallback);
}));
} else if (this.deleteCurrentPhoto) {
this.deletePhoto();
this.saveStakeholder(callback, errorCallback);
} else {
this.saveStakeholder(callback, errorCallback);
}
}
public saveStakeholder(callback: Function, errorCallback: Function = null) {
if (this.isNew) {
let defaultStakeholder = this.defaultStakeholders.find(value => value._id === this.stakeholderFb.getRawValue().defaultId);
this.stakeholderFb.setValue(this.stakeholderUtils.createFunderFromDefaultProfile(this.stakeholderFb.getRawValue(),
(defaultStakeholder ? defaultStakeholder.topics : []), this.stakeholderFb.getRawValue().isDefault));
this.removePhoto();
if(this.stakeholderFb.getRawValue().isDefault) {
this.stakeholderFb.get('defaultId').setValue(null);
}
this.subscriptions.push(this.stakeholderService.buildStakeholder(this.properties.monitorServiceAPIURL,
this.stakeholderFb.getRawValue()).subscribe(stakeholder => {
this.notification.entity = stakeholder._id;
this.notification.stakeholder = stakeholder.alias;
this.notification.stakeholderType = stakeholder.type;
this.notification.groups = [Role.curator(stakeholder.type)];
this.notify.sendNotification(this.notification);
NotificationHandler.rise(stakeholder.name + ' has been <b>successfully created</b>');
callback(stakeholder);
this.loading = false;
}, error => {
NotificationHandler.rise('An error has occurred. Please try again later', 'danger');
if (errorCallback) {
errorCallback(error)
}
this.loading = false;
}));
} else {
this.subscriptions.push(this.stakeholderService.saveElement(this.properties.monitorServiceAPIURL, this.stakeholderFb.getRawValue()).subscribe(stakeholder => {
this.notification.entity = stakeholder._id;
this.notification.stakeholder = stakeholder.alias;
this.notification.stakeholderType = stakeholder.type;
this.notification.groups = [Role.curator(stakeholder.type), Role.manager(stakeholder.type, stakeholder.alias)];
this.notify.sendNotification(this.notification);
NotificationHandler.rise(stakeholder.name + ' has been <b>successfully saved</b>');
callback(stakeholder);
this.loading = false;
}, error => {
NotificationHandler.rise('An error has occurred. Please try again later', 'danger');
if (errorCallback) {
errorCallback(error)
}
this.loading = false;
}));
}
}
fileChangeEvent(event) {
if (event.target.files && event.target.files[0]) {
this.file = event.target.files[0];
if (this.file.type !== 'image/png' && this.file.type !== 'image/jpeg') {
this.uploadError = 'You must choose a file with type: image/png or image/jpeg!';
this.stakeholderFb.get('isUpload').setValue(false);
this.stakeholderFb.get('isUpload').markAsDirty();
this.removePhoto();
} else if (this.file.size > this.maxsize) {
this.uploadError = 'File exceeds size\'s limit! Maximum resolution is 256x256 pixels.';
this.stakeholderFb.get('isUpload').setValue(false);
this.stakeholderFb.get('isUpload').markAsDirty();
this.removePhoto();
} else {
this.uploadError = null;
const reader = new FileReader();
reader.readAsDataURL(this.file);
reader.onload = () => {
this.photo = reader.result;
this.stakeholderFb.get('isUpload').setValue(true);
this.stakeholderFb.get('isUpload').markAsDirty();
};
}
}
}
initPhoto() {
if (this.stakeholderFb.getRawValue().isUpload) {
this.photo = this.properties.utilsService + "/download/" + this.stakeholderFb.get('logoUrl').value;
}
}
removePhoto() {
if (this.file) {
if (typeof document != 'undefined') {
(<HTMLInputElement>document.getElementById("photo")).value = "";
}
this.initPhoto();
this.file = null;
}
}
remove() {
this.stakeholderFb.get('isUpload').setValue(false);
this.stakeholderFb.get('isUpload').markAsDirty();
this.removePhoto();
this.stakeholderFb.get('logoUrl').setValue(null);
if (this.stakeholder.isUpload) {
this.deleteCurrentPhoto = true;
}
}
public deletePhoto() {
if (this.stakeholder.logoUrl && this.stakeholder.isUpload) {
this.subscriptions.push(this.utilsService.deletePhoto(this.properties.utilsService + '/delete/' +
encodeURIComponent(this.stakeholder.type) + "/" + encodeURIComponent(this.stakeholder.alias) + "/" + this.stakeholder.logoUrl).subscribe());
}
}
}

View File

@ -0,0 +1,14 @@
import {NgModule} from "@angular/core";
import {EditStakeholderComponent} from "./edit-stakeholder.component";
import {CommonModule} from "@angular/common";
import {InputModule} from "../../../sharedComponents/input/input.module";
import {ReactiveFormsModule} from "@angular/forms";
import {IconsModule} from "../../../utils/icons/icons.module";
import {NotifyFormModule} from "../../../notifications/notify-form/notify-form.module";
@NgModule({
imports: [CommonModule, InputModule, ReactiveFormsModule, IconsModule, NotifyFormModule],
declarations: [EditStakeholderComponent],
exports: [EditStakeholderComponent]
})
export class EditStakeholderModule {}

View File

@ -0,0 +1,19 @@
import {NgModule} from '@angular/core';
import {RouterModule} from '@angular/router';
import {PreviousRouteRecorder} from '../../utils/piwik/previousRouteRecorder.guard';
import {GeneralComponent} from "./general.component";
@NgModule({
imports: [
RouterModule.forChild([
{
path: '',
component: GeneralComponent,
canDeactivate: [PreviousRouteRecorder],
data: {hasSidebar: true}
}
])
]
})
export class GeneralRoutingModule {
}

View File

@ -0,0 +1,29 @@
<div page-content>
<div actions>
<sidebar-mobile-toggle class="uk-margin-top uk-hidden@m uk-display-block"></sidebar-mobile-toggle>
<div class="uk-section-xsmall uk-container uk-margin-top">
<div class="uk-flex uk-flex-center uk-flex-right@m">
<button class="uk-button uk-button-default uk-margin-right"
(click)="reset()" [class.uk-disabled]="loading || !editStakeholderComponent.dirty"
[disabled]="loading || !editStakeholderComponent.dirty">Reset
</button>
<button class="uk-button uk-button-primary" [class.uk-disabled]="loading || editStakeholderComponent.disabled"
(click)="save()" [disabled]="loading || editStakeholderComponent.disabled">Save
</button>
</div>
</div>
</div>
<div inner>
<div *ngIf="stakeholder" class="uk-container">
<div class="uk-position-relative" style="min-height: 60vh">
<div [class.uk-hidden]="loading" class="uk-section uk-section-small">
<edit-stakeholder #editStakeholderComponent [disableAlias]="true"></edit-stakeholder>
</div>
<div *ngIf="loading" class="uk-position-center">
<loading *ngIf="loading"></loading>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,75 @@
import {ChangeDetectorRef, Component, OnDestroy, OnInit, ViewChild} from "@angular/core";
import {StakeholderService} from "../../monitor/services/stakeholder.service";
import {EnvProperties} from "../../utils/properties/env-properties";
import {Stakeholder} from "../../monitor/entities/stakeholder";
import { Subscription, zip} from "rxjs";
import {EditStakeholderComponent} from "./edit-stakeholder/edit-stakeholder.component";
import {properties} from "src/environments/environment";
import {Title} from "@angular/platform-browser";
@Component({
selector: 'general',
templateUrl: "./general.component.html"
})
export class GeneralComponent implements OnInit, OnDestroy {
public stakeholder: Stakeholder;
public alias: string[];
public properties: EnvProperties = properties;
public defaultStakeholders: Stakeholder[];
public loading: boolean = false;
private subscriptions: any[] = [];
@ViewChild('editStakeholderComponent') editStakeholderComponent: EditStakeholderComponent;
constructor(private stakeholderService: StakeholderService,
private cdr: ChangeDetectorRef,
private title: Title) {
}
ngOnInit() {
this.loading = true;
this.subscriptions.push(this.stakeholderService.getStakeholderAsObservable().subscribe(stakeholder => {
this.stakeholder = stakeholder;
this.cdr.detectChanges();
if(this.stakeholder) {
this.title.setTitle(this.stakeholder.name + " | General");
let data = zip(
this.stakeholderService.getDefaultStakeholders(this.properties.monitorServiceAPIURL),
this.stakeholderService.getAlias(this.properties.monitorServiceAPIURL)
);
this.subscriptions.push(data.subscribe(res => {
this.defaultStakeholders = res[0];
this.alias = res[1];
this.reset();
this.loading = false;
}));
}
}));
}
public reset() {
this.editStakeholderComponent.init(this.stakeholder, this.alias, this.defaultStakeholders, this.stakeholder.defaultId == null, false)
}
public save() {
this.loading = true;
this.editStakeholderComponent.save((stakeholder) => {
this.stakeholder = stakeholder;
this.stakeholderService.setStakeholder(this.stakeholder);
this.reset();
this.loading = false;
}, (error) => {
console.error(error);
this.loading = false;
});
}
ngOnDestroy() {
this.subscriptions.forEach(subscription => {
if(subscription instanceof Subscription) {
subscription.unsubscribe();
}
});
}
}

View File

@ -0,0 +1,40 @@
import {NgModule} from "@angular/core";
import {GeneralComponent} from "./general.component";
import {GeneralRoutingModule} from "./general-routing.module";
import {PreviousRouteRecorder} from "../../utils/piwik/previousRouteRecorder.guard";
import {CommonModule} from "@angular/common";
import {RouterModule} from "@angular/router";
import {InputModule} from "../../sharedComponents/input/input.module";
import {LoadingModule} from "../../utils/loading/loading.module";
import {AlertModalModule} from "../../utils/modal/alertModal.module";
import {ReactiveFormsModule} from "@angular/forms";
import {EditStakeholderModule} from "./edit-stakeholder/edit-stakeholder.module";
import {PageContentModule} from "../../dashboard/sharedComponents/page-content/page-content.module";
import {LogoUrlPipeModule} from "../../utils/pipes/logoUrlPipe.module";
import {
SidebarMobileToggleModule
} from "../../dashboard/sharedComponents/sidebar/sidebar-mobile-toggle/sidebar-mobile-toggle.module";
@NgModule({
declarations: [GeneralComponent],
imports: [
GeneralRoutingModule,
CommonModule,
RouterModule,
InputModule,
LoadingModule,
AlertModalModule,
ReactiveFormsModule,
EditStakeholderModule,
PageContentModule,
LogoUrlPipeModule,
SidebarMobileToggleModule
],
providers: [
PreviousRouteRecorder,
],
exports: [GeneralComponent]
})
export class GeneralModule {
}

View File

@ -0,0 +1,490 @@
<div *ngIf="stakeholder && canEdit" class="uk-section">
<div *ngIf="numberSections">
<h5 class="uk-text-bold">Number Indicators</h5>
<div class="uk-grid uk-grid-large uk-child-width-1-1" uk-grid>
<div *ngFor="let number of numbers; let i=index">
<div class="section">
<div class="tools">
<div class="uk-flex uk-flex-middle">
<a [class.uk-disabled]="editing" class="" (click)="createSection(i, 'number')"
uk-tooltip="Create a new section">
<icon name="add" [flex]="true"></icon>
</a>
<a *ngIf="!number.defaultId" [attr.uk-tooltip]="'Delete section'"
(click)="deleteSectionOpen(number, i, 'number', 'delete')">
<icon name="close" [flex]="true"></icon>
</a>
<!-- <ng-container *ngIf="!stakeholder.defaultId">-->
<!-- <button [disabled]="editing || number.defaultId " class="md-btn md-btn-mini"-->
<!-- [title]="(number.defaultId?'Default sections cannot be deleted':'Delete all related sections')"-->
<!-- (click)="deleteSectionOpen(number, i, 'number', 'delete')"><i class="material-icons">highlight_off</i>-->
<!-- </button>-->
<!-- <button [disabled]="editing || number.defaultId " class="md-btn md-btn-mini"-->
<!-- [title]="(number.defaultId?'Default sections cannot be deleted':'Delete section and disconnect related')"-->
<!-- (click)="deleteSectionOpen(number, i, 'number', 'disconnect')"><i class="material-icons">link_off</i>-->
<!-- </button>-->
<!-- </ng-container>-->
</div>
</div>
<div *ngIf="numberSections.at(i)" class="uk-margin-medium-bottom">
<div input [formInput]="numberSections.at(i).get('title')"
(focusEmitter)="saveSection($event, numberSections.at(i), i, 'number')"
class="uk-width-1-3@m uk-width-1-1" placeholder="Title" inputClass="border-bottom"></div>
</div>
<div [id]="'number-' + number._id" class="uk-grid uk-grid-small uk-grid-match" uk-sortable="group: number" uk-grid>
<ng-template ngFor [ngForOf]="number.indicators" let-indicator let-j="index">
<div *ngIf="indicator" [id]="indicator._id"
[ngClass]="getNumberClassBySize(indicator.width)">
<div class="uk-card uk-card-default uk-padding-small number-card uk-position-relative">
<div *ngIf="!dragging"
class="uk-position-top-right uk-margin-small-right uk-margin-small-top">
<a class="uk-link-reset uk-flex uk-flex-middle" [class.uk-disabled]="editing">
<icon [flex]="true" [name]="stakeholderUtils.visibilityIcon.get(indicator.visibility)" ratio="0.6"></icon>
<icon [flex]="true" name="more_vert"></icon>
</a>
<div #element class="uk-dropdown" uk-dropdown="mode: click; pos: bottom-left; offset: 5; delay-hide: 0">
<ul class="uk-nav uk-dropdown-nav">
<ng-container *ngIf="isCurator">
<li><a (click)="editNumberIndicatorOpen(number, indicator._id); hide(element)">Edit</a></li>
<li class="uk-nav-divider"></li>
</ng-container>
<ng-template ngFor [ngForOf]="stakeholderUtils.visibility" let-v>
<li>
<a (click)="changeIndicatorStatus(number._id, indicator, v.value);hide(element)">
<div class="uk-flex uk-flex-middle">
<icon [flex]="true" [name]="v.icon" ratio="0.6"></icon>
<span class="uk-margin-small-left uk-width-expand">{{v.label}}</span>
<icon *ngIf="indicator.visibility === v.value" [flex]="true" name="done" class="uk-text-secondary" ratio="0.8"></icon>
</div>
</a>
</li>
</ng-template>
<ng-container *ngIf="!indicator.defaultId && !editing && isCurator">
<li class="uk-nav-divider">
<li><a (click)="deleteIndicatorOpen(number, indicator._id, 'number', 'delete');hide(element)">Delete</a></li>
</ng-container>
</ul>
</div>
</div>
<div class="uk-text-small uk-text-truncate uk-margin-xsmall-bottom uk-margin-right">{{indicator.name}}</div>
<div class="number uk-text-small uk-text-bold">
<span *ngIf="numberResults.get(i + '-' + j)" [innerHTML]="(indicator.indicatorPaths[0].format == 'NUMBER'?(numberResults.get(i + '-' + j) | numberRound: 2:1:stakeholder.locale):(numberResults.get(i + '-' + j) | numberPercentage: stakeholder.locale))"></span>
<span *ngIf="!numberResults.get(i + '-' + j)">--</span>
</div>
</div>
</div>
</ng-template>
</div>
<div *ngIf="isCurator" class="uk-margin-top">
<div class="uk-grid uk-grid-small" uk-grid>
<div [ngClass]="getNumberClassBySize('small')">
<a class="uk-card uk-card-default number-card uk-padding-small uk-flex uk-flex-middle uk-link-reset" (click)="editNumberIndicatorOpen(number)">
<div class="uk-text-background uk-margin-right">
<icon name="add" [flex]="true" ratio="1.5"></icon>
</div>
<div class="uk-text-bold uk-margin-remove">
Create a number Indicator
</div>
</a>
</div>
</div>
</div>
</div>
</div>
<div>
<ng-container *ngTemplateOutlet="new_section; context:{type: 'number'}"></ng-container>
</div>
</div>
</div>
<div *ngIf="chartSections" class="uk-margin-large-top">
<h5 class="uk-text-bold">Chart Indicators</h5>
<div class="uk-grid uk-grid-large uk-child-width-1-1" uk-grid>
<div *ngFor="let chart of charts; let i=index">
<div class="section uk-margin-top uk-padding-small">
<div class="tools">
<div class="uk-flex uk-flex-middle">
<a [class.uk-disabled]="editing" class="" (click)="createSection(i)"
title="Create a new section">
<icon name="add" [flex]="true"></icon>
</a>
<a *ngIf="!chart.defaultId" [attr.uk-tooltip]="'Delete section'"
(click)="deleteSectionOpen(chart, i, 'chart', 'delete')">
<icon name="close" [flex]="true"></icon>
</a>
<!-- <ng-container *ngIf="!stakeholder.defaultId">-->
<!-- <button [disabled]="editing || chart.defaultId " class="md-btn md-btn-mini"-->
<!-- [title]="(chart.defaultId?'Default sections cannot be deleted':'Delete all related sections')"-->
<!-- (click)="deleteSectionOpen(chart, i, 'chart', 'delete')"><i class="material-icons">highlight_off</i>-->
<!-- </button>-->
<!-- <button [disabled]="editing || chart.defaultId " class="md-btn md-btn-mini"-->
<!-- [title]="(chart.defaultId?'Default sections cannot be deleted':'Delete section and disconnect related')"-->
<!-- (click)="deleteSectionOpen(chart, i, 'chart', 'disconnect')"><i class="material-icons">link_off</i>-->
<!-- </button>-->
<!-- </ng-container>-->
</div>
</div>
<div *ngIf="chartSections.at(i)"
class="uk-margin-medium-bottom">
<div input [formInput]="chartSections.at(i).get('title')"
(focusEmitter)="saveSection($event, chartSections.at(i), i)"
class="uk-width-1-3@m uk-width-1-1" placeholder="Title" inputClass="border-bottom"></div>
</div>
<div [id]="'chart-' + chart._id" class="uk-grid uk-grid-small uk-grid-match" uk-sortable="group: chart" uk-grid>
<ng-template ngFor [ngForOf]="chart.indicators" let-indicator let-j="index">
<div *ngIf="indicator" [id]="indicator._id" [ngClass]="getChartClassBySize(indicator.width)">
<div class="uk-card uk-card-default uk-card-body uk-position-relative">
<div *ngIf="!dragging"
class="uk-position-top-right uk-margin-small-right uk-margin-small-top">
<a class="uk-link-reset uk-flex uk-flex-middle" [class.uk-disabled]="editing">
<icon [flex]="true" [name]="stakeholderUtils.visibilityIcon.get(indicator.visibility)" ratio="0.6"></icon>
<icon [flex]="true" name="more_vert"></icon>
</a>
<div #element class="uk-dropdown" uk-dropdown="mode: click; pos: bottom-left; offset: 5; delay-hide: 0">
<ul class="uk-nav uk-dropdown-nav">
<ng-container *ngIf="isCurator">
<li><a (click)="editChartIndicatorOpen(chart, indicator._id); hide(element)">Edit</a></li>
<li class="uk-nav-divider"></li>
</ng-container>
<ng-template ngFor [ngForOf]="stakeholderUtils.visibility" let-v>
<li>
<a (click)="changeIndicatorStatus(chart._id, indicator, v.value);">
<div class="uk-flex uk-flex-middle">
<icon [flex]="true" [name]="v.icon" ratio="0.6"></icon>
<span class="uk-margin-small-left uk-width-expand">{{v.label}}</span>
<icon *ngIf="indicator.visibility === v.value" [flex]="true" name="done" class="uk-text-secondary" ratio="0.8"></icon>
</div>
</a>
</li>
</ng-template>
<ng-container *ngIf="!indicator.defaultId && !editing && isCurator">
<li class="uk-nav-divider">
<li><a (click)="deleteIndicatorOpen(chart, indicator._id, 'chart', 'delete');hide(element)">Delete</a></li>
</ng-container>
</ul>
</div>
</div>
<div>
<div *ngIf="indicator.name" class="uk-text-center uk-text-bold uk-margin-small-bottom">
{{indicator.name}}
</div>
<iframe *ngIf="!properties.disableFrameLoad && indicator.indicatorPaths[0] && indicator.indicatorPaths[0].source !=='image' &&
safeUrls.get(indicatorUtils.getFullUrl(stakeholder, indicator.indicatorPaths[0]))"
allowfullscreen="true" mozallowfullscreen="true" webkitallowfullscreen="true"
[src]="safeUrls.get(indicatorUtils.getFullUrl(stakeholder, indicator.indicatorPaths[0]))"
class="uk-width-1-1" [ngClass]="'uk-height-' + (indicator.height?indicator.height.toLowerCase():'medium')"
[class.uk-blend-multiply]="!isFullscreen"></iframe>
<div *ngIf="properties.disableFrameLoad && indicator.indicatorPaths &&
indicator.indicatorPaths.length > 0 && indicator.indicatorPaths[0].source !=='image'">
<img class="uk-width-1-1 uk-blend-multiply" [ngClass]="'uk-height-' + (indicator.height?indicator.height.toLowerCase():'medium')"
src="assets/chart-placeholder.png">
</div>
<div *ngIf="indicator.indicatorPaths && indicator.indicatorPaths[0] &&
indicator.indicatorPaths[0].source === 'image'">
<img class="uk-width-1-1 uk-blend-multiply" [ngClass]="'uk-height-' + (indicator.height?indicator.height.toLowerCase():'medium')"
[src]="indicator.indicatorPaths[0].url">
</div>
<!--<ng-container *ngTemplateOutlet="description; context: {indicator:indicator}"></ng-container>-->
</div>
</div>
</div>
</ng-template>
</div>
<div *ngIf="isCurator" class="uk-margin-top">
<div class="uk-grid uk-grid-small uk-grid-match" uk-grid>
<div [ngClass]="getChartClassBySize('small')">
<div class=" uk-card uk-card-default uk-card-body clickable" (click)="editChartIndicatorOpen(chart)">
<h6 class="uk-text-bold uk-text-center">
Create a custom indicator
</h6>
<div class="uk-text-muted uk-text-small">
Use our advance tool to create a custom Indicator that suit the needs of your funding
KPI's.
</div>
<div class="uk-flex uk-flex-center uk-text-background uk-margin-medium-top">
<icon name="add" ratio="3" [flex]="true"></icon>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div>
<ng-container *ngTemplateOutlet="new_section; context:{type: 'chart'}"></ng-container>
</div>
</div>
</div>
</div>
<modal-alert #editNumberModal
[large]="true" classTitle="uk-background-primary uk-light"
(alertOutput)="saveIndicator()"
[okDisabled]="numberIndicatorFb && (numberIndicatorFb.invalid || numberIndicatorFb.pristine)">
<div *ngIf="editing" class="uk-position-relative uk-height-large">
<loading class="uk-position-center"></loading>
</div>
<div [class.uk-hidden]="editing" class="uk-padding-small">
<div *ngIf="numberIndicatorFb" class="uk-grid" [formGroup]="numberIndicatorFb" uk-grid>
<div input class="uk-width-1-1" [formInput]="numberIndicatorFb.get('name')" placeholder="Title"></div>
<div *ngIf="stakeholder.defaultId != '-1' && ( (indicator.description && indicator.description.length > 0) || !stakeholder.defaultId)"
input class="uk-width-1-1" [formInput]="numberIndicatorFb.get('description')" placeholder="Profile description" type="textarea">
</div>
<div input class="uk-width-1-1" *ngIf="stakeholder.defaultId" [formInput]="numberIndicatorFb.get('additionalDescription')"
placeholder="Description" type="textarea">
</div>
<div input class="uk-width-1-2@m" [formInput]="numberIndicatorFb.get('visibility')"
placeholder="Visibility" [options]="stakeholderUtils.visibility" type="select">
</div>
<div input class="uk-width-1-2@m" [formInput]="numberIndicatorFb.get('width')"
placeholder="Number Size" [options]="indicatorUtils.indicatorSizes" type="select">
</div>
<div *ngIf="numberIndicatorPaths" formArrayName="indicatorPaths">
<div *ngFor="let indicatorPath of numberIndicatorPaths.controls; let i=index" [formGroupName]="i">
<div class="uk-grid" uk-grid>
<div class="uk-width-1-1">
<div class="uk-grid" uk-grid>
<div class="uk-width-1-1 uk-flex uk-flex-middle">
<div input class="uk-width-expand" [formInput]="indicatorPath.get('url')" placeholder="Number URL">
<div *ngIf="urlParameterizedMessage" warning>{{urlParameterizedMessage}}</div>
</div>
<div class='uk-padding-small'>
<a class="uk-link-reset" (click)="copyToClipboard(indicatorPath.get('url').value)"><icon [flex]="true" name="content_copy"></icon></a>
</div>
</div>
<div *ngIf="showCheckForSchemaEnhancements" class="uk-width-1-1">
<div class="uk-alert uk-alert-warning">
There are schema enhancements that can be applied in this query.<a
(click)="indicatorPath.get('url').setValue(indicatorUtils.applySchemaEnhancements(indicatorPath.get('url').value)); indicatorPath.get('url').markAsDirty()">Apply
now</a>
</div>
</div>
<div class="uk-width-1-2@m">
<div input [formInput]="indicatorPath.get('source')" placeholder="Source"
[options]="isAdministrator?indicatorUtils.allSourceTypes:indicatorUtils.sourceTypes" type="select">
</div>
</div>
<div class="uk-width-1-2@m">
<div input [formInput]="indicatorPath.get('format')" placeholder="Format"
[options]="indicatorUtils.formats" type="select">
</div>
</div>
</div>
</div>
<div formArrayName="jsonPath" class="uk-width-1-1">
<h6 class="uk-text-bold uk-margin-remove-bottom">
<span>JSON Path</span>
</h6>
<div *ngIf="numberIndicatorPaths.at(i).get('result').invalid && numberIndicatorPaths.at(i).get('result').errors.required">
<div class="uk-text-danger uk-text-small">
This JSON path is not valid or the result has not been calculated yet.
Please press calculate on box below to see the result.
</div>
</div>
<div class="uk-grid uk-child-width-1-3@m uk-child-width-1-1 uk-margin-top uk-flex-middle" uk-grid>
<div *ngFor="let jsonPath of getJsonPath(i).controls; let j=index" class="uk-flex uk-flex-middle">
<div input class="uk-width-1-1" [formInput]="jsonPath" [placeholder]="'Level ' + +(j + 1)"></div>
<a [class.uk-invisible]="getJsonPath(i).length === 1 || numberIndicatorFb.get('defaultId').value"
class="uk-margin-small-left uk-text-danger"
[class.uk-disabled]="getJsonPath(i).disabled"
(click)="removeJsonPath(i, j)">
<icon name="close"></icon>
</a>
<span [class.uk-invisible]="getJsonPath(i).disabled || j === (getJsonPath(i).controls.length - 1)" class="uk-text-center uk-margin-small-left">
<icon name="east"></icon>
</span>
</div>
<div *ngIf="indicator.defaultId === null">
<button class="uk-icon-button uk-button-primary" (click)="addJsonPath(i)">
<icon name="add" [flex]="true"></icon>
</button>
</div>
</div>
</div>
<div class="uk-width-1-1 uk-flex uk-flex-center">
<div class="uk-flex uk-position-relative">
<span class="uk-padding number number-preview uk-flex uk-flex-column uk-flex-center uk-text-center">
<span *ngIf="numberIndicatorPaths.at(i).get('result').valid && numberIndicatorPaths.at(i).get('result').value !== 0"
[innerHTML]="(numberIndicatorPaths.at(i).get('format').value == 'NUMBER'?(numberIndicatorPaths.at(i).get('result').value | numberRound: 2:1:stakeholder.locale):(numberIndicatorPaths.at(i).get('result').value | numberPercentage: stakeholder.locale))">
</span>
<span *ngIf="numberIndicatorPaths.at(i).get('result').valid && numberIndicatorPaths.at(i).get('result').value === 0">
--
</span>
</span>
<div *ngIf="numberIndicatorPaths.at(i).get('result').invalid"
class="uk-width-1-1 uk-height-1-1 refresh-indicator">
<div class="uk-position-relative uk-height-1-1">
<a class="uk-position-center uk-text-center uk-text-small uk-link-reset"
[class.uk-disabled]="numberIndicatorPaths.at(i).get('url').invalid"
(click)="validateJsonPath(i, true)">
<div>
<icon name="refresh"></icon>
</div>
<span *ngIf="numberIndicatorPaths.at(i).get('result').errors.required">Calculate</span>
<span *ngIf="numberIndicatorPaths.at(i).get('result').errors.validating">Calculating...</span>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div #editNumberNotify notify-form class="uk-width-1-1 uk-margin-medium-top"></div>
</div>
</modal-alert>
<modal-alert #editChartModal [large]="true" (alertOutput)="saveIndicator()" classTitle="uk-background-primary uk-light"
[okDisabled]="chartIndicatorFb && (chartIndicatorFb.invalid || chartIndicatorFb.pristine)">
<div *ngIf="editing" class="uk-position-relative uk-height-large">
<loading class="uk-position-center"></loading>
</div>
<div [class.uk-hidden]="editing" class="uk-padding-small">
<div *ngIf="chartIndicatorFb" [formGroup]="chartIndicatorFb" class="uk-grid" uk-grid>
<div input class="uk-width-1-1" [formInput]="chartIndicatorFb.get('name')" placeholder="Title"></div>
<div *ngIf="stakeholder.defaultId != '-1' && ((indicator.description && indicator.description.length > 0) || !stakeholder.defaultId)"
input class="uk-width-1-1" [formInput]="chartIndicatorFb.get('description')" placeholder="Default Description" type="textarea">
</div>
<div *ngIf="stakeholder.defaultId" input class="uk-width-1-1" [formInput]="chartIndicatorFb.get('additionalDescription')"
placeholder="Description" type="textarea">
</div>
<div input class="uk-width-1-2@m" [formInput]="chartIndicatorFb.get('visibility')"
placeholder="Status" [options]="stakeholderUtils.visibility" type="select">
</div>
<div input class="uk-width-1-2@m" [formInput]="chartIndicatorFb.get('width')" placeholder="Chart width"
[options]="indicatorUtils.indicatorSizes" type="select">
</div>
<div input class="uk-width-1-2@m" [formInput]="chartIndicatorFb.get('height')" placeholder="Chart height"
[options]="indicatorUtils.indicatorSizes" type="select">
</div>
<div *ngIf="chartIndicatorPaths" formArrayName="indicatorPaths" class="uk-width-1-1">
<div *ngFor="let indicatorPath of chartIndicatorPaths.controls; let i=index;"
[formGroupName]="i" class="uk-grid" uk-grid>
<div class="uk-width-1-1 uk-flex uk-flex-middle">
<div input class="uk-width-expand" [title]="indicatorPath.get('url').disabled?'Default chart URLs cannot change':''"
[formInput]="indicatorPath.get('url')" placeholder="Chart URL">
<div *ngIf="urlParameterizedMessage" warning>{{urlParameterizedMessage}}</div>
</div>
<div class='uk-padding-small'>
<a class="uk-link-reset" (click)="copyToClipboard(indicatorPath.get('url').value)"><icon [flex]="true" name="content_copy"></icon></a>
</div>
</div>
<div *ngIf="showCheckForSchemaEnhancements" class=" uk-width-1-1 ">
<div class="uk-alert uk-alert-warning">
There are schema enhancements that can be applied in this query. <a
(click)="indicatorPath.get('url').setValue(indicatorUtils.applySchemaEnhancements(indicatorPath.get('url').value)); indicatorPath.get('url').markAsDirty()">Apply
now</a>
</div>
</div>
<div class="uk-width-1-1" formArrayName="parameters">
<div class="uk-grid" uk-grid>
<div *ngIf="getParameter(i, 'title')" input class="uk-width-1-1" [formInput]="getParameter(i, 'title').get('value')"
placeholder="Chart Title"></div>
<div *ngIf="getParameter(i, 'subtitle')" input class="uk-width-1-1" placeholder="Chart Subtitle" [formInput]="getParameter(i, 'subtitle').get('value')" label="Chart Subtitle"></div>
<div *ngIf="!getParameter(i, 'type')" input class="uk-width-1-3@s" [formInput]="indicatorPath.get('type')" placeholder="Chart Type"
[options]="(indicatorPath.get('type').value == 'table' && getParameter(i, 'data_title_0'))?indicatorUtils.getChartTypes(indicatorPath.get('type').value):indicatorUtils.allChartTypes"
type="select"></div>
<div *ngIf="getParameter(i, 'type')" input class="uk-width-1-3@s" [formInput]="getParameter(i, 'type').get('value')" placeholder="Chart Type"
[options]="indicatorUtils.getChartTypes(getParameter(i, 'type').get('value').value)" type="select"></div>
<div *ngIf="getParameter(i, 'xAxisTitle')" input class="uk-width-1-3@s" [formInput]="getParameter(i, 'xAxisTitle').get('value')"
placeholder="X-Axis Title"></div>
<div *ngIf="getParameter(i, 'yAxisTitle')" input class="uk-width-1-3@s" [formInput]="getParameter(i, 'yAxisTitle').get('value')"
placeholder="Y-Axis Title"></div>
<div *ngIf="getParameter(i, 'data_title_0')" input class="uk-width-1-3@s" [formInput]="getParameter(i, 'data_title_0').get('value')"
placeholder="Data Title"></div>
<div *ngIf="getParameter(i, 'data_title_1')" input class="uk-width-1-3@s" [formInput]="getParameter(i, 'data_title_1').get('value')"
placeholder="Data Title"></div>
<div *ngIf="getParameter(i, 'start_year')" input class="uk-width-1-3@s" [formInput]="getParameter(i, 'start_year').get('value')"
placeholder="Year (From)"></div>
<div *ngIf="getParameter(i, 'end_year')" input class="uk-width-1-3@s" [formInput]="getParameter(i, 'end_year').get('value')"
placeholder="Year (To)"></div>
</div>
</div>
<div *ngIf="indicator && indicator.indicatorPaths[i] && indicator.indicatorPaths[i].safeResourceUrl"
class="uk-margin-medium-top uk-position-relative uk-width-1-1 uk-flex uk-flex-center">
<div *ngIf="(hasDifference(i)) && !indicatorPath.invalid"
class="uk-width-1-1 uk-height-large refresh-indicator">
<div class="uk-position-relative uk-height-1-1">
<a class="uk-position-center uk-text-center uk-link-reset" (click)="refreshIndicator()">
<div>
<icon name="refresh"></icon>
</div>
<span>Click to refresh the graph view</span>
</a>
</div>
</div>
<iframe *ngIf="indicator.indicatorPaths[i].source !== 'image'" [class.uk-blend-multiply]="!isFullscreen"
[src]="indicator.indicatorPaths[i].safeResourceUrl" allowfullscreen="true" mozallowfullscreen="true" webkitallowfullscreen="true"
class="uk-width-1-1 uk-height-large"></iframe>
<!-- <div *ngIf="properties.disableFrameLoad && indicator.indicatorPaths[i].source !== 'image'" class="uk-alert uk-alert-danger uk-text-center">I frames-->
<!-- preview is disabled</div>-->
<div *ngIf="indicator.indicatorPaths[i].source === 'image'">
<img class="uk-width-1-1 uk-height-large uk-blend-multiply" [src]="indicator.indicatorPaths[i].url">
</div>
</div>
</div>
</div>
</div>
<div #editChartNotify notify-form class="uk-width-1-1 uk-margin-medium-top"></div>
</div>
</modal-alert>
<modal-alert #deleteModal (alertOutput)="deleteIndicator()" [overflowBody]="false" classTitle="uk-background-primary uk-light">
<div [class.uk-invisible]="editing" class="uk-position-relative">
<div *ngIf="editing">
<loading class="uk-position-center"></loading>
</div>
You are about to delete <span class="uk-text-bold" *ngIf="indicator && index !== -1">
"{{indicator.name ? indicator.name : (indicator.indicatorPaths[0]?.parameters?.title?indicator.indicatorPaths[0].parameters.title:'')}}"</span> indicator permanently.
<div *ngIf="indicatorChildrenActionOnDelete == 'delete'" class="uk-text-bold">
Indicators of all profiles based on this default indicator, will be deleted as well.
</div>
<!-- <span *ngIf="indicatorChildrenActionOnDelete == 'disconnect'" class="uk-text-bold">-->
<!-- Indicators of all profiles based on this default indicator, will not be marked as copied from default anymore.-->
<!-- </span>-->
Are you sure you want to proceed?
<div #deleteNotify notify-form class="uk-width-1-1 uk-margin-medium-top"></div>
</div>
</modal-alert>
<!--<modal-alert #deleteAllModal (alertOutput)="deleteIndicator('delete')">
You are about to delete <span class="uk-text-bold" *ngIf="indicator && index !== -1">
"{{indicator.name ? indicator.name : indicator.indicatorPaths[0].parameters.title}}"</span> indicator permanently.
<span class="uk-text-bold">Indicators of all profiles based on this default indicator, will be deleted as well.</span>
Are you sure you want to proceed?
</modal-alert>
<modal-alert #deleteAndDisconnectModal (alertOutput)="deleteIndicator('disconnect')">
You are about to delete <span class="uk-text-bold" *ngIf="indicator && index !== -1">
"{{indicator.name ? indicator.name : indicator.indicatorPaths[0].parameters.title}}"</span> indicator permanently.
<span class="uk-text-bold">Indicators of all profiles based on this default indicator, will not be marked as copied from default anymore.</span>
Are you sure you want to proceed?
</modal-alert>-->
<modal-alert #deleteSectionModal (alertOutput)="deleteSection()" [overflowBody]="false" classTitle="uk-background-primary uk-light">
<div [class.uk-invisible]="editing" class="uk-position-relative">
<div *ngIf="editing">
<loading class="uk-position-center"></loading>
</div>
You are about to delete this section and its indicators permanently.
<div *ngIf="sectionChildrenActionOnDelete == 'delete' && !stakeholder.defaultId" class="uk-text-bold">
Sections of all profiles based on this default section and their contents, will be deleted as well.
</div>
<!-- <span *ngIf="sectionChildrenActionOnDelete == 'disconnect'" class="uk-text-bold">-->
<!-- Sections of all profiles based on this default section and their contents, will not be marked as copied from default anymore.-->
<!-- </span>-->
Are you sure you want to proceed?
</div>
</modal-alert>
<!--<modal-alert #deleteNumberSectionModal (alertOutput)="deleteSection('number')">
You are about to delete this section and its indicators permanently.
Are you sure you want to proceed?
</modal-alert>
<modal-alert #deleteChartSectionModal (alertOutput)="deleteSection()">
You are about to delete this section and its indicators permanently.
Are you sure you want to proceed?
</modal-alert>-->
<ng-template #new_section let-type="type">
<div class="section">
<div class="uk-flex uk-flex-center" (click)="createSection(-1, type)">
<button class="uk-button uk-button-primary uk-flex uk-flex-middle">
<icon name="add" [flex]="true"></icon>
<span class="uk-margin-small-left">New section</span>
</button>
</div>
</div>
</ng-template>

View File

@ -0,0 +1,53 @@
@import (reference) "~src/assets/openaire-theme/less/_import-variables";
.number-preview {
border: @global-border-width solid @global-border;
background: transparent;
border-radius: @global-border-radius;
min-width: 100px;
min-height: 70px;
}
.refresh-indicator {
background-color: @global-overlay-background;
border-radius: @global-border-radius;
position: absolute;
color: @global-inverse-color;
z-index: 1;
}
.section {
padding: 60px 45px;
border-radius: @global-border-radius;
border: @global-border-width solid @global-border;
position: relative;
background: @global-inverse-color;
border-left: 5px @global-primary-background solid;
.tools {
position: absolute;
top: 0;
left: 50%;
transform: translate(-50%, -100%);
max-width: 50px;
padding: 5px 10px;
background-image: @global-primary-gradient;
color: @global-inverse-color;
-webkit-clip-path: polygon(20% 5%, 80% 5%, 100% 100%, 0% 100%);
clip-path: polygon(20% 5%, 80% 5%, 100% 100%, 0% 100%);
display: none;
}
&:hover {
.tools {
display: block;
a {
color: currentColor;
&:hover {
text-decoration: none;
}
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,19 @@
import {NgModule} from '@angular/core';
import {RouterModule} from '@angular/router';
import {PreviousRouteRecorder} from '../../utils/piwik/previousRouteRecorder.guard';
import {TopicComponent} from "./topic.component";
import {CanExitGuard} from "../../utils/can-exit.guard";
@NgModule({
imports: [
RouterModule.forChild([
{
path: '',
component: TopicComponent,
canDeactivate: [PreviousRouteRecorder, CanExitGuard]
}
])
]
})
export class TopicRoutingModule {
}

View File

@ -0,0 +1,380 @@
<aside *ngIf="stakeholder" id="sidebar_main">
<div sidebar-content>
<div class="back">
<a [routerLink]="'/admin/' + stakeholder.alias" class="uk-flex uk-flex-middle uk-flex-center">
<div class="uk-width-auto">
<icon name="west" [flex]="true" ratio="1.3"></icon>
</div>
<span class="uk-width-expand uk-text-truncate uk-margin-left hide-on-close">Indicators</span>
</a>
</div>
<div class="menu_section uk-margin-large-top">
<ul class="uk-list uk-nav uk-nav-default" transition-group [id]="'topics'"
uk-nav="duration: 400">
<li *ngFor="let topic of stakeholder.topics; let i=index" class="uk-parent" [class.uk-active]="topicIndex === i"
transition-group-item>
<a [routerLink]="'/admin/'+stakeholder.alias + '/indicators/' + topic.alias"
[title]="topic.name" class="uk-visible-toggle uk-flex uk-flex-middle">
<div *ngIf="topic.icon" class="uk-width-auto">
<icon class="menu-icon" [svg]="topic.icon" ratio="0.9" [flex]="true"></icon>
</div>
<span [class.hide-on-close]="topic.icon"
class="uk-width-expand uk-text-truncate uk-margin-small-left">
{{topic.name}}
</span>
<span class="uk-margin-xsmall-left hide-on-close" [class.uk-invisible-hover]="topicIndex !== i"
(click)="$event.stopPropagation();$event.preventDefault()">
<a class="uk-link-reset uk-flex uk-flex-middle">
<icon [flex]="true" [name]="stakeholderUtils.visibilityIcon.get(topic.visibility)"
ratio="0.6"></icon>
<icon [flex]="true" name="more_vert"></icon>
</a>
<div #element
uk-dropdown="mode: click; pos: bottom-left; offset: 5; delay-hide: 0; flip: false; container: body">
<ul class="uk-nav uk-dropdown-nav">
<ng-container *ngIf="isCurator">
<li>
<a (click)="editTopicOpen(i); hide(element)">
<div class="uk-flex uk-flex-middle">
<icon [flex]="true" name="edit" ratio="0.6"></icon>
<span class="uk-margin-small-left uk-width-expand">Edit</span>
</div>
</a>
</li>
<li *ngIf="i > 0 || i < stakeholder.topics.length - 1" class="uk-nav-divider"></li>
<li *ngIf="i > 0">
<a (click)="hide(element);moveTopic(i)">
<div class="uk-flex uk-flex-middle">
<icon [flex]="true" name="north" ratio="0.6"></icon>
<span class="uk-margin-small-left uk-width-expand">Move Up</span>
</div>
</a>
</li>
<li *ngIf="i < stakeholder.topics.length - 1">
<a (click)="hide(element);moveTopic(i, i + 1)">
<div class="uk-flex uk-flex-middle">
<icon [flex]="true" name="south" ratio="0.6"></icon>
<span class="uk-margin-small-left uk-width-expand">Move Down</span>
</div>
</a>
</li>
<li class="uk-nav-divider"></li>
</ng-container>
<ng-template ngFor [ngForOf]="stakeholderUtils.visibility" let-v>
<li [class.uk-active]="topic.visibility === v.value">
<a (click)="openVisibilityModal(i, v.value, 'topic'); hide(element)">
<div class="uk-flex uk-flex-middle">
<icon [flex]="true" [name]="v.icon" ratio="0.6"></icon>
<span class="uk-margin-small-left uk-width-expand">{{v.label}}</span>
<icon *ngIf="topic.visibility === v.value" [flex]="true" name="done"
class="uk-text-secondary" ratio="0.8"></icon>
</div>
</a>
</li>
</ng-template>
<ng-container *ngIf="!topic.defaultId && isCurator">
<li class="uk-nav-divider">
<li>
<a (click)="deleteTopicOpen(i, 'delete'); hide(element)">
<div class="uk-flex uk-flex-middle">
<icon [flex]="true" name="delete" ratio="0.6"></icon>
<span class="uk-margin-small-left uk-width-expand">Delete</span>
</div>
</a>
<!--<ng-container *ngIf="!stakeholder.defaultId">
<a (click)="deleteTopicOpen(i, 'delete'); hide(element)">Delete from all profiles</a>
<a (click)="deleteTopicOpen(i, 'disconnect'); hide(element)">Delete and disconnect from all profiles</a>
</ng-container>-->
</li>
</ng-container>
</ul>
</div>
</span>
<span class="uk-nav-parent-icon hide-on-close"></span>
</a>
<ul *ngIf="isBrowser || topicIndex === i" class="uk-nav-sub" [id]="'categories-' + i.toString()"
transition-group>
<li *ngFor="let category of topic.categories; let j=index" transition-group-item class="uk-visible-toggle"
[class.uk-active]="categoryIndex == j">
<a (click)="chooseCategory(j)" [title]="category.name">
<div class="uk-flex uk-flex-middle uk-width-1-1">
<span class="uk-width-expand uk-text-truncate">{{category.name}}</span>
<span class="uk-margin-xsmall-left hide-on-close" [class.uk-invisible-hover]="categoryIndex !== j"
(click)="$event.stopPropagation();$event.preventDefault()">
<a class="uk-link-reset uk-flex uk-flex-middle">
<icon [flex]="true" [name]="stakeholderUtils.visibilityIcon.get(category.visibility)"
ratio="0.6"></icon>
<icon [flex]="true" name="more_vert"></icon>
</a>
<div #element
uk-dropdown="mode: click; pos: bottom-left; offset: 5; delay-hide: 0; flip: false; container: body">
<ul class="uk-nav uk-dropdown-nav">
<ng-container *ngIf="isCurator">
<li>
<a (click)="editCategoryOpen(j); hide(element)">
<div class="uk-flex uk-flex-middle">
<icon [flex]="true" name="edit" ratio="0.6"></icon>
<span class="uk-margin-small-left uk-width-expand">Edit</span>
</div>
</a>
</li>
<li *ngIf="j > 0 || j < stakeholder.topics[topicIndex].categories.length - 1"
class="uk-nav-divider"></li>
<li *ngIf="j > 0">
<a (click)="hide(element);moveCategory(j)">
<div class="uk-flex uk-flex-middle">
<icon [flex]="true" name="north" ratio="0.6"></icon>
<span class="uk-margin-small-left uk-width-expand">Move Up</span>
</div>
</a>
</li>
<li *ngIf="j < stakeholder.topics[topicIndex].categories.length - 1">
<a (click)="hide(element);moveCategory(j, j + 1)">
<div class="uk-flex uk-flex-middle">
<icon [flex]="true" name="south" ratio="0.6"></icon>
<span class="uk-margin-small-left uk-width-expand">Move Down</span>
</div>
</a>
</li>
<li class="uk-nav-divider"></li>
</ng-container>
<ng-template ngFor [ngForOf]="stakeholderUtils.visibility" let-v>
<li [class.uk-active]="category.visibility === v.value">
<a (click)="openVisibilityModal(j, v.value, 'category'); hide(element)">
<div class="uk-flex uk-flex-middle">
<icon [flex]="true" [name]="v.icon" ratio="0.6"></icon>
<span class="uk-margin-small-left uk-width-expand">{{v.label}}</span>
<icon *ngIf="category.visibility === v.value" [flex]="true" name="done"
class="uk-text-secondary" ratio="0.8"></icon>
</div>
</a>
</li>
</ng-template>
<ng-container *ngIf="!category.defaultId && isCurator">
<li class="uk-nav-divider">
<li>
<a (click)="deleteCategoryOpen(j, 'delete'); hide(element)">
<div class="uk-flex uk-flex-middle">
<icon [flex]="true" name="delete" ratio="0.6"></icon>
<span class="uk-margin-small-left uk-width-expand">Delete</span>
</div>
</a>
</li>
</ng-container>
</ul>
</div>
</span>
</div>
</a>
</li>
<li *ngIf="isCurator">
<a (click)="editCategoryOpen(); $event.preventDefault()" class="uk-flex uk-flex-middle">
<icon name="add" [flex]="true"></icon>
<span class="hide-on-close">Create new category</span>
</a>
</li>
</ul>
</li>
<li *ngIf="isCurator" class="hide-on-close">
<a (click)="editTopicOpen(-1); $event.preventDefault()">
<div class="uk-flex uk-flex-middle">
<div class="uk-width-auto">
<icon class="menu-icon" name="add" [flex]="true"></icon>
</div>
<span class="uk-width-expand uk-text-truncate uk-margin-small-left hide-on-close">Create new topic</span>
</div>
</a>
</li>
</ul>
</div>
</div>
</aside>
<div #pageContent *ngIf="stakeholder && filters" page-content>
<div actions>
<div *ngIf="stakeholder.topics.length > 0" class="uk-flex uk-flex-center uk-margin-medium-top uk-flex-right@m uk-width-1-1">
<button class="uk-button uk-button-primary uk-flex uk-flex-middle">
<icon name="visibility" [flex]="true"></icon>
<span class="uk-margin-small-left uk-margin-small-right">Preview</span>
<icon name="expand_more" [flex]="true"></icon>
</button>
<div #element uk-dropdown="mode: click; pos: bottom-left; offset: 5; delay-hide: 0; flip: false">
<ul class="uk-nav uk-dropdown-nav">
<li><a target="_blank"
[routerLink]="'/' + stakeholder.alias + '/' + stakeholder.topics[topicIndex].alias"
[queryParams]="{view: 'PUBLIC'}"
(click)="hide(element)">Public view</a>
</li>
<li><a target="_blank" [routerLink]="'/' + stakeholder.alias + '/' +
stakeholder.topics[topicIndex].alias"
[queryParams]="{view: 'RESTRICTED'}"
(click)="hide(element)">Restricted view</a>
</li>
<!--<li class="disabled"><a class="uk-disabled uk-text-muted"
uk-tooltip="Note: available only in administration dashboard"
(click)="hide(element)">Private view</a>
</li>-->
</ul>
</div>
</div>
<ul *ngIf="stakeholder.topics.length > 0 && stakeholder.topics[topicIndex].categories.length > 0 && stakeholder.topics[topicIndex].categories[categoryIndex]"
transition-group class="uk-tab uk-margin-xsmall-top" [id]="'subCategories'">
<ng-template ngFor [ngForOf]=" stakeholder.topics[topicIndex].categories[categoryIndex].subCategories"
let-subCategory let-i="index">
<li class="uk-visible-toggle uk-flex" [class.uk-active]="subCategoryIndex === i" transition-group-item>
<a (click)="chooseSubcategory(i)">
<span class="uk-text-uppercase">{{subCategory.name}}</span>
</a>
<span class="uk-flex uk-flex-column uk-flex-center uk-margin-small-left"
[class.uk-invisible-hover]="subCategoryIndex !== i"
(click)="$event.stopPropagation();$event.preventDefault()">
<a class="uk-link-reset uk-flex uk-flex-middle">
<icon [flex]="true" [name]="stakeholderUtils.visibilityIcon.get(subCategory.visibility)"
ratio="0.6"></icon>
<icon [flex]="true" name="more_vert"></icon>
</a>
<div #element uk-dropdown="mode: click; pos: bottom-left; offset: 5; delay-hide: 0; container: body">
<ul class="uk-nav uk-dropdown-nav">
<ng-container *ngIf="isCurator">
<li>
<a (click)="editSubCategoryOpen(i); hide(element)">
<div class="uk-flex uk-flex-middle">
<icon [flex]="true" name="edit" ratio="0.6"></icon>
<span class="uk-margin-small-left uk-width-expand">Edit</span>
</div>
</a>
</li>
<li *ngIf="i > 0 || i < stakeholder.topics[topicIndex].categories[categoryIndex].subCategories.length - 1"
class="uk-nav-divider"></li>
<li *ngIf="i > 0">
<a (click)="hide(element);moveSubCategory(i)">
<div class="uk-flex uk-flex-middle">
<icon [flex]="true" name="west" ratio="0.6"></icon>
<span class="uk-margin-small-left uk-width-expand">Move Left</span>
</div>
</a>
</li>
<li *ngIf="i < stakeholder.topics[topicIndex].categories[categoryIndex].subCategories.length - 1">
<a (click)="hide(element);moveSubCategory(i, i + 1)">
<div class="uk-flex uk-flex-middle">
<icon [flex]="true" name="east" ratio="0.6"></icon>
<span class="uk-margin-small-left uk-width-expand">Move Right</span>
</div>
</a>
</li>
<li class="uk-nav-divider"></li>
<li *ngIf="indicators">
<a (click)=" indicators.exportIndicators(i);hide(element)">
<div class="uk-flex uk-flex-middle">
<icon [flex]="true" name="download" ratio="0.6"></icon>
<span class="uk-margin-small-left uk-width-expand">Export indicators</span>
</div>
</a>
</li>
<li *ngIf="indicators">
<a (click)="file.value = ''; this.index=i; file.click(); hide(element)">
<div class="uk-flex uk-flex-middle">
<icon [flex]="true" name="upload" ratio="0.6"></icon>
<span class="uk-margin-small-left uk-width-expand">Import indicators</span>
</div>
</a>
</li>
<li class="uk-nav-divider"></li>
</ng-container>
<ng-template ngFor [ngForOf]="stakeholderUtils.visibility" let-v>
<li [class.uk-active]="subCategory.visibility === v.value">
<a (click)="openVisibilityModal(i, v.value, 'subcategory'); hide(element)">
<div class="uk-flex uk-flex-middle">
<icon [flex]="true" [name]="v.icon" ratio="0.6"></icon>
<span class="uk-margin-small-left uk-width-expand">{{v.label}}</span>
<icon *ngIf="subCategory.visibility === v.value" [flex]="true" name="done"
class="uk-text-secondary" ratio="0.8"></icon>
</div>
</a>
</li>
</ng-template>
<ng-container *ngIf="!subCategory.defaultId && isCurator">
<li class="uk-nav-divider">
<li>
<a (click)="deleteSubcategoryOpen(i, 'delete'); hide(element)">
<div class="uk-flex uk-flex-middle">
<icon [flex]="true" name="delete" ratio="0.6"></icon>
<span class="uk-margin-small-left uk-width-expand">Delete</span>
</div>
</a>
</li>
</ng-container>
</ul>
</div>
</span>
</li>
</ng-template>
<li *ngIf="isCurator">
<a (click)="editSubCategoryOpen(); $event.preventDefault()" class="uk-flex uk-flex-middle">
<icon name="add" [flex]="true"></icon>
<span class="uk-text-uppercase">Create new subcategory</span>
</a>
</li>
</ul>
</div>
<div inner>
<input #file id="import-file" type="file" class="uk-hidden"
(change)="indicators.fileChangeEvent($event, this.index)"/>
<indicators #indicators [topicIndex]="topicIndex" [categoryIndex]="categoryIndex"
[subcategoryIndex]="subCategoryIndex" [user]="user"
[stakeholder]="stakeholder" [changed]="change.asObservable()"></indicators>
</div>
</div>
<modal-alert #deleteModal classTitle="uk-background-primary uk-light" (alertOutput)="deleteElement()"
[overflowBody]="false">
<div [class.uk-invisible]="loading" class="uk-position-relative">
<div *ngIf="loading">
<loading class="uk-position-center"></loading>
</div>
You are about to delete <span class="uk-text-bold" *ngIf="element">"{{element.name}}"</span> {{type}} permanently.
<div *ngIf="elementChildrenActionOnDelete == 'delete'" class="uk-text-bold">
{{getPluralTypeName()}} of all profiles based on this default {{type}}, will be deleted as well.
</div>
Are you sure you want to proceed?
</div>
</modal-alert>
<modal-alert #editModal classTitle="uk-background-primary uk-light" (alertOutput)="saveElement()"
[okDisabled]="form && (form.invalid || form.pristine)" [large]="true">
<div *ngIf="loading" class="uk-position-relative uk-height-large">
<loading class="uk-position-center"></loading>
</div>
<div *ngIf="form" [class.uk-hidden]="loading"
class="uk-grid uk-padding uk-padding-remove-horizontal uk-child-width-1-1" [formGroup]="form" uk-grid>
<div input [formInput]="form.get('name')" class="uk-width-1-2@m" placeholder="Title"></div>
<div input [formInput]="form.get('visibility')" class="uk-width-1-2@m" placeholder="Status"
[options]="stakeholderUtils.visibility" type="select"></div>
<div input [formInput]="form.get('description')" placeholder="Description" type="textarea" rows="4"></div>
<div *ngIf="form.get('icon')" input [formInput]="form.get('icon')" placeholder="Icon(SVG)" type="textarea"></div>
</div>
</modal-alert>
<modal-alert #visibilityModal [large]="false" classTitle="uk-background-primary uk-light">
<div class="">
You have the option to change the visibility status of your {{type}}, with or without applying the changed status to
its contents.
</div>
<div class="uk-flex uk-flex-center uk-margin-medium-top uk-margin-medium-bottom">
<button class="uk-button uk-button-primary" (click)="changeElementStatus()">
Change {{type}} status
</button>
</div>
<div class="uk-flex uk-flex-center uk-margin-medium-bottom">
<button class="uk-button uk-button-primary" (click)="changeElementStatus(true)">
Change {{type}} and its contents' status
</button>
</div>
<div class="uk-text-small">
<p class="uk-text-bold uk-text-uppercase uk-margin-remove">Note:</p>
<div>
<span class="uk-text-bold uk-text-italic">The status of the {{type}} prevails the status of its contents.</span>
For example, if a {{type}}'s status is private, while it has
<span *ngIf="type == 'topic'">a category, subcategory or an indicator</span>
<span *ngIf="type == 'category'">a subcategory or an indicator</span>
<span *ngIf="type == 'subcategory'">an indicator</span>
that is public, the private status of the {{type}} dominates.
</div>
</div>
</modal-alert>

View File

@ -0,0 +1,795 @@
import {
AfterViewInit,
ChangeDetectorRef,
Component, Inject,
OnDestroy,
OnInit, PLATFORM_ID,
QueryList,
ViewChild,
ViewChildren
} from '@angular/core';
import {ActivatedRoute, Router} from '@angular/router';
import {Title} from '@angular/platform-browser';
import {EnvProperties} from '../../utils/properties/env-properties';
import {Category, Stakeholder, SubCategory, Topic, Visibility} from "../../monitor/entities/stakeholder";
import {StakeholderService} from "../../monitor/services/stakeholder.service";
import {HelperFunctions} from "../../utils/HelperFunctions.class";
import {AlertModal} from "../../utils/modal/alert";
import {BehaviorSubject, Subject, Subscriber, Subscription} from "rxjs";
import {UntypedFormBuilder, UntypedFormGroup, Validators} from "@angular/forms";
import {StakeholderUtils} from "../utils/indicator-utils";
import {StringUtils} from "../../utils/string-utils.class";
import {IDeactivateComponent} from "../../utils/can-exit.guard";
import {LayoutService} from "../../dashboard/sharedComponents/sidebar/layout.service";
import {Option} from "../../sharedComponents/input/input.component";
import {properties} from "src/environments/environment";
import {Session, User} from "../../login/utils/helper.class";
import {UserManagementService} from "../../services/user-management.service";
import {TransitionGroupComponent} from "../../utils/transition-group/transition-group.component";
import {NotificationHandler} from "../../utils/notification-handler";
declare var UIkit;
@Component({
selector: 'topic',
templateUrl: './topic.component.html',
})
export class TopicComponent implements OnInit, OnDestroy, AfterViewInit, IDeactivateComponent {
private topicSubscriptions: any[] = [];
private subscriptions: any[] = [];
public properties: EnvProperties = properties;
public stakeholderUtils: StakeholderUtils = new StakeholderUtils();
public loading: boolean = false;
public stakeholder: Stakeholder;
public user: User;
/**
* Stakeholder change event
* */
public change: Subject<void> = new Subject<void>();
/**
* Current topic
**/
public topicIndexSubject: BehaviorSubject<number> = new BehaviorSubject<number>(0);
public topicIndex: number = 0;
/**
* Current category
*/
public categoryIndexSubject: BehaviorSubject<number> = new BehaviorSubject<number>(0);
public categoryIndex: number = 0;
/**
* Current Subcategory
*/
public subCategoryIndexSubject: BehaviorSubject<number> = new BehaviorSubject<number>(0);
public subCategoryIndex: number = 0;
/**
* Current element and index of topic, category or subcategory to be deleted.
*/
public form: UntypedFormGroup;
public element: Topic | Category | SubCategory;
public type: 'topic' | 'category' | 'subcategory' = "topic";
public index: number = -1;
public visibility: Visibility;
@ViewChild('deleteModal', {static: true}) deleteModal: AlertModal;
@ViewChild('editModal', {static: true}) editModal: AlertModal;
@ViewChild('visibilityModal', {static: true}) visibilityModal: AlertModal;
@ViewChildren(TransitionGroupComponent) transitions: QueryList<TransitionGroupComponent>;
public elementChildrenActionOnDelete: string;
public filters: UntypedFormGroup;
public all: Option = {
value: 'all',
label: 'All'
};
constructor(
private route: ActivatedRoute,
private router: Router,
private title: Title,
private fb: UntypedFormBuilder,
private stakeholderService: StakeholderService,
private userManagementService: UserManagementService,
private layoutService: LayoutService,
private cdr: ChangeDetectorRef,
@Inject(PLATFORM_ID) private platformId) {
}
public ngOnInit() {
let subscription: Subscription;
this.subscriptions.push(this.topicIndexSubject.asObservable().subscribe(index => {
this.topicChanged(() => {
this.topicIndex = index;
});
}));
this.subscriptions.push(this.categoryIndexSubject.asObservable().subscribe(index => {
this.categoryChanged(() => {
this.categoryIndex = index;
});
}));
this.subscriptions.push(this.subCategoryIndexSubject.asObservable().subscribe(index => {
this.subCategoryChanged(() => {
this.subCategoryIndex = index;
});
}));
this.subscriptions.push(this.route.params.subscribe(params => {
if (subscription) {
subscription.unsubscribe();
}
subscription = this.stakeholderService.getStakeholderAsObservable().subscribe(stakeholder => {
if (stakeholder) {
this.stakeholder = stakeholder;
if (params['topic']) {
this.chooseTopic(this.stakeholder.topics.findIndex(topic => topic.alias === params['topic']));
} else {
this.chooseTopic(0);
}
this.chooseCategory(0);
this.filters = this.fb.group({
chartType: this.fb.control('all'),
status: this.fb.control('all'),
keyword: this.fb.control('')
});
if (this.topicIndex === -1) {
this.navigateToError();
} else {
this.title.setTitle(stakeholder.name + " | Indicators");
}
}
});
this.topicSubscriptions.push(subscription);
}));
this.topicSubscriptions.push(this.userManagementService.getUserInfo().subscribe(user => {
this.user = user;
}))
}
ngAfterViewInit() {
if(this.topics) {
let activeIndex = UIkit.nav(this.topics.element.nativeElement).items.findIndex(item => item.classList.contains('uk-open'));
if(activeIndex !== this.topicIndex) {
setTimeout(() => {
UIkit.nav(this.topics.element.nativeElement).toggle(this.topicIndex, true);
});
}
}
}
get isBrowser() {
return this.platformId === 'browser';
}
public ngOnDestroy() {
this.topicSubscriptions.forEach(value => {
if (value instanceof Subscriber) {
value.unsubscribe();
}
});
this.subscriptions.forEach(value => {
if (value instanceof Subscriber) {
value.unsubscribe();
}
});
}
canExit(): boolean {
this.topicSubscriptions.forEach(value => {
if (value instanceof Subscriber) {
value.unsubscribe();
}
});
this.stakeholderService.setStakeholder(this.stakeholder);
return true;
}
private findById(id: string) {
return this.transitions?this.transitions.find(item => item.id === id):null;
}
get topics(): TransitionGroupComponent {
return this.findById('topics');
}
get categories(): TransitionGroupComponent {
return this.findById('categories-' + this.topicIndex);
}
get subCategories(): TransitionGroupComponent {
return this.findById('subCategories');
}
hide(element: any) {
UIkit.dropdown(element).hide();
}
stakeholderChanged() {
this.change.next();
}
public saveElement() {
if (this.type === "topic") {
this.saveTopic();
} else if (this.type === "category") {
this.saveCategory();
} else {
this.saveSubCategory();
}
}
public deleteElement() {
if (this.type === "topic") {
this.deleteTopic();
} else if (this.type === "category") {
this.deleteCategory();
} else {
this.deleteSubcategory();
}
}
public changeElementStatus(propagate: boolean = false) {
if (this.type === "topic") {
this.changeTopicStatus(propagate);
} else if (this.type === "category") {
this.changeCategoryStatus(propagate);
} else {
this.changeSubcategoryStatus(propagate);
}
}
public chooseTopic(topicIndex: number) {
this.topicIndexSubject.next(topicIndex);
}
topicChanged(callback: Function, save: boolean = false) {
if(this.topics && save) {
this.topics.disable();
}
if(this.categories) {
this.categories.disable();
}
if(this.subCategories) {
this.subCategories.disable();
}
if(callback) {
callback();
}
this.cdr.detectChanges();
if(this.topics && save) {
this.topics.init();
this.topics.enable();
}
if(this.categories) {
this.categories.init();
this.categories.enable();
}
if(this.subCategories) {
this.subCategories.init();
this.subCategories.enable();
}
}
private buildTopic(topic: Topic) {
let topics = this.stakeholder.topics.filter(element => element._id !== topic._id);
this.form = this.fb.group({
_id: this.fb.control(topic._id),
name: this.fb.control(topic.name, Validators.required),
description: this.fb.control(topic.description),
creationDate: this.fb.control(topic.creationDate),
alias: this.fb.control(topic.alias, [
Validators.required,
this.stakeholderUtils.aliasValidator(topics)
]
),
visibility: this.fb.control(topic.visibility),
defaultId: this.fb.control(topic.defaultId),
categories: this.fb.control(topic.categories),
icon: this.fb.control(topic.icon)
});
this.topicSubscriptions.push(this.form.get('name').valueChanges.subscribe(value => {
let i = 1;
value = this.stakeholderUtils.generateAlias(value);
this.form.controls['alias'].setValue(value);
while (this.form.get('alias').invalid) {
this.form.controls['alias'].setValue(value + i);
i++;
}
}));
}
public editTopicOpen(index = -1) {
this.index = index;
this.type = 'topic';
if (index === -1) {
this.buildTopic(new Topic(null, null, null, "PUBLIC"));
} else {
this.buildTopic(this.stakeholder.topics[index]);
}
this.editOpen();
}
public saveTopic() {
if (!this.form.invalid) {
let path = [this.stakeholder._id];
let callback = (topic: Topic): void => {
this.topicChanged(() => {
if (this.index === -1) {
this.stakeholder.topics.push(topic);
} else {
this.stakeholder.topics[this.index] = HelperFunctions.copy(topic);
}
}, true);
};
if (this.index === -1) {
this.save('Topic has been successfully created', path, this.form.value, callback);
} else {
this.save('Topic has been successfully saved', path, this.form.value, callback);
}
}
}
public changeTopicStatus(propagate: boolean = false) {
let path = [
this.stakeholder._id,
this.stakeholder.topics[this.index]._id
];
let callback = (topic: Topic): void => {
this.topicChanged(() => {
this.stakeholder.topics[this.index] = HelperFunctions.copy(topic);
}, true);
}
this.changeStatus(this.stakeholder.topics[this.index], path, this.visibility, callback, propagate);
this.visibilityModal.cancel();
}
public deleteTopicOpen(index = this.topicIndex, childrenAction: string = null) {
this.type = 'topic';
this.index = index;
this.element = this.stakeholder.topics[this.index];
this.deleteOpen(childrenAction);
}
public deleteTopic() {
let path: string[] = [
this.stakeholder._id,
this.stakeholder.topics[this.index]._id
];
let callback = (): void => {
this.topicChanged(() => {
this.stakeholder.topics.splice(this.index, 1);
if(this.topicIndex === this.index) {
this.chooseTopic(Math.max(0, this.index - 1));
}
}, true);
};
this.delete('Topic has been successfully be deleted', path, callback, (this.topicIndex === this.index));
}
public moveTopic(index: number, newIndex: number = index - 1) {
this.topics.init();
let path = [this.stakeholder._id];
let ids = this.stakeholder.topics.map(topic => topic._id);
HelperFunctions.swap(ids, index, newIndex);
this.stakeholderService.reorderElements(properties.monitorServiceAPIURL, path, ids).subscribe(() => {
HelperFunctions.swap(this.stakeholder.topics, index, newIndex);
if(this.topicIndex === index) {
this.chooseTopic(newIndex);
} else if(this.topicIndex === newIndex) {
this.chooseTopic(index);
}
}, error => {
NotificationHandler.rise(error.error.message)
});
}
public chooseCategory(index: number) {
this.categoryIndexSubject.next(index);
this.chooseSubcategory(0);
}
categoryChanged(callback: Function, save: boolean = false) {
if(this.categories && save) {
this.categories.disable();
}
if(this.subCategories) {
this.subCategories.disable();
}
if(callback) {
callback();
}
this.cdr.detectChanges();
if(this.categories && save) {
this.categories.init();
this.categories.enable();
}
if(this.subCategories) {
this.subCategories.init();
this.subCategories.enable();
}
}
private buildCategory(category: Category) {
let categories = this.stakeholder.topics[this.topicIndex].categories.filter(element => element._id !== category._id);
this.form = this.fb.group({
_id: this.fb.control(category._id),
name: this.fb.control(category.name, Validators.required),
description: this.fb.control(category.description),
creationDate: this.fb.control(category.creationDate),
alias: this.fb.control(category.alias, [
Validators.required,
this.stakeholderUtils.aliasValidator(categories)
]
),
visibility: this.fb.control(category.visibility),
defaultId: this.fb.control(category.defaultId),
subCategories: this.fb.control(category.subCategories)
});
this.topicSubscriptions.push(this.form.get('name').valueChanges.subscribe(value => {
let i = 1;
value = this.stakeholderUtils.generateAlias(value);
this.form.controls['alias'].setValue(value);
while (this.form.get('alias').invalid) {
this.form.controls['alias'].setValue(value + i);
i++;
}
}));
}
public editCategoryOpen(index: number = -1) {
this.index = index;
this.type = 'category';
if (index === -1) {
this.buildCategory(new Category(null, null, null, "PUBLIC"));
} else {
this.buildCategory(this.stakeholder.topics[this.topicIndex].categories[index]);
}
this.editOpen();
}
public saveCategory() {
if (!this.form.invalid) {
let path = [this.stakeholder._id, this.stakeholder.topics[this.topicIndex]._id];
let callback = (category: Category): void => {
this.categoryChanged(() => {
if (this.index === -1) {
this.stakeholder.topics[this.topicIndex].categories.push(category);
this.categories.init();
} else {
this.stakeholder.topics[this.topicIndex].categories[this.index] = HelperFunctions.copy(category);
}
}, true);
};
if (this.index === -1) {
this.save('Category has been successfully created', path, this.form.value, callback);
} else {
this.save('Category has been successfully saved', path, this.form.value, callback);
}
}
}
public changeCategoryStatus(propagate: boolean = false) {
let path = [
this.stakeholder._id,
this.stakeholder.topics[this.topicIndex]._id,
this.stakeholder.topics[this.topicIndex].categories[this.index]._id
];
let callback = (category: Category): void => {
this.categoryChanged(() => {
this.stakeholder.topics[this.topicIndex].categories[this.index] = HelperFunctions.copy(category);
}, true);
}
this.changeStatus(this.stakeholder.topics[this.topicIndex].categories[this.index], path, this.visibility, callback, propagate);
this.visibilityModal.cancel();
}
public deleteCategoryOpen(index: number, childrenAction: string = null) {
this.type = 'category';
this.index = index;
this.element = this.stakeholder.topics[this.topicIndex].categories[this.index];
this.deleteOpen(childrenAction);
}
public deleteCategory() {
let path: string[] = [
this.stakeholder._id,
this.stakeholder.topics[this.topicIndex]._id,
this.stakeholder.topics[this.topicIndex].categories[this.index]._id
];
let callback = (): void => {
this.categoryChanged(() => {
this.stakeholder.topics[this.topicIndex].categories.splice(this.index, 1);
if(this.categoryIndex === this.index) {
this.chooseCategory(Math.max(0, this.index - 1));
}
}, true);
};
this.delete('Category has been successfully be deleted', path, callback);
}
public moveCategory(index: number, newIndex: number = index - 1) {
this.categories.init();
let path = [this.stakeholder._id, this.stakeholder.topics[this.topicIndex]._id];
let ids = this.stakeholder.topics[this.topicIndex].categories.map(category => category._id);
HelperFunctions.swap(ids, index, newIndex);
this.stakeholderService.reorderElements(properties.monitorServiceAPIURL, path, ids).subscribe(() => {
HelperFunctions.swap(this.stakeholder.topics[this.topicIndex].categories, index, newIndex);
if(this.categoryIndex === index) {
this.chooseCategory(newIndex);
} else if(this.categoryIndex === newIndex) {
this.chooseCategory(index);
}
}, error => {
NotificationHandler.rise(error.error.message)
});
}
chooseSubcategory(subcategoryIndex: number) {
this.subCategoryIndexSubject.next(subcategoryIndex);
}
subCategoryChanged(callback: Function, save: boolean = false) {
if(this.subCategories && save) {
this.subCategories.disable();
}
if(callback) {
callback();
}
this.cdr.detectChanges();
if(this.subCategories && save) {
this.subCategories.init();
this.subCategories.enable();
}
}
private buildSubcategory(subCategory: SubCategory) {
let subCategories = this.stakeholder.topics[this.topicIndex].categories[this.categoryIndex].subCategories.filter(element => element._id !== subCategory._id);
this.form = this.fb.group({
_id: this.fb.control(subCategory._id),
name: this.fb.control(subCategory.name, Validators.required),
description: this.fb.control(subCategory.description),
creationDate: this.fb.control(subCategory.creationDate),
alias: this.fb.control(subCategory.alias, [
Validators.required,
this.stakeholderUtils.aliasValidator(subCategories)
]
),
visibility: this.fb.control(subCategory.visibility),
defaultId: this.fb.control(subCategory.defaultId),
charts: this.fb.control(subCategory.charts),
numbers: this.fb.control(subCategory.numbers)
});
this.topicSubscriptions.push(this.form.get('name').valueChanges.subscribe(value => {
let i = 1;
value = this.stakeholderUtils.generateAlias(value);
this.form.controls['alias'].setValue(value);
while (this.form.get('alias').invalid) {
this.form.controls['alias'].setValue(value + i);
i++;
}
}));
}
public editSubCategoryOpen(index: number = -1) {
this.index = index;
this.type = 'subcategory';
if (index === -1) {
this.buildSubcategory(new SubCategory(null, null, null, "PUBLIC"));
} else {
this.buildSubcategory(this.stakeholder.topics[this.topicIndex].categories[this.categoryIndex].subCategories[index]);
}
this.editOpen();
}
public saveSubCategory() {
if (!this.form.invalid) {
let path: string[] = [
this.stakeholder._id,
this.stakeholder.topics[this.topicIndex]._id,
this.stakeholder.topics[this.topicIndex].categories[this.categoryIndex]._id
];
let callback = (subCategory: SubCategory): void => {
this.subCategoryChanged(() => {
if (this.index === -1) {
this.stakeholder.topics[this.topicIndex].categories[this.categoryIndex].subCategories.push(subCategory);
} else {
this.stakeholder.topics[this.topicIndex].categories[this.categoryIndex].subCategories[this.index] = HelperFunctions.copy(subCategory);
}
}, true);
};
if (this.index === -1) {
this.save('Subcategory has been successfully created', path, this.form.value, callback);
} else {
this.save('Subcategory has been successfully saved', path, this.form.value, callback);
}
}
}
public changeSubcategoryStatus(propagate: boolean = false) {
let path = [
this.stakeholder._id,
this.stakeholder.topics[this.topicIndex]._id,
this.stakeholder.topics[this.topicIndex].categories[this.categoryIndex]._id,
this.stakeholder.topics[this.topicIndex].categories[this.categoryIndex].subCategories[this.index]._id
];
let callback = (subcategory: SubCategory): void => {
this.subCategoryChanged(() => {
this.stakeholder.topics[this.topicIndex].categories[this.categoryIndex].subCategories[this.index] = HelperFunctions.copy(subcategory);
}, true);
}
this.changeStatus(this.stakeholder.topics[this.topicIndex].categories[this.categoryIndex].subCategories[this.index], path, this.visibility, callback, propagate);
this.visibilityModal.cancel();
}
public deleteSubcategoryOpen(index, childrenAction: string = null) {
this.type = 'subcategory';
this.index = index;
this.element = this.stakeholder.topics[this.topicIndex].categories[this.categoryIndex].subCategories[this.index];
this.deleteOpen(childrenAction);
}
public deleteSubcategory() {
let path: string[] = [
this.stakeholder._id,
this.stakeholder.topics[this.topicIndex]._id,
this.stakeholder.topics[this.topicIndex].categories[this.categoryIndex]._id,
this.stakeholder.topics[this.topicIndex].categories[this.categoryIndex].subCategories[this.index]._id
];
let callback = (): void => {
this.subCategoryChanged(() => {
this.stakeholder.topics[this.topicIndex].categories[this.categoryIndex].subCategories.splice(this.index, 1);
if(this.subCategoryIndex === this.index) {
this.chooseSubcategory(Math.max(0, this.index - 1));
}
}, true);
};
this.delete('Subcategory has been successfully be deleted', path, callback);
}
public moveSubCategory(index: number, newIndex: number = index - 1) {
this.subCategories.init();
let path = [this.stakeholder._id, this.stakeholder.topics[this.topicIndex]._id, this.stakeholder.topics[this.topicIndex].categories[this.categoryIndex]._id];
let ids = this.stakeholder.topics[this.topicIndex].categories[this.categoryIndex].subCategories.map(subCategory => subCategory._id);
HelperFunctions.swap(ids, index, newIndex);
this.stakeholderService.reorderElements(properties.monitorServiceAPIURL, path, ids).subscribe(() => {
HelperFunctions.swap(this.stakeholder.topics[this.topicIndex].categories[this.categoryIndex].subCategories, index, newIndex);
if(this.subCategoryIndex === index) {
this.chooseSubcategory(newIndex);
} else if(this.subCategoryIndex === newIndex) {
this.chooseSubcategory(index);
}
}, error => {
NotificationHandler.rise(error.error.message)
});
}
private navigateToError() {
this.router.navigate([this.properties.errorLink], {queryParams: {'page': this.router.url}});
}
get isCurator(): boolean {
return Session.isPortalAdministrator(this.user) || Session.isCurator(this.stakeholder.type, this.user);
}
private editOpen() {
this.editModal.cancelButtonText = 'Cancel';
this.editModal.okButtonLeft = false;
this.editModal.alertMessage = false;
if (this.index === -1) {
this.editModal.alertTitle = 'Create a new ' + this.type;
this.editModal.okButtonText = 'Create';
} else {
this.editModal.alertTitle = 'Edit ' + this.type + '\'s information ';
this.editModal.okButtonText = 'Save';
}
this.editModal.stayOpen = true;
this.editModal.open();
}
private deleteOpen(childrenAction: string = null) {
this.elementChildrenActionOnDelete = null;
if (childrenAction == "delete") {
this.elementChildrenActionOnDelete = childrenAction;
} else if (childrenAction == "disconnect") {
this.elementChildrenActionOnDelete = childrenAction;
}
this.deleteModal.alertTitle = 'Delete ' + this.type;
this.deleteModal.cancelButtonText = 'No';
this.deleteModal.okButtonText = 'Yes';
// this.deleteModal.cancelButton = false;
// this.deleteModal.okButton = false;
this.deleteModal.stayOpen = true;
this.deleteModal.open();
}
private save(message: string, path: string[], saveElement: any, callback: Function, redirect = false) {
this.loading = true;
this.topicSubscriptions.push(this.stakeholderService.saveElement(this.properties.monitorServiceAPIURL, saveElement, path).subscribe(saveElement => {
callback(saveElement);
this.stakeholderChanged();
this.loading = false;
this.editModal.cancel();
NotificationHandler.rise(message);
if (redirect) {
this.router.navigate(['../' + saveElement.alias], {
relativeTo: this.route
});
}
}, error => {
this.loading = false;
this.editModal.cancel();
NotificationHandler.rise(error.error.message, 'danger');
}));
}
private delete(message: string, path: string[], callback: Function, redirect = false) {
this.loading = true;
this.topicSubscriptions.push(this.stakeholderService.deleteElement(this.properties.monitorServiceAPIURL, path, this.elementChildrenActionOnDelete).subscribe(() => {
callback();
this.stakeholderChanged();
this.loading = false;
this.deleteModal.cancel();
NotificationHandler.rise(message);
if (redirect) {
this.back();
}
}, error => {
this.loading = false;
this.deleteModal.cancel();
NotificationHandler.rise(error.error.message, 'danger');
}));
}
private changeStatus(element: Topic | Category | SubCategory, path: string[], visibility: Visibility, callback: Function = null, propagate: boolean = false) {
this.topicSubscriptions.push(this.stakeholderService.changeVisibility(this.properties.monitorServiceAPIURL, path, visibility, propagate).subscribe(returnedElement => {
if(propagate) {
callback(returnedElement);
NotificationHandler.rise(StringUtils.capitalize(this.type) + ' has been <b>successfully changed</b> to ' + returnedElement.visibility.toLowerCase());
} else {
element.visibility = returnedElement.visibility;
NotificationHandler.rise(StringUtils.capitalize(this.type) + ' has been <b>successfully changed</b> to ' + element.visibility.toLowerCase());
}
}, error => {
NotificationHandler.rise(error.error.message, 'danger');
}));
}
back() {
this.router.navigate(['../'], {
relativeTo: this.route
});
}
public getPluralTypeName(): string {
if (this.type == "topic") {
return "Topics";
} else if (this.type == "category") {
return "Categories";
} else if (this.type == "subcategory") {
return "Subcategories";
} else {
return this.type;
}
}
public get isSmallScreen() {
return this.layoutService.isSmallScreen;
}
public get open() {
return this.layoutService.open;
}
public toggleOpen(event: MouseEvent) {
event.preventDefault();
this.layoutService.setOpen(!this.open);
}
public openVisibilityModal(index: number, visibility: Visibility, type: any) {
this.index = index;
this.visibility = visibility;
this.type = type;
this.visibilityModal.alertTitle = 'Visibility Status';
this.visibilityModal.alertFooter = false;
this.visibilityModal.open();
}
}

View File

@ -0,0 +1,49 @@
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {PreviousRouteRecorder} from '../../utils/piwik/previousRouteRecorder.guard';
import {PiwikService} from '../../utils/piwik/piwik.service';
import {TopicComponent} from "./topic.component";
import {TopicRoutingModule} from "./topic-routing.module";
import {RouterModule} from "@angular/router";
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
import {IndicatorsComponent} from "./indicators.component";
import {AlertModalModule} from "../../utils/modal/alertModal.module";
import {InputModule} from "../../sharedComponents/input/input.module";
import {ClickModule} from "../../utils/click/click.module";
import {IconsService} from "../../utils/icons/icons.service";
import {earth, incognito, restricted} from "../../utils/icons/icons";
import {IconsModule} from "../../utils/icons/icons.module";
import {PageContentModule} from "../../dashboard/sharedComponents/page-content/page-content.module";
import {LoadingModule} from "../../utils/loading/loading.module";
import {NotifyFormModule} from "../../notifications/notify-form/notify-form.module";
import {LogoUrlPipeModule} from "../../utils/pipes/logoUrlPipe.module";
import {TransitionGroupModule} from "../../utils/transition-group/transition-group.module";
import {NumberRoundModule} from "../../utils/pipes/number-round.module";
import {SideBarModule} from "../../dashboard/sharedComponents/sidebar/sideBar.module";
import {
SidebarMobileToggleModule
} from "../../dashboard/sharedComponents/sidebar/sidebar-mobile-toggle/sidebar-mobile-toggle.module";
@NgModule({
imports: [
CommonModule, TopicRoutingModule, ClickModule, RouterModule, FormsModule, AlertModalModule,
ReactiveFormsModule, InputModule, IconsModule, PageContentModule, LoadingModule, NotifyFormModule, LogoUrlPipeModule, TransitionGroupModule, NumberRoundModule, SideBarModule, SidebarMobileToggleModule
],
declarations: [
TopicComponent, IndicatorsComponent
],
providers: [
PreviousRouteRecorder,
PiwikService
],
exports: [
TopicComponent
]
})
export class TopicModule {
constructor(private iconsService: IconsService) {
this.iconsService.registerIcons([earth, incognito, restricted]);
}
}

View File

@ -0,0 +1,43 @@
import {Injectable} from '@angular/core';
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot, UrlTree } from '@angular/router';
import {map, take, tap} from "rxjs/operators";
import {UserManagementService} from "../../services/user-management.service";
import {LoginErrorCodes} from "../../login/utils/guardHelper.class";
import {Session} from "../../login/utils/helper.class";
import {StakeholderService} from "../../monitor/services/stakeholder.service";
import {Observable, zip} from "rxjs";
@Injectable()
export class AdminDashboardGuard {
constructor(private router: Router,
private stakeholderService: StakeholderService,
private userManagementService: UserManagementService) {
}
check(path: string, alias: string): Observable<boolean> | boolean {
let errorCode = LoginErrorCodes.NOT_LOGIN;
return zip(
this.userManagementService.getUserInfo(), this.stakeholderService.getStakeholder(alias)
).pipe(take(1),map(res => {
if(res[0]) {
errorCode = LoginErrorCodes.NOT_ADMIN;
}
return res[0] && res[1] && (Session.isPortalAdministrator(res[0]) ||
Session.isCurator(res[1].type, res[0]) || Session.isManager(res[1].type, res[1].alias, res[0]))
}),tap(authorized => {
if(!authorized){
this.router.navigate(['/user-info'], {queryParams: {'errorCode': errorCode, 'redirectUrl':path}});
}
}));
}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
return this.check(state.url, route.params.stakeholder);
}
canActivateChild(childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
return this.check(state.url, childRoute.params.stakeholder);
}
}

View File

@ -0,0 +1,9 @@
@import (reference) "~src/assets/openaire-theme/less/_import-variables.less";
.cache-progress {
position: fixed;
bottom: 0;
right: 0;
transform: translate(-50%, -50%);
z-index: @global-z-index;
}

View File

@ -0,0 +1,78 @@
import {Component, Inject, Input, OnChanges, OnDestroy, OnInit, PLATFORM_ID, SimpleChanges} from "@angular/core";
import {Report} from "./cache-indicators";
import {CacheIndicatorsService} from "./cache-indicators.service";
import {interval, Subject, Subscription} from "rxjs";
import {map, switchMap, takeUntil} from "rxjs/operators";
@Component({
selector: 'cache-indicators',
template: `
<div *ngIf="report" class="cache-progress">
<div class="uk-position-relative" [attr.uk-tooltip]="'Caching indicators process for ' + alias">
<div class="uk-progress-circle" [attr.percentage]="report.percentage?report.percentage:0" [style]="'--percentage: ' + (report.percentage?report.percentage:0)"></div>
<button *ngIf="report.percentage === 100" (click)="clear()" class="uk-icon-button uk-icon-button-xsmall uk-button-default uk-position-top-right"><icon name="close" [flex]="true" ratio="0.8"></icon></button>
</div>
</div>
`,
styleUrls: ['cache-indicators.component.less']
})
export class CacheIndicatorsComponent implements OnInit, OnChanges, OnDestroy {
report: Report;
subscriptions: Subscription[] = [];
interval: number = 10000;
readonly destroy$ = new Subject();
@Input() alias: string;
constructor(private cacheIndicatorsService: CacheIndicatorsService,
@Inject(PLATFORM_ID) private platformId) {
}
ngOnInit() {
this.getReport();
}
ngOnChanges(changes: SimpleChanges) {
if(changes.alias) {
this.getReport();
}
}
getReport() {
this.clear();
this.subscriptions.push(this.cacheIndicatorsService.getReport(this.alias).subscribe(report => {
this.getReportInterval(report);
}));
}
getReportInterval(report: Report) {
if(this.isBrowser && (this.report || !report?.completed)) {
this.report = report;
this.subscriptions.push(interval(this.interval).pipe(
map(() => this.cacheIndicatorsService.getReport(this.alias)),
switchMap(report => report),
takeUntil(this.destroy$)).subscribe(report => {
console.log(this.alias);
this.report = report;
if(this.report.completed) {
this.destroy$.next();
}
}));
}
}
clear() {
this.subscriptions.forEach(subscription => {
subscription.unsubscribe();
})
this.report = null;
}
get isBrowser() {
return this.platformId === 'browser';
}
ngOnDestroy() {
this.clear();
}
}

View File

@ -0,0 +1,11 @@
import {NgModule} from "@angular/core";
import {CommonModule} from "@angular/common";
import {CacheIndicatorsComponent} from "./cache-indicators.component";
import {IconsModule} from "../../../utils/icons/icons.module";
@NgModule({
imports: [CommonModule, IconsModule],
declarations: [CacheIndicatorsComponent],
exports: [CacheIndicatorsComponent]
})
export class CacheIndicatorsModule {}

View File

@ -0,0 +1,24 @@
import {Injectable} from "@angular/core";
import {HttpClient} from "@angular/common/http";
import {properties} from "src/environments/environment";
import {CustomOptions} from "../../../services/servicesUtils/customOptions.class";
import {map} from "rxjs/operators";
@Injectable({
providedIn: 'root'
})
export class CacheIndicatorsService {
constructor(private http: HttpClient) {
}
createReport(alias: string) {
return this.http.post<any>(properties.domain + properties.baseLink + '/cache/' + alias, {}, CustomOptions.registryOptions())
.pipe(map(res => res.report));
}
getReport(alias: string) {
return this.http.get<any>(properties.domain + properties.baseLink + '/cache/' + alias, CustomOptions.registryOptions())
.pipe(map(res => res.report));
}
}

View File

@ -0,0 +1,261 @@
import {IndicatorType, Stakeholder} from "../../../monitor/entities/stakeholder";
import axios from "axios";
import {IndicatorUtils} from "../indicator-utils";
import {Composer} from "../../../utils/email/composer";
import {properties} from "src/environments/environment";
export interface CacheItem {
reportId: string,
type: IndicatorType,
url: string
}
export class Report {
creator: string;
name: string;
success: number;
errors: {
url: string,
status: number
}[];
total: number;
completed: boolean;
percentage: number
constructor(total: number, name: string, creator: string) {
this.creator = creator;
this.name = name;
this.success = 0;
this.errors = [];
this.total = total;
this.completed = false;
}
setPercentage() {
this.percentage = Math.floor((this.success + this.errors.length) / this.total * 100);
}
}
export class CacheIndicators {
private static BATCH_SIZE = 10;
private reports: Map<string, Report> = new Map<string, Report>();
private queue: CacheItem[] = [];
private process: Promise<void>;
private isFinished: boolean = true;
stakeholderToCacheItems(stakeholder: Stakeholder) {
let cacheItems: CacheItem[] = [];
let indicatorUtils = new IndicatorUtils();
stakeholder.topics.forEach(topic => {
topic.categories.forEach(category => {
category.subCategories.forEach(subCategory => {
subCategory.numbers.forEach(section => {
section.indicators.forEach(indicator => {
indicator.indicatorPaths.forEach(indicatorPath => {
let url = indicatorUtils.getNumberUrl(indicatorPath.source, indicatorUtils.getFullUrl(stakeholder, indicatorPath));
cacheItems.push({
reportId: stakeholder._id,
type: 'number',
url: url
});
});
});
});
subCategory.charts.forEach(section => {
section.indicators.forEach(indicator => {
indicator.indicatorPaths.forEach(indicatorPath => {
let url = indicatorUtils.getChartUrl(indicatorPath.source, indicatorUtils.getFullUrl(stakeholder, indicatorPath));
cacheItems.push({
reportId: stakeholder._id,
type: 'chart',
url: url
});
});
});
});
});
});
});
return cacheItems;
}
public exists(id: string) {
return this.reports.has(id);
}
public completed(id: string) {
return !this.exists(id) || this.reports.get(id).completed;
}
public createReport(id: string, cacheItems: CacheItem[], name: string, creator: string) {
let report = new Report(cacheItems.length, name, creator);
this.reports.set(id, report);
this.addItemsToQueue(cacheItems);
return report;
}
public getReport(id: string) {
return this.reports.get(id);
}
private async processQueue() {
this.isFinished = false;
while (this.queue.length > 0) {
let batch = this.queue.splice(0, CacheIndicators.BATCH_SIZE);
await this.processBatch(batch);
}
}
private async processBatch(batch: CacheItem[]) {
let promises: Promise<any>[] = [];
let ids = new Set<string>();
batch.forEach(item => {
let promise;
ids.add(item.reportId);
if (item.type === 'chart') {
let [url, json] = item.url.split('?json=');
json = decodeURIComponent(json);
json = statsToolParser(JSON.parse(json));
promise = axios.post(url, json);
} else {
promise = axios.get(item.url);
}
promises.push(promise.then(response => {
let report = this.reports.get(item.reportId);
if (report) {
report.success++;
report.setPercentage();
}
return response;
}).catch(error => {
let report = this.reports.get(item.reportId);
if (report) {
report.errors.push({url: item.url, status: error.response.status});
report.setPercentage();
}
return error.response;
}));
});
await Promise.all(promises);
ids.forEach(id => {
let report = this.reports.get(id);
if (report?.percentage === 100) {
report.completed = true;
this.sendEmail(report);
}
});
}
private addItemsToQueue(cacheItems: CacheItem[]) {
cacheItems.forEach(item => {
this.queue.push(item);
});
if (this.isFinished) {
this.processQueue().then(() => {
this.isFinished = true;
});
}
}
sendEmail(report: Report) {
let email = Composer.composeEmailToReportCachingProcess(report);
axios.post(properties.adminToolsAPIURL + "sendMail/", email).catch(error => {
console.error(error);
});
}
}
export function statsToolParser(dataJSONobj: any): any {
let RequestInfoObj = Object.assign({});
switch (dataJSONobj.library) {
case "GoogleCharts":
//Pass the Chart library to ChartDataFormatter
RequestInfoObj.library = dataJSONobj.library;
RequestInfoObj.orderBy = dataJSONobj.orderBy;
//Create ChartInfo Object Array
RequestInfoObj.chartsInfo = [];
//Create ChartInfo and pass the Chart data queries to ChartDataFormatter
//along with the requested Chart type
RequestInfoObj.chartsInfo = dataJSONobj.chartDescription.queriesInfo;
break;
case "eCharts":
//Pass the Chart library to ChartDataFormatter
RequestInfoObj.library = dataJSONobj.library;
RequestInfoObj.orderBy = dataJSONobj.orderBy;
//Create ChartInfo Object Array
RequestInfoObj.chartsInfo = [];
//Create ChartInfo and pass the Chart data queries to ChartDataFormatter
//along with the requested Chart type
for (let index = 0; index < dataJSONobj.chartDescription.queries.length; index++) {
let element = dataJSONobj.chartDescription.queries[index];
var ChartInfoObj = Object.assign({});
if (element.type === undefined)
ChartInfoObj.type = dataJSONobj.chartDescription.series[index].type;
else
ChartInfoObj.type = element.type;
if (element.name === undefined)
ChartInfoObj.name = null;
else
ChartInfoObj.name = element.name;
ChartInfoObj.query = element.query;
RequestInfoObj.chartsInfo.push(ChartInfoObj);
}
break;
case "HighCharts":
RequestInfoObj.library = dataJSONobj.library;
RequestInfoObj.orderBy = dataJSONobj.orderBy;
//Pass the Chart type to ChartDataFormatter
var defaultType = dataJSONobj.chartDescription.chart.type;
//Create ChartInfo Object Array
RequestInfoObj.chartsInfo = [];
//Create ChartInfo and pass the Chart data queries to ChartDataFormatter
//along with the requested Chart type
dataJSONobj.chartDescription.queries.forEach(element => {
var ChartInfoObj = Object.assign({});
if (element.type === undefined)
ChartInfoObj.type = defaultType;
else
ChartInfoObj.type = element.type;
if (element.name === undefined)
ChartInfoObj.name = null;
else
ChartInfoObj.name = element.name;
ChartInfoObj.query = element.query;
RequestInfoObj.chartsInfo.push(ChartInfoObj);
});
break;
case "HighMaps":
RequestInfoObj.library = dataJSONobj.library;
//Create ChartInfo Object Array
RequestInfoObj.chartsInfo = [];
//Create ChartInfo and pass the Chart data queries to ChartDataFormatter
dataJSONobj.mapDescription.queries.forEach(element => {
var ChartInfoObj = Object.assign({});
if (element.name === undefined)
ChartInfoObj.name = null;
else
ChartInfoObj.name = element.name;
ChartInfoObj.query = element.query;
RequestInfoObj.chartsInfo.push(ChartInfoObj);
});
break;
default:
console.log("Unsupported Library: " + dataJSONobj.library);
}
return RequestInfoObj;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,24 @@
import {Injectable} from '@angular/core';
import {HttpClient} from "@angular/common/http";
import {Observable} from "rxjs";
import {SourceType} from "../../../monitor/entities/stakeholder";
import {IndicatorUtils} from "../indicator-utils";
@Injectable({
providedIn: 'root'
})
export class StatisticsService {
indicatorsUtils = new IndicatorUtils();
constructor(private http: HttpClient) {}
getNumbers(source: SourceType, url: string): Observable<any> {
if (source !== null) {
return this.http.get<any>(this.indicatorsUtils.getNumberUrl(source, url));
} else {
return this.http.get<any>(url);
}
}
}

View File

@ -0,0 +1,19 @@
import {Injectable} from "@angular/core";
import {HttpClient} from "@angular/common/http";
import {properties} from "src/environments/environment";
import {Observable} from "rxjs";
import {map} from "rxjs/operators";
@Injectable({
providedIn: 'root'
})
export class StatsProfilesService {
constructor(private http: HttpClient) {
}
getStatsProfiles(): Observable<string[]> {
return this.http.get<any[]>(properties.monitorStatsFrameUrl + 'schema/profiles')
.pipe(map(profiles => profiles.map(profile => profile.name)));
}
}

View File

@ -1,6 +1,7 @@
import {SafeResourceUrl} from "@angular/platform-browser"; import {SafeResourceUrl} from "@angular/platform-browser";
import {properties} from "../../../../environments/environment"; import {properties} from "../../../../environments/environment";
import {Session, User} from "../../login/utils/helper.class"; import {Session, User} from "../../login/utils/helper.class";
import {Option} from "../../sharedComponents/input/input.component";
export const ChartHelper = { export const ChartHelper = {
prefix: "((__", prefix: "((__",
@ -309,3 +310,10 @@ export enum StakeholderEntities {
ORGANIZATIONS = 'Research Institutions', ORGANIZATIONS = 'Research Institutions',
PROJECTS = 'Projects' PROJECTS = 'Projects'
} }
export let stakeholderTypes: Option[] = [
{value: 'funder', label: StakeholderEntities.FUNDER},
{value: 'ri', label: StakeholderEntities.RI},
{value: 'project', label: StakeholderEntities.PROJECT},
{value: 'organization', label: StakeholderEntities.ORGANIZATION}
];

View File

@ -5,6 +5,7 @@ import {Meta, Title} from "@angular/platform-browser";
import {SEOService} from "../../sharedComponents/SEO/SEO.service"; import {SEOService} from "../../sharedComponents/SEO/SEO.service";
import {Breadcrumb} from "../../utils/breadcrumbs/breadcrumbs.component"; import {Breadcrumb} from "../../utils/breadcrumbs/breadcrumbs.component";
import {Subscriber} from "rxjs"; import {Subscriber} from "rxjs";
import {StakeholderEntities} from "../entities/stakeholder";
@Component({ @Component({
selector: 'indicator-themes-page', selector: 'indicator-themes-page',
@ -53,9 +54,9 @@ import {Subscriber} from "rxjs";
This is the current set of indicator themes we cover. Well keep enriching it as new requests and data are coming into the <a href="https://graph.openaire.eu" class="text-graph" target="_blank">OpenAIRE Graph</a>. We are at your disposal, should you have any recommendations! This is the current set of indicator themes we cover. Well keep enriching it as new requests and data are coming into the <a href="https://graph.openaire.eu" class="text-graph" target="_blank">OpenAIRE Graph</a>. We are at your disposal, should you have any recommendations!
</p> </p>
<p> <p>
Check out the indicator pages (for <a [routerLink]="['../funder']" [relativeTo]="route">funders</a>, Check out the indicator pages (for <a [routerLink]="['../funder']" [relativeTo]="route" class="uk-text-lowercase">{{entities.FUNDERS}}</a>,
<a [routerLink]="['../organization']" [relativeTo]="route">research institutions</a> and <a [routerLink]="['../organization']" [relativeTo]="route" class="uk-text-lowercase">{{entities.ORGANIZATIONS}}</a> and
<a [routerLink]="['../ri']" [relativeTo]="route">research initiatives</a>) <a [routerLink]="['../ri']" [relativeTo]="route" class="uk-text-lowercase">{{entities.RIS}}</a>)
for the specific indicators for each type of dashboard, and the <a [routerLink]="['../../methodology']" [relativeTo]="route">methodology and terminology</a> page on how we produce the metrics. for the specific indicators for each type of dashboard, and the <a [routerLink]="['../../methodology']" [relativeTo]="route">methodology and terminology</a> page on how we produce the metrics.
</p> </p>
</div> </div>
@ -67,6 +68,7 @@ import {Subscriber} from "rxjs";
export class IndicatorThemesComponent implements OnInit, OnDestroy { export class IndicatorThemesComponent implements OnInit, OnDestroy {
private subscriptions: any[] = []; private subscriptions: any[] = [];
public properties = properties; public properties = properties;
public entities = StakeholderEntities;
public breadcrumbs: Breadcrumb[] = [{name: 'home', route: '/'}, {name: 'Resources - Themes'}]; public breadcrumbs: Breadcrumb[] = [{name: 'home', route: '/'}, {name: 'Resources - Themes'}];
constructor(private router: Router, constructor(private router: Router,

View File

@ -1,4 +1,3 @@
import {COOKIE} from '../../login/utils/helper.class';
import {HttpHeaders} from "@angular/common/http"; import {HttpHeaders} from "@angular/common/http";
export type MediaType = 'application/json' | 'text/plain' export type MediaType = 'application/json' | 'text/plain'