}
applyOrgFilter(col: GridColumn) {
- const org: IdlObject = (col.filterValue as unknown) as IdlObject;
+ let org: IdlObject = (col.filterValue as unknown) as IdlObject;
if (org == null) {
this.clearFilter(col);
return;
}
+ org = this.org.get(org); // if coming from a Named Filter Set, filterValue would be an an org id
const ous: any[] = new Array();
if (col.filterIncludeOrgDescendants || col.filterIncludeOrgAncestors) {
if (col.filterIncludeOrgAncestors) {
--- /dev/null
+<ng-template #dialogContent>
+ <div class="modal-header bg-info">
+ <h4 class="modal-title">
+ <span i18n>Manage Grid Filters</span>
+ </h4>
+ <button type="button" class="close"
+ i18n-aria-label aria-label="Close" (click)="close()">
+ <span aria-hidden="true">×</span>
+ </button>
+</div>
+<div class="modal-body">
+ <form #manageFiltersForm="ngForm" role="form" class="form-validated common-form striped-odd">
+ <div class="form-group row">
+ <div class="col-lg-3">
+ <label for="session_name" i18n>Save as</label>
+ </div>
+ <div class="col-lg-9">
+ <input
+ class="form-control"
+ id="session_name" name="session_name"
+ type="text" pattern="[\s\S]*\S[\s\S]*"
+ placeholder="Name..." i18n-placeholder
+ required="false"
+ (ngModelChange)="saveFilterNameModelChanged.next($event)"
+ [ngModel]="saveFilterName"/>
+ <div *ngIf="nameCollision" class="alert alert-warning" i18n>
+ An existing Filter Set with the same name will be overwritten if you Save.
+ </div>
+ </div>
+ </div>
+ <div class="form-group row">
+ <div class="col-lg-3">
+ </div>
+ <div class="col-lg-9">
+ <button type="button" class="btn btn-success" [disabled]="saveFilterName === ''"
+ (click)="gridContext.saveFilters(saveFilterName); refreshEntries(); this.nameCollision = true; close()" i18n>
+ Save Active Filters</button>
+ </div>
+ </div>
+ <div class="form-group row">
+ <div class="col-lg-3">
+ <label for="filter_sets" i18n>Filter Sets</label>
+ </div>
+ <div class="col-lg-9">
+ <eg-combobox #namedFilterSetSelector
+ domId="filterSets" name="filter_sets" [entries]="filterSetEntries"
+ placeholder="Filter Sets..." i18n-placeholder>
+ </eg-combobox>
+ </div>
+ </div>
+ <div class="form-group row">
+ <div class="col-lg-3">
+ </div>
+ <div class="col-lg-9">
+ <button type="button" class="btn btn-success" [disabled]="!namedFilterSetSelector.selectedId"
+ (click)="gridContext.loadFilters(namedFilterSetSelector.selectedId); close();" i18n>
+ Load Filter Set</button>
+ <div *ngIf="gridContext.filtersSet()" class="alert alert-warning" i18n>
+ Your active filters will be replaced with this filter set if you Load.
+ </div>
+ </div>
+ </div>
+ <div class="form-group row">
+ <div class="col-lg-3">
+ </div>
+ <div class="col-lg-9">
+ <button type="button" class="btn btn-success" [disabled]="!namedFilterSetSelector.selectedId"
+ (click)="gridContext.deleteFilters(namedFilterSetSelector.selectedId); namedFilterSetSelector.selectedId = ''; refreshEntries();" i18n>
+ Delete Filter Set</button>
+ </div>
+ </div>
+ </form>
+</div>
+<div class="modal-footer">
+ <button type="button" class="btn btn-success" [disabled]="manageFiltersForm.invalid"
+ (click)="gridContext.removeFilters(); close(false);" i18n>
+ Remove Filters</button>
+ <button type="button" class="btn btn-secondary"
+ (click)="close()" i18n>
+ Cancel</button>
+</div>
+</ng-template>
--- /dev/null
+import {AlertDialogComponent} from '@eg/share/dialog/alert.component';
+import {AuthService} from '@eg/core/auth.service';
+import {ComboboxEntry, ComboboxComponent} from '@eg/share/combobox/combobox.component';
+import {Component, Input, OnInit, ViewChild, Renderer2} from '@angular/core';
+import {GridContext} from '@eg/share/grid/grid';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {EventService} from '@eg/core/event.service';
+import {FormControl} from '@angular/forms';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {NgForm} from '@angular/forms';
+import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {ProgressDialogComponent} from '@eg/share/dialog/progress.component';
+import {ServerStoreService} from '@eg/core/server-store.service';
+import {StringComponent} from '@eg/share/string/string.component';
+import {Subject, Subscription, Observable, from, EMPTY, throwError} from 'rxjs';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {debounceTime, distinctUntilChanged, switchMap, takeLast, finalize} from 'rxjs/operators';
+
+@Component({
+ selector: 'eg-grid-manage-filters-dialog',
+ templateUrl: './grid-manage-filters-dialog.component.html'
+})
+
+export class GridManageFiltersDialogComponent extends DialogComponent implements OnInit {
+
+ @Input() gridContext: GridContext;
+
+ subscriptions: Subscription[] = [];
+
+ saveFilterName: string = '';
+ saveFilterNameModelChanged: Subject<string> = new Subject<string>();
+ nameCollision: boolean = false;
+
+ filterSetEntries: ComboboxEntry[] = [];
+
+ @ViewChild('manageFiltersForm', { static: false}) manageFiltersForm: NgForm;
+ @ViewChild('namedFilterSetSelector', { static: true}) namedFilterSetSelector: ComboboxComponent;
+
+ constructor(
+ private modal: NgbModal,
+ private auth: AuthService,
+ private evt: EventService,
+ private net: NetService,
+ private toast: ToastService,
+ private idl: IdlService,
+ private pcrud: PcrudService,
+ private renderer: Renderer2,
+ private store: ServerStoreService,
+ ) {
+ super(modal);
+ }
+
+ ngOnInit() {
+
+ this.subscriptions.push( this.onOpen$.subscribe(
+ _ => {
+ const el = this.renderer.selectRootElement('#session_name');
+ if (el) { el.focus(); el.select(); }
+ }
+ ));
+
+ this.subscriptions.push(
+ this.saveFilterNameModelChanged
+ .pipe(
+ debounceTime(300),
+ distinctUntilChanged()
+ )
+ .subscribe( newText => {
+ this.saveFilterName = newText;
+ this.nameCollision = false;
+ this.store.getItem('eg.grid.filters.' + this.gridContext.persistKey).then( setting => {
+ if (setting) {
+ if (setting[newText]) {
+ this.nameCollision = true;
+ }
+ }
+ });
+ })
+ );
+
+ this.refreshEntries();
+
+ console.log('manage-filters-dialog this', this);
+ }
+
+ ngOnDestroy() {
+ this.subscriptions.forEach((subscription) => {
+ subscription.unsubscribe();
+ });
+ }
+
+ refreshEntries() {
+ this.filterSetEntries = [];
+ this.store.getItem('eg.grid.filters.' + this.gridContext.persistKey).then( setting => {
+ console.log('getItem, setting =',setting);
+ if (setting /* for testing only: && Object.keys( setting ).length > 0 */) {
+ Object.keys(setting).forEach( key => {
+ this.filterSetEntries.push({ id: key, label: key });
+ });
+ } else {
+ if (this.gridContext.migrateLegacyFilterSets) {
+ this.attemptLegacyFilterSetMigration();
+ }
+ }
+ if (this.namedFilterSetSelector && this.filterSetEntries.length > 0) {
+ this.namedFilterSetSelector.selected = this.filterSetEntries[0];
+ }
+ });
+ }
+
+ legacyFieldMap(legacy_field: string): string {
+ if (this.gridContext.idlClass === 'uvuv') {
+ if (legacy_field === 'url_id') { return 'url'; }
+ if (legacy_field === 'attempt_id') { return 'id'; }
+ if (legacy_field === 'res_time') { return 'res_time'; }
+ if (legacy_field === 'res_code') { return 'res_code'; }
+ if (legacy_field === 'res_text') { return 'res_text'; }
+ if (legacy_field === 'req_time') { return 'req_time'; }
+ return 'url.' + legacy_field;
+ } else {
+ if (legacy_field === 'url_id') { return 'id'; }
+ }
+
+ return legacy_field;
+ }
+
+ legacyOperatorValueMap(field_name: string, field_datatype: string, legacy_operator: string, legacy_value: any): any {
+ let operator = legacy_operator;
+ let value = legacy_value;
+ let filterOperator = legacy_operator;
+ let filterValue = legacy_value;
+ let filterInputDisabled = false;
+ let filterIncludeOrgAncestors = false;
+ let filterIncludeOrgDescendants = false;
+ let notSupported = false;
+ switch(legacy_operator) {
+ case '=': case '!=': case '>': case '<': case '>=': case '<=':
+ /* same */
+ break;
+ case 'in': case 'not in':
+ case 'between': case 'not between':
+ /* not supported, warn user */
+ operator = undefined;
+ value = undefined;
+ filterOperator = '=';
+ filterValue = undefined;
+ notSupported = true;
+ break;
+ case 'null':
+ operator = '=';
+ value = undefined;
+ filterOperator = '=';
+ filterValue = null;
+ break;
+ case 'not null':
+ operator = '!=';
+ value = undefined;
+ filterOperator = '!=';
+ filterValue = null;
+ break;
+ case 'like': case 'not like':
+ value = '%' + filterValue + '%';
+ /* not like needs special handling further below */
+ break;
+ }
+ if (notSupported) {
+ return undefined;
+ }
+
+ let filter = {}
+ let mappedFieldName = this.legacyFieldMap(field_name);
+ filter[mappedFieldName] = {};
+ if (operator === 'not like') {
+ filter[mappedFieldName]['-not'] = {};
+ filter[mappedFieldName]['-not'][mappedFieldName] = {};
+ filter[mappedFieldName]['-not'][mappedFieldName]['like'] = value;
+ } else {
+ filter[mappedFieldName][operator] = value;
+ }
+
+ let control = {
+ isFiltered: true,
+ filterValue: filterValue,
+ filterOperator: filterOperator,
+ filterInputDisabled: filterInputDisabled,
+ filterIncludeOrgAncestors: filterIncludeOrgAncestors,
+ filterIncludeOrgDescendants: filterIncludeOrgDescendants
+ }
+
+ return [ filter, control ];
+ }
+
+ attemptLegacyFilterSetMigration() {
+ // The legacy interface allows you to define multiple filters for the same column, which our current filters
+ // do not support (well, the dataSource.filters part can, but not the grid.context.filterControls). The legacy
+ // filters also have an unintuitive additive behavior if you do that. We should take the last filter and warn
+ // the user if this happens. None of the filters for date columns is working correctly in the legacy UI, so no
+ // need to map those. We also not able to support between, not between, in, and not in.
+ this.pcrud.search('cfdfs', {'interface':this.gridContext.migrateLegacyFilterSets},{},{'atomic':true}).subscribe(
+ (legacySets) => {
+ legacySets.forEach( s => {
+ let obj = {
+ 'filters' : {},
+ 'controls' : {}
+ };
+ console.log('migrating legacy set ' + s.name(), s );
+ JSON.parse( s.filters() ).forEach( f => {
+ let mappedFieldName = this.legacyFieldMap(f.field);
+ let c = this.gridContext.columnSet.getColByName( mappedFieldName );
+ if (c) {
+ let r = this.legacyOperatorValueMap(f.field, c.datatype, f.operator, f.value || f.values);
+ console.log(f.field, r);
+ obj['filters'][mappedFieldName] = [ r[0] ];
+ obj['controls'][mappedFieldName] = r[1];
+ } else {
+ console.log('with legacy set ' + s.name()
+ + ', column not found for ' + f.field + ' (' + this.legacyFieldMap( f.field) + ')');
+ }
+ });
+ if (Object.keys(obj.filters).length > 0) {
+ this.store.getItem('eg.grid.filters.' + this.gridContext.persistKey).then( setting => {
+ console.log('saveFilters, setting = ', setting);
+ setting ||= {};
+ setting[s.name()] = obj;
+ console.log('saving ' + s.name(), JSON.stringify(obj));
+ this.store.setItem('eg.grid.filters.' + this.gridContext.persistKey, setting).then( res => {
+ this.refreshEntries();
+ console.log('save toast here',res);
+ });
+ });
+ }
+ });
+ }
+ );
+ }
+}
<eg-grid-toolbar-actions-editor #toolbarActionsEditor [gridContext]="gridContext">
</eg-grid-toolbar-actions-editor>
+<eg-grid-manage-filters-dialog #gridManageFiltersDialog [gridContext]="gridContext">
+</eg-grid-manage-filters-dialog>
+
<div class="eg-grid-toolbar mb-2">
+
<div class="btn-toolbar">
<span class="fw-bold me-2" *ngIf="gridContext.toolbarLabel">
{{gridContext.toolbarLabel}}
<!-- buttons -->
<div class="btn-grp" *ngIf="gridContext.toolbarButtons.length || gridContext.isFilterable">
<!-- special case for remove filters button -->
- <button *ngIf="gridContext.isFilterable"
- class="btn btn-outline-dark me-1" (click)="gridContext.removeFilters()"
- [disabled]="!gridContext.filtersSet() || gridContext.dataSource.requestingData" i18n>
- Remove Filters
- </button>
+ <ng-container *ngIf="gridContext.isFilterable">
+ <button *ngIf="!gridContext.allowNamedFilterSets"
+ class="btn btn-outline-dark me-1" (click)="gridContext.removeFilters()"
+ [disabled]="!gridContext.filtersSet() || gridContext.dataSource.requestingData" i18n>
+ Remove Filters
+ </button>
+ <button *ngIf="gridContext.allowNamedFilterSets"
+ class="btn btn-outline-dark me-1"
+ (click)="gridManageFiltersDialog.open().subscribe()"
+ [disabled]="gridContext.dataSource.requestingData" i18n>
+ Manage Filters
+ </button>
+ </ng-container>
<ng-container *ngFor="let btn of gridContext.toolbarButtons">
<label
*ngIf="btn.adjacentPreceedingLabel"
// should be displayed
@Input() filterable: boolean;
+ // allowNamedFilterSets: true if the result filtering
+ // controls can be saved or loaded via a name
+ @Input() allowNamedFilterSets: boolean;
+
+ // migrateLegacyFilterSets: if set to a legacy filter interface type
+ // (i.e. url_verify), attempt to migrate any legacy filter sets for
+ // that interface.
+ @Input() migrateLegacyFilterSets: string;
+
// sticky grid header
//
// stickyHeader: true of the grid header should be
this.context.autoGeneratedColumnOrder = this.autoGeneratedColumnOrder;
this.context.isSortable = this.sortable === true;
this.context.isFilterable = this.filterable === true;
+ this.context.allowNamedFilterSets = this.allowNamedFilterSets === true;
+ this.context.migrateLegacyFilterSets = this.migrateLegacyFilterSets;
this.context.stickyGridHeader = this.stickyHeader === true;
this.context.isMultiSortable = this.multiSortable === true;
this.context.useLocalSort = this.useLocalSort === true;
import {GridFilterControlComponent} from './grid-filter-control.component';
import {GridToolbarActionsEditorComponent} from './grid-toolbar-actions-editor.component';
import {GridFlatDataService} from './grid-flat-data.service';
+import {GridManageFiltersDialogComponent} from './grid-manage-filters-dialog.component';
@NgModule({
GridColumnWidthComponent,
GridPrintComponent,
GridFilterControlComponent,
- GridToolbarActionsEditorComponent
+ GridToolbarActionsEditorComponent,
+ GridManageFiltersDialogComponent
],
imports: [
EgCommonModule,
/**
* Collection of grid related classses and interfaces.
*/
-import {TemplateRef, EventEmitter, QueryList} from '@angular/core';
+import {TemplateRef, EventEmitter, QueryList, ViewChild} from '@angular/core';
import {Observable, Subscription, empty} from 'rxjs';
import {IdlService, IdlObject} from '@eg/core/idl.service';
import {OrgService} from '@eg/core/org.service';
this.filterIncludeOrgDescendants = false;
}
+ loadFilter(f) {
+ this.isFiltered = f.isFiltered;
+ this.filterValue = f.filterValue;
+ this.filterOperator = f.filterOperator;
+ this.filterInputDisabled = f.filterInputDisabled;
+ this.filterIncludeOrgAncestors = f.filterIncludeOrgAncestors;
+ this.filterIncludeOrgDescendants = f.IncludeOrgDescendants;
+ }
+
+ getIdlId(value: any) {
+ const obj: IdlObject = (value as unknown) as IdlObject;
+ return obj.id();
+ }
+
+ getFilter() {
+ return {
+ 'isFiltered': this.isFiltered,
+ 'filterValue': typeof this.filterValue === 'object' ? this.getIdlId(this.filterValue) : this.filterValue,
+ 'filterOperator': this.filterOperator,
+ 'filterInputDisabled': this.filterInputDisabled,
+ 'filterIncludeOrgAncestors': this.filterIncludeOrgAncestors,
+ 'filterIncludeOrgDescendants': this.filterIncludeOrgDescendants
+ }
+ }
+
clone(): GridColumn {
const col = new GridColumn();
idlClass: string;
isSortable: boolean;
isFilterable: boolean;
+ allowNamedFilterSets: boolean;
+ migrateLegacyFilterSets: string;
stickyGridHeader: boolean;
isMultiSortable: boolean;
useLocalSort: boolean;
this.filterControls.forEach(ctl => ctl.reset());
this.reload();
}
+ saveFilters(asName: string): void {
+ const obj = {
+ 'filters' : this.dataSource.filters, // filters isn't 100% reversible to column filter values, so...
+ 'controls' : Object.fromEntries(new Map( this.columnSet.columns.map( c => [c.name, c.getFilter()] ) ))
+ }
+ this.store.getItem('eg.grid.filters.' + this.persistKey).then( setting => {
+ console.log('saveFilters, setting = ', setting);
+ setting ||= {};
+ setting[asName] = obj;
+ console.log('saving ' + asName, JSON.stringify(obj));
+ this.store.setItem('eg.grid.filters.' + this.persistKey, setting).then( res => {
+ console.log('save toast here',res);
+ });
+ });
+ }
+ deleteFilters(withName: string): void {
+ this.store.getItem('eg.grid.filters.' + this.persistKey).then( setting => {
+ if (setting) {
+ setting[withName] = undefined;
+ if (setting[withName]) {
+ delete setting[withName]; /* not releasing right away */
+ } else {
+ console.warn('Could not find ' + withName + ' in eg.grid.filters.' + this.persistKey,setting);
+ }
+ this.store.setItem('eg.grid.filters.' + this.persistKey, setting).then( res => {
+ console.log('delete toast here',res);
+ });
+ } else {
+ console.warn('Could not find setting eg.grid.filters.' + this.persistKey, setting);
+ }
+ });
+ }
+ loadFilters(fromName: string): void {
+ console.log('fromName',fromName);
+ this.store.getItem('eg.grid.filters.' + this.persistKey).then( setting => {
+ if (setting) {
+ const obj = setting[fromName];
+ if (obj) {
+ this.dataSource.filters = obj.filters;
+ Object.keys(obj.controls).forEach( col_name => {
+ let col = this.columnSet.columns.find(c => c.name === col_name);
+ if (col) {
+ col.loadFilter( obj.controls[col_name] );
+ }
+ });
+ this.reload();
+ } else {
+ console.warn('Could not find ' + fromName + ' in eg.grid.filters.' + this.persistKey, obj);
+ }
+ } else {
+ console.warn('Could not find setting eg.grid.filters.' + this.persistKey, setting);
+ }
+ });
+ }
filtersSet(): boolean {
return Object.keys(this.dataSource.filters).length > 0;
}
"node_modules/@types"
],
"lib": [
- "es2018",
+ "es2019",
"dom"
]
}