-
-
-

{{'USERS.LISTING.TITLE' | translate}}

-
- +
+
+ +
+
+

{{'USER-LISTING.TITLE' | translate}}

+ + +
+
+
- - -
- - - - - - - - - + - - {{'USERS.LISTING.NAME' | translate}} - - - {{row.name}} - - + - - {{'USERS.LISTING.EMAIL' | translate}} - {{row.email}} - + + - - {{'USERS.LISTING.LAST-LOGGED-IN' | translate}} - {{row.lastloggedin | date:'shortDate'}} - - - - - {{'USERS.LISTING.ROLES' | translate}} - - - - - - - - - - - -
+ + + +
+
+ + {{item?.name | nullifyValue}} +
+
+ + + + {{'USER-LISTING.FIELDS.CREATED-AT' | translate}}: + + {{item?.createdAt | dateTimeFormatter : 'short' | nullifyValue}} + + +
+
+ + + {{'USER-LISTING.FIELDS.UPDATED-AT' | translate}}: + + {{item?.updatedAt | dateTimeFormatter : 'short' | nullifyValue}} + + + +
+
+
+ + +
+ + +
+
+ + +
+
+ + +
+
{{row.name}}
+ +
+
\ No newline at end of file diff --git a/dmp-frontend/src/app/ui/admin/user/listing/user-listing.component.scss b/dmp-frontend/src/app/ui/admin/user/listing/user-listing.component.scss index 6e01f8143..243385ef3 100644 --- a/dmp-frontend/src/app/ui/admin/user/listing/user-listing.component.scss +++ b/dmp-frontend/src/app/ui/admin/user/listing/user-listing.component.scss @@ -1,91 +1,68 @@ -.mat-table { - margin-top: 47px; - margin-bottom: 20px; - background: #fafafa 0% 0% no-repeat padding-box; - // box-shadow: 0px 5px 12px #00000038; - border-radius: 4px; - - .mat-header-row { - 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; + margin-top: 1.3rem; + margin-left: 1rem; + margin-right: 2rem; - .mat-card { - margin: 1em 0; - } - - mat-row:hover { - background-color: #eef5f6; - } - - mat-row:nth-child(odd) { - // background-color: #0c748914; - // background-color: #eef0fb; - } - - .export-btn { - background: #ffffff 0% 0% no-repeat padding-box; - border-radius: 30px; - width: 145px; - color: var(--primary-color); - border: 1px solid var(--primary-color); - } - - .export-icon { - font-size: 20px; - } + .mat-header-row{ + background: #f3f5f8; + } + .mat-card { + margin: 16px 0; + padding: 0px; + } + + .mat-row { + cursor: pointer; + min-height: 4.5em; + } + + mat-row:hover { + background-color: #eef5f6; + } + .mat-fab-bottom-right { + float: right; + z-index: 5; + } +} +.create-btn { + border-radius: 30px; + background-color: var(--secondary-color); + padding-left: 2em; + padding-right: 2em; + // color: #000; + + .button-text{ + display: inline-block; + } } -:host ::ng-deep .mat-paginator-container { - // flex-direction: row-reverse !important; - // justify-content: space-between !important; - // height: 30px; - // min-height: 30px !important; - background-color: #f6f6f6; +.dlt-btn { + color: rgba(0, 0, 0, 0.54); } -// ::ng-deep .mat-paginator-page-size { -// height: 43px; -// } +.status-chip{ + + border-radius: 20px; + padding-left: 1em; + padding-right: 1em; + padding-top: 0.2em; + font-size: .8em; +} -// ::ng-deep .mat-paginator-range-label { -// margin: 15px 32px 0 24px !important; -// } +.status-chip-finalized{ + color: #568b5a; + background: #9dd1a1 0% 0% no-repeat padding-box; +} -// ::ng-deep .mat-paginator-range-actions { -// width: 55% !important; -// min-height: 43px !important; -// justify-content: space-between; -// } +.status-chip-draft{ + color: #00c4ff; + background: #d3f5ff 0% 0% no-repeat padding-box; +} -// ::ng-deep .mat-paginator-navigation-previous { -// margin-left: auto !important; -// } - -// ::ng-deep .mat-icon-button { -// height: 30px !important; -// font-size: 12px !important; -// } +.user-avatar { + height: 40px; + width: 40px; + border-radius: 50%; + flex-shrink: 0; + object-fit: cover; +} diff --git a/dmp-frontend/src/app/ui/admin/user/listing/user-listing.component.ts b/dmp-frontend/src/app/ui/admin/user/listing/user-listing.component.ts index db85ab957..3bee61484 100644 --- a/dmp-frontend/src/app/ui/admin/user/listing/user-listing.component.ts +++ b/dmp-frontend/src/app/ui/admin/user/listing/user-listing.component.ts @@ -1,155 +1,195 @@ - -import { Observable, merge as observableMerge, of as observableOf } from 'rxjs'; - -import { DataSource } from '@angular/cdk/table'; -import { HttpClient } from '@angular/common/http'; -import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core'; -import { MatPaginator } from '@angular/material/paginator'; -import { MatSnackBar } from '@angular/material/snack-bar'; -import { MatSort } from '@angular/material/sort'; -import { MatomoService } from '@app/core/services/matomo/matomo-service'; -import { BaseComponent } from '@common/base/base.component'; +import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { ActivatedRoute, Router } from '@angular/router'; +import { IsActive } from '@app/core/common/enum/is-active.enum'; +import { User, UserAdditionalInfo, UserContactInfo, UserRole } from '@app/core/model/user/user'; +import { UserLookup } from '@app/core/query/user.lookup'; +import { AuthService } from '@app/core/services/auth/auth.service'; +import { SnackBarNotificationLevel, UiNotificationService } from '@app/core/services/notification/ui-notification-service'; +import { UserService } from '@app/core/services/user/user.service'; +import { EnumUtils } from '@app/core/services/utilities/enum-utils.service'; +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 * as FileSaver from 'file-saver'; -import { catchError, map, startWith, switchMap, takeUntil } from 'rxjs/operators'; -import { DataTableRequest } from '../../../../core/model/data-table/data-table-request'; -import { UserListingModel } from '../../../../core/model/user/user-listing'; -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 { - - 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 { - 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 = new Array(); - if (this._sort.active) { fields = this._sort.direction === 'asc' ? ['+' + this._sort.active] : ['-' + this._sort.active]; } - const request = new DataTableRequest(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 - } -} +import { Observable } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +import { nameof } from 'ts-simple-nameof'; @Component({ - selector: 'app-user-listing-component', templateUrl: './user-listing.component.html', styleUrls: ['./user-listing.component.scss'] }) -export class UserListingComponent extends BaseComponent implements OnInit, AfterViewInit { +export class UserListingComponent extends BaseListingComponent implements OnInit { + publish = false; + userSettingsKey = { key: 'UserListingUserSettings' }; + propertiesAvailableForOrder: ColumnDefinition[]; - @ViewChild(MatPaginator, { static: true }) _paginator: MatPaginator; - @ViewChild(MatSort, { static: true }) sort: MatSort; - @ViewChild(UserCriteriaComponent, { static: true }) criteria: UserCriteriaComponent; + @ViewChild('roleCellTemplate', { static: true }) roleCellTemplate?: TemplateRef; + @ViewChild('nameCellTemplate', { static: true }) nameCellTemplate?: TemplateRef; + @ViewChild(HybridListingComponent, { static: true }) hybridListingComponent: HybridListingComponent; - // breadCrumbs: Observable; - dataSource: UsersDataSource | null; - displayedColumns: String[] = ['avatar', 'name', 'email', 'lastloggedin', 'roles']; + private readonly lookupFields: string[] = [ + nameof(x => x.id), + nameof(x => x.name), + [nameof(x => x.contacts), nameof(x => x.id)].join('.'), + [nameof(x => x.contacts), nameof(x => x.type)].join('.'), + [nameof(x => x.contacts), nameof(x => x.value)].join('.'), + [nameof(x => x.roles), nameof(x => x.id)].join('.'), + [nameof(x => x.roles), nameof(x => x.role)].join('.'), + [nameof(x => x.additionalInfo), nameof(x => x.avatarUrl)].join('.'), + nameof(x => x.updatedAt), + nameof(x => x.createdAt), + nameof(x => x.hash), + nameof(x => x.isActive) + ]; + + rowIdentity = x => x.id; constructor( - private userService: UserServiceOld, - private languageService: TranslateService, - public snackBar: MatSnackBar, - private httpClient: HttpClient, - private matomoService: MatomoService, + protected router: Router, + protected route: ActivatedRoute, + protected uiNotificationService: UiNotificationService, + protected httpErrorHandlingService: HttpErrorHandlingService, + protected queryParamsService: QueryParamsService, + private userService: UserService, + public authService: AuthService, + private pipeService: PipeService, + public enumUtils: EnumUtils, + private language: TranslateService, + private dialog: MatDialog, 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() { - this.matomoService.trackPageView('Admin: Users'); - // this.breadCrumbs = observableOf([{ - // parentComponentName: null, - // label: this.languageService.instant('NAV-BAR.USERS-BREADCRUMB'), - // url: "/users" - // }]); - //this.refresh(); //called on ngAfterViewInit with default criteria + super.ngOnInit(); } - ngAfterViewInit() { - setTimeout(() => { - this.criteria.setRefreshCallback(() => this.refresh()); - this.criteria.setCriteria(this.getDefaultCriteria()); - this.criteria.controlModified(); - }); + protected initializeLookup(): UserLookup { + const lookup = new UserLookup(); + lookup.metadata = { countAll: true }; + lookup.page = { offset: 0, size: this.ITEMS_PER_PAGE }; + lookup.isActive = [IsActive.Active]; + lookup.order = { items: [this.toDescSortField(nameof(x => x.createdAt))] }; + this.updateOrderUiFields(lookup.order); + + lookup.project = { + fields: this.lookupFields + }; + + return lookup; } - 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); + protected setupColumns() { + this.gridColumns.push(...[{ + prop: nameof(x => x.name), + sortable: true, + languageName: 'USER-LISTING.FIELDS.NAME', + cellTemplate: this.nameCellTemplate + }, + { + prop: nameof(x => x.contacts), + sortable: true, + languageName: 'USER-LISTING.FIELDS.CONTACT-INFO', + valueFunction: (item: User) => (item?.contacts ?? []).map(x => x.value).join(', ') + }, + { + prop: nameof(x => x.createdAt), + sortable: true, + languageName: 'USER-LISTING.FIELDS.CREATED-AT', + pipe: this.pipeService.getPipe(DataTableDateTimeFormatPipe).withFormat('short') + }, + { + prop: nameof(x => x.updatedAt), + sortable: true, + languageName: 'USER-LISTING.FIELDS.UPDATED-AT', + pipe: this.pipeService.getPipe(DataTableDateTimeFormatPipe).withFormat('short') + }, + { + prop: nameof(x => x.roles), + languageName: 'USER-LISTING.FIELDS.ROLES', + alwaysShown: true, + maxWidth: 300, + cellTemplate: this.roleCellTemplate + } + ]); + this.propertiesAvailableForOrder = this.gridColumns.filter(x => x.sortable); } - getDefaultCriteria(): UserCriteria { - const defaultCriteria = new UserCriteria(); - return defaultCriteria; + // + // Listing Component functions + // + onColumnsChanged(event: ColumnsChangedEvent) { + super.onColumnsChanged(event); + this.onColumnsChangedInternal(event.properties.map(x => x.toString())); } - // Export user mails - exportUsers() { - this.userService.downloadCSV() + 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> { + 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) + ); + } + }); + } + } + + onCallbackSuccess(): void { + this.uiNotificationService.snackBarNotification(this.language.instant('GENERAL.SNACK-BAR.SUCCESSFUL-DELETE'), SnackBarNotificationLevel.Success); + this.ngOnInit(); + } + + onUserRowActivated(event: RowActivateEvent, baseRoute: string = null) { + // Override default event to prevent click action + } + + // + // Export + // + + export() { //TODO: send lookup to backend to export only filtered + this.userService.exportCSV() .pipe(takeUntil(this._destroyed)) .subscribe(response => { 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) { (ev.target as HTMLImageElement).src = 'assets/images/profile-placeholder.png'; } diff --git a/dmp-frontend/src/app/ui/admin/user/user.module.ts b/dmp-frontend/src/app/ui/admin/user/user.module.ts index 38dae797a..aaa36defe 100644 --- a/dmp-frontend/src/app/ui/admin/user/user.module.ts +++ b/dmp-frontend/src/app/ui/admin/user/user.module.ts @@ -1,24 +1,32 @@ -import { NgModule } from '@angular/core'; -import { FormattingModule } from '@app/core/formatting.module'; +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { CommonFormattingModule } from '@common/formatting/common-formatting.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 { UserCriteriaComponent } from './listing/criteria/user-criteria.component'; -import { UserRoleEditorComponent } from './listing/role-editor/user-role-editor.component'; +import { UserListingFiltersComponent } from './listing/filters/user-listing-filters.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({ - imports: [ - CommonUiModule, - CommonFormsModule, - FormattingModule, - UserRoutingModule - ], declarations: [ UserListingComponent, - UserCriteriaComponent, - UserRoleEditorComponent + // UserEditorComponent, + UserRoleEditorComponent, + UserListingFiltersComponent ], + imports: [ + CommonModule, + CommonUiModule, + CommonFormsModule, + CommonFormattingModule, + UsersRoutingModule, + HybridListingModule, + TextFilterModule, + UserSettingsModule + ] }) - -export class UserModule { } \ No newline at end of file +export class UsersModule { } diff --git a/dmp-frontend/src/app/ui/admin/user/user.routing.ts b/dmp-frontend/src/app/ui/admin/user/user.routing.ts index 90e8fa5f9..2515d0e9f 100644 --- a/dmp-frontend/src/app/ui/admin/user/user.routing.ts +++ b/dmp-frontend/src/app/ui/admin/user/user.routing.ts @@ -1,15 +1,19 @@ -import { NgModule } from '@angular/core'; -import { RouterModule, Routes } from '@angular/router'; -import { UserListingComponent } from './listing/user-listing.component'; -import { AdminAuthGuard } from '@app/core/admin-auth-guard.service'; +import { NgModule } from "@angular/core"; +import { RouterModule, Routes } from "@angular/router"; +import { AuthGuard } from "@app/core/auth-guard.service"; +import { UserListingComponent } from "./listing/user-listing.component"; 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({ imports: [RouterModule.forChild(routes)], - exports: [RouterModule] + exports: [RouterModule], }) -export class UserRoutingModule { } +export class UsersRoutingModule { } diff --git a/dmp-frontend/src/assets/i18n/en.json b/dmp-frontend/src/assets/i18n/en.json index 93d351fec..f9fc44bb3 100644 --- a/dmp-frontend/src/assets/i18n/en.json +++ b/dmp-frontend/src/assets/i18n/en.json @@ -1787,20 +1787,25 @@ "CANCEL": "Cancel" } }, - "USERS": { - "LISTING": { - "TITLE": "Users", - "EMAIL": "Email", - "LAST-LOGGED-IN": "Last Logged In", - "LABEL": "Label", + "USER-LISTING": { + "TITLE": "Users", + "FIELDS": { + "CONTACT-INFO": "Email", "ROLES": "Roles", "NAME": "Name", - "PERMISSIONS": "Permissions", - "EXPORT": "Export users" + "UPDATED-AT": "Updated", + "CREATED-AT": "Created" + }, + "FILTER": { + "TITLE": "Filters", + "IS-ACTIVE": "Is Active", + "CANCEL": "Cancel", + "APPLY-FILTERS": "Apply filters" }, "ACTIONS": { "EDIT": "Edit", - "SAVE": "Save" + "SAVE": "Save", + "EXPORT": "Export users" } }, "TYPES": {