]> source.dussan.org Git - archiva.git/blob
f7d2188a8081417b69fd7184f320f8676805978a
[archiva.git] /
1 /*
2  * Licensed to the Apache Software Foundation (ASF) under one
3  * or more contributor license agreements.  See the NOTICE file
4  * distributed with this work for additional information
5  * regarding copyright ownership.  The ASF licenses this file
6  * to you under the Apache License, Version 2.0 (the
7  * "License"); you may not use this file except in compliance
8  * with the License.  You may obtain a copy of the License at
9  *
10  * http://www.apache.org/licenses/LICENSE-2.0
11  * Unless required by applicable law or agreed to in writing,
12  * software distributed under the License is distributed on an
13  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14  * KIND, either express or implied.  See the License for the
15  * specific language governing permissions and limitations
16  * under the License.
17  */
18
19 import {AfterViewInit, Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
20 import {concat, merge, Observable, of, Subject} from "rxjs";
21 import {debounceTime, distinctUntilChanged, filter, map, pluck, startWith, switchMap} from "rxjs/operators";
22 import {EntityService} from "@app/model/entity-service";
23 import {FieldToggle} from "@app/model/field-toggle";
24 import {PageQuery} from "../model/page-query";
25 import {LoadingValue} from '../model/loading-value';
26 import {PagedResult} from "@app/model/paged-result";
27
28
29 /**
30  * This component has a search field and pagination section. Entering data in the search field, or
31  * a button click on the pagination triggers a call to a service method, that returns the entity data.
32  * The service must implement the {@link EntityService} interface.
33  *
34  * The content is displayed between the search input and the pagination section. To use the data, you should
35  * add an identifier and refer to the item$ variable:
36  * ```
37  * <app-paginated-entities #parent>
38  *   <table>
39  *       <tr ngFor="let entity in parent.item$ | async" >
40  *           <td>{{entity.id}}</td>
41  *       </tr>
42  *   </table>
43  * </app-paginated-entities>
44  * ```
45  *
46  * @typeparam T The type of the retrieved entity elements.
47  */
48 @Component({
49     selector: 'app-paginated-entities',
50     templateUrl: './paginated-entities.component.html',
51     styleUrls: ['./paginated-entities.component.scss']
52 })
53 export class PaginatedEntitiesComponent<T> implements OnInit, FieldToggle, AfterViewInit {
54
55     @Input() id: string;
56
57     /**
58      * This must be set, if you use the component. This service retrieves the entity data.
59      */
60     @Input() service: EntityService<T>;
61
62     /**
63      * The number of elements per page retrieved
64      */
65     @Input() pageSize = 10;
66
67     /**
68      * Two-Way-Binding attribute for sorting field
69      */
70     @Input() sortField = [];
71     /**
72      * Two-Way Binding attribute for sort order
73      */
74     @Input() sortOrder = "asc";
75
76     /**
77      * Pagination controls
78      */
79     @Input() pagination = {
80         maxSize: 5,
81         rotate: true,
82         boundaryLinks: true,
83         ellipses: false
84     }
85
86     /**
87      * If true, all controls are displayed, if the total count is 0
88      */
89     @Input()
90     displayIfEmpty:boolean=true;
91     /**
92      * Sets the translation key, for the text to display, if displayIfEmpty=false and the total count is 0.
93      */
94     @Input()
95     displayKeyIfEmpty:string='form.emptyContent';
96
97     /**
98      * If set to true, all controls are displayed, even if there is only one page to display.
99      * Otherwise the controls are not displayed, if there is only a single page of results.
100      */
101     @Input()
102     displayControlsIfSinglePage:boolean=true;
103
104
105
106     /**
107      * The current page that is selected
108      */
109     page = 1;
110     /**
111      * The current search term entered in the search field
112      */
113     searchTerm: string;
114
115     /**
116      * Event thrown, if the page value changes
117      */
118     @Output() pageChange: EventEmitter<number> = new EventEmitter<number>();
119     /**
120      * Event thrown, if the search term changes
121      */
122     @Output() searchTermChange: EventEmitter<string> = new EventEmitter<string>();
123
124     @Output() sortFieldChange: EventEmitter<string[]> = new EventEmitter<string[]>();
125
126     @Output() sortOrderChange: EventEmitter<string> = new EventEmitter<string>();
127
128     /**
129      * The total number of elements available for the given search term
130      */
131     public total$: Observable<number>;
132     /**
133      * The entity items retrieved from the service
134      */
135     public items$: Observable<LoadingValue<PagedResult<T>>>;
136
137     /**
138      * true, if the current page result value represents a result with multiple pages,
139      * otherwise false.
140      */
141     public multiplePages$:Observable<boolean>;
142
143     private pageStream: Subject<number> = new Subject<number>();
144     private searchTermStream: Subject<string> = new Subject<string>();
145
146     constructor() {
147         // console.log("Construct " + this.id);
148         this.items$=null;
149         this.total$=null;
150         this.multiplePages$=null;
151     }
152
153     ngOnInit(): void {
154         // console.log("Pag Init " + this.id);
155         // We combine the sources for the page and the search input field to a observable 'source'
156         const pageSource = this.pageStream.pipe(map(pageNumber => {
157             return new PageQuery(this.searchTerm, pageNumber);
158         }));
159         const searchSource = this.searchTermStream.pipe(
160             debounceTime(1000),
161             distinctUntilChanged(),
162             map(searchTerm => {
163                 this.searchTerm = searchTerm;
164                 return new PageQuery(searchTerm, 1)
165             }));
166         const source = merge(pageSource, searchSource).pipe(
167             startWith(new PageQuery(this.searchTerm, this.page)),
168             switchMap((params: PageQuery) =>
169                 concat(
170                     of(LoadingValue.start<PagedResult<T>>()),
171                     this.service(params.search, (params.page - 1) * this.pageSize, this.pageSize, this.sortField, this.sortOrder)
172                         .pipe(map(pagedResult=>LoadingValue.finish<PagedResult<T>>(pagedResult)))
173                 )
174             )
175             );
176         this.total$ = source.pipe(filter(val=>val.hasValue()),map(val=>val.value),
177             pluck('pagination', 'total_count'));
178         this.multiplePages$ = source.pipe(filter(val => val.hasValue()),
179             map(val => val.value.pagination.total_count > val.value.pagination.limit));
180         this.items$ = source;
181     }
182
183     search(terms: string) {
184         // console.log("Keystroke " + terms);
185         this.searchTermChange.emit(terms);
186         this.searchTermStream.next(terms)
187     }
188
189     public changePage(pageNumber: number) {
190         // console.log("Page change " +pageNumber);
191         this.pageChange.emit(pageNumber);
192         this.pageStream.next(pageNumber);
193     }
194
195     private compareArrays(a1: string[], a2: string[]) {
196         let i = a1.length;
197         while (i--) {
198             if (a1[i] !== a2[i]) return false;
199         }
200         return true
201     }
202
203     toggleSortField(fieldName: string) {
204         this.toggleField([fieldName]);
205     }
206
207     toggleField(fieldArray: string[]) {
208         // console.log("Changing sort field " + fieldArray);
209         let sortOrderChanged: boolean = false;
210         let sortFieldChanged: boolean = false;
211         if (!this.compareArrays(this.sortField, fieldArray)) {
212           // console.log("Fields differ: " + this.sortField + " - " + fieldArray);
213             this.sortField = fieldArray;
214             if (this.sortOrder != 'asc') {
215                 this.sortOrder = 'asc';
216                 sortOrderChanged = true;
217             }
218             sortFieldChanged = true;
219         } else {
220             if (this.sortOrder == "asc") {
221                 this.sortOrder = "desc";
222             } else {
223                 this.sortOrder = "asc";
224             }
225           // console.log("Toggled sort order: " + this.sortOrder);
226             sortOrderChanged = true;
227         }
228         if (sortOrderChanged) {
229           //console.log("Sort order changed: "+this.sortOrder)
230             this.sortOrderChange.emit(this.sortOrder);
231         }
232         if (sortFieldChanged) {
233             this.sortFieldChange.emit(this.sortField);
234         }
235         if (sortFieldChanged || sortOrderChanged) {
236             this.page = 1;
237             this.changePage(this.page);
238         }
239     }
240
241     ngAfterViewInit(): void {
242         // console.log("Pag afterViewInit " + this.id);
243         // We emit the current value to push them to the containing reading components
244         this.sortOrderChange.emit(this.sortOrder);
245         this.sortFieldChange.emit(this.sortField);
246     }
247
248
249     public changeService(newService : EntityService<T>): void {
250         this.service = newService;
251         this.changePage(1);
252     }
253
254 }