LP1846042 Angular grid filter dropdown improvements
[Evergreen.git] / Open-ILS / src / eg2 / src / app / share / grid / grid-filter-control.component.ts
1 import {Component, Input, OnInit, QueryList, ViewChild, ViewChildren} from '@angular/core';
2 import {GridContext, GridColumn} from './grid';
3 import {IdlObject} from '@eg/core/idl.service';
4 import {ComboboxComponent} from '@eg/share/combobox/combobox.component';
5 import {DateSelectComponent} from '@eg/share/date-select/date-select.component';
6 import {OrgSelectComponent} from '@eg/share/org-select/org-select.component';
7 import {OrgService} from '@eg/core/org.service';
8 import {NgbDropdown} from '@ng-bootstrap/ng-bootstrap';
9 import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
10
11 @Component({
12   selector: 'eg-grid-filter-control',
13   templateUrl: './grid-filter-control.component.html'
14 })
15
16 export class GridFilterControlComponent implements OnInit {
17
18     @Input() context: GridContext;
19     @Input() col:     GridColumn;
20
21
22     @ViewChildren(ComboboxComponent)   filterComboboxes: QueryList<ComboboxComponent>;
23     @ViewChildren(OrgSelectComponent)  orgSelects: QueryList<OrgSelectComponent>;
24     @ViewChildren(NgbDropdown)         dropdowns: QueryList<NgbDropdown>;
25
26     @ViewChild('dateSelectOne') dateSelectOne: DateSelectComponent;
27     @ViewChild('dateSelectTwo') dateSelectTwo: DateSelectComponent;
28
29     // So we can use (ngModelChange) on the link combobox
30     linkFilterEntry: ComboboxEntry = null;
31
32     constructor(
33         private org: OrgService
34     ) {}
35
36     ngOnInit() { }
37
38     operatorChanged(col: GridColumn) {
39         if (col.filterOperator === 'null' || col.filterOperator === 'not null') {
40             col.filterInputDisabled = true;
41             col.filterValue = undefined;
42         } else {
43             col.filterInputDisabled = false;
44         }
45     }
46
47     applyOrgFilter(col: GridColumn) {
48         const org: IdlObject = (col.filterValue as unknown) as IdlObject;
49
50         if (org == null) {
51             this.clearFilter(col);
52             return;
53         }
54         const ous: any[] = new Array();
55         if (col.filterIncludeOrgDescendants || col.filterIncludeOrgAncestors) {
56             if (col.filterIncludeOrgAncestors) {
57                 ous.push(...this.org.ancestors(org, true));
58             }
59             if (col.filterIncludeOrgDescendants) {
60                 ous.push(...this.org.descendants(org, true));
61             }
62         } else {
63             ous.push(org.id());
64         }
65         const filt: any = {};
66         filt[col.name] = {};
67         const op: string = (col.filterOperator === '=' ? 'in' : 'not in');
68         filt[col.name][op] = ous;
69         this.context.dataSource.filters[col.name] = [ filt ];
70         col.isFiltered = true;
71         this.context.reload();
72
73         this.closeDropdown();
74     }
75
76     applyLinkFilter(col: GridColumn) {
77         if (col.filterValue) {
78             this.applyFilter(col);
79
80         } else {
81             // Value was cleared from the combobox
82             this.clearFilter(col);
83         }
84     }
85
86     // TODO: this was copied from date-select and
87     // really belongs in a date service
88     localDateFromYmd(ymd: string): Date {
89         const parts = ymd.split('-');
90         return new Date(
91             Number(parts[0]), Number(parts[1]) - 1, Number(parts[2]));
92     }
93
94     applyDateFilter(col: GridColumn) {
95         const dateStr = this.dateSelectOne.currentAsYmd();
96         const endDateStr =
97             this.dateSelectTwo ? this.dateSelectTwo.currentAsYmd() : null;
98
99         if (endDateStr && !dateStr) {
100             // User has applied a second date (e.g. between) but cleared
101             // the first date.  Avoid applying the filter until
102             // dateStr gets a value or endDateStr is cleared or the
103             // operator changes.
104             return;
105         }
106
107         if (col.filterOperator === 'null' || col.filterOperator === 'not null') {
108             this.applyFilter(col);
109         } else {
110             if (dateStr == null) {
111                 this.clearFilter(col);
112                 return;
113             }
114             const date: Date = this.localDateFromYmd(dateStr);
115             let date1 = new Date();
116             let date2 = new Date();
117             const op: string = col.filterOperator;
118             const filt: Object = {};
119             const filt2: Object = {};
120             const filters = new Array();
121             if (col.filterOperator === '>') {
122                 date1 = date;
123                 date1.setHours(23);
124                 date1.setMinutes(59);
125                 date1.setSeconds(59);
126                 filt[op] = date1.toISOString();
127                 if (col.name === 'dob') { filt[op] = dateStr; } // special case
128                 filt2[col.name] = filt;
129                 filters.push(filt2);
130             } else if (col.filterOperator === '>=') {
131                 date1 = date;
132                 filt[op] = date1.toISOString();
133                 if (col.name === 'dob') { filt[op] = dateStr; } // special case
134                 filt2[col.name] = filt;
135                 filters.push(filt2);
136             } else if (col.filterOperator === '<') {
137                 date1 = date;
138                 filt[op] = date1.toISOString();
139                 if (col.name === 'dob') { filt[op] = dateStr; } // special case
140                 filt2[col.name] = filt;
141                 filters.push(filt2);
142             } else if (col.filterOperator === '<=') {
143                 date1 = date;
144                 date1.setHours(23);
145                 date1.setMinutes(59);
146                 date1.setSeconds(59);
147                 filt[op] = date1.toISOString();
148                 if (col.name === 'dob') { filt[op] = dateStr; } // special case
149                 filt2[col.name] = filt;
150                 filters.push(filt2);
151             } else if (col.filterOperator === '=') {
152                 date1 = new Date(date.valueOf());
153                 filt['>='] = date1.toISOString();
154                 if (col.name === 'dob') { filt['>='] = dateStr; } // special case
155                 filt2[col.name] = filt;
156                 filters.push(filt2);
157
158                 date2 = new Date(date.valueOf());
159                 date2.setHours(23);
160                 date2.setMinutes(59);
161                 date2.setSeconds(59);
162                 const filt_a: Object = {};
163                 const filt2_a: Object = {};
164                 filt_a['<='] = date2.toISOString();
165                 if (col.name === 'dob') { filt_a['<='] = dateStr; } // special case
166                 filt2_a[col.name] = filt_a;
167                 filters.push(filt2_a);
168             } else if (col.filterOperator === '!=') {
169                 date1 = new Date(date.valueOf());
170                 filt['<'] = date1.toISOString();
171                 if (col.name === 'dob') { filt['<'] = dateStr; } // special case
172                 filt2[col.name] = filt;
173
174                 date2 = new Date(date.valueOf());
175                 date2.setHours(23);
176                 date2.setMinutes(59);
177                 date2.setSeconds(59);
178                 const filt_a: Object = {};
179                 const filt2_a: Object = {};
180                 filt_a['>'] = date2.toISOString();
181                 if (col.name === 'dob') { filt_a['>'] = dateStr; } // special case
182                 filt2_a[col.name] = filt_a;
183
184                 const date_filt: any = { '-or': [] };
185                 date_filt['-or'].push(filt2);
186                 date_filt['-or'].push(filt2_a);
187                 filters.push(date_filt);
188             } else if (col.filterOperator === 'between') {
189
190                 if (!endDateStr) {
191                     // User has not applied the second date yet.
192                     return;
193                 }
194
195                 date1 = date;
196                 date2 = this.localDateFromYmd(endDateStr);
197
198                 let date1op = '>=';
199                 let date2op = '<=';
200                 if (date1 > date2) {
201                     // don't make user care about the order
202                     // they enter the dates in
203                     date1op = '<=';
204                     date2op = '>=';
205                 }
206                 filt[date1op] = date1.toISOString();
207                 if (col.name === 'dob') { filt['>='] = dateStr; } // special case
208                 filt2[col.name] = filt;
209                 filters.push(filt2);
210
211                 date2.setHours(23);
212                 date2.setMinutes(59);
213                 date2.setSeconds(59);
214                 const filt_a: Object = {};
215                 const filt2_a: Object = {};
216                 filt_a[date2op] = date2.toISOString();
217                 if (col.name === 'dob') { filt_a['<='] = endDateStr; } // special case
218                 filt2_a[col.name] = filt_a;
219                 filters.push(filt2_a);
220             }
221             this.context.dataSource.filters[col.name] = filters;
222             col.isFiltered = true;
223             this.context.reload();
224             this.closeDropdown();
225         }
226     }
227
228     clearDateFilter(col: GridColumn) {
229         delete this.context.dataSource.filters[col.name];
230         col.isFiltered = false;
231         this.context.reload();
232         this.closeDropdown();
233     }
234     applyBooleanFilter(col: GridColumn) {
235         if (!col.filterValue || col.filterValue === '') {
236             delete this.context.dataSource.filters[col.name];
237             col.isFiltered = false;
238             this.context.reload();
239         } else {
240             const val: string = col.filterValue;
241             const op = '=';
242             const filt: Object = {};
243             filt[op] = val;
244             const filt2: Object = {};
245             filt2[col.name] = filt;
246             this.context.dataSource.filters[col.name] = [ filt2 ];
247             col.isFiltered = true;
248             this.context.reload();
249         }
250
251         this.closeDropdown();
252     }
253
254     applyFilterCommon(col: GridColumn) {
255
256         switch (col.datatype) {
257             case 'link':
258                 col.filterValue =
259                     this.linkFilterEntry ? this.linkFilterEntry.id : null;
260                 return this.applyLinkFilter(col);
261             case 'bool':
262                 return this.applyBooleanFilter(col);
263             case 'timestamp':
264                 return this.applyDateFilter(col);
265             case 'org_unit':
266                 return this.applyOrgFilter(col);
267             default:
268                 return this.applyFilter(col);
269         }
270     }
271
272     applyFilter(col: GridColumn) {
273         // fallback if the operator somehow was not set yet
274         if (col.filterOperator === undefined) { col.filterOperator = '='; }
275
276         if ( (col.filterOperator !== 'null') && (col.filterOperator !== 'not null') &&
277              (!col.filterValue || col.filterValue === '') &&
278              (col.filterValue !== '0') ) {
279             // if value is empty and we're _not_ checking for null/not null, clear
280             // the filter
281             delete this.context.dataSource.filters[col.name];
282             col.isFiltered = false;
283         } else {
284             let op: string = col.filterOperator;
285             let val: string = col.filterValue;
286             if (col.filterOperator === 'null') {
287                 op  = '=';
288                 val = null;
289             } else if (col.filterOperator === 'not null') {
290                 op  = '!=';
291                 val = null;
292             } else if (col.filterOperator === 'like' || col.filterOperator === 'not like') {
293                 val = '%' + val + '%';
294             } else if (col.filterOperator === 'startswith') {
295                 op = 'like';
296                 val = val + '%';
297             } else if (col.filterOperator === 'endswith') {
298                 op = 'like';
299                 val = '%' + val;
300             }
301             const filt: any = {};
302             if (col.filterOperator === 'not like') {
303                 filt['-not'] = {};
304                 filt['-not'][col.name] = {};
305                 filt['-not'][col.name]['like'] = val;
306                 this.context.dataSource.filters[col.name] = [ filt ];
307                 col.isFiltered = true;
308             } else {
309                 filt[col.name] = {};
310                 filt[col.name][op] = val;
311                 this.context.dataSource.filters[col.name] = [ filt ];
312                 col.isFiltered = true;
313             }
314         }
315         this.context.reload();
316         this.closeDropdown();
317     }
318     clearFilter(col: GridColumn) {
319         // clear filter values...
320         col.removeFilter();
321         // ... and inform the data source
322         delete this.context.dataSource.filters[col.name];
323         col.isFiltered = false;
324         this.reset();
325         this.context.reload();
326         this.closeDropdown();
327     }
328
329     closeDropdown() {
330         // Timeout allows actions to occur before closing (some) dropdows
331         // clears the values (e.g. link selector)
332         setTimeout(() => this.dropdowns.forEach(drp => { drp.close(); }));
333     }
334
335     reset() {
336         this.filterComboboxes.forEach(ctl => { ctl.applyEntryId(null); });
337         this.orgSelects.forEach(ctl => { ctl.reset(); });
338         if (this.dateSelectOne) { this.dateSelectOne.reset(); }
339         if (this.dateSelectTwo) { this.dateSelectTwo.reset(); }
340     }
341 }
342