Merge branch 'dmp-refactoring' of code-repo.d4science.org:MaDgiK-CITE/argos into dmp-refactoring

This commit is contained in:
Efstratios Giannopoulos 2023-11-28 17:24:47 +02:00
commit d3d57e8838
27 changed files with 829 additions and 666 deletions

View File

@ -1,6 +1,8 @@
package eu.eudat.model.mapper.publicapi; package eu.eudat.model.mapper.publicapi;
import eu.eudat.model.Dmp; import eu.eudat.model.Dmp;
import eu.eudat.model.publicapi.grant.GrantPublicOverviewModel;
import eu.eudat.model.publicapi.organisation.OrganizationPublicModel;
import eu.eudat.model.publicapi.overviewmodels.DataManagementPlanPublicModel; import eu.eudat.model.publicapi.overviewmodels.DataManagementPlanPublicModel;
import eu.eudat.model.publicapi.researcher.ResearcherPublicModel; import eu.eudat.model.publicapi.researcher.ResearcherPublicModel;
import eu.eudat.model.publicapi.user.UserInfoPublicModel; import eu.eudat.model.publicapi.user.UserInfoPublicModel;
@ -22,6 +24,8 @@ public class DmpToPublicApiDmpMapper {
model.setUsers(dmp.getDmpUsers().stream().map(UserInfoPublicModel::fromDmpUser).toList()); model.setUsers(dmp.getDmpUsers().stream().map(UserInfoPublicModel::fromDmpUser).toList());
model.setResearchers(dmp.getDmpReferences().stream().map(ResearcherPublicModel::fromDmpReference).filter(Objects::isNull).toList()); model.setResearchers(dmp.getDmpReferences().stream().map(ResearcherPublicModel::fromDmpReference).filter(Objects::isNull).toList());
model.setGrant(GrantPublicOverviewModel.fromDmpReferences(dmp.getDmpReferences()));
model.setOrganisations(dmp.getDmpReferences().stream().map(OrganizationPublicModel::fromDmpReference).filter(Objects::isNull).toList());
model.setCreatedAt(Date.from(dmp.getCreatedAt())); model.setCreatedAt(Date.from(dmp.getCreatedAt()));
model.setModifiedAt(Date.from(dmp.getUpdatedAt())); model.setModifiedAt(Date.from(dmp.getUpdatedAt()));

View File

@ -1,9 +1,15 @@
package eu.eudat.model.publicapi.grant; package eu.eudat.model.publicapi.grant;
import eu.eudat.commons.enums.ReferenceType;
import eu.eudat.data.old.Grant; import eu.eudat.data.old.Grant;
import eu.eudat.model.DmpReference;
import eu.eudat.model.Reference;
import eu.eudat.model.publicapi.funder.FunderPublicOverviewModel; import eu.eudat.model.publicapi.funder.FunderPublicOverviewModel;
import eu.eudat.model.referencedefinition.Field;
import java.time.Instant;
import java.util.Date; import java.util.Date;
import java.util.List;
import java.util.UUID; import java.util.UUID;
public class GrantPublicOverviewModel { public class GrantPublicOverviewModel {
@ -72,6 +78,37 @@ public class GrantPublicOverviewModel {
this.funder = funder; this.funder = funder;
} }
public static GrantPublicOverviewModel fromDmpReferences(List<DmpReference> dmpReferences) {
FunderPublicOverviewModel funder = null;
for (DmpReference dmpReference : dmpReferences) {
if (dmpReference.getReference().getType() == ReferenceType.Funder) {
funder = new FunderPublicOverviewModel();
Reference reference = dmpReference.getReference();
funder.setId(reference.getId());
funder.setLabel(reference.getLabel());
continue;
}
if (dmpReference.getReference().getType() != ReferenceType.Grants)
continue;
GrantPublicOverviewModel model = new GrantPublicOverviewModel();
Reference reference = dmpReference.getReference();
model.setId(reference.getId());
model.setDescription(reference.getDescription());
model.setAbbreviation(reference.getAbbreviation());
model.setLabel(reference.getLabel());
model.setFunder(funder);
Field startDate = reference.getDefinition().getFields().stream().filter(x -> x.getCode().equals("startDate")).toList().get(0);
if (startDate != null) model.setStartDate(Date.from(Instant.parse(startDate.getValue())));
Field endDate = reference.getDefinition().getFields().stream().filter(x -> x.getCode().equals("endDate")).toList().get(0);
if (startDate != null) model.setEndDate(Date.from(Instant.parse(endDate.getValue())));
Field uri = reference.getDefinition().getFields().stream().filter(x -> x.getCode().equals("uri")).toList().get(0);
if (uri != null) model.setUri(uri.getValue());
return model;
}
return null;
}
public GrantPublicOverviewModel fromDataModel(Grant entity) { public GrantPublicOverviewModel fromDataModel(Grant entity) {
this.id = entity.getId(); this.id = entity.getId();
this.label = entity.getLabel(); this.label = entity.getLabel();

View File

@ -1,6 +1,9 @@
package eu.eudat.model.publicapi.organisation; package eu.eudat.model.publicapi.organisation;
import eu.eudat.commons.enums.ReferenceType;
import eu.eudat.data.old.Organisation; import eu.eudat.data.old.Organisation;
import eu.eudat.model.DmpReference;
import eu.eudat.model.Reference;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
@ -64,55 +67,18 @@ public class OrganizationPublicModel {
this.key = key; this.key = key;
} }
public OrganizationPublicModel fromDataModel(Organisation entity) { public static OrganizationPublicModel fromDmpReference(DmpReference dmpReference) {
this.id = entity.getId().toString(); if (dmpReference.getReference().getType() != ReferenceType.Organizations)
this.name = entity.getLabel(); return null;
this.label = entity.getUri();
if (entity.getReference() != null) {
this.reference = entity.getReference();
this.key = entity.getReference().substring(0, entity.getReference().indexOf(":"));
}
return this;
}
public Organisation toDataModel() {
Organisation organisationEntity = new Organisation();
if (this.key != null && this.reference != null) {
if (this.reference.startsWith(this.key + ":")) {
organisationEntity.setReference(this.reference);
} else {
organisationEntity.setReference(this.key + ":" + this.reference);
}
}
if (this.id != null) {
organisationEntity.setId(UUID.fromString(this.id));
}
organisationEntity.setLabel(this.name);
organisationEntity.setUri(this.label);
organisationEntity.setCreated(new Date());
organisationEntity.setStatus((short) this.status);
return organisationEntity;
}
public static OrganizationPublicModel fromMap(HashMap<String, Object> map) {
OrganizationPublicModel model = new OrganizationPublicModel(); OrganizationPublicModel model = new OrganizationPublicModel();
if (map != null) { Reference reference = dmpReference.getReference();
model.id = (String) map.get("id"); model.setId(reference.getId().toString());
model.key = (String) map.get("key"); model.setReference(reference.getReference());
model.label = (String) map.get("label"); model.setLabel(reference.getLabel());
model.name = (String) map.get("name"); model.setName(reference.getLabel());
model.reference = (String) map.get("reference"); if (reference.getReference() != null)
model.status = (int) map.get("status"); model.setKey(reference.getReference().substring(0, reference.getReference().indexOf(":")));
model.tag = (String) map.get("tag");
}
return model; return model;
} }
public String generateLabel() {
return this.getName();
}
public String getHint() {
return null;
}
} }

View File

@ -2,14 +2,11 @@ package eu.eudat.model.publicapi.researcher;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import eu.eudat.commons.enums.ReferenceType; import eu.eudat.commons.enums.ReferenceType;
import eu.eudat.data.old.Researcher;
import eu.eudat.model.DmpReference; import eu.eudat.model.DmpReference;
import eu.eudat.model.Reference;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.util.Date;
import java.util.UUID;
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
public class ResearcherPublicModel { public class ResearcherPublicModel {
private static final Logger logger = LoggerFactory.getLogger(ResearcherPublicModel.class); private static final Logger logger = LoggerFactory.getLogger(ResearcherPublicModel.class);
@ -74,10 +71,11 @@ public class ResearcherPublicModel {
if (dmpReference.getReference().getType() != ReferenceType.Researcher) if (dmpReference.getReference().getType() != ReferenceType.Researcher)
return null; return null;
ResearcherPublicModel model = new ResearcherPublicModel(); ResearcherPublicModel model = new ResearcherPublicModel();
model.setId(dmpReference.getReference().getId().toString()); Reference reference = dmpReference.getReference();
model.setReference(dmpReference.getReference().getReference()); model.setId(reference.getId().toString());
model.setLabel(dmpReference.getReference().getLabel()); model.setReference(reference.getReference());
model.setName(dmpReference.getReference().getLabel()); model.setLabel(reference.getLabel());
model.setName(reference.getLabel());
String[] refParts = dmpReference.getReference().getReference().split(":"); String[] refParts = dmpReference.getReference().getReference().split(":");
String source = refParts[0]; String source = refParts[0];
if (source.equals("dmp")) if (source.equals("dmp"))

View File

@ -161,6 +161,9 @@ public class PublicDmpsDocumentation extends BaseController {
String.join(".", Dmp._dmpReferences, String.join(".", DmpReference._reference, Reference._id)), String.join(".", Dmp._dmpReferences, String.join(".", DmpReference._reference, Reference._id)),
String.join(".", Dmp._dmpReferences, String.join(".", DmpReference._reference, Reference._reference)), String.join(".", Dmp._dmpReferences, String.join(".", DmpReference._reference, Reference._reference)),
String.join(".", Dmp._dmpReferences, String.join(".", DmpReference._reference, Reference._label)), String.join(".", Dmp._dmpReferences, String.join(".", DmpReference._reference, Reference._label)),
String.join(".", Dmp._dmpReferences, String.join(".", DmpReference._reference, Reference._abbreviation)),
String.join(".", Dmp._dmpReferences, String.join(".", DmpReference._reference, Reference._description)),
String.join(".", Dmp._dmpReferences, String.join(".", DmpReference._reference, Reference._definition)),
Dmp._createdAt, Dmp._createdAt,
Dmp._updatedAt, Dmp._updatedAt,
Dmp._finalizedAt Dmp._finalizedAt

View File

@ -217,7 +217,7 @@ const appRoutes: Routes = [
}, },
{ {
path: 'users', path: 'users',
loadChildren: () => import('./ui/admin/user/user.module').then(m => m.UserModule), loadChildren: () => import('./ui/admin/user/user.module').then(m => m.UsersModule),
data: { data: {
breadcrumb: true, breadcrumb: true,
title: 'GENERAL.TITLES.USERS' title: 'GENERAL.TITLES.USERS'

View File

@ -34,6 +34,12 @@ export enum AppPermission {
EditTenant = "EditTenant", EditTenant = "EditTenant",
DeleteTenant = "DeleteTenant", DeleteTenant = "DeleteTenant",
//User
BrowseUser = "BrowseUser",
EditUser = "EditUser",
DeleteUser = "DeleteUser",
ExportUsers = "ExportUsers",
//Reference //Reference
BrowseReference = "BrowseReference", BrowseReference = "BrowseReference",
EditReference = "EditReference", EditReference = "EditReference",

View File

@ -29,14 +29,20 @@ import { ReferenceSourceType } from '@app/core/common/enum/reference-source-type
export class EnumUtils { export class EnumUtils {
constructor(private language: TranslateService) { } constructor(private language: TranslateService) { }
public getEnumValues(T): Array<any> { public getEnumValues<T>(T): Array<T> {
let keys = Object.keys(T);
keys = keys.slice(0, keys.length / 2); //getting all numeric values
const values: Array<Number> = keys.map(Number); const numericValues: T[] = Object.keys(T).map(key => T[key]).filter(value => typeof (value) === 'number');
return values; if (numericValues.length > 0) { return numericValues; }
//getting all string values
const stringValues: T[] = Object.keys(T).map(key => T[key]).filter(value => typeof (value) === 'string');
if (stringValues.length > 0) { return stringValues; }
return [];
} }
convertFromPrincipalAppRole(status: AppRole): string { toAppRoleString(status: AppRole): string {
switch (status) { switch (status) {
case AppRole.Admin: return this.language.instant('TYPES.APP-ROLE.ADMIN'); case AppRole.Admin: return this.language.instant('TYPES.APP-ROLE.ADMIN');
case AppRole.User: return this.language.instant('TYPES.APP-ROLE.USER'); case AppRole.User: return this.language.instant('TYPES.APP-ROLE.USER');

View File

@ -57,9 +57,9 @@ export class DmpBlueprintEditorComponent extends BaseEditor<DmpBlueprintEditorMo
selectedSystemFields: Array<DmpBlueprintSystemFieldType> = []; selectedSystemFields: Array<DmpBlueprintSystemFieldType> = [];
dmpBlueprintSectionFieldCategory = DmpBlueprintSectionFieldCategory; dmpBlueprintSectionFieldCategory = DmpBlueprintSectionFieldCategory;
dmpBlueprintSystemFieldType = DmpBlueprintSystemFieldType; dmpBlueprintSystemFieldType = DmpBlueprintSystemFieldType;
public dmpBlueprintSystemFieldTypeEnum = this.enumUtils.getEnumValues(DmpBlueprintSystemFieldType); public dmpBlueprintSystemFieldTypeEnum = this.enumUtils.getEnumValues<DmpBlueprintSystemFieldType>(DmpBlueprintSystemFieldType);
dmpBlueprintExtraFieldDataType = DmpBlueprintExtraFieldDataType; dmpBlueprintExtraFieldDataType = DmpBlueprintExtraFieldDataType;
public dmpBlueprintExtraFieldDataTypeEnum = this.enumUtils.getEnumValues(DmpBlueprintExtraFieldDataType); public dmpBlueprintExtraFieldDataTypeEnum = this.enumUtils.getEnumValues<DmpBlueprintExtraFieldDataType>(DmpBlueprintExtraFieldDataType);
blueprintsAutoCompleteConfiguration: MultipleAutoCompleteConfiguration = { blueprintsAutoCompleteConfiguration: MultipleAutoCompleteConfiguration = {
filterFn: this.filterDescriptionTempaltes.bind(this), filterFn: this.filterDescriptionTempaltes.bind(this),

View File

@ -3,15 +3,17 @@ import { Component, OnInit } from '@angular/core';
import { FormArray, UntypedFormGroup } from '@angular/forms'; import { FormArray, UntypedFormGroup } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { ReferenceTypeService } from '@app/core/services/reference-type/reference-type.service';
import { SnackBarNotificationLevel, UiNotificationService } from '@app/core/services/notification/ui-notification-service'; import { SnackBarNotificationLevel, UiNotificationService } from '@app/core/services/notification/ui-notification-service';
import { ReferenceTypeService } from '@app/core/services/reference-type/reference-type.service';
import { EnumUtils } from '@app/core/services/utilities/enum-utils.service'; import { EnumUtils } from '@app/core/services/utilities/enum-utils.service';
// import { BreadcrumbItem } from '@app/ui/misc/breadcrumb/definition/breadcrumb-item'; // import { BreadcrumbItem } from '@app/ui/misc/breadcrumb/definition/breadcrumb-item';
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { IsActive } from '@app/core/common/enum/is-active.enum'; import { IsActive } from '@app/core/common/enum/is-active.enum';
import { AppPermission } from '@app/core/common/enum/permission.enum'; import { AppPermission } from '@app/core/common/enum/permission.enum';
import { ReferenceType, ReferenceTypePersist, ResultFieldsMappingConfiguration } from '@app/core/model/reference-type/reference-type'; import { ReferenceFieldDataType } from '@app/core/common/enum/reference-field-data-type';
import { ReferenceTypeExternalApiHTTPMethodType } from '@app/core/common/enum/reference-type-external-api-http-method-type';
import { ReferenceTypeSourceType } from '@app/core/common/enum/reference-type-source-type';
import { ReferenceType, ReferenceTypePersist } from '@app/core/model/reference-type/reference-type';
import { AuthService } from '@app/core/services/auth/auth.service'; import { AuthService } from '@app/core/services/auth/auth.service';
import { LoggingService } from '@app/core/services/logging/logging-service'; import { LoggingService } from '@app/core/services/logging/logging-service';
import { QueryParamsService } from '@app/core/services/utilities/query-params.service'; import { QueryParamsService } from '@app/core/services/utilities/query-params.service';
@ -23,12 +25,9 @@ import { FilterService } from '@common/modules/text-filter/filter-service';
import { Guid } from '@common/types/guid'; import { Guid } from '@common/types/guid';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { map, takeUntil } from 'rxjs/operators'; import { map, takeUntil } from 'rxjs/operators';
import { DependencyPropertyEditorModel, QueryConfigEditorModel, ReferenceTypeEditorModel, ReferenceTypeFieldEditorModel, ReferenceTypeSourceBaseConfigurationEditorModel, ReferenceTypeSourceBaseDependencyEditorModel, ReferenceTypeStaticOptionEditorModel, ResultFieldsMappingConfigurationEditorModel } from './reference-type-editor.model';
import { ReferenceTypeEditorResolver } from './reference-type-editor.resolver'; import { ReferenceTypeEditorResolver } from './reference-type-editor.resolver';
import { ReferenceTypeEditorService } from './reference-type-editor.service'; import { ReferenceTypeEditorService } from './reference-type-editor.service';
import { DependencyPropertyEditorModel, QueryConfigEditorModel, ReferenceTypeEditorModel, ReferenceTypeFieldEditorModel, ReferenceTypeSourceBaseConfigurationEditorModel, ReferenceTypeSourceBaseDependencyEditorModel, ReferenceTypeStaticOptionEditorModel, ResultFieldsMappingConfigurationEditorModel } from './reference-type-editor.model';
import { ReferenceFieldDataType } from '@app/core/common/enum/reference-field-data-type';
import { ReferenceTypeSourceType } from '@app/core/common/enum/reference-type-source-type';
import { ReferenceTypeExternalApiHTTPMethodType } from '@app/core/common/enum/reference-type-external-api-http-method-type';
@Component({ @Component({
@ -45,9 +44,9 @@ export class ReferenceTypeEditorComponent extends BaseEditor<ReferenceTypeEditor
showInactiveDetails = false; showInactiveDetails = false;
referenceTypeSourceType = ReferenceTypeSourceType; referenceTypeSourceType = ReferenceTypeSourceType;
referenceTypeExternalApiHTTPMethodType = ReferenceTypeExternalApiHTTPMethodType; referenceTypeExternalApiHTTPMethodType = ReferenceTypeExternalApiHTTPMethodType;
public referenceTypeSourceTypeEnum = this.enumUtils.getEnumValues(ReferenceTypeSourceType); public referenceTypeSourceTypeEnum = this.enumUtils.getEnumValues<ReferenceTypeSourceType>(ReferenceTypeSourceType);
public referenceFieldDataTypeEnum = this.enumUtils.getEnumValues(ReferenceFieldDataType); public referenceFieldDataTypeEnum = this.enumUtils.getEnumValues<ReferenceFieldDataType>(ReferenceFieldDataType);
public referenceTypeExternalApiHTTPMethodTypeEnum = this.enumUtils.getEnumValues(ReferenceTypeExternalApiHTTPMethodType); public referenceTypeExternalApiHTTPMethodTypeEnum = this.enumUtils.getEnumValues<ReferenceTypeExternalApiHTTPMethodType>(ReferenceTypeExternalApiHTTPMethodType);
referenceTypes: ReferenceType[] = null; referenceTypes: ReferenceType[] = null;
sourceKeys: string[] = []; sourceKeys: string[] = [];
sourceKeysMap: Map<string, string[]> = new Map<string, string[]>(); sourceKeysMap: Map<string, string[]> = new Map<string, string[]>();

View File

@ -1,18 +0,0 @@
<form class="user-roles-criteria" [formGroup]="formGroup">
<div class="container-fluid p-0">
<div class="row">
<span class="d-flex col-auto align-items-center pr-0 show">{{'CRITERIA.USERS.SHOW' | translate}}:</span>
<mat-form-field appearance="outline" class="sort-form col-auto">
<mat-select placeholder="{{'CRITERIA.USERS.ROLE' | translate}}" formControlName="appRoles" multiple>
<mat-option *ngFor="let role of rolesEnum" [value]="role">{{enumUtils.convertFromPrincipalAppRole(role)}}</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="search-form ml-auto col-auto" floatLabel="never">
<mat-icon matSuffix>search</mat-icon>
<input matInput placeholder="{{'CRITERIA.USERS.LABEL'| translate}}" formControlName="like">
<!-- <mat-error *ngIf="formGroup.get('like').hasError('backendError')">{{formGroup.get('like').getError('backendError').message}}</mat-error> -->
</mat-form-field>
</div>
</div>
</form>

View File

@ -1,33 +0,0 @@
.user-roles-criteria {
.container-fluid {
margin-top: 5rem;
}
.show {
color: #A8A8A8;
}
.search-form {
text-align: left;
width: 20rem;
}
.search-form mat-icon {
color: var(--primary-color);
}
}
::ng-deep .search-form .mat-form-field-wrapper {
background-color: white !important;
padding-bottom: 0 !important;
}
::ng-deep .sort-form .mat-form-field-wrapper {
background-color: white !important;
padding-bottom: 0 !important;
}
::ng-deep .mat-form-field-appearance-outline .mat-form-field-infix {
padding: 0.3rem 0rem 0.6rem 0rem !important;
}

View File

@ -1,58 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { AppRole } from '@app/core/common/enum/app-role';
import { UserCriteria } from '@app/core/query/user/user-criteria';
import { EnumUtils } from '@app/core/services/utilities/enum-utils.service';
import { BaseCriteriaComponent } from '@app/ui/misc/criteria/base-criteria.component';
import { ValidationErrorModel } from '@common/forms/validation/error-model/validation-error-model';
import { debounceTime, takeUntil } from 'rxjs/operators';
@Component({
selector: 'app-user-criteria-component',
templateUrl: './user-criteria.component.html',
styleUrls: ['./user-criteria.component.scss']
})
export class UserCriteriaComponent extends BaseCriteriaComponent implements OnInit {
public rolesEnum = this.enumUtils.getEnumValues(AppRole);
public criteria: UserCriteria = new UserCriteria();
constructor(
private formBuilder: UntypedFormBuilder,
public enumUtils: EnumUtils
) {
super(new ValidationErrorModel());
}
ngOnInit() {
super.ngOnInit();
if (this.criteria == null) { this.criteria = new UserCriteria(); }
if (this.formGroup == null) { this.formGroup = this.buildForm(); }
this.registerListeners();
}
setCriteria(criteria: UserCriteria): void {
this.criteria = criteria;
this.formGroup = this.buildForm();
this.registerListeners();
}
registerListeners() {
this.formGroup.valueChanges
.pipe(takeUntil(this._destroyed), debounceTime(300))
.subscribe(x => this.controlModified());
}
public fromJSONObject(item: any): UserCriteria {
this.criteria = new UserCriteria();
this.criteria.label = item.Label;
this.criteria.appRoles = item.appRoles;
return this.criteria;
}
buildForm(): UntypedFormGroup {
return this.formBuilder.group({
like: [this.criteria.label, []],
appRoles: [this.criteria.appRoles, []],
});
}
}

View File

@ -0,0 +1,36 @@
<div class="d-flex align-items-center gap-1-rem">
<button mat-flat-button [matMenuTriggerFor]="filterMenu" #filterMenuTrigger="matMenuTrigger" (click)="updateFilters()" class="filter-button">
<mat-icon aria-hidden="false" [matBadgeHidden]="!appliedFilterCount" [matBadge]="appliedFilterCount" matBadgeColor="warn" matBadgeSize="small">filter_alt</mat-icon>
{{'COMMONS.LISTING-COMPONENT.SEARCH-FILTER-BTN' | translate}}
</button>
<mat-menu #filterMenu>
<div class="p-3" (click)="$event?.stopPropagation?.()">
<div class="search-listing-filters-container">
<div class="d-flex align-items-center justify-content-between">
<h4>{{'USER-LISTING.FILTER.TITLE' | translate}}</h4>
<button color="accent" mat-button (click)="clearFilters()">
{{'COMMONS.LISTING-COMPONENT.CLEAR-ALL-FILTERS' | translate}}
</button>
</div>
<mat-slide-toggle labelPosition="before" [(ngModel)]="internalFilters.isActive">
{{'USER-LISTING.FILTER.IS-ACTIVE' | translate}}
</mat-slide-toggle>
<div class="d-flex justify-content-end align-items-center mt-4 gap-1-rem">
<button mat-stroked-button color="primary" (click)="filterMenuTrigger?.closeMenu()">
{{'USER-LISTING.FILTER.CANCEL' | translate}}
</button>
<button mat-raised-button color="primary" (click)="filterMenuTrigger.closeMenu(); applyFilters();">
{{'USER-LISTING.FILTER.APPLY-FILTERS' | translate}}
</button>
</div>
</div>
</div>
</mat-menu>
<app-expandable-search-field [(value)]=internalFilters.like (valueChange)="onSearchTermChange($event)" />
</div>

View File

@ -0,0 +1,25 @@
.user-listing-filters {
}
::ng-deep.mat-mdc-menu-panel {
max-width: 100% !important;
height: 100% !important;
}
:host::ng-deep.mat-mdc-menu-content:not(:empty) {
padding-top: 0 !important;
}
.filter-button{
padding-top: .6rem;
padding-bottom: .6rem;
// .mat-icon{
// font-size: 1.5em;
// width: 1.2em;
// height: 1.2em;
// }
}

View File

@ -0,0 +1,94 @@
import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core';
import { IsActive } from '@app/core/common/enum/is-active.enum';
import { UserFilter } from '@app/core/query/user.lookup';
import { EnumUtils } from '@app/core/services/utilities/enum-utils.service';
import { BaseComponent } from '@common/base/base.component';
import { nameof } from 'ts-simple-nameof';
@Component({
selector: 'app-user-listing-filters',
templateUrl: './user-listing-filters.component.html',
styleUrls: ['./user-listing-filters.component.scss']
})
export class UserListingFiltersComponent extends BaseComponent implements OnInit, OnChanges {
@Input() readonly filter: UserFilter;
@Output() filterChange = new EventEmitter<UserFilter>();
// * State
internalFilters: UserListingFilters = this._getEmptyFilters();
protected appliedFilterCount: number = 0;
constructor(
public enumUtils: EnumUtils,
) { super(); }
ngOnInit() {
}
ngOnChanges(changes: SimpleChanges): void {
const filterChange = changes[nameof<UserListingFiltersComponent>(x => x.filter)]?.currentValue as UserFilter;
if (filterChange) {
this.updateFilters()
}
}
onSearchTermChange(searchTerm: string): void {
this.applyFilters()
}
protected updateFilters(): void {
this.internalFilters = this._parseToInternalFilters(this.filter);
this.appliedFilterCount = this._computeAppliedFilters(this.internalFilters);
}
protected applyFilters(): void {
const { isActive, like } = this.internalFilters ?? {}
this.filterChange.emit({
...this.filter,
like,
isActive: isActive ? [IsActive.Active] : [IsActive.Inactive]
})
}
private _parseToInternalFilters(inputFilter: UserFilter): UserListingFilters {
if (!inputFilter) {
return this._getEmptyFilters();
}
let { excludedIds, ids, isActive, like } = inputFilter;
return {
isActive: (isActive ?? [])?.includes(IsActive.Active) || !isActive?.length,
like: like
}
}
private _getEmptyFilters(): UserListingFilters {
return {
isActive: true,
like: null,
}
}
private _computeAppliedFilters(filters: UserListingFilters): number {
let count = 0;
if (filters?.isActive) {
count++
}
return count;
}
clearFilters() {
this.internalFilters = this._getEmptyFilters();
}
}
interface UserListingFilters {
isActive: boolean;
like: string;
}

View File

@ -1,28 +1,27 @@
<form class="row user-role-editor" [formGroup]="formGroup" (ngSubmit)="formSubmit()"> <form class="row user-role-editor" *ngIf="formGroup" [formGroup]="formGroup" (ngSubmit)="formSubmit()">
<div *ngIf="!this.nowEditing"class="roles col"> <div *ngIf="!this.nowEditing"class="roles col">
<ng-container *ngFor="let role of this.formGroup.get('appRoles').value"> <ng-container *ngFor="let role of this.formGroup.get('roles').value">
<div> <div>
<span class="user-role" [ngClass]="{'user': role == 0, 'manager': role == 1, 'admin': role == 2, 'dataset-template-editor': role == 3}"> <span class="user-role" [ngClass]="{'user': role == appRole.User, 'manager': role == appRole.Manager, 'admin': role == appRole.Admin, 'dataset-template-editor': role == appRole.DatasetTemplateEditor}">
{{getPrincipalAppRoleWithLanguage(role)}} {{enumUtils.toAppRoleString(role)}}
</span> </span>
</div> </div>
</ng-container> </ng-container>
</div> </div>
<mat-form-field *ngIf="this.nowEditing" class="select-role roles-width-180 col-auto"> <mat-form-field *ngIf="this.nowEditing" class="select-role roles-width-180 col-auto">
<mat-select formControlName="appRoles" multiple required> <mat-select formControlName="roles" multiple required>
<ng-container *ngFor="let role of getPrincipalAppRoleValues()"> <ng-container *ngFor="let role of appRoleEnumValues">
<mat-option [value]="role">{{getPrincipalAppRoleWithLanguage(role)}}</mat-option> <mat-option [value]="role">{{enumUtils.toAppRoleString(role)}}</mat-option>
</ng-container> </ng-container>
</mat-select> </mat-select>
<mat-error *ngIf="getFormControl('appRoles').hasError('required')">{{'GENERAL.VALIDATION.REQUIRED' | translate}}</mat-error> <mat-error *ngIf="formGroup.get('roles').hasError('required')">{{'GENERAL.VALIDATION.REQUIRED' | translate}}</mat-error>
</mat-form-field> </mat-form-field>
<button *ngIf="!this.nowEditing" class="col-auto" mat-icon-button color="primary" type="button" (click)="editItem()"> <button *ngIf="!this.nowEditing" class="col-auto" mat-icon-button color="primary" type="button" (click)="editItem()">
<!-- <mat-icon class="mat-24">edit</mat-icon> --> <mat-icon class="mat-24" matTooltip="{{'USER-LISTING.ACTIONS.EDIT' | translate}}">edit</mat-icon>
<span class="row-action">{{'USERS.ACTIONS.EDIT' | translate}}</span> <span class="row-action"></span>
</button> </button>
<button *ngIf="this.nowEditing" class="col-auto" mat-icon-button color="primary" type="submit"> <button *ngIf="this.nowEditing" class="col-auto save-button" mat-icon-button color="primary" type="submit">
<!-- <mat-icon class="mat-24">save</mat-icon> --> <mat-icon class="mat-24" matTooltip="{{'USER-LISTING.ACTIONS.SAVE' | translate}}">save</mat-icon>
<span class="row-action">{{'USERS.ACTIONS.SAVE' | translate}}</span>
</button> </button>
</form> </form>

View File

@ -102,6 +102,10 @@
padding-left: 10px; padding-left: 10px;
padding-right: 10px; padding-right: 10px;
} }
.save-button {
margin-bottom: 1em;
}
} }
:host ::ng-deep .mat-form-field-wrapper { :host ::ng-deep .mat-form-field-wrapper {

View File

@ -1,68 +1,89 @@
import { Component, Input, OnInit } from '@angular/core'; import { Component, Input, OnInit } from '@angular/core';
import { AbstractControl, UntypedFormArray, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup } from '@angular/forms'; import { AbstractControl, FormArray, FormBuilder, FormControl, FormGroup, UntypedFormGroup } from '@angular/forms';
import { AppRole } from '@app/core/common/enum/app-role'; import { User, UserRolePatchPersist } from '@app/core/model/user/user';
import { UserListingModel } from '@app/core/model/user/user-listing';
import { SnackBarNotificationLevel, UiNotificationService } from '@app/core/services/notification/ui-notification-service'; import { SnackBarNotificationLevel, UiNotificationService } from '@app/core/services/notification/ui-notification-service';
import { UserServiceOld } from '@app/core/services/user/user.service-old'; import { UserService } from '@app/core/services/user/user.service';
import { EnumUtils } from '@app/core/services/utilities/enum-utils.service'; import { EnumUtils } from '@app/core/services/utilities/enum-utils.service';
import { BaseEditor } from '@common/base/base-editor';
import { BaseComponent } from '@common/base/base.component'; import { BaseComponent } from '@common/base/base.component';
import { Validation, ValidationContext } from '@common/forms/validation/validation-context'; import { Validation, ValidationContext } from '@common/forms/validation/validation-context';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { UserRolePatchEditorModel } from './user-role-editor.model';
import { AuthService } from '@app/core/services/auth/auth.service';
import { AppPermission } from '@app/core/common/enum/permission.enum';
import { DescriptionTemplateTypeEditorService } from '@app/ui/admin/description-types/editor/description-template-type-editor.service';
import { UserRoleEditorService } from './user-role-editor.service';
import { LoggingService } from '@app/core/services/logging/logging-service';
import { FormService } from '@common/forms/form-service';
import { HttpErrorResponse } from '@angular/common/http';
import { HttpError, HttpErrorHandlingService } from '@common/modules/errors/error-handling/http-error-handling.service';
import { AppRole } from '@app/core/common/enum/app-role';
@Component({ @Component({
selector: 'app-user-role-editor-component', selector: 'app-user-role-editor-component',
templateUrl: './user-role-editor.component.html', templateUrl: './user-role-editor.component.html',
styleUrls: ['./user-role-editor.component.scss'] styleUrls: ['./user-role-editor.component.scss'],
providers: [UserRoleEditorService]
}) })
export class UserRoleEditorComponent extends BaseComponent implements OnInit { export class UserRoleEditorComponent extends BaseComponent implements OnInit {
@Input() public item: UserListingModel; @Input() public item: User;
public formGroup: UntypedFormGroup = null; public formGroup: UntypedFormGroup = null;
public nowEditing = false; public nowEditing = false;
lookupParams: any;
editorModel: UserRolePatchEditorModel;
appRole = AppRole;
public appRoleEnumValues = this.enumUtils.getEnumValues<AppRole>(AppRole);
constructor( constructor(
private language: TranslateService, private language: TranslateService,
private userService: UserServiceOld, private userService: UserService,
private formBuilder: UntypedFormBuilder, private formService: FormService,
private logger: LoggingService,
private enumUtils: EnumUtils, private enumUtils: EnumUtils,
private uiNotificationService: UiNotificationService private uiNotificationService: UiNotificationService,
private authService: AuthService,
private userRoleEditorService: UserRoleEditorService,
private httpErrorHandlingService: HttpErrorHandlingService,
) { super(); } ) { super(); }
ngOnInit() { ngOnInit() {
if (this.formGroup == null) { this.formGroup = this.buildForm(); } if (this.formGroup == null) { this.prepareForm(this.item); }
} }
buildForm(): UntypedFormGroup { prepareForm(data: User) {
const context: ValidationContext = this.createValidationContext(); try {
this.editorModel = data ? new UserRolePatchEditorModel().fromModel(data) : new UserRolePatchEditorModel();
return this.formBuilder.group({ this.buildForm();
appRoles: new UntypedFormControl({ value: this.item.appRoles, disabled: true }, context.getValidation('appRoles').validators) } catch (error) {
}); this.logger.error('Could not parse user role item: ' + data + error);
this.uiNotificationService.snackBarNotification(this.language.instant('COMMONS.ERRORS.DEFAULT'), SnackBarNotificationLevel.Error);
}
} }
createValidationContext(): ValidationContext { buildForm() {
const validationContext: ValidationContext = new ValidationContext(); this.formGroup = this.editorModel.buildForm(null, this.authService.hasPermission(AppPermission.EditUser));
const validationArray: Validation[] = new Array<Validation>(); this.userRoleEditorService.setValidationErrorModel(this.editorModel.validationErrorModel);
}
validationArray.push({ key: 'appRoles' }); persistEntity(onSuccess?: (response) => void): void {
const formData = this.formService.getValue(this.formGroup.value) as UserRolePatchPersist;
validationContext.validation = validationArray; this.userService.persistRoles(formData)
return validationContext; .pipe(takeUntil(this._destroyed)).subscribe(
complete => onSuccess ? onSuccess(complete) : this.onCallbackSuccess(),
error => this.onCallbackError(error)
);
} }
formSubmit(): void { formSubmit(): void {
this.formService.touchAllFormFields(this.formGroup);
if (!this.isFormValid()) {
return;
}
const modifiedItem = this.item; this.persistEntity();
modifiedItem.appRoles = this.getFormControl('appRoles').value;
if (!this.isFormValid()) { return; }
this.userService.updateRoles(modifiedItem)
.pipe(takeUntil(this._destroyed))
.subscribe(
(res) => this.onCallbackSuccess(),
(error) => this.onCallbackError(error)
);
} }
editItem(): void { editItem(): void {
@ -70,75 +91,28 @@ export class UserRoleEditorComponent extends BaseComponent implements OnInit {
this.nowEditing = true; this.nowEditing = true;
} }
isFormValid(): boolean { public isFormValid() {
this.touchAllFormFields(this.formGroup);
this.validateAllFormFields(this.formGroup);
return this.formGroup.valid; return this.formGroup.valid;
} }
getFormData(): any {
return this.formGroup.value;
}
getFormControl(controlName: string): AbstractControl {
return this.formGroup.get(controlName);
}
validateAllFormFields(formControl: AbstractControl) {
if (formControl instanceof UntypedFormControl) {
formControl.updateValueAndValidity({ emitEvent: false });
} else if (formControl instanceof UntypedFormGroup) {
Object.keys(formControl.controls).forEach(item => {
const control = formControl.get(item);
this.validateAllFormFields(control);
});
} else if (formControl instanceof UntypedFormArray) {
formControl.controls.forEach(item => {
this.validateAllFormFields(item);
});
}
}
touchAllFormFields(formControl: AbstractControl) {
if (formControl instanceof UntypedFormControl) {
formControl.markAsTouched();
} else if (formControl instanceof UntypedFormGroup) {
Object.keys(formControl.controls).forEach(item => {
const control = formControl.get(item);
this.touchAllFormFields(control);
});
} else if (formControl instanceof UntypedFormArray) {
formControl.controls.forEach(item => {
this.touchAllFormFields(item);
});
}
}
onCallbackSuccess() { onCallbackSuccess() {
this.nowEditing = false; this.nowEditing = false;
this.formGroup.disable(); this.formGroup.disable();
this.uiNotificationService.snackBarNotification(this.language.instant('GENERAL.SNACK-BAR.SUCCESSFUL-UPDATE'), SnackBarNotificationLevel.Success); this.uiNotificationService.snackBarNotification(this.language.instant('GENERAL.SNACK-BAR.SUCCESSFUL-UPDATE'), SnackBarNotificationLevel.Success);
} }
onCallbackError(error: any) { onCallbackError(errorResponse: HttpErrorResponse) {
this.validateAllFormFields(this.formGroup); const error: HttpError = this.httpErrorHandlingService.getError(errorResponse);
this.uiNotificationService.snackBarNotification(this.language.instant('GENERAL.SNACK-BAR.UNSUCCESSFUL-UPDATE'), SnackBarNotificationLevel.Error); if (error.statusCode === 400) {
this.editorModel.validationErrorModel.fromJSONObject(errorResponse.error);
this.formService.validateAllFormFields(this.formGroup);
} else {
this.uiNotificationService.snackBarNotification(error.getMessagesString(), SnackBarNotificationLevel.Warning);
}
} }
getPrincipalAppRoleValues(): Number[] { clearErrorModel() {
let keys: string[] = Object.keys(AppRole); this.editorModel.validationErrorModel.clear();
keys = keys.slice(0, keys.length / 2); this.formService.validateAllFormFields(this.formGroup);
const values: Number[] = keys.map(Number);
return values;
}
getPrincipalAppRoleWithLanguage(role: AppRole): string {
let result = '';
this.language.get(this.enumUtils.convertFromPrincipalAppRole(role))
.pipe(takeUntil(this._destroyed))
.subscribe((value: string) => {
result = value;
});
return result;
} }
} }

View File

@ -0,0 +1,49 @@
import { UntypedFormBuilder, UntypedFormGroup } from "@angular/forms";
import { User, UserRolePatchPersist } from "@app/core/model/user/user";
import { BackendErrorValidator } from "@common/forms/validation/custom-validator";
import { ValidationErrorModel } from "@common/forms/validation/error-model/validation-error-model";
import { Validation, ValidationContext } from "@common/forms/validation/validation-context";
import { Guid } from "@common/types/guid";
export class UserRolePatchEditorModel implements UserRolePatchPersist {
id: Guid;
roles: String[] = [];
hash: string;
permissions: string[];
public validationErrorModel: ValidationErrorModel = new ValidationErrorModel();
protected formBuilder: UntypedFormBuilder = new UntypedFormBuilder();
constructor() { }
public fromModel(item: User): UserRolePatchEditorModel {
if (item) {
this.id = item.id;
if (item.roles != null)this.roles = item.roles.map(x => x.role);
this.hash = item.hash;
}
return this;
}
buildForm(context: ValidationContext = null, disabled: boolean = false): UntypedFormGroup {
if (context == null) { context = this.createValidationContext(); }
return this.formBuilder.group({
id: [{ value: this.id, disabled: disabled }, context.getValidation('id').validators],
roles: [{ value: this.roles, disabled: disabled }, context.getValidation('roles').validators],
hash: [{ value: this.hash, disabled: disabled }, context.getValidation('hash').validators],
});
}
createValidationContext(): ValidationContext {
const baseContext: ValidationContext = new ValidationContext();
const baseValidationArray: Validation[] = new Array<Validation>();
baseValidationArray.push({ key: 'id', validators: [BackendErrorValidator(this.validationErrorModel, 'id')] });
baseValidationArray.push({ key: 'roles', validators: [BackendErrorValidator(this.validationErrorModel, 'roles')] });
baseValidationArray.push({ key: 'hash', validators: [] });
baseContext.validation = baseValidationArray;
return baseContext;
}
}

View File

@ -0,0 +1,15 @@
import { Injectable } from "@angular/core";
import { ValidationErrorModel } from "@common/forms/validation/error-model/validation-error-model";
@Injectable()
export class UserRoleEditorService {
private validationErrorModel: ValidationErrorModel;
public setValidationErrorModel(validationErrorModel: ValidationErrorModel): void {
this.validationErrorModel = validationErrorModel;
}
public getValidationErrorModel(): ValidationErrorModel {
return this.validationErrorModel;
}
}

View File

@ -1,57 +1,77 @@
<div class="main-content"> <div class="row user-listing">
<div class="container-fluid user-listing"> <div class="col-md-8 offset-md-2">
<div class="d-flex align-items-center">
<h3>{{'USERS.LISTING.TITLE' | translate}}</h3> <div class="row mb-4 mt-3">
<div class="col-auto ml-auto"> <div class="col">
<button mat-raised-button type="button" class="export-btn" (click)="exportUsers()">{{'USERS.LISTING.EXPORT' | translate}}</button> <h4>{{'USER-LISTING.TITLE' | translate}}</h4>
<app-navigation-breadcrumb />
</div>
<div class="col-auto">
<button mat-raised-button class="create-btn" (click)="export()" *ngIf="authService.hasPermission(authService.permissionEnum.ExportUsers)">
<mat-icon>download</mat-icon>
{{'USER-LISTING.ACTIONS.EXPORT' | translate}}
</button>
</div> </div>
</div> </div>
<app-user-criteria-component></app-user-criteria-component>
<!-- <mat-card class="mat-card"> -->
<div class="mat-elevation-z6">
<mat-table [dataSource]="dataSource" matSort>
<!-- Column Definition: Label --> <app-hybrid-listing [rows]="gridRows" [columns]="gridColumns" [visibleColumns]="visibleColumns" [count]="totalElements" [offset]="currentPageNumber" [limit]="lookup.page.size" [defaultSort]="lookup.order?.items" [externalSorting]="true" (rowActivated)="onUserRowActivated($event)" (pageLoad)="alterPage($event)" (columnSort)="onColumnSort($event)" (columnsChanged)="onColumnsChanged($event)" [listItemTemplate]="listItemTemplate">
<ng-container cdkColumnDef="avatar">
<mat-header-cell *matHeaderCellDef></mat-header-cell> <app-user-listing-filters hybrid-listing-filters [(filter)]="lookup" (filterChange)="filterChanged($event)" />
<mat-cell *matCellDef="let row">
<img mat-card-avatar *ngIf="row.avatarUrl" class="my-mat-card-avatar" [src]="row.avatarUrl" (error)="this.setDefaultAvatar($event)"> <app-user-settings-picker [key]="userSettingsKey" [userPreference]="lookup" (onSettingSelected)="changeSetting($event)" [autoSelectUserSettings]="autoSelectUserSettings" user-preference-settings />
<img mat-card-avatar *ngIf="!row.avatarUrl" class="my-mat-card-avatar" [src]="'assets/images/profile-placeholder.png'"> </app-hybrid-listing>
</mat-cell>
</div>
</div>
<ng-template #listItemTemplate let-item="item" let-isColumnSelected="isColumnSelected">
<div class="d-flex align-items-center p-3 gap-1-rem">
<div class="row">
<ng-container *ngIf="isColumnSelected('name')">
<a class="buttonLinkClass" [routerLink]="'./' + item?.id" class="col-12" (click)="$event.stopPropagation()">{{item?.name | nullifyValue}}</a>
<br />
</ng-container> </ng-container>
<ng-container cdkColumnDef="name"> <ng-container *ngIf="isColumnSelected('createdAt')">
<mat-header-cell *matHeaderCellDef>{{'USERS.LISTING.NAME' | translate}}</mat-header-cell> <span class="col-12">
<mat-cell *matCellDef="let row"> {{'USER-LISTING.FIELDS.CREATED-AT' | translate}}:
<!-- <img mat-card-avatar *ngIf="row.avatarUrl" class="my-mat-card-avatar" [src]="row.avatarUrl"> --> <small>
{{row.name}} {{item?.createdAt | dateTimeFormatter : 'short' | nullifyValue}}
</mat-cell> </small>
</span>
<br>
</ng-container> </ng-container>
<ng-container *ngIf="isColumnSelected('updatedAt')">
<ng-container cdkColumnDef="email"> <span class="col-12">
<mat-header-cell *matHeaderCellDef>{{'USERS.LISTING.EMAIL' | translate}}</mat-header-cell> {{'USER-LISTING.FIELDS.UPDATED-AT' | translate}}:
<mat-cell *matCellDef="let row">{{row.email}}</mat-cell> <small>
{{item?.updatedAt | dateTimeFormatter : 'short' | nullifyValue}}
</small>
</span>
</ng-container> </ng-container>
</div>
</div>
</ng-template>
<ng-container cdkColumnDef="lastloggedin"> <ng-template #roleCellTemplate let-row="row" let-item>
<mat-header-cell *matHeaderCellDef>{{'USERS.LISTING.LAST-LOGGED-IN' | translate}}</mat-header-cell> <div class="row">
<mat-cell *matCellDef="let row">{{row.lastloggedin | date:'shortDate'}}</mat-cell>
</ng-container>
<!-- Column Definition: Roles -->
<ng-container cdkColumnDef="roles">
<mat-header-cell *matHeaderCellDef>{{'USERS.LISTING.ROLES' | translate}}</mat-header-cell>
<mat-cell *matCellDef="let row">
<app-user-role-editor-component style="width: 100%;" [item]="row"></app-user-role-editor-component> <app-user-role-editor-component style="width: 100%;" [item]="row"></app-user-role-editor-component>
</mat-cell> <!-- <div class="col-auto status-chip"
</ng-container> [ngClass]="{'status-chip-finalized': row.status === userStatuses.Finalized, 'status-chip-draft' : row.status === userStatuses.Draft}">
{{enumUtils.toUserStatusString(row.status) | nullifyValue}}
</div> -->
</div>
</ng-template>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row> <ng-template #nameCellTemplate let-row="row" let-item>
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row> <div class="row align-items-center">
<div class="col-auto">
<img *ngIf="row?.additionalInfo?.avatarUrl" class="user-avatar" [src]="row?.additionalInfo?.avatarUrl" (error)="this.setDefaultAvatar($event)">
<img *ngIf="!row?.additionalInfo?.avatarUrl" class="user-avatar" [src]="'assets/images/profile-placeholder.png'">
</div>
<div class="col">{{row.name}}</div>
</mat-table>
<mat-paginator #paginator [length]="dataSource?.totalCount" [pageSizeOptions]="[10, 25, 100]"></mat-paginator>
<!-- </mat-card> -->
</div>
</div>
</div> </div>
</ng-template>

View File

@ -1,91 +1,68 @@
.mat-table { .user-listing {
margin-top: 47px; margin-top: 1.3rem;
margin-bottom: 20px; margin-left: 1rem;
background: #fafafa 0% 0% no-repeat padding-box; margin-right: 2rem;
// box-shadow: 0px 5px 12px #00000038;
border-radius: 4px;
.mat-header-row{ .mat-header-row{
background: #f3f5f8; background: #f3f5f8;
} }
mat-header-cell:first-of-type {
padding-left: 46px;
max-width: 120px;
}
mat-cell:first-of-type {
padding-left: 46px;
max-width: 120px;
}
.my-mat-card-avatar {
height: 40px;
width: 40px;
border-radius: 50%;
flex-shrink: 0;
margin-right: 2.5rem;
}
}
.user-listing {
margin-top: 2rem;
margin-left: 1rem;
margin-right: 2rem;
.mat-card { .mat-card {
margin: 1em 0; margin: 16px 0;
padding: 0px;
}
.mat-row {
cursor: pointer;
min-height: 4.5em;
} }
mat-row:hover { mat-row:hover {
background-color: #eef5f6; background-color: #eef5f6;
} }
.mat-fab-bottom-right {
mat-row:nth-child(odd) { float: right;
// background-color: #0c748914; z-index: 5;
// background-color: #eef0fb;
} }
}
.export-btn { .create-btn {
background: #ffffff 0% 0% no-repeat padding-box;
border-radius: 30px; border-radius: 30px;
width: 145px; background-color: var(--secondary-color);
color: var(--primary-color); padding-left: 2em;
border: 1px solid var(--primary-color); padding-right: 2em;
} // color: #000;
.export-icon { .button-text{
font-size: 20px; display: inline-block;
} }
} }
:host ::ng-deep .mat-paginator-container { .dlt-btn {
// flex-direction: row-reverse !important; color: rgba(0, 0, 0, 0.54);
// justify-content: space-between !important;
// height: 30px;
// min-height: 30px !important;
background-color: #f6f6f6;
} }
// ::ng-deep .mat-paginator-page-size { .status-chip{
// height: 43px;
// }
// ::ng-deep .mat-paginator-range-label { border-radius: 20px;
// margin: 15px 32px 0 24px !important; padding-left: 1em;
// } padding-right: 1em;
padding-top: 0.2em;
font-size: .8em;
}
// ::ng-deep .mat-paginator-range-actions { .status-chip-finalized{
// width: 55% !important; color: #568b5a;
// min-height: 43px !important; background: #9dd1a1 0% 0% no-repeat padding-box;
// justify-content: space-between; }
// }
// ::ng-deep .mat-paginator-navigation-previous { .status-chip-draft{
// margin-left: auto !important; color: #00c4ff;
// } background: #d3f5ff 0% 0% no-repeat padding-box;
}
// ::ng-deep .mat-icon-button { .user-avatar {
// height: 30px !important; height: 40px;
// font-size: 12px !important; width: 40px;
// } border-radius: 50%;
flex-shrink: 0;
object-fit: cover;
}

View File

@ -1,155 +1,195 @@
import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { Observable, merge as observableMerge, of as observableOf } from 'rxjs'; import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Router } from '@angular/router';
import { DataSource } from '@angular/cdk/table'; import { IsActive } from '@app/core/common/enum/is-active.enum';
import { HttpClient } from '@angular/common/http'; import { User, UserAdditionalInfo, UserContactInfo, UserRole } from '@app/core/model/user/user';
import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core'; import { UserLookup } from '@app/core/query/user.lookup';
import { MatPaginator } from '@angular/material/paginator'; import { AuthService } from '@app/core/services/auth/auth.service';
import { MatSnackBar } from '@angular/material/snack-bar'; import { SnackBarNotificationLevel, UiNotificationService } from '@app/core/services/notification/ui-notification-service';
import { MatSort } from '@angular/material/sort'; import { UserService } from '@app/core/services/user/user.service';
import { MatomoService } from '@app/core/services/matomo/matomo-service'; import { EnumUtils } from '@app/core/services/utilities/enum-utils.service';
import { BaseComponent } from '@common/base/base.component'; import { FileUtils } from '@app/core/services/utilities/file-utils.service';
import { QueryParamsService } from '@app/core/services/utilities/query-params.service';
import { BaseListingComponent } from '@common/base/base-listing-component';
import { PipeService } from '@common/formatting/pipe.service';
import { DataTableDateTimeFormatPipe } from '@common/formatting/pipes/date-time-format.pipe';
import { QueryResult } from '@common/model/query-result';
import { ConfirmationDialogComponent } from '@common/modules/confirmation-dialog/confirmation-dialog.component';
import { HttpErrorHandlingService } from '@common/modules/errors/error-handling/http-error-handling.service';
import { ColumnDefinition, ColumnsChangedEvent, HybridListingComponent, PageLoadEvent, RowActivateEvent } from '@common/modules/hybrid-listing/hybrid-listing.component';
import { Guid } from '@common/types/guid';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import * as FileSaver from 'file-saver'; import * as FileSaver from 'file-saver';
import { catchError, map, startWith, switchMap, takeUntil } from 'rxjs/operators'; import { Observable } from 'rxjs';
import { DataTableRequest } from '../../../../core/model/data-table/data-table-request'; import { takeUntil } from 'rxjs/operators';
import { UserListingModel } from '../../../../core/model/user/user-listing'; import { nameof } from 'ts-simple-nameof';
import { UserCriteria } from '../../../../core/query/user/user-criteria';
import { UserServiceOld } from '../../../../core/services/user/user.service-old';
import { SnackBarNotificationComponent } from '../../../../library/notification/snack-bar/snack-bar-notification.component';
// import { BreadcrumbItem } from '../../../misc/breadcrumb/definition/breadcrumb-item';
import { FileUtils } from '@app/core/services/utilities/file-utils.service';
import { UserCriteriaComponent } from './criteria/user-criteria.component';
export class UsersDataSource extends DataSource<UserListingModel> {
totalCount = 0;
constructor(
private _service: UserServiceOld,
private _paginator: MatPaginator,
private _sort: MatSort,
private _languageService: TranslateService,
private _snackBar: MatSnackBar,
private _criteria: UserCriteriaComponent
) {
super();
//this._paginator.page.pipe(takeUntil(this._destroyed)).subscribe((pageEvent: PageEvent) => {
// this.store.dispatch(new LoadPhotosRequestAction(pageEvent.pageIndex, pageEvent.pageSize))
//})
}
connect(): Observable<UserListingModel[]> {
const displayDataChanges = [
this._paginator.page
//this._sort.matSortChange
];
// If the user changes the sort order, reset back to the first page.
//this._sort.matSortChange.pipe(takeUntil(this._destroyed)).subscribe(() => {
// this._paginator.pageIndex = 0;
//})
return observableMerge(...displayDataChanges).pipe(
startWith(null),
switchMap(() => {
const startIndex = this._paginator.pageIndex * this._paginator.pageSize;
let fields: Array<string> = new Array();
if (this._sort.active) { fields = this._sort.direction === 'asc' ? ['+' + this._sort.active] : ['-' + this._sort.active]; }
const request = new DataTableRequest<UserCriteria>(startIndex, this._paginator.pageSize, { fields: fields });
request.criteria = this._criteria.getFormData();
return this._service.getPaged(request);
}),
catchError((error: any) => {
this._snackBar.openFromComponent(SnackBarNotificationComponent, {
data: { message: 'GENERAL.SNACK-BAR.FORMS-BAD-REQUEST', language: this._languageService },
duration: 3000,
});
this._criteria.onCallbackError(error);
return observableOf(null);
}),
map(result => {
return result;
}),
map(result => {
if (!result) { return []; }
if (this._paginator.pageIndex === 0) { this.totalCount = result.totalCount; }
//result.data.forEach((element: any) => {
// const roles: String[] = [];
// element.roles.forEach((role: any) => {
// this._languageService.get(this._utilities.convertFromPrincipalAppRole(role)).pipe(takeUntil(this._destroyed)).subscribe(
// value => roles.push(value)
// );
// });
// element.roles = roles;
//});
return result.data;
}),);
}
disconnect() {
// No-op
}
}
@Component({ @Component({
selector: 'app-user-listing-component',
templateUrl: './user-listing.component.html', templateUrl: './user-listing.component.html',
styleUrls: ['./user-listing.component.scss'] styleUrls: ['./user-listing.component.scss']
}) })
export class UserListingComponent extends BaseComponent implements OnInit, AfterViewInit { export class UserListingComponent extends BaseListingComponent<User, UserLookup> implements OnInit {
publish = false;
userSettingsKey = { key: 'UserListingUserSettings' };
propertiesAvailableForOrder: ColumnDefinition[];
@ViewChild(MatPaginator, { static: true }) _paginator: MatPaginator; @ViewChild('roleCellTemplate', { static: true }) roleCellTemplate?: TemplateRef<any>;
@ViewChild(MatSort, { static: true }) sort: MatSort; @ViewChild('nameCellTemplate', { static: true }) nameCellTemplate?: TemplateRef<any>;
@ViewChild(UserCriteriaComponent, { static: true }) criteria: UserCriteriaComponent; @ViewChild(HybridListingComponent, { static: true }) hybridListingComponent: HybridListingComponent;
// breadCrumbs: Observable<BreadcrumbItem[]>; private readonly lookupFields: string[] = [
dataSource: UsersDataSource | null; nameof<User>(x => x.id),
displayedColumns: String[] = ['avatar', 'name', 'email', 'lastloggedin', 'roles']; nameof<User>(x => x.name),
[nameof<User>(x => x.contacts), nameof<UserContactInfo>(x => x.id)].join('.'),
[nameof<User>(x => x.contacts), nameof<UserContactInfo>(x => x.type)].join('.'),
[nameof<User>(x => x.contacts), nameof<UserContactInfo>(x => x.value)].join('.'),
[nameof<User>(x => x.roles), nameof<UserRole>(x => x.id)].join('.'),
[nameof<User>(x => x.roles), nameof<UserRole>(x => x.role)].join('.'),
[nameof<User>(x => x.additionalInfo), nameof<UserAdditionalInfo>(x => x.avatarUrl)].join('.'),
nameof<User>(x => x.updatedAt),
nameof<User>(x => x.createdAt),
nameof<User>(x => x.hash),
nameof<User>(x => x.isActive)
];
rowIdentity = x => x.id;
constructor( constructor(
private userService: UserServiceOld, protected router: Router,
private languageService: TranslateService, protected route: ActivatedRoute,
public snackBar: MatSnackBar, protected uiNotificationService: UiNotificationService,
private httpClient: HttpClient, protected httpErrorHandlingService: HttpErrorHandlingService,
private matomoService: MatomoService, protected queryParamsService: QueryParamsService,
private userService: UserService,
public authService: AuthService,
private pipeService: PipeService,
public enumUtils: EnumUtils,
private language: TranslateService,
private dialog: MatDialog,
private fileUtils: FileUtils private fileUtils: FileUtils
) { ) {
super(); super(router, route, uiNotificationService, httpErrorHandlingService, queryParamsService);
// Lookup setup
// Default lookup values are defined in the user settings class.
this.lookup = this.initializeLookup();
} }
ngOnInit() { ngOnInit() {
this.matomoService.trackPageView('Admin: Users'); super.ngOnInit();
// this.breadCrumbs = observableOf([{
// parentComponentName: null,
// label: this.languageService.instant('NAV-BAR.USERS-BREADCRUMB'),
// url: "/users"
// }]);
//this.refresh(); //called on ngAfterViewInit with default criteria
} }
ngAfterViewInit() { protected initializeLookup(): UserLookup {
setTimeout(() => { const lookup = new UserLookup();
this.criteria.setRefreshCallback(() => this.refresh()); lookup.metadata = { countAll: true };
this.criteria.setCriteria(this.getDefaultCriteria()); lookup.page = { offset: 0, size: this.ITEMS_PER_PAGE };
this.criteria.controlModified(); lookup.isActive = [IsActive.Active];
lookup.order = { items: [this.toDescSortField(nameof<User>(x => x.createdAt))] };
this.updateOrderUiFields(lookup.order);
lookup.project = {
fields: this.lookupFields
};
return lookup;
}
protected setupColumns() {
this.gridColumns.push(...[{
prop: nameof<User>(x => x.name),
sortable: true,
languageName: 'USER-LISTING.FIELDS.NAME',
cellTemplate: this.nameCellTemplate
},
{
prop: nameof<User>(x => x.contacts),
sortable: true,
languageName: 'USER-LISTING.FIELDS.CONTACT-INFO',
valueFunction: (item: User) => (item?.contacts ?? []).map(x => x.value).join(', ')
},
{
prop: nameof<User>(x => x.createdAt),
sortable: true,
languageName: 'USER-LISTING.FIELDS.CREATED-AT',
pipe: this.pipeService.getPipe<DataTableDateTimeFormatPipe>(DataTableDateTimeFormatPipe).withFormat('short')
},
{
prop: nameof<User>(x => x.updatedAt),
sortable: true,
languageName: 'USER-LISTING.FIELDS.UPDATED-AT',
pipe: this.pipeService.getPipe<DataTableDateTimeFormatPipe>(DataTableDateTimeFormatPipe).withFormat('short')
},
{
prop: nameof<User>(x => x.roles),
languageName: 'USER-LISTING.FIELDS.ROLES',
alwaysShown: true,
maxWidth: 300,
cellTemplate: this.roleCellTemplate
}
]);
this.propertiesAvailableForOrder = this.gridColumns.filter(x => x.sortable);
}
//
// Listing Component functions
//
onColumnsChanged(event: ColumnsChangedEvent) {
super.onColumnsChanged(event);
this.onColumnsChangedInternal(event.properties.map(x => x.toString()));
}
private onColumnsChangedInternal(columns: string[]) {
// Here are defined the projection fields that always requested from the api.
const fields = new Set(this.lookupFields);
this.gridColumns.map(x => x.prop)
.filter(x => !columns?.includes(x as string))
.forEach(item => {
fields.delete(item as string)
});
this.lookup.project = { fields: [...fields] };
this.onPageLoad({ offset: 0 } as PageLoadEvent);
}
protected loadListing(): Observable<QueryResult<User>> {
return this.userService.query(this.lookup);
}
public deleteType(id: Guid) {
if (id) {
const dialogRef = this.dialog.open(ConfirmationDialogComponent, {
data: {
isDeleteConfirmation: true,
message: this.language.instant('GENERAL.CONFIRMATION-DIALOG.DELETE-ITEM'),
confirmButton: this.language.instant('GENERAL.CONFIRMATION-DIALOG.ACTIONS.CONFIRM'),
cancelButton: this.language.instant('GENERAL.CONFIRMATION-DIALOG.ACTIONS.CANCEL')
}
});
dialogRef.afterClosed().pipe(takeUntil(this._destroyed)).subscribe(result => {
if (result) {
this.userService.delete(id).pipe(takeUntil(this._destroyed))
.subscribe(
complete => this.onCallbackSuccess(),
error => this.onCallbackError(error)
);
}
}); });
} }
refresh() {
this._paginator.pageSize = 10;
this._paginator.pageIndex = 0;
this.dataSource = new UsersDataSource(this.userService, this._paginator, this.sort, this.languageService, this.snackBar, this.criteria);
} }
getDefaultCriteria(): UserCriteria { onCallbackSuccess(): void {
const defaultCriteria = new UserCriteria(); this.uiNotificationService.snackBarNotification(this.language.instant('GENERAL.SNACK-BAR.SUCCESSFUL-DELETE'), SnackBarNotificationLevel.Success);
return defaultCriteria; this.ngOnInit();
} }
// Export user mails onUserRowActivated(event: RowActivateEvent, baseRoute: string = null) {
exportUsers() { // Override default event to prevent click action
this.userService.downloadCSV() }
//
// Export
//
export() { //TODO: send lookup to backend to export only filtered
this.userService.exportCSV()
.pipe(takeUntil(this._destroyed)) .pipe(takeUntil(this._destroyed))
.subscribe(response => { .subscribe(response => {
const blob = new Blob([response.body], { type: 'application/csv' }); const blob = new Blob([response.body], { type: 'application/csv' });
@ -158,6 +198,9 @@ export class UserListingComponent extends BaseComponent implements OnInit, After
}); });
} }
//
// Avatar
//
public setDefaultAvatar(ev: Event) { public setDefaultAvatar(ev: Event) {
(ev.target as HTMLImageElement).src = 'assets/images/profile-placeholder.png'; (ev.target as HTMLImageElement).src = 'assets/images/profile-placeholder.png';
} }

View File

@ -1,24 +1,32 @@
import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common';
import { FormattingModule } from '@app/core/formatting.module'; import { NgModule } from '@angular/core';
import { CommonFormattingModule } from '@common/formatting/common-formatting.module';
import { CommonFormsModule } from '@common/forms/common-forms.module'; import { CommonFormsModule } from '@common/forms/common-forms.module';
import { HybridListingModule } from '@common/modules/hybrid-listing/hybrid-listing.module';
import { TextFilterModule } from '@common/modules/text-filter/text-filter.module';
import { UserSettingsModule } from '@common/modules/user-settings/user-settings.module';
import { CommonUiModule } from '@common/ui/common-ui.module'; import { CommonUiModule } from '@common/ui/common-ui.module';
import { UserCriteriaComponent } from './listing/criteria/user-criteria.component'; import { UserListingFiltersComponent } from './listing/filters/user-listing-filters.component';
import { UserRoleEditorComponent } from './listing/role-editor/user-role-editor.component';
import { UserListingComponent } from './listing/user-listing.component'; import { UserListingComponent } from './listing/user-listing.component';
import { UserRoutingModule } from './user.routing'; import { UsersRoutingModule } from './user.routing';
import { UserRoleEditorComponent } from './listing/role-editor/user-role-editor.component';
@NgModule({ @NgModule({
imports: [
CommonUiModule,
CommonFormsModule,
FormattingModule,
UserRoutingModule
],
declarations: [ declarations: [
UserListingComponent, UserListingComponent,
UserCriteriaComponent, // UserEditorComponent,
UserRoleEditorComponent UserRoleEditorComponent,
UserListingFiltersComponent
], ],
imports: [
CommonModule,
CommonUiModule,
CommonFormsModule,
CommonFormattingModule,
UsersRoutingModule,
HybridListingModule,
TextFilterModule,
UserSettingsModule
]
}) })
export class UsersModule { }
export class UserModule { }

View File

@ -1,15 +1,19 @@
import { NgModule } from '@angular/core'; import { NgModule } from "@angular/core";
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from "@angular/router";
import { UserListingComponent } from './listing/user-listing.component'; import { AuthGuard } from "@app/core/auth-guard.service";
import { AdminAuthGuard } from '@app/core/admin-auth-guard.service'; import { UserListingComponent } from "./listing/user-listing.component";
const routes: Routes = [ const routes: Routes = [
{ path: '', component: UserListingComponent, canActivate: [AdminAuthGuard] }, {
// { path: ':id', component: UserProfileComponent } path: '',
]; component: UserListingComponent,
canActivate: [AuthGuard]
},
{ path: '**', loadChildren: () => import('@common/modules/page-not-found/page-not-found.module').then(m => m.PageNotFoundModule) },
]
@NgModule({ @NgModule({
imports: [RouterModule.forChild(routes)], imports: [RouterModule.forChild(routes)],
exports: [RouterModule] exports: [RouterModule],
}) })
export class UserRoutingModule { } export class UsersRoutingModule { }

View File

@ -1787,20 +1787,25 @@
"CANCEL": "Cancel" "CANCEL": "Cancel"
} }
}, },
"USERS": { "USER-LISTING": {
"LISTING": {
"TITLE": "Users", "TITLE": "Users",
"EMAIL": "Email", "FIELDS": {
"LAST-LOGGED-IN": "Last Logged In", "CONTACT-INFO": "Email",
"LABEL": "Label",
"ROLES": "Roles", "ROLES": "Roles",
"NAME": "Name", "NAME": "Name",
"PERMISSIONS": "Permissions", "UPDATED-AT": "Updated",
"EXPORT": "Export users" "CREATED-AT": "Created"
},
"FILTER": {
"TITLE": "Filters",
"IS-ACTIVE": "Is Active",
"CANCEL": "Cancel",
"APPLY-FILTERS": "Apply filters"
}, },
"ACTIONS": { "ACTIONS": {
"EDIT": "Edit", "EDIT": "Edit",
"SAVE": "Save" "SAVE": "Save",
"EXPORT": "Export users"
} }
}, },
"TYPES": { "TYPES": {