Components for new angular app

This commit is contained in:
Martin Stockhammer 2020-11-09 00:35:36 +01:00
parent 0e2187bf0e
commit ec828c8745
15 changed files with 540 additions and 94 deletions

View File

@ -43,6 +43,8 @@ import { ManageUsersListComponent } from './modules/user/users/manage-users-list
import { ManageUsersAddComponent } from './modules/user/users/manage-users-add/manage-users-add.component';
import { NgbPaginationModule, NgbTooltipModule} from "@ng-bootstrap/ng-bootstrap";
import { PaginatedEntitiesComponent } from './modules/general/paginated-entities/paginated-entities.component';
import { SortedTableHeaderComponent } from './modules/general/sorted-table-header/sorted-table-header.component';
import { SortedTableHeaderRowComponent } from './modules/general/sorted-table-header-row/sorted-table-header-row.component';
@NgModule({
@ -65,6 +67,8 @@ import { PaginatedEntitiesComponent } from './modules/general/paginated-entities
ManageUsersListComponent,
ManageUsersAddComponent,
PaginatedEntitiesComponent,
SortedTableHeaderComponent,
SortedTableHeaderRowComponent,
],
imports: [
BrowserModule,

View File

@ -24,5 +24,5 @@ import {Observable} from "rxjs";
* @typeparam T The type of the entity that is returned from the service
*/
export interface EntityService<T> {
(searchTerm:string,offset:number,limit:number,orderBy:string,order:string):Observable<PagedResult<T>>
(searchTerm:string,offset:number,limit:number,orderBy:string[],order:string):Observable<PagedResult<T>>
}

View File

@ -0,0 +1,21 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export interface FieldToggle {
toggleField(fieldName: string[]);
}

View File

@ -16,12 +16,11 @@
* under the License.
*/
import {Component, OnInit, Input, Output, EventEmitter} from '@angular/core';
import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
import {merge, Observable, Subject} from "rxjs";
import {UserInfo} from "../../../model/user-info";
import {TranslateService} from "@ngx-translate/core";
import {debounceTime, distinctUntilChanged, map, mergeMap, pluck, share, startWith} from "rxjs/operators";
import {EntityService} from "../../../model/entity-service";
import {FieldToggle} from "../../../model/field-toggle";
/**
@ -48,26 +47,35 @@ import {EntityService} from "../../../model/entity-service";
templateUrl: './paginated-entities.component.html',
styleUrls: ['./paginated-entities.component.scss']
})
export class PaginatedEntitiesComponent<T> implements OnInit {
export class PaginatedEntitiesComponent<T> implements OnInit, FieldToggle {
/**
* This must be set, if you use the component. This service retrieves the entity data.
*/
@Input() service : EntityService<T>;
@Input() service: EntityService<T>;
/**
* The number of elements per page retrieved
*/
@Input() pageSize = 10;
/**
* Two-Way-Binding attribute for sorting field
*/
@Input() sortField = [];
/**
* Two-Way Binding attribute for sort order
*/
@Input() sortOrder = "asc";
/**
* Pagination controls
*/
@Input() pagination = {
maxSize:5,
rotate:true,
boundaryLinks:true,
ellipses:false
maxSize: 5,
rotate: true,
boundaryLinks: true,
ellipses: false
}
/**
@ -82,11 +90,15 @@ export class PaginatedEntitiesComponent<T> implements OnInit {
/**
* Event thrown, if the page value changes
*/
@Output() pageEvent : EventEmitter<number> = new EventEmitter<number>();
@Output() pageChange: EventEmitter<number> = new EventEmitter<number>();
/**
* Event thrown, if the search term changes
*/
@Output() searchTermEvent: EventEmitter<string> = new EventEmitter<string>();
@Output() searchTermChange: EventEmitter<string> = new EventEmitter<string>();
@Output() sortFieldChange: EventEmitter<string[]> = new EventEmitter<string[]>();
@Output() sortOrderChange: EventEmitter<string> = new EventEmitter<string>();
/**
* The total number of elements available for the given search term
@ -100,7 +112,8 @@ export class PaginatedEntitiesComponent<T> implements OnInit {
private pageStream: Subject<number> = new Subject<number>();
private searchTermStream: Subject<string> = new Subject<string>();
constructor() { }
constructor() {
}
ngOnInit(): void {
// We combine the sources for the page and the search input field to a observable 'source'
@ -117,22 +130,67 @@ export class PaginatedEntitiesComponent<T> implements OnInit {
const source = merge(pageSource, searchSource).pipe(
startWith({search: this.searchTerm, page: this.page}),
mergeMap((params: { search: string, page: number }) => {
return this.service(params.search, (params.page - 1) * this.pageSize, this.pageSize, "", "asc");
}),share());
this.total$ = source.pipe(pluck('pagination','totalCount'));
return this.service(params.search, (params.page - 1) * this.pageSize, this.pageSize, this.sortField, this.sortOrder);
}), share());
this.total$ = source.pipe(pluck('pagination', 'totalCount'));
this.items$ = source.pipe(pluck('data'));
}
search(terms: string) {
// console.log("Keystroke " + terms);
this.searchTermEvent.emit(terms);
this.searchTermChange.emit(terms);
this.searchTermStream.next(terms)
}
changePage(pageNumber : number) {
changePage(pageNumber: number) {
// console.log("Page change " +pageNumber);
this.pageEvent.emit(pageNumber);
this.pageChange.emit(pageNumber);
this.pageStream.next(pageNumber);
}
private compareArrays(a1: string[], a2: string[]) {
let i = a1.length;
while (i--) {
if (a1[i] !== a2[i]) return false;
}
return true
}
toggleSortField(fieldName: string) {
this.toggleField([fieldName]);
}
toggleField(fieldArray: string[]) {
console.log("Changing sort field " + fieldArray);
let sortOrderChanged: boolean = false;
let sortFieldChanged: boolean = false;
if (!this.compareArrays(this.sortField, fieldArray)) {
console.log("Fields differ: " + this.sortField + " - " + fieldArray);
this.sortField = fieldArray;
if (this.sortOrder != 'asc') {
this.sortOrder = 'asc';
sortOrderChanged = true;
}
sortFieldChanged = true;
} else {
if (this.sortOrder == "asc") {
this.sortOrder = "desc";
} else {
this.sortOrder = "asc";
}
console.log("Toggled sort order: " + this.sortOrder);
sortOrderChanged = true;
}
if (sortOrderChanged) {
console.log("Sort order changed: "+this.sortOrder)
this.sortOrderChange.emit(this.sortOrder);
}
if (sortFieldChanged) {
this.sortFieldChange.emit(this.sortField);
}
if (sortFieldChanged || sortOrderChanged) {
this.changePage(1);
}
}
}

View File

@ -0,0 +1,19 @@
<!--
~ Licensed to the Apache Software Foundation (ASF) under one
~ or more contributor license agreements. See the NOTICE file
~ distributed with this work for additional information
~ regarding copyright ownership. The ASF licenses this file
~ to you under the Apache License, Version 2.0 (the
~ "License"); you may not use this file except in compliance
~ with the License. You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~ Unless required by applicable law or agreed to in writing,
~ software distributed under the License is distributed on an
~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
~ KIND, either express or implied. See the License for the
~ specific language governing permissions and limitations
~ under the License.
-->
<ng-content ></ng-content>

View File

@ -0,0 +1,18 @@
/*!
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

View File

@ -0,0 +1,43 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SortedTableHeaderRowComponent } from './sorted-table-header-row.component';
describe('SortedTableHeaderRowComponent', () => {
let component: SortedTableHeaderRowComponent;
let fixture: ComponentFixture<SortedTableHeaderRowComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ SortedTableHeaderRowComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(SortedTableHeaderRowComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,79 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import {
AfterViewChecked, AfterViewInit,
Component,
Input,
OnInit,
QueryList,
TemplateRef,
ViewChild,
ViewChildren,
ViewContainerRef,
ContentChildren, AfterContentInit, AfterContentChecked, ChangeDetectorRef, Output, EventEmitter
} from '@angular/core';
import {FieldToggle} from "../../../model/field-toggle";
import {SortedTableHeaderComponent} from "../sorted-table-header/sorted-table-header.component";
import { delay, startWith } from 'rxjs/operators';
@Component({
selector: 'tr[sorted]',
templateUrl: './sorted-table-header-row.component.html',
styleUrls: ['./sorted-table-header-row.component.scss']
})
export class SortedTableHeaderRowComponent implements OnInit, AfterViewInit, AfterContentInit, AfterContentChecked {
@Input() sortFieldEmitter: EventEmitter<string[]>;
@Input() sortOrderEmitter: EventEmitter<string>;
@Input() sortFields: string[];
@Input() sortOrder: string;
@Input() toggleObserver: FieldToggle;
@ContentChildren(SortedTableHeaderComponent, { descendants: true }) contentChilds: QueryList<SortedTableHeaderComponent>;
constructor(private readonly viewContainer: ViewContainerRef) {
}
ngAfterContentChecked(): void {
}
ngAfterContentInit(): void {
this.contentChilds.changes.pipe(startWith(this.contentChilds), delay(0)).subscribe(() => {
this.contentChilds.forEach((colComponent, index) => {
console.log("Children " + colComponent);
colComponent.registerSortFieldEmitter(this.sortFieldEmitter);
colComponent.registerSortOrderEmitter(this.sortOrderEmitter);
colComponent.sortOrder = this.sortOrder;
colComponent.currentFieldArray = this.sortFields;
colComponent.toggleObserver = this.toggleObserver;
});
});
}
ngOnInit(): void {
}
ngAfterViewInit(): void {
}
}

View File

@ -0,0 +1,24 @@
<!--
~ Licensed to the Apache Software Foundation (ASF) under one
~ or more contributor license agreements. See the NOTICE file
~ distributed with this work for additional information
~ regarding copyright ownership. The ASF licenses this file
~ to you under the Apache License, Version 2.0 (the
~ "License"); you may not use this file except in compliance
~ with the License. You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~ Unless required by applicable law or agreed to in writing,
~ software distributed under the License is distributed on an
~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
~ KIND, either express or implied. See the License for the
~ specific language governing permissions and limitations
~ under the License.
-->
<ng-template #content>
<th scope="col" (click)="toggleSortField()">
<ng-container *ngIf="contentText!=null && contentText!=''" >{{contentText | translate}}</ng-container>
<ng-content></ng-content>
<span *ngIf="sortCheck()" class="fas" [ngClass]="isAscending()?'fa-sort-alpha-up':'fa-sort-alpha-down'"></span></th>
</ng-template>

View File

@ -0,0 +1,18 @@
/*!
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

View File

@ -0,0 +1,43 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SortedTableHeaderComponent } from './sorted-table-header.component';
describe('SortedTableHeaderComponent', () => {
let component: SortedTableHeaderComponent;
let fixture: ComponentFixture<SortedTableHeaderComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ SortedTableHeaderComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(SortedTableHeaderComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,90 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import {
Component,
OnInit,
Input,
ViewContainerRef,
ViewChild,
TemplateRef,
ChangeDetectorRef,
AfterViewChecked, EventEmitter, Output
} from '@angular/core';
import {FieldToggle} from "../../../model/field-toggle";
import { ChangeDetectionStrategy } from '@angular/core';
@Component({
host: { style: 'display:none' },
selector: 'app-th-sorted',
templateUrl: './sorted-table-header.component.html',
styleUrls: ['./sorted-table-header.component.scss']
})
export class SortedTableHeaderComponent implements OnInit, AfterViewChecked {
@Input() fieldArray: string[];
currentFieldArray: string[];
sortOrder: string;
toggleObserver: FieldToggle;
@Input() contentText:string;
@ViewChild('content', { static: true }) content: TemplateRef<{}>;
constructor(private readonly viewContainer: ViewContainerRef) { }
ngOnInit(): void {
this.viewContainer.createEmbeddedView(this.content);
}
ngAfterViewChecked() {
}
toggleSortField() {
console.log("Toggling sort field " + this.fieldArray);
this.toggleObserver.toggleField(this.fieldArray);
}
private compareArrays(a1: string[], a2: string[]) {
if (a1==null || a2==null) {
return false;
}
let i = a1.length;
while (i--) {
if (a1[i] !== a2[i]) return false;
}
return true
}
sortCheck() {
return this.compareArrays(this.fieldArray, this.currentFieldArray);
}
isAscending() :boolean {
console.log("Is ascending: " + this.sortOrder);
return this.sortOrder == 'asc';
}
registerSortOrderEmitter(emitter : EventEmitter<string>) {
emitter.subscribe((field) => this.sortOrder = field);
}
registerSortFieldEmitter(emitter : EventEmitter<string[]>) {
emitter.subscribe((field) => this.currentFieldArray = field);
}
}

View File

@ -17,24 +17,28 @@
~ under the License.
-->
<app-paginated-entities [service]="service" pageSize="5"We #parent>
<app-paginated-entities [service]="service" pageSize="5" [(sortField)]="sortField" [(sortOrder)]="sortOrder"
#parent>
<table class="table table-striped table-bordered">
<thead class="thead-light">
<tr>
<th scope="col">{{'users.list.table.head.user_id' | translate}}</th>
<th scope="col">{{'users.list.table.head.fullName' | translate}}</th>
<th scope="col">{{'users.list.table.head.email' | translate}}</th>
<th scope="col"><span class="fas fa-check" placement="top"
[ngbTooltip]="heads.validated" [attr.aria-label]="heads.validated"></span>
</th>
<tr sorted [sortFieldEmitter]="parent.sortFieldChange" [sortOrder]="sortOrder" [sortFields]="sortField"
[sortOrderEmitter]="parent.sortOrderChange" [toggleObserver]="parent" >
<app-th-sorted [fieldArray]="['user_id']" contentText="users.list.table.head.user_id"></app-th-sorted>
<app-th-sorted contentText="users.list.table.head.fullName" [fieldArray]="['fullName']" ></app-th-sorted>
<app-th-sorted contentText="users.list.table.head.email" [fieldArray]="['email']"></app-th-sorted>
<app-th-sorted [fieldArray]="['validated','user_id']">
<span class="fas fa-check" placement="top"
[ngbTooltip]="heads.validated" [attr.aria-label]="heads.validated">
</span>
</app-th-sorted>
<th scope="col"><span class="fas fa-lock" placement="top"
[ngbTooltip]="heads.locked" [attr.aria-label]="heads.locked"></span></th>
<th scope="col"><span class="fa fa-chevron-circle-right" placement="top"
[ngbTooltip]="heads.pwchange" [attr.aria-label]="heads.pwchange"></span>
</th>
<th scope="col">{{'users.list.table.head.lastLogin' | translate}}</th>
<th scope="col">{{'users.list.table.head.created' | translate}}</th>
<app-th-sorted contentText="users.list.table.head.created" [fieldArray]="['created']" ></app-th-sorted>
<th scope="col">{{'users.list.table.head.lastPwChange' | translate}}</th>
</tr>
</thead>

View File

@ -25,7 +25,6 @@ import {EntityService} from "../../../../model/entity-service";
import {Observable, of} from "rxjs";
import {PagedResult} from "../../../../model/paged-result";
@Component({
selector: 'app-manage-users-list',
templateUrl: './manage-users-list.component.html',
@ -35,10 +34,13 @@ export class ManageUsersListComponent implements OnInit {
@Input() heads: any;
service : EntityService<UserInfo>;
sortField = ["user_id"];
sortOrder = "asc";
constructor(private translator: TranslateService, private userService : UserService) {
this.service = function (searchTerm: string, offset: number, limit: number, orderBy: string, order: string) : Observable<PagedResult<UserInfo>> {
this.service = function (searchTerm: string, offset: number, limit: number, orderBy: string[], order: string) : Observable<PagedResult<UserInfo>> {
console.log("Retrieving data " + searchTerm + "," + offset + "," + limit + "," + orderBy + "," + order);
return userService.query(searchTerm, offset, limit, orderBy, order);
}
@ -53,9 +55,29 @@ export class ManageUsersListComponent implements OnInit {
this.heads[suffix] = this.translator.instant('users.list.table.head.' + suffix);
}
});
}
changeSortOrder(order:string) {
if (this.sortOrder!=order) {
this.sortOrder = order;
}
}
private compareArrays(a1: string[], a2: string[]) {
let i = a1.length;
while (i--) {
if (a1[i] !== a2[i]) return false;
}
return true
}
sortCheck(fieldArray:string[]) {
return this.compareArrays(this.sortField, fieldArray);
}
isAscending() : boolean {
return this.sortOrder == "asc";
}

View File

@ -258,12 +258,15 @@ export class UserService implements OnInit, OnDestroy {
this.authenticated = false;
}
public query(searchTerm : string, offset : number = 0, limit : number = 10, orderBy : string = 'user_id', order: string = 'asc') : Observable<PagedResult<UserInfo>> {
public query(searchTerm : string, offset : number = 0, limit : number = 10, orderBy : string[] = ['user_id'], order: string = 'asc') : Observable<PagedResult<UserInfo>> {
console.log("getUserList " + searchTerm + "," + offset + "," + limit + "," + orderBy + "," + order);
if (searchTerm==null) {
searchTerm=""
}
return this.rest.executeRestCall<PagedResult<UserInfo>>("get", "redback", "users", {'q':searchTerm, 'offset':offset,'limit':limit});
if (orderBy==null || orderBy.length==0) {
orderBy = ['user_id'];
}
return this.rest.executeRestCall<PagedResult<UserInfo>>("get", "redback", "users", {'q':searchTerm, 'offset':offset,'limit':limit,'orderBy':orderBy,'order':order});
}
}