From a532085a750f2f00e831c6e53c678c12a485656a Mon Sep 17 00:00:00 2001 From: Jason Etheridge Date: Sat, 4 Mar 2023 10:45:11 -0500 Subject: [PATCH] lp1993824: linkchecker; UI Signed-off-by: Jason Etheridge --- .../staff/cat/linkchecker/attempts.component.html | 37 +++ .../staff/cat/linkchecker/attempts.component.ts | 146 +++++++++ .../cat/linkchecker/linkchecker.component.html | 83 +++++ .../staff/cat/linkchecker/linkchecker.component.ts | 263 ++++++++++++++++ .../staff/cat/linkchecker/linkchecker.module.ts | 33 ++ .../linkchecker/new-session-dialog.component.html | 149 +++++++++ .../linkchecker/new-session-dialog.component.ts | 345 +++++++++++++++++++++ .../app/staff/cat/linkchecker/routing.module.ts | 25 ++ .../app/staff/cat/linkchecker/urls.component.html | 33 ++ .../app/staff/cat/linkchecker/urls.component.ts | 181 +++++++++++ .../src/eg2/src/app/staff/cat/routing.module.ts | 4 + Open-ILS/src/eg2/src/app/staff/nav.component.html | 2 +- 12 files changed, 1300 insertions(+), 1 deletion(-) create mode 100644 Open-ILS/src/eg2/src/app/staff/cat/linkchecker/attempts.component.html create mode 100644 Open-ILS/src/eg2/src/app/staff/cat/linkchecker/attempts.component.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/cat/linkchecker/linkchecker.component.html create mode 100644 Open-ILS/src/eg2/src/app/staff/cat/linkchecker/linkchecker.component.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/cat/linkchecker/linkchecker.module.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/cat/linkchecker/new-session-dialog.component.html create mode 100644 Open-ILS/src/eg2/src/app/staff/cat/linkchecker/new-session-dialog.component.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/cat/linkchecker/routing.module.ts create mode 100644 Open-ILS/src/eg2/src/app/staff/cat/linkchecker/urls.component.html create mode 100644 Open-ILS/src/eg2/src/app/staff/cat/linkchecker/urls.component.ts diff --git a/Open-ILS/src/eg2/src/app/staff/cat/linkchecker/attempts.component.html b/Open-ILS/src/eg2/src/app/staff/cat/linkchecker/attempts.component.html new file mode 100644 index 0000000000..69c22fc323 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/cat/linkchecker/attempts.component.html @@ -0,0 +1,37 @@ + +
+ + {batches, plural, =0 {Attempts for All Batches} =1 {Attempts for Batch ID {{batches}}} other {Attempts for Batch IDs {{batches}}}} + +
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Open-ILS/src/eg2/src/app/staff/cat/linkchecker/attempts.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/linkchecker/attempts.component.ts new file mode 100644 index 0000000000..b80296bac8 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/cat/linkchecker/attempts.component.ts @@ -0,0 +1,146 @@ +import * as Moment from 'moment-timezone'; +import {ActivatedRoute} from '@angular/router'; +import {AuthService} from '@eg/core/auth.service'; +import {ComboboxEntry} from '@eg/share/combobox/combobox.component'; +import {Component, OnInit, ViewChild, Input, TemplateRef} from '@angular/core'; +import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component'; +import {DateSelectComponent} from '@eg/share/date-select/date-select.component'; +import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component'; +import {FormGroup, FormControl} from '@angular/forms'; +import {FormatService} from '@eg/core/format.service'; +import {GridComponent} from '@eg/share/grid/grid.component'; +import {GridDataSource, GridColumn, GridRowFlairEntry, GridCellTextGenerator} from '@eg/share/grid/grid'; +import {GridFlatDataService} from '@eg/share/grid/grid-flat-data.service'; +import {HtmlToTxtService} from '@eg/share/util/htmltotxt.service'; +import {IdlService, IdlObject} from '@eg/core/idl.service'; +import {NgbDate} from '@ng-bootstrap/ng-bootstrap'; +import {OrgFamily} from '@eg/share/org-family-select/org-family-select.component'; +import {OrgService} from '@eg/core/org.service'; +import {Pager} from '@eg/share/util/pager'; +import {PcrudService} from '@eg/core/pcrud.service'; +import {PermService} from '@eg/core/perm.service'; +import {PrintService} from '@eg/share/print/print.service'; +import {ProgressDialogComponent} from '@eg/share/dialog/progress.component'; +import {SampleDataService} from '@eg/share/util/sample-data.service'; +import {StringComponent} from '@eg/share/string/string.component'; +import {StringService} from '@eg/share/string/string.service'; +import {ToastService} from '@eg/share/toast/toast.service'; +import {map, take} from 'rxjs/operators'; +import {timer as observableTimer, Observable, of} from 'rxjs'; + +@Component({ + templateUrl: 'attempts.component.html' +}) +export class LinkCheckerAttemptsComponent implements OnInit { + + batches: number[] = []; + + batchesIdlClass = 'uvva'; + batchesSessionField = 'session'; + + attemptsIdlClass = 'uvuv'; + attemptsSortField = 'id'; + attemptsBatchField = 'attempt'; + attemptsFleshFields = { + 'uvuv' : ['url','attempt'], + 'uvu' : ['item','url_selector','redirect_from'], + 'uvsbrem' : ['target_biblio_record_entry'], + 'bre' : ['simple_record'] + }; + attemptsFleshDepth = 4; + attemptsIdlClassDef: any; + attemptsPKeyField: string; + + attemptsPermaCrud: any; + attemptsPerms: string; + + @ViewChild('grid', { static: true }) grid: GridComponent; + dataSource: GridDataSource = new GridDataSource(); + noSelectedRows: boolean; + oneSelectedRow: boolean; + + constructor( + private auth: AuthService, + private flatData: GridFlatDataService, + private format: FormatService, + private idl: IdlService, + private org: OrgService, + private pcrud: PcrudService, + private perm: PermService, + private route: ActivatedRoute, + ) {} + + ngOnInit() { + this.route.queryParams.subscribe( params => { + if (params.batches) { + this.batches = JSON.parse( params.batches ); + } + if (params.sessions) { + if (this.batches.length === 0) { + // TODO: make initDataSource wait for this optional parameter to be fielded when available + // Maybe it would be easier to have the dialog caller convert sessions to batches instead. + // For now, this is a kludge to prevent empty-in list error if/when grid loads before batches is populated. + this.batches.push(-1); + } + var batchSearch = {}; + batchSearch[ this.batchesSessionField ] = JSON.parse( params.sessions ); + this.pcrud.search(this.batchesIdlClass,batchSearch).subscribe( + (batch) => { + if (this.batches.length === 1 && this.batches[0] === -1) { + this.batches = []; // undo our kludge, otherwise -1 would be in the dialog title + } + this.batches.push( batch.id() ); + }, + (err) => { + console.log('pcrud.search.uvs err', err); + }, + () => { + this.batches = Array.from( new Set( this.batches ) ); + this.grid.reload(); + } + ); + } + }); + + this.attemptsIdlClassDef = this.idl.classes[this.attemptsIdlClass]; + this.attemptsPKeyField = this.attemptsIdlClassDef.pkey || 'id'; + + this.attemptsPermaCrud = this.attemptsIdlClassDef.permacrud || {}; + if (this.attemptsPermaCrud.retrieve) { + this.attemptsPerms = this.attemptsPermaCrud.retrieve.perms; + } + + this.initDataSource(); + this.gridSelectionChange( [] ); + } + + gridSelectionChange(keys: string[]) { + this.noSelectedRows = (keys.length === 0); + this.oneSelectedRow = (keys.length === 1); + var rows = this.grid.context.getSelectedRows(); + } + + initDataSource() { + this.dataSource.getRows = (pager: Pager, sort: any[]) => { + + let query: any = {} + + if (this.batches) { + query[this.attemptsBatchField] = this.batches; + } + + let query_filters = []; + Object.keys(this.dataSource.filters).forEach(key => { + query_filters = query_filters.concat( this.dataSource.filters[key] ); + }); + + if (query_filters.length > 0) { + query['-and'] = query_filters; + } + + return this.flatData.getRows( + this.grid.context, query, pager, sort); + }; + } + +} diff --git a/Open-ILS/src/eg2/src/app/staff/cat/linkchecker/linkchecker.component.html b/Open-ILS/src/eg2/src/app/staff/cat/linkchecker/linkchecker.component.html new file mode 100644 index 0000000000..870e4d0d10 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/cat/linkchecker/linkchecker.component.html @@ -0,0 +1,83 @@ + + + + + + + +
+
+ + + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Open-ILS/src/eg2/src/app/staff/cat/linkchecker/linkchecker.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/linkchecker/linkchecker.component.ts new file mode 100644 index 0000000000..3df2e17312 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/cat/linkchecker/linkchecker.component.ts @@ -0,0 +1,263 @@ +import * as Moment from 'moment-timezone'; +import {Router, ActivatedRoute} from '@angular/router'; +import {AuthService} from '@eg/core/auth.service'; +import {ComboboxEntry} from '@eg/share/combobox/combobox.component'; +import {Component, OnInit, ViewChild, Input, TemplateRef} from '@angular/core'; +import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component'; +import {DateSelectComponent} from '@eg/share/date-select/date-select.component'; +import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component'; +import {NewSessionDialogComponent} from './new-session-dialog.component'; +import {FormGroup, FormControl} from '@angular/forms'; +import {FormatService} from '@eg/core/format.service'; +import {GridComponent} from '@eg/share/grid/grid.component'; +import {GridDataSource, GridColumn, GridRowFlairEntry, GridCellTextGenerator} from '@eg/share/grid/grid'; +import {GridFlatDataService} from '@eg/share/grid/grid-flat-data.service'; +import {HtmlToTxtService} from '@eg/share/util/htmltotxt.service'; +import {IdlService, IdlObject} from '@eg/core/idl.service'; +import {NetService} from '@eg/core/net.service'; +import {NgbDate} from '@ng-bootstrap/ng-bootstrap'; +import {OrgFamily} from '@eg/share/org-family-select/org-family-select.component'; +import {OrgService} from '@eg/core/org.service'; +import {Pager} from '@eg/share/util/pager'; +import {PcrudService} from '@eg/core/pcrud.service'; +import {PermService} from '@eg/core/perm.service'; +import {PrintService} from '@eg/share/print/print.service'; +import {ProgressDialogComponent} from '@eg/share/dialog/progress.component'; +import {SampleDataService} from '@eg/share/util/sample-data.service'; +import {StringComponent} from '@eg/share/string/string.component'; +import {StringService} from '@eg/share/string/string.service'; +import {ToastService} from '@eg/share/toast/toast.service'; +import {map, take} from 'rxjs/operators'; +import {timer as observableTimer, Observable, of} from 'rxjs'; + +@Component({ + templateUrl: 'linkchecker.component.html' +}) +export class LinkCheckerComponent implements OnInit { + + viewIdlClass = 'uvsa'; + viewSortField = 'name'; + viewOrgField = 'owning_lib'; + viewFleshFields = { 'uvsa' : ['creator','owning_lib','container','usr'] }; + viewFleshDepth = 1; + viewIdlClassDef: any; + viewPKeyField: string; + + sessionIdlClass = 'uvs'; + sessionIdlClassDef: any; + + batchIdlClass = 'uvva'; + batchIdlClassDef: any; + + canCreateSession: boolean; + canCreateBatch: boolean; + viewPermaCrud: any; + + dataSource: GridDataSource = new GridDataSource(); + contextOrg: IdlObject; + searchOrgs: OrgFamily; + viewPerms: string; + + @ViewChild('grid', { static: true }) grid: GridComponent; + noSelectedRows: boolean; + oneSelectedRow: boolean; + onlyBatchesSelected: boolean; + + @ViewChild('newSessionDialog', { static: true }) newSessionDialog: NewSessionDialogComponent; + @ViewChild('createSuccessString', { static: false }) createSuccessString: StringComponent; + @ViewChild('createFailedString', { static: false }) createFailedString: StringComponent; + @ViewChild('deleteSessionConfirmDialog', { static: true }) deleteSessionConfirmDialog: ConfirmDialogComponent; + + constructor( + private auth: AuthService, + private flatData: GridFlatDataService, + private format: FormatService, + private idl: IdlService, + private net: NetService, + private org: OrgService, + private pcrud: PcrudService, + private perm: PermService, + private route: ActivatedRoute, + private router: Router, + private toast: ToastService, + ) {} + + ngOnInit() { + + this.sessionIdlClassDef = this.idl.classes[this.sessionIdlClass]; + this.batchIdlClassDef = this.idl.classes[this.batchIdlClass]; + this.viewIdlClassDef = this.idl.classes[this.viewIdlClass]; + this.viewPKeyField = this.viewIdlClassDef.pkey || 'id'; + + this.viewPermaCrud = this.viewIdlClassDef.permacrud || {}; + if (this.viewPermaCrud.retrieve) { + this.viewPerms = this.viewPermaCrud.retrieve.perms; + } + + const contextOrg = this.route.snapshot.queryParamMap.get('contextOrg'); + this.applyOrgValues(Number(contextOrg)); + + this.checkCreateSessionPerms(); + this.checkCreateBatchPerms(); + + this.initDataSource(); + this.gridSelectionChange( [] ); + } + + viewSessionUrls() { + var rows = this.grid.context.getSelectedRows(); + const ids = Array.from( new Set( rows.map(x => Number(x.session_id)) ) ); + this.router.navigate(['/staff/cat/linkchecker/urls/'], { queryParams: { sessions: JSON.stringify(ids) } }); + } + + viewSessionAttempts() { + var rows = this.grid.context.getSelectedRows(); + const ids = Array.from( new Set( rows.map(x => Number(x.session_id)) ) ); + this.router.navigate(['/staff/cat/linkchecker/attempts/'], { queryParams: { sessions: JSON.stringify(ids) } }); + } + + viewBatchAttempts() { + var rows = this.grid.context.getSelectedRows(); + const ids = Array.from( new Set( rows.map(x => Number(x.batch_id)) ) ); + this.router.navigate(['/staff/cat/linkchecker/attempts/'], { queryParams: { batches: JSON.stringify(ids) } }); + } + + applyOrgValues(orgId?: number) { + this.contextOrg = this.org.get(orgId) || this.org.get(this.auth.user().ws_ou()) || this.org.root(); + this.searchOrgs = {primaryOrgId: this.contextOrg.id()}; + } + + gridSelectionChange(keys: string[]) { + var rows = this.grid.context.getSelectedRows(); + + this.noSelectedRows = (rows.length === 0); + this.oneSelectedRow = (rows.length === 1); + this.onlyBatchesSelected = ! this.noSelectedRows; + + rows.forEach(row => { + if (!row.batch_id) { + this.onlyBatchesSelected = false; + } + }); + } + + checkCreateSessionPerms() { + this.canCreateSession = false; + const pc = this.sessionIdlClassDef.permacrud || {}; + const perms = pc.create ? pc.create.perms : []; + if (perms.length === 0) { return; } + + this.perm.hasWorkPermAt(perms, true).then(permMap => { + Object.keys(permMap).forEach(key => { + if (permMap[key].length > 0) { + this.canCreateSession = true; + } + }); + }); + } + + checkCreateBatchPerms() { + this.canCreateBatch = false; + const pc = this.batchIdlClassDef.permacrud || {}; + const perms = pc.create ? pc.create.perms : []; + if (perms.length === 0) { return; } + + this.perm.hasWorkPermAt(perms, true).then(permMap => { + Object.keys(permMap).forEach(key => { + if (permMap[key].length > 0) { + this.canCreateBatch = true; + } + }); + }); + } + + newSessionWrapper(optionalSessionToClone) { + this.newSessionDialog.sessionToClone = optionalSessionToClone; + this.newSessionDialog.open({size: 'lg'}).subscribe( (res) => { + console.log('dialog res', res); + if (res && res['sessionId']) { + if (res['viewURLs']) { + this.router.navigate(['/staff/cat/linkchecker/urls/'], + { queryParams: { sessions: JSON.stringify([ Number(res['sessionId']) ]) } }); + } else if (res['viewAttempts']) { + this.router.navigate(['/staff/cat/linkchecker/attempts/'], + { queryParams: { sessions: JSON.stringify([ Number(res['sessionId']) ]) } }); + } else { + this.grid.reload(); + } + } + }); + } + + cloneSelectedSession() { + var rows = this.grid.context.getSelectedRows(); + if (rows.length !== 1) { return; } + this.newSessionWrapper(rows[0]); + } + + deleteSelectedSessions() { + var rows = this.grid.context.getSelectedRows(); + if (rows.length === 0) { return; } + + this.deleteSessionConfirmDialog.open().subscribe(doIt => { + if (!doIt) { return; } + + var session_ids = rows.map(r => r.session_id ); + + var that = this; + function delete_next(ids) { + var id = ids.pop(); + if (id) { + that.net.request( + 'open-ils.url_verify', + 'open-ils.url_verify.session.delete', + that.auth.token(), + id, + ).subscribe( + (res) => { + console.log('session.delete res', res); + // toast + }, + (err) => { + console.log('session.delete err', err); + // toast + }, + () => { + console.log('session.delete finis'); + delete_next(ids); + } + ); + } else { + setTimeout( () => { that.grid.reload(); } ); + } + } + + delete_next(session_ids); + + }); + } + + initDataSource() { + this.dataSource.getRows = (pager: Pager, sort: any[]) => { + + let query: any = {} + + if (this.searchOrgs || this.contextOrg) { + query[this.viewOrgField] = + this.searchOrgs.orgIds || [this.contextOrg.id()]; + } + + let query_filters = []; + Object.keys(this.dataSource.filters).forEach(key => { + query_filters = query_filters.concat( this.dataSource.filters[key] ); + }); + + if (query_filters.length > 0) { + query['-and'] = query_filters; + } + + return this.flatData.getRows( + this.grid.context, query, pager, sort); + }; + } +} diff --git a/Open-ILS/src/eg2/src/app/staff/cat/linkchecker/linkchecker.module.ts b/Open-ILS/src/eg2/src/app/staff/cat/linkchecker/linkchecker.module.ts new file mode 100644 index 0000000000..ea1622a791 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/cat/linkchecker/linkchecker.module.ts @@ -0,0 +1,33 @@ +import {NgModule} from '@angular/core'; +import {StaffCommonModule} from '@eg/staff/common.module'; +import {AdminCommonModule} from '@eg/staff/admin/common.module'; +import {CommonWidgetsModule} from '@eg/share/common-widgets.module'; +import {LinkCheckerRoutingModule} from './routing.module'; +import {LinkCheckerComponent} from './linkchecker.component'; +import {LinkCheckerUrlsComponent} from './urls.component'; +import {LinkCheckerAttemptsComponent} from './attempts.component'; +import {NewSessionDialogComponent} from './new-session-dialog.component'; +import {HttpClientModule} from '@angular/common/http'; +import {OrgFamilySelectModule} from '@eg/share/org-family-select/org-family-select.module'; + +@NgModule({ + declarations: [ + LinkCheckerComponent, + LinkCheckerUrlsComponent, + LinkCheckerAttemptsComponent, + NewSessionDialogComponent + ], + imports: [ + StaffCommonModule, + AdminCommonModule, + HttpClientModule, + CommonWidgetsModule, + OrgFamilySelectModule, + LinkCheckerRoutingModule + ], + providers: [ + ] +}) + +export class LinkCheckerModule { +} diff --git a/Open-ILS/src/eg2/src/app/staff/cat/linkchecker/new-session-dialog.component.html b/Open-ILS/src/eg2/src/app/staff/cat/linkchecker/new-session-dialog.component.html new file mode 100644 index 0000000000..dfbe23669e --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/cat/linkchecker/new-session-dialog.component.html @@ -0,0 +1,149 @@ + + + + + + + + diff --git a/Open-ILS/src/eg2/src/app/staff/cat/linkchecker/new-session-dialog.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/linkchecker/new-session-dialog.component.ts new file mode 100644 index 0000000000..62420b53b4 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/cat/linkchecker/new-session-dialog.component.ts @@ -0,0 +1,345 @@ +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 {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 {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-new-session-dialog', + templateUrl: './new-session-dialog.component.html' +}) + +export class NewSessionDialogComponent extends DialogComponent implements OnInit { + + @Input() sessionToClone: IdlObject; // not really a "session", but a combined session/batch view + + progressText: string = ''; + + savedSearchIdlClass = 'asq'; + + encounteredError: boolean = false; + nameCollision: boolean = false; + + subscriptions: Subscription[] = []; + + sessionId: number; + sessionName: string = ''; + sessionNameModelChanged: Subject = new Subject(); + + sessionOwningLibrary: IdlObject; + sessionSearchScope: IdlObject; + sessionSearch: string = ''; + sessionSavedSearch: number = null; + + selectorModels: any = { + 'tag' : [], + 'subfields' : [] + } + savedSearchEntries: ComboboxEntry[] = []; + savedSearchObjectCache: any = {}; + + @ViewChild('newSessionForm', { static: false}) newSessionForm: NgForm; + @ViewChild('savedSearchSelector', { static: true}) savedSearchSelector: ComboboxComponent; + @ViewChild('fail', { static: true }) private fail: AlertDialogComponent; + @ViewChild('progress', { static: true }) private progress: ProgressDialogComponent; + + 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, + ) { + super(modal); + } + + ngOnInit() { + + this.subscriptions.push( this.onOpen$.subscribe( + _ => { + this.stopProgressMeter(); + if (this.sessionToClone) { + this.sessionName = 'Copy of ' + this.sessionToClone.name(); + this.sessionOwningLibrary = this.sessionToClone.owning_lib().id(); + this.sessionSearch = this.sessionToClone.search(); + this.selectorModels = { 'tag' : ['856'], 'subfields' : ['u'] }; + this.pcrud.search('uvus', {'session':this.sessionToClone.session_id()},{},{'atomic':true}).subscribe( + (list) => { + console.log('list',list); + list.forEach( (s,idx) => { + let xpath = s.xpath(); + this.selectorModels.tag[idx] = xpath.match(/tag='(\d+)'/)[1]; + this.selectorModels.subfields[idx] = ''; + let matches = xpath.matchAll(/code='(.)'/g); + for (const match of matches) { + this.selectorModels.subfields[idx] += match[1]; + } + console.log('idx',idx); + console.log('xpath',xpath); + console.log('tag',this.selectorModels.tag[idx]); + console.log('subfields',this.selectorModels.subfields[idx]); + }); + } + ); + } + const el = this.renderer.selectRootElement('#session_name'); + if (el) { el.focus(); el.select(); } + } + )); + + this.sessionOwningLibrary = this.auth.user().ws_ou(); + this.selectorModels['tag'][0] = '856'; + this.selectorModels['subfields'][0] = 'u'; + + this.subscriptions.push( + this.pcrud.retrieveAll(this.savedSearchIdlClass).subscribe(search => { + this.savedSearchEntries.push({id: search.id(), label: search.label()}); + this.savedSearchObjectCache[ search.id() ] = search; + } + )); + + this.subscriptions.push( + this.sessionNameModelChanged + .pipe( + debounceTime(300), + distinctUntilChanged() + ) + .subscribe( newText => { + this.sessionName = newText; + this.nameCollision = false; + this.subscriptions.push( + this.pcrud.search('uvs',{ + owning_lib: this.sessionOwningLibrary, + name: this.sessionName},{}) + .subscribe( + result => { this.nameCollision = true; } + ) + ); + }) + ); + console.log('new-session-dialog this', this); + } + + ngOnDestroy() { + this.subscriptions.forEach((subscription) => { + subscription.unsubscribe(); + }); + } + + applyOwningLibrary(p: any) { + // [applyOrgId]="sessionOwningLibrary" is working fine + } + + applySearchScope(p: any) { + // [applyOrgId]="sessionSearchScope" was not working fine. + // This also preserves null's, which is important since we'll + // reapply this scope if applicable after applying a saved + // search. + this.sessionSearchScope = p; + if (p) { + this.sessionSearch = this.sessionSearch.replace( + /^(.*)(site\(.+?\))(.*)$/, + "$1site(" + p.shortname() + ")$3" + ); + if (! this.sessionSearch.match(/site\(.+?\)/)) { + this.sessionSearch += ' site(' + p.shortname() + ')' + } + this.applySessionSearch( this.sessionSearch ); + } + } + + applySavedSearch(p: any) { + var obj = this.savedSearchObjectCache[p.id]; + if (obj) { + this.sessionSearch = obj.query_text(); + this.applySearchScope( this.sessionSearchScope ); + } + } + + applySessionSearch(p: any) { + } + + // https://stackoverflow.com/questions/42322968/angular2-dynamic-input-field-lose-focus-when-input-changes + trackByIdx(index: any, item: any) { + return index; + } + + addSelectorRow(index: number): void { + this.selectorModels['tag'].splice(index, 0, ''); + this.selectorModels['subfields'].splice(index, 0, ''); + } + + delSelectorRow(index: number): void { + this.selectorModels['tag'].splice(index, 1); + this.selectorModels['subfields'].splice(index, 1); + } + + stopProgressMeter() { + this.progress.close(); + this.progress.reset(); + } + + resetProgressMeter(s: string) { + this.progressText = s; + this.progress.reset(); + } + + startProgressMeter(s: string) { + this.progressText = s; + this.progress.reset(); + this.progress.open(); + } + + createNewSession(options) { + /////////////////////////////////////////////// + this.startProgressMeter($localize`Creating session...`); + this.subscriptions.push(this.net.request( + 'open-ils.url_verify', + 'open-ils.url_verify.session.create', + this.auth.token(), + this.sessionName, + this.sessionSearch, + this.sessionOwningLibrary + ).subscribe( + (res) => { + if (this.evt.parse(res)) { + console.error('session.create ils_event',res); + this.fail.open(); + this.stopProgressMeter(); + this.close(false); + } else { + this.sessionId = res; + options['sessionId'] = res; + ///////////////////////////////////////////////////// + this.resetProgressMeter($localize`Creating URL selectors...`); + this.subscriptions.push( + this.createUrlSelectors().subscribe( + (res2) => { + if (this.evt.parse(res2)) { + console.error('url_selector.create error',res2); + this.fail.open(); + this.stopProgressMeter(); + this.close(false); + } else { + console.log('url_selector',res2); + } + }, + (err2) => { + console.error('url_selector.create error',err2); + this.fail.open(); + this.stopProgressMeter(); + this.close(false); + }, + () => { + //////////////////////////////////////////////////////////// + this.resetProgressMeter($localize`Searching and extracting URLs...`); + this.subscriptions.push(this.net.request( + 'open-ils.url_verify', + 'open-ils.url_verify.session.search_and_extract', + this.auth.token(), + this.sessionId + ).subscribe( + (res3) => { + console.log('res3',res3); + if (!this.progress.hasMax()) { + // first response returned by the API is the number of search results + // We'll become a determinate progress meter for this section + this.progress.update({max: res3, value: 0}); + } else { + // subsequent responses are the number of URLs extracted from each search result + this.progress.increment(); + } + }, + (err3) => { + console.log('err3',err3); + this.stopProgressMeter(); + this.close(false); + }, + () => { + if (options['fullAuto']) { + options['viewURLs'] = false; + options['viewAttempts'] = true; + ///////////////////////////////////////////// + this.resetProgressMeter($localize`Verifying URLs...`); + this.subscriptions.push(this.net.request( + 'open-ils.url_verify', + 'open-ils.url_verify.session.verify', + this.auth.token(), + this.sessionId + ).subscribe( + (res4) => { + console.log('res4',res4); + this.progress.update({max: res4['url_count'], value: res4['total_processed']}); + }, + (err4) => { + this.stopProgressMeter(); + console.log('err4',err4); + this.close(false); + }, + () => { + this.nameCollision = true; + this.stopProgressMeter(); + this.close(options); + } + )); + } else { + this.nameCollision = true; + this.stopProgressMeter(); + this.close(options); + } + } + )); + } + ) + ); + } + }, + (err) => { + console.error('session.create error',err); + this.fail.open(); + this.stopProgressMeter(); + this.close(false); + } + )); + } + + createUrlSelectors(): Observable { + // Examples: + //*[@tag='856']/*[@code='u'] + //*[@tag='956']/*[@code='a' or @code='b' or @code='c'] + console.log('createUrlSelectors'); + var xpaths: string[] = []; + var selectors: IdlObject[] = []; + for (var i = 0; i < this.selectorModels['tag'].length; i++) { + let tag = this.selectorModels['tag'][i]; + let subfields = this.selectorModels['subfields'][i]; + let xpath = "//*[@tag='" + tag + "']/*[" + subfields.split('').map( e => "@code='" + e + "'" ).join(' or ') + ']'; + xpaths.push(xpath); + } + xpaths = Array.from( new Set( xpaths ) ); // dedupe + selectors = xpaths.map( _xpath => { + var uvus = this.idl.create('uvus'); + uvus.isnew(true); + uvus.session(this.sessionId); + uvus.xpath( _xpath ); + return uvus; + }); + return this.pcrud.create(selectors); + } + +} diff --git a/Open-ILS/src/eg2/src/app/staff/cat/linkchecker/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/cat/linkchecker/routing.module.ts new file mode 100644 index 0000000000..761236efbc --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/cat/linkchecker/routing.module.ts @@ -0,0 +1,25 @@ +import {NgModule} from '@angular/core'; +import {RouterModule, Routes} from '@angular/router'; +import {LinkCheckerComponent} from './linkchecker.component'; +import {LinkCheckerUrlsComponent} from './urls.component'; +import {LinkCheckerAttemptsComponent} from './attempts.component'; + +const routes: Routes = [{ + path: '', + component: LinkCheckerComponent +}, { + path: 'urls', + component: LinkCheckerUrlsComponent +}, { + path: 'attempts', + component: LinkCheckerAttemptsComponent +}]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], + providers: [] +}) + +export class LinkCheckerRoutingModule {} + diff --git a/Open-ILS/src/eg2/src/app/staff/cat/linkchecker/urls.component.html b/Open-ILS/src/eg2/src/app/staff/cat/linkchecker/urls.component.html new file mode 100644 index 0000000000..49e766bdf9 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/cat/linkchecker/urls.component.html @@ -0,0 +1,33 @@ + +
+ + {sessions, plural, =0 {URLs for All Sessions} =1 {URLs for Session ID {{sessions}}} other {URLs for Session IDs {{sessions}}}} + +
+ + + + + + + + + + + + + + + diff --git a/Open-ILS/src/eg2/src/app/staff/cat/linkchecker/urls.component.ts b/Open-ILS/src/eg2/src/app/staff/cat/linkchecker/urls.component.ts new file mode 100644 index 0000000000..02abdeabb0 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/cat/linkchecker/urls.component.ts @@ -0,0 +1,181 @@ +import * as Moment from 'moment-timezone'; +import {Router, ActivatedRoute} from '@angular/router'; +import {AuthService} from '@eg/core/auth.service'; +import {ComboboxEntry} from '@eg/share/combobox/combobox.component'; +import {Component, OnInit, ViewChild, Input, TemplateRef} from '@angular/core'; +import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component'; +import {DateSelectComponent} from '@eg/share/date-select/date-select.component'; +import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component'; +import {FormGroup, FormControl} from '@angular/forms'; +import {FormatService} from '@eg/core/format.service'; +import {GridComponent} from '@eg/share/grid/grid.component'; +import {GridDataSource, GridColumn, GridRowFlairEntry, GridCellTextGenerator} from '@eg/share/grid/grid'; +import {GridFlatDataService} from '@eg/share/grid/grid-flat-data.service'; +import {HtmlToTxtService} from '@eg/share/util/htmltotxt.service'; +import {IdlService, IdlObject} from '@eg/core/idl.service'; +import {NetService} from '@eg/core/net.service'; +import {NgbDate} from '@ng-bootstrap/ng-bootstrap'; +import {OrgFamily} from '@eg/share/org-family-select/org-family-select.component'; +import {OrgService} from '@eg/core/org.service'; +import {Pager} from '@eg/share/util/pager'; +import {PcrudService} from '@eg/core/pcrud.service'; +import {PermService} from '@eg/core/perm.service'; +import {PrintService} from '@eg/share/print/print.service'; +import {ProgressDialogComponent} from '@eg/share/dialog/progress.component'; +import {SampleDataService} from '@eg/share/util/sample-data.service'; +import {StringComponent} from '@eg/share/string/string.component'; +import {StringService} from '@eg/share/string/string.service'; +import {ToastService} from '@eg/share/toast/toast.service'; +import {map, take} from 'rxjs/operators'; +import {timer as observableTimer, Observable, of} from 'rxjs'; + +@Component({ + templateUrl: 'urls.component.html' +}) +export class LinkCheckerUrlsComponent implements OnInit { + + sessions: number[]; + newBatches: number[] = []; + urlsIdlClass = 'uvu'; + urlsSortField = 'name'; + urlsSessionField = 'session'; + urlsFleshFields = { + 'uvu' : ['item','url_selector'], + 'uvsbrem' : ['target_biblio_record_entry'], + 'bre' : ['simple_record'] + }; + urlsFleshDepth = 3; + urlsIdlClassDef: any; + urlsPKeyField: string; + + urlsPermaCrud: any; + urlsPerms: string; + + @ViewChild('progress', { static: true }) private progress: ProgressDialogComponent; + progressText: string = ''; + + @ViewChild('grid', { static: true }) grid: GridComponent; + dataSource: GridDataSource = new GridDataSource(); + noSelectedRows: boolean; + oneSelectedRow: boolean; + + constructor( + private auth: AuthService, + private flatData: GridFlatDataService, + private format: FormatService, + private idl: IdlService, + private net: NetService, + private org: OrgService, + private pcrud: PcrudService, + private perm: PermService, + private route: ActivatedRoute, + private router: Router, + ) {} + + ngOnInit() { + this.route.queryParams.subscribe( params => { + if (params.sessions) { + this.sessions = JSON.parse( params.sessions ); + this.grid.reload(); + } + }); + + this.urlsIdlClassDef = this.idl.classes[this.urlsIdlClass]; + this.urlsPKeyField = this.urlsIdlClassDef.pkey || 'id'; + + this.urlsPermaCrud = this.urlsIdlClassDef.permacrud || {}; + if (this.urlsPermaCrud.retrieve) { + this.urlsPerms = this.urlsPermaCrud.retrieve.perms; + } + + this.initDataSource(); + this.gridSelectionChange( [] ); + } + + gridSelectionChange(keys: string[]) { + this.noSelectedRows = (keys.length === 0); + this.oneSelectedRow = (keys.length === 1); + var rows = this.grid.context.getSelectedRows(); + } + + initDataSource() { + this.dataSource.getRows = (pager: Pager, sort: any[]) => { + + let query: any = {} + + if (this.sessions) { + query[this.urlsSessionField] = this.sessions; + } + + let query_filters = []; + Object.keys(this.dataSource.filters).forEach(key => { + query_filters = query_filters.concat( this.dataSource.filters[key] ); + }); + + if (query_filters.length > 0) { + query['-and'] = query_filters; + } + + return this.flatData.getRows( + this.grid.context, query, pager, sort); + }; + } + + stopProgressMeter() { + this.progress.close(); + this.progress.reset(); + } + + resetProgressMeter(s: string) { + this.progressText = s; + this.progress.reset(); + } + + startProgressMeter(s: string) { + this.progressText = s; + this.progress.reset(); + this.progress.open(); + } + + verifyUrlsFilteredForSession(rows,ses_ids) { + var ses_id = ses_ids.pop(); + if (ses_id) { + this.resetProgressMeter($localize`Verifying selected URLs for Session ${ses_id}...`); + console.log('Verifying selected URLs for Session ' + ses_id); + this.net.request( + 'open-ils.url_verify', + 'open-ils.url_verify.session.verify', + this.auth.token(), + ses_id, + rows.filter( url => url.session() === ses_id ).map( url => url.id() ) + ).subscribe( + (res) => { + console.log('res',res); + this.progress.update({max: res['url_count'], value: res['total_processed']}); + if (res['attempt']) { // last response + this.newBatches.push(res.attempt.id()); + } + }, + (err) => { + this.stopProgressMeter(); + console.log('err',err); + }, + () => { + this.verifyUrlsFilteredForSession(rows,ses_ids); + } + ); + } else { + console.log('go to attempts page'); + this.stopProgressMeter(); + this.router.navigate(['/staff/cat/linkchecker/attempts/'], { + queryParams: { batches: JSON.stringify( Array.from( new Set( this.newBatches ) ) ) } }); + } + } + + verifySelectedUrls() { + const rows = this.grid.context.getSelectedRows(); + let session_ids = Array.from( new Set( rows.map(x => Number(x.session())) ) ); + this.startProgressMeter($localize`Verifying selected URLs for Sessions ${session_ids}...`); + this.verifyUrlsFilteredForSession(rows,session_ids); + } +} diff --git a/Open-ILS/src/eg2/src/app/staff/cat/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/cat/routing.module.ts index cc0d4eb46b..65319b6a67 100644 --- a/Open-ILS/src/eg2/src/app/staff/cat/routing.module.ts +++ b/Open-ILS/src/eg2/src/app/staff/cat/routing.module.ts @@ -11,6 +11,10 @@ const routes: Routes = [ loadChildren: () => import('./authority/authority.module').then(m => m.AuthorityModule) }, { + path: 'linkchecker', + loadChildren: () => + import('./linkchecker/linkchecker.module').then(m => m.LinkCheckerModule) + }, { path: 'marcbatch', loadChildren: () => import('./marcbatch/marcbatch.module').then(m => m.MarcBatchModule) diff --git a/Open-ILS/src/eg2/src/app/staff/nav.component.html b/Open-ILS/src/eg2/src/app/staff/nav.component.html index 7054ed6853..faf8c5f057 100644 --- a/Open-ILS/src/eg2/src/app/staff/nav.component.html +++ b/Open-ILS/src/eg2/src/app/staff/nav.component.html @@ -286,7 +286,7 @@ MARC Batch Edit - + Link Checker -- 2.11.0