You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@archiva.apache.org by ma...@apache.org on 2020/11/08 23:35:42 UTC

[archiva] branch master updated: Components for new angular app

This is an automated email from the ASF dual-hosted git repository.

martin_s pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/archiva.git


The following commit(s) were added to refs/heads/master by this push:
     new ec828c8  Components for new angular app
ec828c8 is described below

commit ec828c8745637e7cc51ea4cb3a1717f0588f0f1a
Author: Martin Stockhammer <ma...@apache.org>
AuthorDate: Mon Nov 9 00:35:36 2020 +0100

    Components for new angular app
---
 .../src/main/archiva-web/src/app/app.module.ts     |   4 +
 .../archiva-web/src/app/model/entity-service.ts    |   2 +-
 .../model/{entity-service.ts => field-toggle.ts}   |  11 +-
 .../paginated-entities.component.ts                | 240 +++++++++++++--------
 .../sorted-table-header-row.component.html         |  19 ++
 .../sorted-table-header-row.component.scss}        |  12 +-
 .../sorted-table-header-row.component.spec.ts}     |  33 ++-
 .../sorted-table-header-row.component.ts           |  79 +++++++
 .../sorted-table-header.component.html             |  24 +++
 .../sorted-table-header.component.scss}            |  12 +-
 .../sorted-table-header.component.spec.ts}         |  33 ++-
 .../sorted-table-header.component.ts               |  90 ++++++++
 .../manage-users-list.component.html               |  22 +-
 .../manage-users-list.component.ts                 |  26 ++-
 .../archiva-web/src/app/services/user.service.ts   |   7 +-
 15 files changed, 460 insertions(+), 154 deletions(-)

diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/app.module.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/app.module.ts
index 9e35162..f3ddcc7 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/app.module.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/app.module.ts
@@ -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,
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/entity-service.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/entity-service.ts
index a9e7ce8..bdf649e 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/entity-service.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/entity-service.ts
@@ -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>>
 }
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/entity-service.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/field-toggle.ts
similarity index 68%
copy from archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/entity-service.ts
copy to archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/field-toggle.ts
index a9e7ce8..e0d0e3f 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/entity-service.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/field-toggle.ts
@@ -16,13 +16,6 @@
  * under the License.
  */
 
-import {PagedResult} from "./paged-result";
-import {Observable} from "rxjs";
-
-/**
- * This is a functional interface that is used to retrieve entity data.
- * @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>>
+export interface FieldToggle {
+    toggleField(fieldName: string[]);
 }
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/general/paginated-entities/paginated-entities.component.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/general/paginated-entities/paginated-entities.component.ts
index 2e5123b..1d0cba1 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/general/paginated-entities/paginated-entities.component.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/general/paginated-entities/paginated-entities.component.ts
@@ -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";
 
 
 /**
@@ -44,95 +43,154 @@ import {EntityService} from "../../../model/entity-service";
  * @typeparam T The type of the retrieved entity elements.
  */
 @Component({
-  selector: 'app-paginated-entities',
-  templateUrl: './paginated-entities.component.html',
-  styleUrls: ['./paginated-entities.component.scss']
+    selector: 'app-paginated-entities',
+    templateUrl: './paginated-entities.component.html',
+    styleUrls: ['./paginated-entities.component.scss']
 })
-export class PaginatedEntitiesComponent<T> implements OnInit {
-
-  /**
-   * This must be set, if you use the component. This service retrieves the entity data.
-   */
-  @Input() service : EntityService<T>;
-
-  /**
-   * The number of elements per page retrieved
-   */
-  @Input() pageSize = 10;
-
-  /**
-   * Pagination controls
-   */
-  @Input() pagination = {
-    maxSize:5,
-    rotate:true,
-    boundaryLinks:true,
-    ellipses:false
-  }
-
-  /**
-   * The current page that is selected
-   */
-  page = 1;
-  /**
-   * The current search term entered in the search field
-   */
-  searchTerm: string;
-
-  /**
-   * Event thrown, if the page value changes
-   */
-  @Output() pageEvent : EventEmitter<number> = new EventEmitter<number>();
-  /**
-   * Event thrown, if the search term changes
-   */
-  @Output() searchTermEvent: EventEmitter<string> = new EventEmitter<string>();
-
-  /**
-   * The total number of elements available for the given search term
-   */
-  total$: Observable<number>;
-  /**
-   * The entity items retrieved from the service
-   */
-  items$: Observable<T[]>;
-
-  private pageStream: Subject<number> = new Subject<number>();
-  private searchTermStream: Subject<string> = new Subject<string>();
-
-  constructor() { }
-
-  ngOnInit(): void {
-    // We combine the sources for the page and the search input field to a observable 'source'
-    const pageSource = this.pageStream.pipe(map(pageNumber => {
-      return {search: this.searchTerm, page: pageNumber}
-    }));
-    const searchSource = this.searchTermStream.pipe(
-        debounceTime(1000),
-        distinctUntilChanged(),
-        map(searchTerm => {
-          this.searchTerm = searchTerm;
-          return {search: searchTerm, page: 1}
+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>;
+
+    /**
+     * 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
+    }
+
+    /**
+     * The current page that is selected
+     */
+    page = 1;
+    /**
+     * The current search term entered in the search field
+     */
+    searchTerm: string;
+
+    /**
+     * Event thrown, if the page value changes
+     */
+    @Output() pageChange: EventEmitter<number> = new EventEmitter<number>();
+    /**
+     * Event thrown, if the search term changes
+     */
+    @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
+     */
+    total$: Observable<number>;
+    /**
+     * The entity items retrieved from the service
+     */
+    items$: Observable<T[]>;
+
+    private pageStream: Subject<number> = new Subject<number>();
+    private searchTermStream: Subject<string> = new Subject<string>();
+
+    constructor() {
+    }
+
+    ngOnInit(): void {
+        // We combine the sources for the page and the search input field to a observable 'source'
+        const pageSource = this.pageStream.pipe(map(pageNumber => {
+            return {search: this.searchTerm, page: pageNumber}
         }));
-    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'));
-    this.items$ = source.pipe(pluck('data'));
-  }
-
-  search(terms: string) {
-    // console.log("Keystroke " + terms);
-    this.searchTermEvent.emit(terms);
-    this.searchTermStream.next(terms)
-  }
-
-  changePage(pageNumber : number) {
-    // console.log("Page change " +pageNumber);
-    this.pageEvent.emit(pageNumber);
-    this.pageStream.next(pageNumber);
-  }
+        const searchSource = this.searchTermStream.pipe(
+            debounceTime(1000),
+            distinctUntilChanged(),
+            map(searchTerm => {
+                this.searchTerm = searchTerm;
+                return {search: searchTerm, page: 1}
+            }));
+        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, 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.searchTermChange.emit(terms);
+        this.searchTermStream.next(terms)
+    }
+
+    changePage(pageNumber: number) {
+        // console.log("Page change " +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);
+        }
+    }
 
 }
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/general/sorted-table-header-row/sorted-table-header-row.component.html b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/general/sorted-table-header-row/sorted-table-header-row.component.html
new file mode 100644
index 0000000..ff210c9
--- /dev/null
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/general/sorted-table-header-row/sorted-table-header-row.component.html
@@ -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>
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/entity-service.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/general/sorted-table-header-row/sorted-table-header-row.component.scss
similarity index 67%
copy from archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/entity-service.ts
copy to archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/general/sorted-table-header-row/sorted-table-header-row.component.scss
index a9e7ce8..343c3b1 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/entity-service.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/general/sorted-table-header-row/sorted-table-header-row.component.scss
@@ -1,4 +1,4 @@
-/*
+/*!
  * 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
@@ -16,13 +16,3 @@
  * under the License.
  */
 
-import {PagedResult} from "./paged-result";
-import {Observable} from "rxjs";
-
-/**
- * This is a functional interface that is used to retrieve entity data.
- * @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>>
-}
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/entity-service.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/general/sorted-table-header-row/sorted-table-header-row.component.spec.ts
similarity index 52%
copy from archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/entity-service.ts
copy to archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/general/sorted-table-header-row/sorted-table-header-row.component.spec.ts
index a9e7ce8..a8cb6fb 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/entity-service.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/general/sorted-table-header-row/sorted-table-header-row.component.spec.ts
@@ -16,13 +16,28 @@
  * under the License.
  */
 
-import {PagedResult} from "./paged-result";
-import {Observable} from "rxjs";
+import { ComponentFixture, TestBed } from '@angular/core/testing';
 
-/**
- * This is a functional interface that is used to retrieve entity data.
- * @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>>
-}
+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();
+  });
+});
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/general/sorted-table-header-row/sorted-table-header-row.component.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/general/sorted-table-header-row/sorted-table-header-row.component.ts
new file mode 100644
index 0000000..4de4c74
--- /dev/null
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/general/sorted-table-header-row/sorted-table-header-row.component.ts
@@ -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 {
+
+  }
+
+}
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/general/sorted-table-header/sorted-table-header.component.html b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/general/sorted-table-header/sorted-table-header.component.html
new file mode 100644
index 0000000..ff896b2
--- /dev/null
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/general/sorted-table-header/sorted-table-header.component.html
@@ -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>
\ No newline at end of file
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/entity-service.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/general/sorted-table-header/sorted-table-header.component.scss
similarity index 67%
copy from archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/entity-service.ts
copy to archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/general/sorted-table-header/sorted-table-header.component.scss
index a9e7ce8..343c3b1 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/entity-service.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/general/sorted-table-header/sorted-table-header.component.scss
@@ -1,4 +1,4 @@
-/*
+/*!
  * 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
@@ -16,13 +16,3 @@
  * under the License.
  */
 
-import {PagedResult} from "./paged-result";
-import {Observable} from "rxjs";
-
-/**
- * This is a functional interface that is used to retrieve entity data.
- * @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>>
-}
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/entity-service.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/general/sorted-table-header/sorted-table-header.component.spec.ts
similarity index 53%
copy from archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/entity-service.ts
copy to archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/general/sorted-table-header/sorted-table-header.component.spec.ts
index a9e7ce8..96bbe92 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/model/entity-service.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/general/sorted-table-header/sorted-table-header.component.spec.ts
@@ -16,13 +16,28 @@
  * under the License.
  */
 
-import {PagedResult} from "./paged-result";
-import {Observable} from "rxjs";
+import { ComponentFixture, TestBed } from '@angular/core/testing';
 
-/**
- * This is a functional interface that is used to retrieve entity data.
- * @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>>
-}
+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();
+  });
+});
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/general/sorted-table-header/sorted-table-header.component.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/general/sorted-table-header/sorted-table-header.component.ts
new file mode 100644
index 0000000..bc297d5
--- /dev/null
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/general/sorted-table-header/sorted-table-header.component.ts
@@ -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);
+  }
+
+}
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/user/users/manage-users-list/manage-users-list.component.html b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/user/users/manage-users-list/manage-users-list.component.html
index 56b02ae..bfdc6fb 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/user/users/manage-users-list/manage-users-list.component.html
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/user/users/manage-users-list/manage-users-list.component.html
@@ -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>
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/user/users/manage-users-list/manage-users-list.component.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/user/users/manage-users-list/manage-users-list.component.ts
index 8e91dd3..d2a073a 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/user/users/manage-users-list/manage-users-list.component.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/modules/user/users/manage-users-list/manage-users-list.component.ts
@@ -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";
   }
 
 
diff --git a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/user.service.ts b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/user.service.ts
index bdeeafa..68f9857 100644
--- a/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/user.service.ts
+++ b/archiva-modules/archiva-web/archiva-webapp/src/main/archiva-web/src/app/services/user.service.ts
@@ -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});
     }
 
 }