From a0aaffbe54df3a1a30162016239068b6e2fb234d Mon Sep 17 00:00:00 2001 From: Jason Boyer Date: Tue, 14 Dec 2021 14:14:35 -0500 Subject: [PATCH] Simple Reporter Angular App Simply put, it reports. Sponsored-by: C/W MARS Sponsored-by: Missouri Evergreen Consortium Signed-off-by: Jason Boyer Signed-off-by: rfrasur Signed-off-by: Mike Rylander --- Open-ILS/src/eg2/package-lock.json | 15 + Open-ILS/src/eg2/package.json | 3 +- .../eg2/src/app/staff/reporter/routing.module.ts | 18 + .../app/staff/reporter/simple/routing.module.ts | 40 + .../reporter/simple/simple-reporter.component.html | 18 + .../reporter/simple/simple-reporter.component.ts | 34 + .../reporter/simple/simple-reporter.module.ts | 34 + .../reporter/simple/simple-reporter.service.ts | 914 +++++++++++++++++++++ .../staff/reporter/simple/sr-editor.component.css | 7 + .../staff/reporter/simple/sr-editor.component.html | 102 +++ .../staff/reporter/simple/sr-editor.component.ts | 239 ++++++ .../reporter/simple/sr-field-chooser.component.css | 9 + .../simple/sr-field-chooser.component.html | 108 +++ .../reporter/simple/sr-field-chooser.component.ts | 97 +++ .../staff/reporter/simple/sr-field.component.css | 29 + .../staff/reporter/simple/sr-field.component.html | 210 +++++ .../staff/reporter/simple/sr-field.component.ts | 187 +++++ .../reporter/simple/sr-my-outputs.component.html | 72 ++ .../reporter/simple/sr-my-outputs.component.ts | 90 ++ .../reporter/simple/sr-my-reports.component.html | 66 ++ .../reporter/simple/sr-my-reports.component.ts | 165 ++++ .../simple/sr-output-options.component.html | 65 ++ .../reporter/simple/sr-output-options.component.ts | 34 + .../reporter/simple/sr-sort-order.component.css | 5 + .../reporter/simple/sr-sort-order.component.html | 59 ++ .../reporter/simple/sr-sort-order.component.ts | 83 ++ 26 files changed, 2702 insertions(+), 1 deletion(-) create mode 100644 Open-ILS/src/eg2/src/app/staff/reporter/routing.module.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/reporter/simple/routing.module.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/reporter/simple/simple-reporter.component.html create mode 100644 Open-ILS/src/eg2/src/app/staff/reporter/simple/simple-reporter.component.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/reporter/simple/simple-reporter.module.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/reporter/simple/simple-reporter.service.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-editor.component.css create mode 100644 Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-editor.component.html create mode 100644 Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-editor.component.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-field-chooser.component.css create mode 100644 Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-field-chooser.component.html create mode 100644 Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-field-chooser.component.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-field.component.css create mode 100644 Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-field.component.html create mode 100644 Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-field.component.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-my-outputs.component.html create mode 100644 Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-my-outputs.component.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-my-reports.component.html create mode 100644 Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-my-reports.component.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-output-options.component.html create mode 100644 Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-output-options.component.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-sort-order.component.css create mode 100644 Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-sort-order.component.html create mode 100644 Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-sort-order.component.ts diff --git a/Open-ILS/src/eg2/package-lock.json b/Open-ILS/src/eg2/package-lock.json index 21e6ae4116..1c4fb64e6d 100644 --- a/Open-ILS/src/eg2/package-lock.json +++ b/Open-ILS/src/eg2/package-lock.json @@ -11839,6 +11839,21 @@ "postcss-selector-parser": "^6.0.4" } }, + "ts-md5": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/ts-md5/-/ts-md5-1.2.9.tgz", + "integrity": "sha512-/Efr7ZfGf8P+d9HXh0PLQD1CDipqD8j9apCFG96pODDoEaFLxXpV4En6tAc6y3fWyfhFGrqtNBRBS+eLVIB2uQ==" + }, + "ts-node": { + "version": "8.10.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.10.2.tgz", + "integrity": "sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA==", + "dev": true, + "requires": { + "browserslist": "^4.16.0", + "postcss-selector-parser": "^6.0.4" + } + }, "stylus": { "version": "0.54.8", "resolved": "https://registry.npmjs.org/stylus/-/stylus-0.54.8.tgz", diff --git a/Open-ILS/src/eg2/package.json b/Open-ILS/src/eg2/package.json index a50141ff2f..eac8ef38d2 100644 --- a/Open-ILS/src/eg2/package.json +++ b/Open-ILS/src/eg2/package.json @@ -38,7 +38,8 @@ "moment-timezone": "^0.5.33", "ngx-cookie": "^5.0.2", "rxjs": "^6.6.2", - "zone.js": "^0.11.4" + "zone.js": "^0.11.4", + "ts-md5": "^1.2.9" }, "//": "NOTE: version of angular/cli should be kept in sync with Open-ILS/src/extras/install/Makefile.common", "devDependencies": { diff --git a/Open-ILS/src/eg2/src/app/staff/reporter/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/reporter/routing.module.ts new file mode 100644 index 0000000000..90426cdb7b --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/reporter/routing.module.ts @@ -0,0 +1,18 @@ +import {NgModule} from '@angular/core'; +import {RouterModule, Routes} from '@angular/router'; + +const routes: Routes = [ + { path: 'simple', + loadChildren: () => + import('./simple/simple-reporter.module').then(m => m.SimpleReporterModule) + } +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) + +export class ReporterRoutingModule { +} + diff --git a/Open-ILS/src/eg2/src/app/staff/reporter/simple/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/reporter/simple/routing.module.ts new file mode 100644 index 0000000000..83fecd39e0 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/reporter/simple/routing.module.ts @@ -0,0 +1,40 @@ +import {NgModule} from '@angular/core'; +import {RouterModule, Routes, RouterStateSnapshot, ActivatedRouteSnapshot} from '@angular/router'; +import {SimpleReporterComponent} from './simple-reporter.component'; +import {SREditorComponent} from './sr-editor.component'; +import {SimpleReporterServiceResolver} from './simple-reporter.service'; + +const routes: Routes = [ + { path: '', + component: SimpleReporterComponent, + resolve: { srSvcResolver: SimpleReporterServiceResolver }, + }, + { path: 'new', + component: SREditorComponent, + resolve: { srSvcResolver: SimpleReporterServiceResolver }, + canDeactivate: ['canLeaveEditor'], + runGuardsAndResolvers: 'always' + }, + { path: 'edit/:id', + component: SREditorComponent, + resolve: { srSvcResolver: SimpleReporterServiceResolver }, + canDeactivate: ['canLeaveEditor'], + runGuardsAndResolvers: 'always' + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], + providers: [SimpleReporterServiceResolver, + { + provide: 'canLeaveEditor', + useValue: (component: SREditorComponent, currentRoute: ActivatedRouteSnapshot, + currentState: RouterStateSnapshot, nextState: RouterStateSnapshot) => component.canLeaveEditor() + } + ] +}) + +export class SimpleReporterRoutingModule { +} + diff --git a/Open-ILS/src/eg2/src/app/staff/reporter/simple/simple-reporter.component.html b/Open-ILS/src/eg2/src/app/staff/reporter/simple/simple-reporter.component.html new file mode 100644 index 0000000000..4723a14ee6 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/reporter/simple/simple-reporter.component.html @@ -0,0 +1,18 @@ + + + +
+
+ +
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/reporter/simple/simple-reporter.component.ts b/Open-ILS/src/eg2/src/app/staff/reporter/simple/simple-reporter.component.ts new file mode 100644 index 0000000000..dcbc038b6c --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/reporter/simple/simple-reporter.component.ts @@ -0,0 +1,34 @@ +import {Component, OnInit, ViewChild} from '@angular/core'; +import {Router} from '@angular/router'; +import {NgbNav} from '@ng-bootstrap/ng-bootstrap'; +import {StaffCommonModule} from '@eg/staff/common.module'; +import {IdlService, IdlObject} from '@eg/core/idl.service'; +import {AuthService} from '@eg/core/auth.service'; +import {PcrudService} from '@eg/core/pcrud.service'; +import {SimpleReporterService} from './simple-reporter.service'; +import {SROutputsComponent} from './sr-my-outputs.component'; + +@Component({ + templateUrl: './simple-reporter.component.html', +}) + +export class SimpleReporterComponent implements OnInit { + + @ViewChild('simpleRptTabs', { static: true }) tabs: NgbNav; + + constructor( + private router: Router, + private auth: AuthService, + private idl: IdlService, + private pcrud: PcrudService, + private srSvc: SimpleReporterService + ) { + + } + + + ngOnInit() { + + } + +} diff --git a/Open-ILS/src/eg2/src/app/staff/reporter/simple/simple-reporter.module.ts b/Open-ILS/src/eg2/src/app/staff/reporter/simple/simple-reporter.module.ts new file mode 100644 index 0000000000..c7f56e2b38 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/reporter/simple/simple-reporter.module.ts @@ -0,0 +1,34 @@ +import {NgModule} from '@angular/core'; +import {StaffCommonModule} from '@eg/staff/common.module'; +import {OrgFamilySelectModule} from '@eg/share/org-family-select/org-family-select.module'; +import {SimpleReporterComponent} from './simple-reporter.component'; +import {SROutputsComponent} from './sr-my-outputs.component'; +import {SRReportsComponent} from './sr-my-reports.component'; +import {SREditorComponent} from './sr-editor.component'; +import {SRFieldChooserComponent} from './sr-field-chooser.component'; +import {SRSortOrderComponent} from './sr-sort-order.component'; +import {SROutputOptionsComponent} from './sr-output-options.component'; +import {SRFieldComponent} from './sr-field.component'; +import {SimpleReporterRoutingModule} from './routing.module'; + +@NgModule({ + declarations: [ + SimpleReporterComponent, + SROutputsComponent, + SRReportsComponent, + SRFieldChooserComponent, + SRSortOrderComponent, + SROutputOptionsComponent, + SRFieldComponent, + SREditorComponent + ], + imports: [ + StaffCommonModule, + SimpleReporterRoutingModule, + OrgFamilySelectModule + ] +}) + +export class SimpleReporterModule { +} + diff --git a/Open-ILS/src/eg2/src/app/staff/reporter/simple/simple-reporter.service.ts b/Open-ILS/src/eg2/src/app/staff/reporter/simple/simple-reporter.service.ts new file mode 100644 index 0000000000..1755cb5c5e --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/reporter/simple/simple-reporter.service.ts @@ -0,0 +1,914 @@ +import {Injectable} from '@angular/core'; +import {Router, Resolve, RouterStateSnapshot, + ActivatedRouteSnapshot} from '@angular/router'; +import * as moment from 'moment-timezone'; +import {Md5} from 'ts-md5'; +import {EMPTY} from 'rxjs'; +import {map, mergeMap, defaultIfEmpty, last} from 'rxjs/operators'; +import {Observable, of, from} from 'rxjs'; +import {AuthService} from '@eg/core/auth.service'; +import {PermService} from '@eg/core/perm.service'; +import {GridDataSource} from '@eg/share/grid/grid'; +import {Pager} from '@eg/share/util/pager'; +import {PcrudService} from '@eg/core/pcrud.service'; +import {IdlService, IdlObject} from '@eg/core/idl.service'; +import {EventService} from '@eg/core/event.service'; + +const defaultFolderName = 'Simple Reporter'; + +const transforms = [ + { + name: 'Bare', + aggregate: false + }, + { + name: 'upper', + aggregate: false, + datatypes: ['text'] + }, + { + name: 'lower', + aggregate: false, + datatypes: ['text'] + }, + { + name: 'substring', + aggregate: false, + datatypes: ['text'] + }, + { + name: 'day_name', + final_datatype: 'text', + aggregate: false, + datatypes: ['timestamp'] + }, + { + name: 'month_name', + final_datatype: 'text', + aggregate: false, + datatypes: ['timestamp'] + }, + { + name: 'doy', + aggregate: false, + final_datatype: 'number', + datatypes: ['timestamp'] + }, + { + name: 'woy', + aggregate: false, + final_datatype: 'number', + datatypes: ['timestamp'] + }, + { + name: 'moy', + aggregate: false, + final_datatype: 'number', + datatypes: ['timestamp'] + }, + { + name: 'qoy', + aggregate: false, + final_datatype: 'number', + datatypes: ['timestamp'] + }, + { + name: 'dom', + aggregate: false, + final_datatype: 'number', + datatypes: ['timestamp'] + }, + { + name: 'dow', + aggregate: false, + final_datatype: 'number', + datatypes: ['timestamp'] + }, + { + name: 'year_trunc', + aggregate: false, + final_datatype: 'number', + datatypes: ['timestamp'] + }, + { + name: 'month_trunc', + aggregate: false, + final_datatype: 'text', + datatypes: ['timestamp'] + }, + { + name: 'date_trunc', + aggregate: false, + datatypes: ['timestamp'] + }, + { + name: 'hour_trunc', + aggregate: false, + datatypes: ['timestamp'] + }, + { + name: 'quarter', + aggregate: false, + datatypes: ['timestamp'] + }, + { + name: 'months_ago', + aggregate: false, + final_datatype: 'number', + datatypes: ['timestamp'] + }, + { + name: 'hod', + aggregate: false, + final_datatype: 'number', + datatypes: ['timestamp'] + }, + { + name: 'quarters_ago', + aggregate: false, + final_datatype: 'number', + datatypes: ['timestamp'] + }, + { + name: 'age', + aggregate: false, + final_datatype: 'interval', + datatypes: ['timestamp'] + }, + { + name: 'first', + aggregate: true + }, + { + name: 'last', + aggregate: true + }, + { + name: 'min', + aggregate: true + }, + { + name: 'max', + aggregate: true + }, + // "Simple" would be to only offer the choice that's almost always what you mean. + /*{ + name: 'count', + aggregate: true + },*/ + { + name: 'count_distinct', + final_datatype: 'number', + aggregate: true + }, + { + name: 'sum', + aggregate: true, + datatypes: ['float', 'int', 'money', 'number'] + }, + { + name: 'average', + aggregate: true, + datatypes: ['float', 'int', 'money', 'number'] + } +]; + +const operators = [ + { + name: '=', + datatypes: ['link', 'text', 'timestamp', 'interval', 'float', 'int', 'money', 'number'], + arity: 1 + }, + { + arity: 1, + datatypes: ['bool', 'org_unit'], + name: '= any' + }, + { + arity: 1, + datatypes: ['bool', 'org_unit'], + name: '<> any' + }, + // If I had a dollar for every time someone wanted a case sensitive substring search, I might be able to buy a coffee. + /*{ + name: 'like', + arity: 1, + datatypes: ['text'] + },*/ + { + name: 'ilike', + arity: 1, + datatypes: ['text'] + }, + { + name: '>', + arity: 1, + datatypes: ['text', 'timestamp', 'interval', 'float', 'int', 'money', 'number'] + }, + { + name: '>=', + arity: 1, + datatypes: ['text', 'timestamp', 'interval', 'float', 'int', 'money', 'number'] + }, + { + name: '<', + arity: 1, + datatypes: ['text', 'timestamp', 'interval', 'float', 'int', 'money', 'number'] + + }, + { + name: '<=', + arity: 1, + datatypes: ['text', 'timestamp', 'interval', 'float', 'int', 'money', 'number'] + + }, + { + name: 'in', + arity: 3, + datatypes: ['text', 'link', 'org_unit', 'float', 'int', 'money', 'number'] + }, + { + name: 'not in', + arity: 3, + datatypes: ['text', 'link', 'org_unit', 'float', 'int', 'money', 'number'] + + }, + { + name: 'between', + arity: 2, + datatypes: ['text', 'timestamp', 'interval', 'float', 'int', 'money', 'number'] + + }, + { + name: 'not between', + arity: 2, + datatypes: ['text', 'timestamp', 'interval', 'float', 'int', 'money', 'number'] + + }, + { + arity: 0, + name: 'is' + }, + { + arity: 0, + name: 'is not' + }, + { + arity: 0, + name: 'is blank', + datatypes: ['text'] + }, + { + arity: 0, + name: 'is not blank', + datatypes: ['text'] + } +]; + +const DEFAULT_TRANSFORM = 'Bare'; +const DEFAULT_OPERATOR = '='; + +export class SRTemplate { + id = -1; + rtIdl: IdlObject = null; + name = ''; + description = ''; // description isn't currently used but someday could be + create_time = null; + fmClass = ''; + displayFields: IdlObject[] = []; + orderByNames: string[] = []; + filterFields: IdlObject[] = []; + isNew = true; + recurring = false; + recurrence = null; + excelOutput = false; + csvOutput = true; + htmlOutput = true; + barCharts = false; + lineCharts = false; + email = ''; + runNow = 'now'; + runTime: moment.Moment = null; + + constructor(idlObj: IdlObject = null) { + if ( idlObj !== null ) { + this.isNew = false; + this.id = Number(idlObj.id()); + this.create_time = idlObj.create_time(); + this.name = idlObj.name(); + this.description = idlObj.description(); + + const simple_report = JSON.parse(idlObj.data()).simple_report; + this.fmClass = simple_report.fmClass; + this.displayFields = simple_report.displayFields; + this.orderByNames = simple_report.orderByNames; + this.filterFields = simple_report.filterFields; + if (idlObj.reports()?.length) { + const activeReport = idlObj.reports().reduce((prev, curr) => + prev.create_time() > curr.create_time() ? prev : curr + ); + if (activeReport) { + this.recurring = activeReport.recur() === 't'; + this.recurrence = activeReport.recurrence(); + } + // then fetch the most recent completed rs + if (activeReport.runs().length) { + const latestSched = activeReport.runs().reduce((prev, curr) => + prev.run_time() > curr.run_time() ? prev : curr + ); + if (latestSched) { + this.excelOutput = latestSched.excel_format() === 't'; + this.csvOutput = latestSched.csv_format() === 't'; + this.htmlOutput = latestSched.html_format() === 't'; + this.barCharts = latestSched.chart_bar() === 't'; + this.lineCharts = latestSched.chart_line() === 't'; + this.email = latestSched.email(); + this.runTime = latestSched.run_time().length ? moment(latestSched.run_time()) : moment(); + this.runNow = this.runTime.isAfter(moment()) ? 'later' : 'now'; + } + } + } + } + } +} + + +@Injectable({ + providedIn: 'root' +}) +export class SimpleReporterService { + + templateFolder: IdlObject = null; + reportFolder: IdlObject = null; + outputFolder: IdlObject = null; + + constructor ( + private evt: EventService, + private auth: AuthService, + private idl: IdlService, + private pcrud: PcrudService + ) { + } + + _initFolders(): Promise { + if (this.templateFolder && + this.reportFolder && + this.outputFolder + ) { + return Promise.resolve([]); + } + + return Promise.all([ + new Promise((resolve, reject) => { + // Verify folders exist, create if not + this.getDefaultFolder('rtf') + .then(f => { + if (f) { + this.templateFolder = f; + resolve(); + } else { + this.createDefaultFolder('rtf') + .then(n => { + this.templateFolder = n; + resolve(); + }); + } + }); + }), + new Promise((resolve, reject) => { + this.getDefaultFolder('rrf') + .then(f => { + if (f) { + this.reportFolder = f; + resolve(); + } else { + this.createDefaultFolder('rrf') + .then(n => { + this.reportFolder = n; + resolve(); + }); + } + }); + }), + new Promise((resolve, reject) => { + this.getDefaultFolder('rof') + .then(f => { + if (f) { + resolve(); + this.outputFolder = f; + } else { + this.createDefaultFolder('rof') + .then(n => { + this.outputFolder = n; + resolve(); + }); + } + }); + }) + ]); + } + + getTransformsForDatatype(datatype: string) { + const ret = []; + transforms.forEach(el => { + if ( typeof el.datatypes === 'undefined' || + (el.datatypes.findIndex(dt => dt === datatype) > -1) ) { + ret.push(el); + } + }); + return ret; + } + + getOperatorsForDatatype(datatype: string) { + const ret = []; + operators.forEach(el => { + if ( typeof el.datatypes === 'undefined' || + (el.datatypes.findIndex(dt => dt === datatype) > -1) ) { + ret.push(el); + } + }); + return ret; + } + + defaultTransform() { + return this.getTransformByName(DEFAULT_TRANSFORM); + } + + defaultOperator(dt) { + if (this.getOperatorByName(DEFAULT_OPERATOR).datatypes.indexOf(dt) >= 0) { + return this.getOperatorByName(DEFAULT_OPERATOR); + } + return this.getOperatorsForDatatype(dt)[0]; + } + + getTransformByName(name: string) { + return { ...transforms[transforms.findIndex(el => el.name === name)] }; + } + + getGenericTransformWithParams(ary: Array) { + const copy = Array.from(ary); + const name = copy.shift(); + const params = []; + const transform = { name: name, aggregate: false }; + + copy.forEach(el => { + switch (el) { // for now there's only user id for the current user, but in future ... ?? + case 'SR__USER_ID': + params.push(this.auth.user().id()); + break; + default: + params.push(el); + } + }); + + if ( params.length ) { + transform['params'] = params; + } + + return transform; + } + + getOperatorByName(name: string) { + return { ...operators[operators.findIndex(el => el.name === name)] }; + } + + getDefaultFolder(fmClass: string): Promise { + return this.pcrud.search(fmClass, + { owner: this.auth.user().id(), 'simple_reporter': 't', name: defaultFolderName }, + {}).toPromise(); + } + + createDefaultFolder(fmClass: string): Promise { + const rf = this.idl.create(fmClass); + rf.isnew(true); + rf.owner(this.auth.user().id()); + rf.simple_reporter(true); + rf.name(defaultFolderName); + return this.pcrud.create(rf).toPromise(); + } + + loadTemplate(id: number): Promise { + const searchOps = { + flesh: 2, + flesh_fields: { + rt: ['reports'], + rr: ['runs'] + } + }; + return this.pcrud.search('rt', { id: id }, searchOps).toPromise(); + } + + saveTemplate( + templ: SRTemplate, + scheduleNow: boolean = false + ): Promise { // IdlObject or Number? It depends! + const rtData = this.buildTemplateData(templ); + + // gather our parameters + const rrData = {}; + templ.filterFields.forEach((el, idx) => { + rrData[el.filter_placeholder] = el.force_filtervalues ? el.force_filtervalues : el.filter_value; + }); + + // Here's where we'd add rr-flags like __do_rollup to rrData + + const rtIdl = this.idl.create('rt'); + const rrIdl = this.idl.create('rr'); + + if ( templ.id === -1 ) { + rtIdl.isnew(true); + rrIdl.isnew(true); + } else { + rtIdl.isnew(false); + rrIdl.isnew(false); + rtIdl.id(templ.id); + rtIdl.create_time(templ.create_time); + } + rtIdl.name(templ.name); + rtIdl.description(templ.description); + rtIdl.data(JSON.stringify(rtData)); + rtIdl.owner(this.auth.user().id()); + rtIdl.folder(this.templateFolder.id()); + + rrIdl.name(templ.name); + rrIdl.data(JSON.stringify(rrData)); + rrIdl.owner(this.auth.user().id()); + rrIdl.folder(this.reportFolder.id()); + rrIdl.template(templ.id); + rrIdl.create_time('now'); // rr create time is serving as the edit time + // of the SR template as a whole + + rrIdl.recur(templ.recurring ? 't' : 'f'); + rrIdl.recurrence(templ.recurrence); + + return this.pcrud.search('rt', { name: rtIdl.name(), folder: rtIdl.folder() }) + .pipe(defaultIfEmpty(rtIdl), map(existing => { + if (existing.id() !== rtIdl.id()) { // oh no! dup name + throw new Error(': Duplicate Report Name'); + } + + if ( templ.id === -1 ) { + return this.pcrud.create(rtIdl).pipe(mergeMap(rt => { + rrIdl.template(rt.id()); + // after saving the rr, return an Observable of the rt + // to the caller + return this.pcrud.create(rrIdl).pipe(mergeMap( + rr => this.scheduleReport(templ, rr, scheduleNow).pipe(mergeMap(rs => of(rt))) + )); + })).toPromise(); + } else { + const emptyRR = this.idl.create('rr'); + emptyRR.id('no_rr'); + return this.pcrud.update(rtIdl).pipe(mergeMap(rtId => { + // we may or may not have the rr already created, so + // test and act accordingly + return this.pcrud.search('rr', { template: rtId }).pipe(defaultIfEmpty(emptyRR), mergeMap(rr => { + if (rr.id() === 'no_rr') { + rrIdl.isnew(true); + return this.pcrud.create(rrIdl).pipe(mergeMap(rr2 => + this.scheduleReport(templ, rr2, scheduleNow).pipe(mergeMap(rs => of(rtId))) + )); + } else { + rr.create_time('now'); // rr create time is serving as the + // edit time of the SR template as a whole + rr.recur(templ.recurring ? 't' : 'f'); + rr.recurrence(templ.recurrence); + rr.data(rrIdl.data()); + return this.pcrud.update(rr).pipe(mergeMap( + rr2 => this.scheduleReport(templ, rr, scheduleNow).pipe(mergeMap(rs => of(rtId) )) + )); + } + })); + })).toPromise(); + } + })).toPromise(); + } + + scheduleReport(templ: SRTemplate, rr: IdlObject, scheduleNow: boolean): Observable { + const rs = this.idl.create('rs'); + if (!scheduleNow) { + return of(rs); // return a placeholder + } + rs.isnew(true); + rs.report(rr.id()); + rs.folder(this.outputFolder.id()); + rs.runner(rr.owner()); + if (templ.runNow === 'now') { + rs.run_time('now'); + } else { + rs.run_time(templ.runTime.toISOString()); + } + rs.email(templ.email); + rs.excel_format(templ.excelOutput ? 't' : 'f'); + rs.csv_format(templ.csvOutput ? 't' : 'f'); + rs.html_format(templ.htmlOutput ? 't' : 'f'); + rs.chart_line(templ.lineCharts ? 't' : 'f'); + rs.chart_bar(templ.barCharts ? 't' : 'f'); + rs.isnew(true); + + // clear any un-run schedules, then add the new one + const emptyRS = this.idl.create('rs'); + emptyRS.id('no_rs'); + return this.pcrud.search('rs', { report: rr.id(), start_time: {'=' : null} }, {}, {atomic: true}).pipe(mergeMap(old_rs => { + if (old_rs.length > 0) { + old_rs.forEach(x => x.isdeleted(true)); + old_rs.push(rs); + return this.pcrud.autoApply(old_rs).pipe(last()); // note that we don't care + // what the last one processed + // actually is + } else { + return this.pcrud.create(rs); + } + })); + } + + // The template generated by this can obviously be trimmed to only those things + // that SQLBuilder.pm cares about, but for now it's basically the same as the + // existing template builder. + buildTemplateData( + templ: SRTemplate + ) { + const fmClass = templ.fmClass; + const sourceClass = this.idl.classes[fmClass]; + const md5Name = Md5.hashStr(fmClass); // Just the one with SR since there are no joins + let conditionCount = 0; + + // The simplified template that can be edited and re-saved + const simpleReport = { + name: templ.name, + fmClass: fmClass, + displayFields: templ.displayFields, + orderByNames: templ.orderByNames, + filterFields: templ.filterFields, + }; + const reportTemplate = { + simple_report: simpleReport, + version: 5, + core_class: fmClass, + 'from': { + alias: md5Name, + path: fmClass + '-' + fmClass, + table: sourceClass.source, + idlclass: fmClass, + label: sourceClass.label + }, + select: [], + where: [], + having: [], + order_by: [] + }; + // fill in select[] and display_cols[] simultaneously + templ.displayFields.forEach((el, idx) => { + reportTemplate.select.push({ + alias: el.alias, + path: fmClass + '-' + el.name, + relation: md5Name, + column: { + colname: el.name, + transform: el.transform.name, + aggregate: el.transform.aggregate + } + }); + + }); // select[] + + // where[] and having[] are the same save for aggregate == true + templ.filterFields.forEach((el, idx) => { + let whereObj = {}; + + whereObj = { + alias: el.alias, + path: fmClass + '-' + el.name, + relation: md5Name, + column: { + colname: el.name, + transform: el.transform.name, + aggregate: el.transform.aggregate + }, + condition: {} + }; + + // No test for el.filterValue because currently all filter values are assigned at schedule time + whereObj['condition'][el.operator.name] = '::P' + conditionCount; + el.filter_placeholder = 'P' + conditionCount; + conditionCount++; + + // handle force transforms + if (el.force_transform) { + whereObj['column']['params'] = el.transform.params; + } + + if ( el.transform.aggregate ) { + reportTemplate.having.push(whereObj); + } else { + reportTemplate.where.push(whereObj); + } + + }); // where[] and having[] + + templ.orderByNames.forEach(ob => { // order_by and select have the same shape + const el = templ.displayFields[templ.displayFields.findIndex(fl => fl.name === ob)]; + reportTemplate.order_by.push({ + alias: el.alias, + path: fmClass + '-' + el.name, + relation: md5Name, + direction: el.direction ? el.direction : 'ascending', + column: { + colname: el.name, + transform: el.transform.name, + aggregate: el.transform.aggregate + } + }); + }); + + return reportTemplate; + } + + getOutputDatasource() { + const gridSource = new GridDataSource(); + + gridSource.sort = [{ name: 'complete_time', dir: 'DESC' }]; + + gridSource.getRows = (pager: Pager, sort: any[]) => { + + // start setting up query + const base: Object = {}; + base['runner'] = this.auth.user().id(); + base['output_folder'] = this.outputFolder.id(); + const query: any = new Array(); + query.push(base); + + // and add any filters + Object.keys(gridSource.filters).forEach(key => { + Object.keys(gridSource.filters[key]).forEach(key2 => { + query.push(gridSource.filters[key][key2]); + }); + }); + + const orderBy: any = {}; + if (sort.length) { + orderBy.rcr = sort[0].name + ' ' + sort[0].dir; + } + + const searchOpts = { + flesh: 2, + flesh_fields: { + rcr: ['run'], + }, + offset: pager.offset, + limit: pager.limit, + order_by: orderBy + }; + + return this.pcrud.search('rcr', query, searchOpts) + .pipe(map(row => { + if ( this.evt.parse(row) ) { + throw new Error(row); + } else { + return { + template_name: row.template_name(), + complete_time: row.complete_time(), + id: row.run().id(), + report_id: row.report(), + template_id: row.template(), + error_code: row.run().error_code(), + error_text: row.run().error_text(), + _rs: row.run() + }; + } + })); + + }; + + return gridSource; + } + + getReportsDatasource() { + const gridSource = new GridDataSource(); + + gridSource.getRows = (pager: Pager, sort: any[]) => { + const orderBy: any = {}; + + if (sort.length) { + orderBy.rt = sort[0].name + ' ' + sort[0].dir; + } else { + orderBy.rt = 'create_time desc'; + } + + // start setting up query + const base: Object = {}; + base['owner'] = this.auth.user().id(); + base['folder'] = this.templateFolder.id(); + + const query: any = new Array(); + query.push(base); + + // and add any filters + Object.keys(gridSource.filters).forEach(key => { + Object.keys(gridSource.filters[key]).forEach(key2 => { + query.push(gridSource.filters[key][key2]); + }); + }); + + const searchOps = { + flesh: 2, + flesh_fields: { + rt: ['reports'], + rr: ['runs'] + }, + offset: pager.offset, + limit: pager.limit, + order_by: orderBy + }; + + return this.pcrud.search('rt', query, searchOps).pipe(map(row => { + let edit_time = null; + let last_run = null; + let past = []; + let future = []; + let next_run = null; + let recurring = false; + + // there should be exactly one rr associated with the template, + // but in case not, we'll just pick the one with the most + // recent create time + if (row.reports().length) { + const activeReport = row.reports().reduce((prev, curr) => + prev.create_time() > curr.create_time() ? prev : curr + ); + if (activeReport) { + // note that we're (ab)using the rr create_time + // to be the edit time of the SR rt + rr combo + edit_time = activeReport.create_time(); + recurring = activeReport.recur() === 't'; + } + // then fetch the most recent completed rs + if (activeReport.runs().length) { + let lastRun = null; + past = activeReport.runs().filter(el => el.start_time() !== null); + if (past.length) { + lastRun = past.reduce((prev, curr) => + prev.complete_time() > curr.complete_time() ? prev : curr + ); + } + if (lastRun) { + last_run = lastRun.complete_time(); + } + + // And the next rs not yet in progress + let nextRun = null; + future = activeReport.runs().filter(el => el.start_time() === null); + if (future.length) { + nextRun = future.reduce((prev, curr) => + prev.run_time() < curr.run_time() ? prev : curr + ); + } + if (nextRun) { + next_run = nextRun.run_time(); + } + } + } + return { + name: row.name(), + rt_id: row.id(), + create_time: row.create_time(), + edit_time: edit_time, + last_run: last_run, + next_run: next_run, + recurring: recurring, + }; + })); + }; + + return gridSource; + } +} + +@Injectable() +export class SimpleReporterServiceResolver implements Resolve> { + + constructor( + private router: Router, + private perm: PermService, + private svc: SimpleReporterService + ) {} + + resolve( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot): Promise { + + return from(this.perm.hasWorkPermHere('RUN_SIMPLE_REPORTS')).pipe(mergeMap( + permResult => { + if (permResult['RUN_SIMPLE_REPORTS']) { + return Promise.all([ + this.svc._initFolders() + ]); + } else { + this.router.navigate(['/staff/no_permission']); + return EMPTY; + } + } + )).toPromise(); + } + +} diff --git a/Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-editor.component.css b/Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-editor.component.css new file mode 100644 index 0000000000..0ea9a45ff9 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-editor.component.css @@ -0,0 +1,7 @@ +.sr-name-empty { + border-left: 5px solid #FA787E; +} + +.sr-name-notempty { + border-left: 5px solid #78FA89; +} diff --git a/Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-editor.component.html b/Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-editor.component.html new file mode 100644 index 0000000000..acfbd88e3d --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-editor.component.html @@ -0,0 +1,102 @@ + + + + + + + + + + + + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + +
+
+
+
+ +
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-editor.component.ts b/Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-editor.component.ts new file mode 100644 index 0000000000..f409b23c24 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-editor.component.ts @@ -0,0 +1,239 @@ +import {Component, OnInit, ViewChild} from '@angular/core'; +import {Router, ActivatedRoute} from '@angular/router'; +import {Location} from '@angular/common'; +import {of} from 'rxjs'; +import {NgbNav} from '@ng-bootstrap/ng-bootstrap'; +import {StaffCommonModule} from '@eg/staff/common.module'; +import {EventService} from '@eg/core/event.service'; +import {ToastService} from '@eg/share/toast/toast.service'; +import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component'; +import {IdlService, IdlObject} from '@eg/core/idl.service'; +import {PcrudService} from '@eg/core/pcrud.service'; +import {StringComponent} from '@eg/share/string/string.component'; +import {SimpleReporterService, SRTemplate} from './simple-reporter.service'; +import {SRFieldChooserComponent} from './sr-field-chooser.component'; +import {SRSortOrderComponent} from './sr-sort-order.component'; +import {SROutputOptionsComponent} from './sr-output-options.component'; + +@Component({ + templateUrl: './sr-editor.component.html', + styleUrls: ['./sr-editor.component.css'], +}) + +export class SREditorComponent implements OnInit { + + rptType = ''; + oldRptType = ''; + name = ''; + templ: SRTemplate = null; + isNew = true; + sourceClass: IdlObject = null; + fieldGroups: IdlObject[] = []; + allFields: IdlObject[] = []; + pageTitle = ''; + forcedFields = 0; + _isDirty = false; + + @ViewChild('templateSaved', { static: true }) templateSavedString: StringComponent; + @ViewChild('templateSaveError', { static: true }) templateSaveErrorString: StringComponent; + @ViewChild('newTitle', { static: true }) newTitleString: StringComponent; + @ViewChild('editTitle', { static: true }) editTitleString: StringComponent; + @ViewChild('srEditorTabs', { static: true }) tabs: NgbNav; + @ViewChild('changeTypeDialog', { static: false }) changeTypeDialog: ConfirmDialogComponent; + @ViewChild('closeFormDialog', { static: false }) closeFormDialog: ConfirmDialogComponent; + + + constructor( + private route: ActivatedRoute, + private router: Router, + private location: Location, + private toast: ToastService, + private evt: EventService, + private idl: IdlService, + private pcrud: PcrudService, + private srSvc: SimpleReporterService + ) { + const id = this.route.snapshot.paramMap.get('id'); + if ( id === null ) { + this.templ = new SRTemplate(); + } else { + this.isNew = false; + this.loadTemplate(Number(id)) + .then( x => this.reloadFields(this.templ.fmClass)); + } + + } + + ngOnInit() { + this._setPageTitle(); + } + + _setPageTitle() { + if ( this.isNew ) { + this.newTitleString.current() + .then(str => this.pageTitle = str ); + } else { + this.editTitleString.current() + .then(str => this.pageTitle = str ); + } + } + + reloadFields(fmClass: string) { + this.allFields = []; + this.forcedFields = 0; + this.sourceClass = this.idl.classes[fmClass]; + ('field_groups' in this.sourceClass) ? + // grab a clone + this.fieldGroups = this.sourceClass.field_groups.map(x => ({...x})) : + this.fieldGroups = []; + + this.sourceClass.fields.forEach(f => { + + f.transform = this.srSvc.defaultTransform(); + f.operator = this.srSvc.defaultOperator(f.datatype); + + if ( f.suggest_transform ) { + f.transform = this.srSvc.getTransformByName(f.suggest_transform); + } + + if ( f.suggest_operator ) { + f.operator = this.srSvc.getOperatorByName(f.suggest_operator); + } + + if ( f.force_transform ) { + if ( typeof f.force_transform === 'string' ) { + f.transform = this.srSvc.getTransformByName(f.force_transform); + } else { + f.transform = this.srSvc.getGenericTransformWithParams(f.force_transform); + } + } + + if ( f.force_operator ) { + f.operator = this.srSvc.getOperatorByName(f.force_operator); + } + + if ( f.force_filter ) { + if ( this.templ.filterFields.findIndex(el => el.name === f.name) === -1 ) { + this.templ.filterFields.push(f); + this.forcedFields++; + } + } + + this.allFields.push(f); + if ( 'field_groups' in f ) { + f.field_groups.forEach(g => { + const idx = this.fieldGroups.findIndex(el => el.name === g); + if ( idx > -1 ) { + if ( !('members' in this.fieldGroups[idx]) ) { + this.fieldGroups[idx].members = []; + } + this.fieldGroups[idx].members.push(f); + } + }); + } + }); + + this.allFields.sort( (a, b) => a.label.localeCompare(b.label) ); + + } + + changeReportType() { + if ( this.oldRptType === '' || (this.templ.displayFields.length === 0 && this.templ.filterFields.length === this.forcedFields) ) { + this.oldRptType = this.rptType; + this.templ = new SRTemplate(); + this.templ.fmClass = this.rptType; + this.reloadFields(this.rptType); + this._isDirty = true; + } else { + return this.changeTypeDialog.open() + .subscribe(confirmed => { + if ( confirmed ) { + this.oldRptType = this.rptType; + this.templ = new SRTemplate(); + this.templ.fmClass = this.rptType; + this.reloadFields(this.rptType); + this._isDirty = true; + } else { + this.rptType = this.oldRptType; + } + }); + } + } + + dirty() { + this._isDirty = true; + } + + isDirty() { + return this._isDirty; + } + + readyToSave() { + return ( this.sourceClass !== null && this.name !== '' ); + } + + readyToSchedule = () => { + return ( this.readyToSave() && this.templ.displayFields.length > 0 ); + } + + canLeaveEditor() { + if ( this.isDirty() ) { + return this.closeFormDialog.open(); + } else { + return of(true); + } + } + + loadTemplate(id: number) { + return this.srSvc.loadTemplate(id) + .then(idl => { + this.templ = new SRTemplate(idl); + this.name = this.templ.name; + this.rptType = this.templ.fmClass; + this.oldRptType = this.templ.fmClass; + }); + } + + saveTemplate = (scheduleNow) => { + this.templ.name = this.name; + + this.srSvc.saveTemplate(this.templ, scheduleNow) + .then(rt => { + this._isDirty = false; + // It appears that calling pcrud.create will return the newly created object, + // while pcrud.update just gives you back the id of the updated object. + if ( typeof rt === 'object' ) { + this.templ = new SRTemplate(rt); // pick up the id and create_time fields + } + this.templateSavedString.current() + .then(str => { + this.toast.success(str); + }); + if (scheduleNow) { + // we're done, so jump to the main page + this.router.navigate(['/staff/reporter/simple']); + } else if (this.isNew) { + // we've successfully saved, so we're no longer new + // adjust page title... + this.isNew = false; + this._setPageTitle(); + // ... and make the URL say that we're editing + const url = this.router.createUrlTree(['/staff/reporter/simple/edit/' + this.templ.id]).toString(); + this.location.go(url); // go without reloading + } + }, + err => { + this.templateSaveErrorString.current() + .then(str => { + this.toast.danger(str + err); + console.error('Error saving template: %o', err); + }); + }); + } + + closeForm() { + this.router.navigate(['/staff/reporter/simple']); + } + +} + diff --git a/Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-field-chooser.component.css b/Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-field-chooser.component.css new file mode 100644 index 0000000000..310fdd7471 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-field-chooser.component.css @@ -0,0 +1,9 @@ +.chooser-row { + display: flex; + justify-content: space-between; +} + +.sr-chooser-display-list { + font-size: 80%; +} + diff --git a/Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-field-chooser.component.html b/Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-field-chooser.component.html new file mode 100644 index 0000000000..a87454490d --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-field-chooser.component.html @@ -0,0 +1,108 @@ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
Fields Selected for Display
+
+
+ + {{f.alias}}{{idx === (listFields.length - 1) ? '' : ', '}} + +
+ + +
+
Field Display Order
+
Filter Fields and Values
+
+
+ + + + + + + + + + + + + + + + + +
+
+ +
diff --git a/Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-field-chooser.component.ts b/Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-field-chooser.component.ts new file mode 100644 index 0000000000..db88650ffe --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-field-chooser.component.ts @@ -0,0 +1,97 @@ +import {Component, Input, Output, EventEmitter, OnInit, ViewChild} from '@angular/core'; +import {NgbAccordion} from '@ng-bootstrap/ng-bootstrap'; +import {IdlService, IdlObject} from '@eg/core/idl.service'; +import {PcrudService} from '@eg/core/pcrud.service'; +import {SimpleReporterService} from './simple-reporter.service'; +import {SRFieldComponent} from './sr-field.component'; + +@Component({ + selector: 'eg-sr-field-chooser', + styleUrls: ['./sr-field-chooser.component.css'], + templateUrl: './sr-field-chooser.component.html' +}) + +export class SRFieldChooserComponent implements OnInit { + + @Input() fieldType = 'display'; + @Input() allFields: IdlObject[] = []; + @Input() fieldGroups: IdlObject[] = []; + @Input() orderByNames: string[] = []; + @Output() orderByNamesChange = new EventEmitter(); + @Input() selectedFields: IdlObject[] = []; + @Output() selectedFieldsChange = new EventEmitter(); + @Input() listFields: IdlObject[] = []; + + @ViewChild('fieldChooser', { static: false }) fieldChooser: NgbAccordion; + @ViewChild('selectedList', { static: false }) selectedList: NgbAccordion; + + constructor( + private idl: IdlService, + private srSvc: SimpleReporterService + ) { + } + + ngOnInit() { + } + + fieldIsSelected(field: IdlObject) { + return this.selectedFields.findIndex(el => el.name === field.name) > -1; + } + + hideField(field: IdlObject) { + if ( typeof field.hide_from === 'undefined' ) { + return false; + } + return (field.hide_from.indexOf(this.fieldType) > -1); + } + + toggleSelect(field: IdlObject) { + const idx = this.selectedFields.findIndex(el => el.name === field.name); + if ( idx > -1 ) { + if ( field.forced_filter ) { return; } // These should just be hidden, but if not... + this.selectedFields.splice(idx, 1); + if ( this.fieldType === 'display' ) { + this.orderByNames.splice(this.orderByNames.findIndex(el => el === field.name), 1); + } + } else { + const f = { ...field }; + + if ( this.fieldType === 'display' ) { + f['alias'] = f.label; // can be edited + this.orderByNames.push(f.name); + } + this.selectedFields.push(f); + } + + this.selectedFieldsChange.emit(this.selectedFields); + + if ( this.fieldType === 'display' ) { + this.orderByNamesChange.emit(this.orderByNames); + } + } + + updateField(field: IdlObject) { + const idx = this.selectedFields.findIndex(el => el.name === field.name); + this.selectedFields[idx] = field; + this.selectedFieldsChange.emit(this.selectedFields); + } + + moveUp(idx: number) { + if ( idx > 0 ) { // should always be the case, but we check anyway + const hold: IdlObject = this.selectedFields[idx - 1]; + this.selectedFields[idx - 1] = this.selectedFields[idx]; + this.selectedFields[idx] = hold; + this.selectedFieldsChange.emit(this.selectedFields); + } + } + + moveDown(idx: number) { + if ( idx < this.selectedFields.length ) { // see above comment + const hold: IdlObject = this.selectedFields[idx + 1]; + this.selectedFields[idx + 1] = this.selectedFields[idx]; + this.selectedFields[idx] = hold; + this.selectedFieldsChange.emit(this.selectedFields); + } + } + +} diff --git a/Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-field.component.css b/Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-field.component.css new file mode 100644 index 0000000000..39003ff862 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-field.component.css @@ -0,0 +1,29 @@ +.sr-field-container { + border-bottom: 1px solid lightgray; + padding-bottom: .75em; + margin-bottom: .75em; +} + +.sr-field-select { + display: flex; +} + +.sr-checkbox { + margin-top: .3em; +} + +.sr-field { + display: flex; + flex-wrap: wrap; + justify-content: space-between; +} + +.sr-field-explainer { + font-size: 80%; +} + +.sr-field-disp-ind { + padding-left: .3em; + padding-bottom: .3em; +} + diff --git a/Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-field.component.html b/Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-field.component.html new file mode 100644 index 0000000000..4ee7f290a5 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-field.component.html @@ -0,0 +1,210 @@ +
+ +
+ +
+ +
+ +
+ +
+ +
+ Name + + ({{field.label}}) +
+
+
+ + +
+ +
+ +
+ Transform + +
+ +
+ Operator + +
+ +
+ +
+ Filter value + +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+
+
+ + and + +
+
+ + and + +
+
{{ '' }}
+
{{ '' }}
+
{{ '' }}
+
+ + and + +
+
+
+ +
+
+
+ + +
+
+ + +
+
+ +
+
+ +
+
+ +
+ +
+ +
+
+
+
+ +
+ Direction + +
+ +
+ + +
+ +
+ +
diff --git a/Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-field.component.ts b/Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-field.component.ts new file mode 100644 index 0000000000..92f126c851 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-field.component.ts @@ -0,0 +1,187 @@ +import {Component, Input, Output, EventEmitter, OnInit} from '@angular/core'; +import {IdlObject} from '@eg/core/idl.service'; +import {OrgService} from '@eg/core/org.service'; +import {AuthService} from '@eg/core/auth.service'; +import {SimpleReporterService} from './simple-reporter.service'; + +@Component({ + selector: 'eg-sr-field', + templateUrl: './sr-field.component.html', + styleUrls: ['./sr-field.component.css'], +}) +export class SRFieldComponent implements OnInit { + + operators = []; + transforms = []; + wsContextOrgs = []; + linkedIdlBaseQuery = {}; + + @Input() field: IdlObject = null; + @Output() fieldChange = new EventEmitter(); + @Input() withAlias = false; + @Input() editAlias = true; + @Input() withTransforms = false; + @Input() withOperators = false; + @Input() withValueInput = false; + @Input() withSelect = false; + @Input() withDeselect = false; + @Input() withSortDirection = false; + @Output() selectEvent = new EventEmitter(); + @Output() deselectEvent = new EventEmitter(); + @Input() selected = false; + @Input() withUpDown = false; + @Output() upEvent = new EventEmitter(); + @Output() downEvent = new EventEmitter(); + @Input() disableUp = false; + @Input() disableDown = false; + + constructor( + private org: OrgService, + private auth: AuthService, + private srSvc: SimpleReporterService + ) { + } + + ngOnInit() { + + if ( this.withTransforms ) { + this.transforms = this.srSvc.getTransformsForDatatype(this.field.datatype); + } + + if ( this.withOperators ) { + this.operators = this.srSvc.getOperatorsForDatatype(this.field.datatype); + } + + this.wsContextOrgs = this.org.fullPath(this.auth.user().ws_ou(), true); + if (this.field.org_filter_field) { + this.linkedIdlBaseQuery[this.field.org_filter_field] = this.wsContextOrgs; + } + } + + clearFilterValue() { + this.field.filter_value = this.field.operator.arity > 1 ? [] : null; + delete this.field._org_family_includeAncestors; + delete this.field._org_family_includeDescendants; + delete this.field._org_family_primaryOrgId; + } + + operatorChange($event) { + const new_op = this.srSvc.getOperatorByName($event.target.value); + if (new_op.arity !== this.field.operator.arity) { // param count of the old and new ops are different + this.field.operator = new_op; + this.clearFilterValue(); // clear the filter value + } else { + this.field.operator = new_op; + } + this.fieldChange.emit(this.field); + } + + transformChange($event) { + const new_transform = this.srSvc.getTransformByName($event.target.value); + + if (new_transform.final_datatype) { // new has a final_datatype + if (this.field.transform.final_datatype) { // and so does old + if (new_transform.final_datatype !== this.field.transform.final_datatype) { // and they're different + this.clearFilterValue(); // clear + } + } else if (new_transform.final_datatype !== this.field.datatype) { // old does not, and base is different from new + this.clearFilterValue(); // clear + } + } else if (this.field.transform.final_datatype) {// old has a final_datatype, new doesn't + if (this.field.transform.final_datatype !== this.field.datatype) { // and it's different from the base type + this.clearFilterValue(); // clear + } + } + + this.field.transform = new_transform; + if (new_transform.final_datatype) { + this.operators = this.srSvc.getOperatorsForDatatype(new_transform.final_datatype); + } else { + this.operators = this.srSvc.getOperatorsForDatatype(this.field.datatype); + } + + this.selectEvent.emit(); + this.fieldChange.emit(this.field); + } + + firstBetweenValue($event) { + if (!Array.isArray(this.field.filter_value)) { + this.field.filter_value = []; + } + this.field.filter_value[0] = $event; + this.fieldChange.emit(this.field); + } + + secondBetweenValue($event) { + if (!Array.isArray(this.field.filter_value)) { + this.field.filter_value = []; + } + this.field.filter_value[1] = $event; + this.fieldChange.emit(this.field); + } + + setSingleValue($event) { + if (Array.isArray(this.field.filter_value)) { + this.field.filter_value = null; + } + this.field.filter_value = $event; + this.fieldChange.emit(this.field); + } + + getBracketListValue(list_value) { + let output = '{'; + if (Array.isArray(list_value)) { + list_value.forEach((v, i) => { + if (i > 0) { + output += ','; + } + output += v; + }); + } + output += '}'; + return output; + } + + setOrgFamilyValue($event) { + this.field.filter_value = this.getBracketListValue($event.orgIds); + this.field._org_family_includeAncestors = $event.includeAncestors; + this.field._org_family_includeDescendants = $event.includeDescendants; + this.field._org_family_primaryOrgId = $event.primaryOrgId; + this.fieldChange.emit(this.field); + } + + setBracketListValue($event) { + if (Array.isArray(this.field.filter_value)) { + this.field.filter_value = null; + } + let valstr = $event; + valstr = valstr.replace(/^{/, ''); + valstr = valstr.replace(/}$/, ''); + const ids = valstr.split(','); + this.field.filter_value = [...ids]; + this.fieldChange.emit(this.field); + } + + directionChange($event) { + this.field['direction'] = $event.target.value; + this.fieldChange.emit(this.field); + } + + selectAction() { + this.selectEvent.emit(); + } + + deselectAction() { + this.deselectEvent.emit(); + } + + upAction() { + this.upEvent.emit(); + } + + downAction() { + this.downEvent.emit(); + } + +} + diff --git a/Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-my-outputs.component.html b/Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-my-outputs.component.html new file mode 100644 index 0000000000..00007ffff8 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-my-outputs.component.html @@ -0,0 +1,72 @@ +{num, plural, =1 {Output Deleted} other {{{num}} Outputs Deleted}} + + +{num, plural, =1 {Are you sure you want to delete this output?} other {Are you sure you want to delete these {{num}} outputs?}} + + + + + + + + Error running report + + + +
+ + + + + + + + + + + + + + + +
diff --git a/Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-my-outputs.component.ts b/Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-my-outputs.component.ts new file mode 100644 index 0000000000..ac9f920bcb --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-my-outputs.component.ts @@ -0,0 +1,90 @@ +import {Component, OnInit, ViewChild} from '@angular/core'; +import {AuthService} from '@eg/core/auth.service'; +import {IdlObject, IdlService} from '@eg/core/idl.service'; +import {PcrudService} from '@eg/core/pcrud.service'; +import {StringComponent} from '@eg/share/string/string.component'; +import {ToastService} from '@eg/share/toast/toast.service'; +import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component'; +import {Pager} from '@eg/share/util/pager'; +import {GridComponent} from '@eg/share/grid/grid.component'; +import {GridDataSource, GridCellTextGenerator} from '@eg/share/grid/grid'; +import {SimpleReporterService} from './simple-reporter.service'; + +@Component({ + selector: 'eg-sr-outputs', + templateUrl: 'sr-my-outputs.component.html', +}) + +export class SROutputsComponent implements OnInit { + + gridSource: GridDataSource; + @ViewChild('srOutputsGrid', { static: true }) outputsGrid: GridComponent; + @ViewChild('confirmDelete', { static: false }) confirmDeleteDialog: ConfirmDialogComponent; + @ViewChild('deleted', { static: true} ) deletedString: StringComponent; + @ViewChild('delete', { static: true} ) confirmDeleteString: StringComponent; + + cellTextGenerator: GridCellTextGenerator; + + constructor( + private auth: AuthService, + private pcrud: PcrudService, + private idl: IdlService, + private toast: ToastService, + private srSvc: SimpleReporterService, + ) { + // These values are all replaced via custom templates and cause warnings if not specified here. + this.cellTextGenerator = { + _output: row => '' + }; + + } + + ngOnInit() { + this.gridSource = this.srSvc.getOutputDatasource(); + + } + + // Expects an rt object with fleshed report to grab the template id. + outputPath(row: any, file: string) { + return `/reporter/${row.template_id}/${row.report_id}/${row.id}/${file}?ses=${this.auth.token()}`; + } + + zeroSelectedRows(rows: any) { + return rows.length === 0; + } + + notOneSelectedRow(rows: any) { + return rows.length !== 1; + } + + deleteOutputs(rows: any[]) { + if ( rows.length <= 0 ) { return; } + this.confirmDeleteString.current({ num: rows.length }) + .then(str => { + this.confirmDeleteDialog.dialogBody = str; + this.confirmDeleteDialog.open() + .subscribe(confirmed => { + if ( confirmed ) { this.doDeleteOutputs(rows.map(x => x._rs)); } + }); + }); + } + + doDeleteOutputs(outs: IdlObject[]) { + const deletedCount = outs.length; + this.pcrud.remove(outs).toPromise() + .then(res => { + this.outputsGrid.reload(); + this.deletedString.current({num: outs.length}) + .then(str => { + this.toast.success(str); + }); + }); + + } + + refreshGrid($event) { + this.outputsGrid.reload(); + } + +} + diff --git a/Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-my-reports.component.html b/Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-my-reports.component.html new file mode 100644 index 0000000000..06547edc51 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-my-reports.component.html @@ -0,0 +1,66 @@ +{ct, plural, =1 {Deleted 1 Report} other {Deleted {{ct}} Reports}} + +{ct, plural, =1 {Failed to Delete 1 Report} other {Failed to Delete {{ct}} Reports}} + +{fail, plural, =1 {Failed to Delete 1 Report But Succeeded In Deleting {{success}}} other {Failed to Delete {{fail}} Reports But Succeeded in Deleting {{success}}}} + +{ct, plural, =1 {Are you sure you want to delete this report and its output?} other {Are you sure you want to delete these {{ct}} reports and their output?}} + +Enter a new name for the clone of: {{old}} + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-my-reports.component.ts b/Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-my-reports.component.ts new file mode 100644 index 0000000000..1ce844f5bb --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-my-reports.component.ts @@ -0,0 +1,165 @@ +import {Component, OnInit, ViewChild} from '@angular/core'; +import {Router, ActivatedRoute} from '@angular/router'; +import {map, concatMap} from 'rxjs/operators'; +import {from} from 'rxjs'; +import {AuthService} from '@eg/core/auth.service'; +import {IdlObject, IdlService} from '@eg/core/idl.service'; +import {PcrudService} from '@eg/core/pcrud.service'; +import {Pager} from '@eg/share/util/pager'; +import {GridComponent} from '@eg/share/grid/grid.component'; +import {GridDataSource, GridCellTextGenerator} from '@eg/share/grid/grid'; +import {SimpleReporterService, SRTemplate} from './simple-reporter.service'; +import {StringComponent} from '@eg/share/string/string.component'; +import {ToastService} from '@eg/share/toast/toast.service'; +import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component'; +import {PromptDialogComponent} from '@eg/share/dialog/prompt.component'; +import {NetService} from '@eg/core/net.service'; + +@Component({ + selector: 'eg-sr-reports', + templateUrl: 'sr-my-reports.component.html', +}) + +export class SRReportsComponent implements OnInit { + + gridSource: GridDataSource; + editSelected: ($event: any) => void; + newReport: ($event: any) => void; + @ViewChild('srReportsGrid', { static: true }) reportsGrid: GridComponent; + @ViewChild('confirmDelete', { static: true }) deleteDialog: ConfirmDialogComponent; + @ViewChild('promptClone', { static: true }) cloneDialog: PromptDialogComponent; + @ViewChild('delete', { static: true} ) deleteString: StringComponent; + @ViewChild('clone', { static: true} ) cloneString: StringComponent; + @ViewChild('deleteSuccess', { static: true} ) deleteSuccessString: StringComponent; + @ViewChild('deleteFailure', { static: true} ) deleteFailureString: StringComponent; + @ViewChild('mixedResults', { static: true} ) mixedResultsString: StringComponent; + @ViewChild('templateSaved', { static: true }) templateSavedString: StringComponent; + @ViewChild('templateSaveError', { static: true }) templateSaveErrorString: StringComponent; + + + cellTextGenerator: GridCellTextGenerator; + + constructor( + private router: Router, + private route: ActivatedRoute, + private auth: AuthService, + private pcrud: PcrudService, + private idl: IdlService, + private srSvc: SimpleReporterService, + private toast: ToastService, + private net: NetService + ) { + } + + ngOnInit() { + this.gridSource = this.srSvc.getReportsDatasource(); + + this.editSelected = ($event) => { + this.router.navigate(['edit', $event[0].rt_id], { relativeTo: this.route }); + }; + + this.newReport = ($event) => { + this.router.navigate(['new'], { relativeTo: this.route }); + }; + } + + zeroSelectedRows(rows: any) { + return rows.length === 0; + } + + notOneSelectedRow(rows: any) { + return rows.length !== 1; + } + + deleteSelected(rows: any) { + if ( rows.length <= 0 ) { return; } + + let successes = 0; + let failures = 0; + + this.deleteString.current({ct: rows.length}) + .then(str => { + this.deleteDialog.dialogBody = str; + this.deleteDialog.open() + .subscribe(confirmed => { + if ( confirmed ) { + from(rows.map(x => x.rt_id)).pipe(concatMap(rt_id => + this.net.request( + 'open-ils.reporter', + 'open-ils.reporter.template.delete.cascade', + this.auth.token(), + rt_id + ).pipe(map(res => ({ + result: res, + rt_id: rt_id + }))) + )).subscribe( + (res) => { + if (Number(res.result) === 2) { + successes++; + } else { + failures++; + } + }, + (err) => {}, + () => { + if (successes === rows.length) { + this.deleteSuccessString.current({ct: successes}).then(str2 => { this.toast.success(str2); }); + } else if (failures && !successes) { + this.deleteFailureString.current({ct: failures}).then(str2 => { this.toast.danger(str2); }); + } else { + this.mixedResultsString.current({fail: failures, success: successes}) + .then(str2 => { this.toast.warning(str2); }); + } + this.reportsGrid.reload(); + } + ); + } + }); + }); + } + + cloneSelected(row: any) { + if ( row.length <= 0 ) { return; } + if ( row.length > 1 ) { return; } + + const rt_row = row[0]; + + this.cloneString.current({old: rt_row.name}) + .then(str => { + this.cloneDialog.dialogBody = str; + this.cloneDialog.promptValue = rt_row.name + ' (Clone)'; + this.cloneDialog.open() + .subscribe(new_name => { + if ( new_name ) { + this.srSvc.loadTemplate(rt_row.rt_id) + .then(idl => { + // build a new clone + const new_templ = new SRTemplate(idl); + new_templ.name = new_name; + new_templ.id = -1; + new_templ.isNew = true; + new_templ.create_time = null; + new_templ.runNow = 'now'; + new_templ.runTime = null; + + // and save it + this.srSvc.saveTemplate(new_templ, false) + .then(rt => { + this.router.navigate(['edit', rt.id()], { relativeTo: this.route }); + }, + err => { + this.templateSaveErrorString.current() + .then(errstr => { + this.toast.danger(errstr + err); + console.error('Error saving template: %o', err); + }); + }); + + }); + } + }); + }); + } + +} diff --git a/Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-output-options.component.html b/Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-output-options.component.html new file mode 100644 index 0000000000..360d797642 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-output-options.component.html @@ -0,0 +1,65 @@ +
+
+ Choose your output format(s) +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ Recurrence +
+
+ + +
+ + + +
+
+
{{ '' // Can't use form-inline here because it breaks the calendar display }} + Scheduling +
+
+ + +
+
+ + +
+
+ +
+
+
+
+ Email +
+ + +
+
+
+ +
+
diff --git a/Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-output-options.component.ts b/Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-output-options.component.ts new file mode 100644 index 0000000000..5c2398da02 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-output-options.component.ts @@ -0,0 +1,34 @@ +import {Component, Input, Output, EventEmitter, OnInit, ViewChild} from '@angular/core'; +import {NgForm} from '@angular/forms'; +import {IdlService, IdlObject} from '@eg/core/idl.service'; +import {SimpleReporterService, SRTemplate} from './simple-reporter.service'; +import * as moment from 'moment-timezone'; + +@Component({ + selector: 'eg-sr-output-options', + templateUrl: './sr-output-options.component.html' +}) + +export class SROutputOptionsComponent implements OnInit { + + @Input() templ: SRTemplate; + @Input() readyToSchedule: () => boolean; + @Input() saveTemplate: (args: any) => void; + + constructor( + private idl: IdlService, + private srSvc: SimpleReporterService + ) { } + + ngOnInit() {} + + defaultTime() { + // When changing to Later for the first time default minutes to the quarter hour + if (this.templ.runNow === 'later' && this.templ.runTime === null) { + const now = moment(); + const nextQ = now.add(15 - (now.minutes() % 15), 'minutes'); + this.templ.runTime = nextQ; + } + } + +} diff --git a/Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-sort-order.component.css b/Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-sort-order.component.css new file mode 100644 index 0000000000..3966475922 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-sort-order.component.css @@ -0,0 +1,5 @@ +.order-row { + display: flex; + justify-content: space-between; +} + diff --git a/Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-sort-order.component.html b/Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-sort-order.component.html new file mode 100644 index 0000000000..1ff38561a7 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-sort-order.component.html @@ -0,0 +1,59 @@ +
+ + + + + +
Field Display Order
+
+ + + + + + + + +
+ +
+ + + + +
Field Sort Order
+
+ + + + + + + + +
+ +
+ +
diff --git a/Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-sort-order.component.ts b/Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-sort-order.component.ts new file mode 100644 index 0000000000..75b7e32b6a --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/reporter/simple/sr-sort-order.component.ts @@ -0,0 +1,83 @@ +import {Component, Input, Output, EventEmitter, OnInit, ViewChild} from '@angular/core'; +import {NgbAccordion} from '@ng-bootstrap/ng-bootstrap'; +import {IdlService, IdlObject} from '@eg/core/idl.service'; +import {PcrudService} from '@eg/core/pcrud.service'; +import {SimpleReporterService} from './simple-reporter.service'; +import {SRFieldComponent} from './sr-field.component'; + +@Component({ + selector: 'eg-sr-sort-order', + styleUrls: ['./sr-sort-order.component.css'], + templateUrl: './sr-sort-order.component.html' +}) + +export class SRSortOrderComponent implements OnInit { + + @Input() fields: IdlObject[] = []; + @Output() fieldsChange = new EventEmitter(); + @Input() orderByNames: string[] = []; + @Output() orderByNamesChange = new EventEmitter(); + + @ViewChild('displayList', { static: false }) displayList: NgbAccordion; + @ViewChild('orderList', { static: false }) orderList: NgbAccordion; + + constructor( + private idl: IdlService, + private srSvc: SimpleReporterService + ) { + } + + ngOnInit() { + } + + updateField(field: IdlObject) { + const idx = this.fields.findIndex(el => el.name === field.name); + this.fields[idx] = field; + this.fieldsChange.emit(this.fields); + } + + moveDisplayUp(idx: number) { + if ( idx > 0 ) { // should always be the case, but we check anyway + const hold: IdlObject = this.fields[idx - 1]; + this.fields[idx - 1] = this.fields[idx]; + this.fields[idx] = hold; + this.fieldsChange.emit(this.fields); + } + } + + moveDisplayDown(idx: number) { + if ( idx < this.fields.length ) { // see above comment + const hold: IdlObject = this.fields[idx + 1]; + this.fields[idx + 1] = this.fields[idx]; + this.fields[idx] = hold; + this.fieldsChange.emit(this.fields); + } + } + + moveOrderUp(idx: number) { + if ( idx > 0 ) { + const hold: string = this.orderByNames[idx - 1]; + this.orderByNames[idx - 1] = this.orderByNames[idx]; + this.orderByNames[idx] = hold; + this.orderByNamesChange.emit(this.orderByNames); + } + } + + moveOrderDown(idx: number) { + if ( idx < this.orderByNames.length ) { + const hold: string = this.orderByNames[idx + 1]; + this.orderByNames[idx + 1] = this.orderByNames[idx]; + this.orderByNames[idx] = hold; + this.orderByNamesChange.emit(this.orderByNames); + } + } + + fieldsInOrderByOrder() { + const sorted = []; + this.orderByNames.forEach(el => { + sorted.push(this.fields[this.fields.findIndex(fl => fl.name === el)]); + }); + return sorted; + } + +} -- 2.11.0