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