export interface GridRowFlairEntry {
icon: string; // name of material icon
- title: string; // tooltip string
+ title?: string; // tooltip string
}
export class GridColumnPersistConf {
--- /dev/null
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes} from '@angular/router';
+
+const routes: Routes = [
+ { path: 'vandelay',
+ loadChildren: '@eg/staff/cat/vandelay/vandelay.module#VandelayModule'
+ }
+];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule]
+})
+
+export class CatRoutingModule {}
--- /dev/null
+
+<ngb-tabset #tabs [activeId]="attrType" (tabChange)="onTabChange($event)">
+ <ngb-tab title="Bibliographic Attributes" i18n-title id="bib">
+ <ng-template ngbTabContent>
+ <div class="mt-3">
+ <eg-admin-page idlClass="vqbrad"></eg-admin-page>
+ </div>
+ </ng-template>
+ </ngb-tab>
+ <ngb-tab title="Authority Attributes" i18n-title id="authority">
+ <ng-template ngbTabContent>
+ <div class="mt-3">
+ <eg-admin-page idlClass="vqarad"></eg-admin-page>
+ </div>
+ </ng-template>
+ </ngb-tab>
+</ngb-tabset>
--- /dev/null
+import {Component, OnInit, ViewChild} from '@angular/core';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import {NgbTabset, NgbTabChangeEvent} from '@ng-bootstrap/ng-bootstrap';
+
+@Component({
+ templateUrl: 'display-attrs.component.html'
+})
+export class DisplayAttrsComponent {
+
+ attrType: string;
+
+ constructor(
+ private router: Router,
+ private route: ActivatedRoute) {
+
+ this.route.paramMap.subscribe((params: ParamMap) => {
+ this.attrType = params.get('atype');
+ });
+ }
+
+ // Changing a tab in the UI means changing the route.
+ // Changing the route ultimately results in changing the tab.
+ onTabChange(evt: NgbTabChangeEvent) {
+ this.attrType = evt.nextId;
+
+ // prevent tab changing until after route navigation
+ evt.preventDefault();
+
+ const url =
+ `/staff/cat/vandelay/display_attrs/${this.attrType}`;
+
+ this.router.navigate([url]);
+ }
+}
+
--- /dev/null
+<h2 i18n>Export Records</h2>
+
+<div class="common-form striped-even form-validated">
+ <div class="row">
+ <div class="col-lg-6">
+ <div class="row"><label>Select a Record Source</label></div>
+ <ngb-accordion [closeOthers]="true" activeIds="csv"
+ (panelChange)="sourceChange($event)">
+ <ngb-panel id="csv" title="CSV File">
+ <ng-template ngbPanelContent>
+ <div class="row">
+ <div class="col-lg-6">
+ <label i18n>Use Field Number</label>
+ </div>
+ <div class="col-lg-6">
+ <input id='csv-input' type="number" class="form-control"
+ [(ngModel)]="fieldNumber"
+ i18n-placeholder placeholder="Starts at 0..."/>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-lg-6">
+ <label i18n>From CSV file</label>
+ </div>
+ <div class="col-lg-6">
+ <input #fileSelector (change)="fileSelected($event)"
+ class="form-control" type="file"/>
+ </div>
+ </div>
+ </ng-template>
+ </ngb-panel>
+ <ngb-panel id="record-id" title="Record ID">
+ <ng-template ngbPanelContent>
+ <div class="row">
+ <div class="col-lg-6">
+ <label i18n>Record ID</label>
+ </div>
+ <div class="col-lg-6">
+ <input id='record-id-input' type="number"
+ class="form-control" [(ngModel)]="recordId"/>
+ </div>
+ </div>
+ </ng-template>
+ </ngb-panel>
+ <ngb-panel id="bucket-id" title="Bucket">
+ <ng-template ngbPanelContent>
+ <div class="row">
+ <div class="col-lg-6">
+ <label i18n>Bucket ID</label>
+ </div>
+ <div class="col-lg-6">
+ <input id='bucket-id-input' type="number"
+ class="form-control" [(ngModel)]="bucketId"/>
+ </div>
+ </div>
+ </ng-template>
+ </ngb-panel>
+ </ngb-accordion>
+ </div><!-- col -->
+ <div class="col-lg-6">
+ <div class="row">
+ <div class="col-lg-6">
+ <label i18n>Record Type</label>
+ </div>
+ <div class="col-lg-6">
+ <select class="form-control" [(ngModel)]="recordType">
+ <option i18n value="biblio">Bibliographic Records</option>
+ <option i18n value="authority">Authority Records</option>
+ </select>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-lg-6">
+ <label i18n>Record Format</label>
+ </div>
+ <div class="col-lg-6">
+ <select class="form-control" [(ngModel)]="recordFormat">
+ <option i18n value="USMARC">MARC21</option>
+ <option i18n value="UNIMARC">UNIMARC</option>
+ <option i18n value="XML">MARC XML</option>
+ <option i18n value="BRE">Evergreen Record Entry</option>
+ </select>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-lg-6">
+ <label i18n>Record Encoding</label>
+ </div>
+ <div class="col-lg-6">
+ <select class="form-control" [(ngModel)]="recordEncoding">
+ <option i18n value="UTF-8">UTF-8</option>
+ <option i18n value="MARC8">MARC8</option>
+ </select>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-lg-6">
+ <label i18n>Include holdings in Bibliographic Records</label>
+ </div>
+ <div class="col-lg-6">
+ <input class="form-check-input" type="checkbox" [(ngModel)]="includeHoldings">
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-lg-10 offset-lg-1">
+ <button class="btn btn-success btn-lg btn-block font-weight-bold"
+ [disabled]="isExporting || !hasNeededData()"
+ (click)="exportRecords()" i18n>Export</button>
+ </div>
+ </div>
+ <div class="row" [hidden]="!isExporting">
+ <div class="col-lg-10 offset-lg-1">
+ <eg-progress-inline #exportProgress></eg-progress-inline>
+ </div>
+ </div>
+ </div><!-- left col -->
+ </div><!-- row -->
+</div>
+
--- /dev/null
+import {Component, AfterViewInit, ViewChild, Renderer2} from '@angular/core';
+import {NgbPanelChangeEvent} from '@ng-bootstrap/ng-bootstrap';
+import {HttpClient, HttpRequest, HttpEventType} from '@angular/common/http';
+import {HttpResponse, HttpErrorResponse} from '@angular/common/http';
+import {saveAs} from 'file-saver/FileSaver';
+import {AuthService} from '@eg/core/auth.service';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {ProgressInlineComponent} from '@eg/share/dialog/progress-inline.component';
+import {VandelayService, VANDELAY_EXPORT_PATH} from './vandelay.service';
+
+
+@Component({
+ templateUrl: 'export.component.html'
+})
+export class ExportComponent implements AfterViewInit {
+
+ recordSource: string;
+ fieldNumber: number;
+ selectedFile: File;
+ recordId: number;
+ bucketId: number;
+ recordType: string;
+ recordFormat: string;
+ recordEncoding: string;
+ includeHoldings: boolean;
+ isExporting: boolean;
+
+ @ViewChild('fileSelector') private fileSelector;
+ @ViewChild('exportProgress')
+ private exportProgress: ProgressInlineComponent;
+
+ constructor(
+ private renderer: Renderer2,
+ private http: HttpClient,
+ private toast: ToastService,
+ private auth: AuthService
+ ) {
+ this.recordType = 'biblio';
+ this.recordFormat = 'USMARC';
+ this.recordEncoding = 'UTF-8';
+ this.includeHoldings = false;
+ }
+
+ ngAfterViewInit() {
+ this.renderer.selectRootElement('#csv-input').focus();
+ }
+
+ sourceChange($event: NgbPanelChangeEvent) {
+ this.recordSource = $event.panelId;
+
+ if ($event.nextState) { // panel opened
+
+ // give the panel a chance to render before focusing input
+ setTimeout(() => {
+ this.renderer.selectRootElement(
+ `#${this.recordSource}-input`).focus();
+ })
+ }
+ }
+
+ fileSelected($event) {
+ this.selectedFile = $event.target.files[0];
+ }
+
+ hasNeededData(): boolean {
+ return Boolean(
+ this.selectedFile || this.recordId || this.bucketId
+ );
+ }
+
+ exportRecords() {
+ this.isExporting = true;
+ this.exportProgress.update({value: 0});
+
+ const formData: FormData = new FormData();
+
+ formData.append('ses', this.auth.token());
+ formData.append('rectype', this.recordType);
+ formData.append('encoding', this.recordEncoding);
+ formData.append('format', this.recordFormat);
+
+ if (this.includeHoldings) {
+ formData.append('holdings', '1');
+ }
+
+ switch (this.recordSource) {
+
+ case 'csv':
+ formData.append('idcolumn', ''+this.fieldNumber);
+ formData.append('idfile',
+ this.selectedFile, this.selectedFile.name);
+ break;
+
+ case 'record-id':
+ formData.append('id', ''+this.recordId);
+ break;
+
+ case 'bucket-id':
+ formData.append('containerid', ''+this.bucketId);
+ break;
+ }
+
+ this.sendExportRequest(formData);
+ }
+
+ sendExportRequest(formData: FormData) {
+
+ const fileName = `export.${this.recordType}.` +
+ `${this.recordEncoding}.${this.recordFormat}`;
+
+ const req = new HttpRequest('POST', VANDELAY_EXPORT_PATH,
+ formData, {reportProgress: true, responseType: 'text'});
+
+ this.http.request(req).subscribe(
+ evt => {
+ console.log(evt);
+ if (evt.type === HttpEventType.DownloadProgress) {
+ // File size not reported by server in advance.
+ this.exportProgress.update({value: evt.loaded});
+
+ } else if (evt instanceof HttpResponse) {
+
+ saveAs(new Blob([evt.body],
+ {type: 'application/octet-stream'}), fileName);
+
+ this.isExporting = false;
+ }
+ },
+
+ (err: HttpErrorResponse) => {
+ console.error(err);
+ this.toast.danger(err.error);
+ this.isExporting = false;
+ }
+ );
+ }
+}
+
--- /dev/null
+import {Component} from '@angular/core';
+
+@Component({
+ template: `<eg-admin-page idlClass="viiad"></eg-admin-page>`
+})
+export class HoldingsProfilesComponent {
+ constructor() {}
+}
+
--- /dev/null
+<div class="row mb-3" *ngIf="importSelection()">
+ <div class="col-lg-2" *ngIf="selectedQueue">
+ <button class="btn btn-info label-with-material-icon"
+ routerLink="/staff/cat/vandelay/queue/{{recordType}}/{{selectedQueue.id}}">
+ <span class="material-icons">arrow_back</span>
+ <span i18n>Return to Queue</span>
+ </button>
+ </div>
+</div>
+
+<h2 i18n>MARC File Upload</h2>
+<div class="common-form striped-odd form-validated ml-3 mr-3">
+ <div class="row">
+ <div class="col-lg-3">
+ <label i18n>Record Type</label>
+ </div>
+ <div class="col-lg-3">
+ <eg-combobox (onChange)="selectEntry($event, 'recordType')"
+ [disabled]="importSelection()"
+ [required]="true"
+ [startId]="recordType" placeholder="Record Type..." i18n-placeholder>
+ <eg-combobox-entry entryId="bib" entryLabel="Bibliographic Records"
+ i18n-entryLabel></eg-combobox-entry>
+ <eg-combobox-entry entryId="authority" entryLabel="Authority Records"
+ i18n-entryLabel></eg-combobox-entry>
+ <eg-combobox-entry entryId="bib-acq" entryLabel="Acquisitions Records"
+ i18n-entryLabel></eg-combobox-entry>
+ </eg-combobox>
+ </div>
+ <div class="col-lg-3">
+ <label i18n>Select a Record Source</label>
+ </div>
+ <div class="col-lg-3">
+ <eg-combobox [entries]="formatEntries('bibSources')"
+ (onChange)="selectEntry($event, 'bibSources')"
+ [startId]="selectedBibSource"
+ placeholder="Record Source..." i18n-placeholder>
+ </eg-combobox>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-lg-3">
+ <label i18n>Select or Create a Qeueue</label>
+ </div>
+ <div class="col-lg-3">
+ <eg-combobox [entries]="formatEntries('allQueues')"
+ [startId]="startQueueId"
+ [startIdFiresOnChange]="true"
+ [disabled]="startQueueId"
+ (onChange)="selectedQueue=$event" i18n-placeholder
+ [required]="true"
+ [allowFreeText]="true" placeholder="Select or Create a Queue...">
+ </eg-combobox>
+ </div>
+ <div class="col-lg-3">
+ <label i18n>Limit Matches to Bucket</label>
+ </div>
+ <div class="col-lg-3">
+ <eg-combobox [entries]="formatEntries('bibBuckets')"
+ [startId]="selectedBucket"
+ [disabled]="(selectedQueue && !selectedQueue.freetext) || importSelection()"
+ (onChange)="selectEntry($event, 'bibBuckets')"
+ placeholder="Buckets..." i18n-placeholder></eg-combobox>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-lg-3">
+ <label i18n>Record Match Set</label>
+ </div>
+ <div class="col-lg-3">
+ <eg-combobox [entries]="formatEntries('matchSets')"
+ [disabled]="(selectedQueue && !selectedQueue.freetext) || importSelection()"
+ [startId]="selectedMatchSet || defaultMatchSet"
+ (onChange)="selectEntry($event, 'matchSets')"
+ placeholder="Match Set..." i18n-placeholder></eg-combobox>
+ </div>
+ <div class="col-lg-3"><label i18n>Import Non-Matching Records</label></div>
+ <div class="col-lg-3">
+ <input class="form-check-input" type="checkbox"
+ [(ngModel)]="importNonMatching">
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-lg-3">
+ <label i18n>Holdings Import Profile</label>
+ </div>
+ <div class="col-lg-3"> <!-- TODO disable for authority -->
+ <eg-combobox [entries]="formatEntries('importItemDefs')"
+ [startId]="selectedHoldingsProfile"
+ [disabled]="(selectedQueue && !selectedQueue.freetext) || importSelection()"
+ (onChange)="selectEntry($event, 'importItemDefs')"
+ placeholder="Holdings Import Profile..." i18n-placeholder>
+ </eg-combobox>
+ </div>
+ <div class="col-lg-3"><label i18n>Merge On Exact Match (901c)</label></div>
+ <div class="col-lg-3">
+ <input class="form-check-input" type="checkbox"
+ [(ngModel)]="mergeOnExact">
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-lg-3">
+ <label i18n>Merge Profile</label>
+ </div>
+ <div class="col-lg-3">
+ <eg-combobox [entries]="formatEntries('mergeProfiles')"
+ (onChange)="selectEntry($event, 'mergeProfiles')"
+ placeholder="Merge Profile..." i18n-placeholder>
+ </eg-combobox>
+ </div>
+ <div class="col-lg-3"><label i18n>Merge On Single Match</label></div>
+ <div class="col-lg-3">
+ <input class="form-check-input" type="checkbox"
+ [(ngModel)]="mergeOnSingleMatch">
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-lg-3">
+ <label i18n>Insufficient Quality Fall-Through Profile</label></div>
+ <div class="col-lg-3">
+ <eg-combobox [entries]="formatEntries('mergeProfiles')"
+ (onChange)="selectEntry($event, 'FallThruMergeProfile')"
+ placeholder="Fall-Through Merge Profile..." i18n-placeholder>
+ </eg-combobox>
+ </div>
+ <div class="col-lg-3"><label i18n>Merge On Best Match</label></div>
+ <div class="col-lg-3">
+ <input class="form-check-input" type="checkbox"
+ [(ngModel)]="mergeOnBestMatch">
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-lg-3">
+ <label i18n>Best/Single Match Minimum Quality Ratio</label></div>
+ <div class="col-lg-3">
+ <input type="number" step="0.1"
+ class="form-control" [(ngModel)]="minQualityRatio">
+ </div>
+ <div class="col-lg-3">
+ <label i18n>Auto-overlay In-process Acquisitions Copies</label></div>
+ <div class="col-lg-3">
+ <input class="form-check-input" type="checkbox"
+ [(ngModel)]="autoOverlayAcqCopies">
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-lg-3">
+ <label i18n>Optional Session Name:</label>
+ </div>
+ <div class="col-lg-3">
+ <input [(ngModel)]="sessionName" class="form-control" type="text"
+ i18n-placeholder placeholder="Session Name..."/>
+ </div>
+ <div class="col-lg-3">
+ <label i18n>Remove MARC Field Groups</label>
+ </div>
+ <div class="col-lg-3" *ngIf="bibTrashGroups.length == 0">
+ <span i18n class="font-italic">No Groups Configured</span>
+ </div>
+ <div class="col-lg-3" *ngIf="bibTrashGroups.length">
+ <select multiple [(ngModel)]="selectedTrashGroups"
+ class="form-control" size="3">
+ <option *ngFor="let grp of bibTrashGroups"
+ value="{{grp.id()}}">{{grp.label()}}</option>
+ </select>
+ </div>
+ </div>
+ <div class="row" *ngIf="!importSelection()">
+ <div class="col-lg-3">
+ <label i18n>File to Upload:</label>
+ </div>
+ <div class="col-lg-3">
+ <input #fileSelector (change)="fileSelected($event)"
+ required class="form-control" type="file"/>
+ </div>
+ </div>
+ <div class="row" *ngIf="importSelection()">
+ <div class="col-lg-3">
+ <label>Import Selected</label>
+ </div>
+ <div class="col-lg-3">
+ <span *ngIf="!importSelection().importQueue" i18n>
+ Importing {{importSelection().recordIds.length}} Record(s)</span>
+ <span *ngIf="importSelection().importQueue" i18n>
+ Importing Queue {{importSelection().queue.name()}}</span>
+ </div>
+ <div class="col-lg-3">
+ <button class="btn btn-outline-info ml-2" (click)="clearSelection()" i18n>
+ Clear Selection
+ </button>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-lg-6 offset-lg-3">
+ <button class="btn btn-success btn-lg btn-block font-weight-bold"
+ [disabled]="isUploading || !hasNeededData()"
+ (click)="upload()" i18n>Upload</button>
+ </div>
+ </div>
+ <!-- hide instead of *ngIf so ViewChild can find the progress bars -->
+ <div class="row" [hidden]="!showProgress || importSelection()">
+ <div class="col-lg-3">
+ <label i18n>Upload Progress</label>
+ </div>
+ <div class="col-lg-6">
+ <eg-progress-inline #uploadProgress></eg-progress-inline>
+ </div>
+ </div>
+ <div class="row" [hidden]="!showProgress || importSelection()">
+ <div class="col-lg-3">
+ <label i18n>Enqueue Progress</label>
+ </div>
+ <div class="col-lg-6">
+ <eg-progress-inline #enqueueProgress></eg-progress-inline>
+ </div>
+ </div>
+ <div class="row" [hidden]="!showProgress">
+ <div class="col-lg-3">
+ <label i18n>Import Progress</label>
+ </div>
+ <div class="col-lg-6">
+ <eg-progress-inline #importProgress></eg-progress-inline>
+ </div>
+ </div>
+ <div class="row" [hidden]="!uploadComplete">
+ <div class="col-lg-6 offset-lg-3">
+ <button class="btn btn-info btn-lg btn-block font-weight-bold"
+ routerLink="/staff/cat/vandelay/queue/{{recordType}}/{{activeQueueId}}"
+ i18n>Go To Queue</button>
+ </div>
+ </div>
+</div>
+
+
--- /dev/null
+import {Component, OnInit, AfterViewInit, Input, ViewChild, OnDestroy} from '@angular/core';
+import {tap} from 'rxjs/operators/tap';
+import {IdlObject} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {EventService} from '@eg/core/event.service';
+import {OrgService} from '@eg/core/org.service';
+import {AuthService} from '@eg/core/auth.service';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+import {VandelayService, VandelayImportSelection,
+ VANDELAY_UPLOAD_PATH} from './vandelay.service';
+import {HttpClient, HttpRequest, HttpEventType} from '@angular/common/http';
+import {HttpResponse, HttpErrorResponse} from '@angular/common/http';
+import {ProgressInlineComponent} from '@eg/share/dialog/progress-inline.component';
+import {Subject} from 'rxjs/Subject';
+
+interface ImportOptions {
+ session_key: string;
+ overlay_map?: {[qrId: number]: /* breId */ number};
+ import_no_match?: boolean;
+ auto_overlay_exact?: boolean;
+ auto_overlay_best_match?: boolean;
+ auto_overlay_1match?: boolean;
+ opp_acq_copy_overlay?: boolean;
+ merge_profile?: any;
+ fall_through_merge_profile?: any;
+ strip_field_groups?: number[];
+ exit_early: boolean;
+}
+
+@Component({
+ templateUrl: 'import.component.html'
+})
+export class ImportComponent implements OnInit, AfterViewInit, OnDestroy {
+
+ recordType: string;
+ selectedQueue: ComboboxEntry; // freetext enabled
+
+ // used for applying a default queue ID value when we have
+ // a load-time queue before the queue combobox entries exist.
+ startQueueId: number;
+
+ bibTrashGroups: IdlObject[];
+ selectedTrashGroups: number[];
+
+ activeQueueId: number;
+ selectedBucket: number;
+ selectedBibSource: number;
+ selectedMatchSet: number;
+ selectedHoldingsProfile: number;
+ selectedMergeProfile: number;
+ selectedFallThruMergeProfile: number;
+ selectedFile: File;
+
+ defaultMatchSet: string;
+
+ importNonMatching: boolean;
+ mergeOnExact: boolean;
+ mergeOnSingleMatch: boolean;
+ mergeOnBestMatch: boolean;
+ minQualityRatio: number;
+ autoOverlayAcqCopies: boolean;
+
+ // True after the first upload, then remains true.
+ showProgress: boolean;
+
+ // Upload in progress.
+ isUploading: boolean;
+
+ // True only after successful upload
+ uploadComplete: boolean;
+
+ // Upload / processsing session key
+ // Generated by the server
+ sessionKey: string;
+
+ // Optional enqueue/import tracker session name.
+ sessionName: string;
+
+ @ViewChild('fileSelector') private fileSelector;
+ @ViewChild('uploadProgress')
+ private uploadProgress: ProgressInlineComponent;
+ @ViewChild('enqueueProgress')
+ private enqueueProgress: ProgressInlineComponent;
+ @ViewChild('importProgress')
+ private importProgress: ProgressInlineComponent;
+
+ constructor(
+ private http: HttpClient,
+ private toast: ToastService,
+ private evt: EventService,
+ private net: NetService,
+ private auth: AuthService,
+ private org: OrgService,
+ private vandelay: VandelayService
+ ) {
+ this.applyDefaults();
+ }
+
+ applyDefaults() {
+ this.minQualityRatio = 0;
+ this.selectedBibSource = 1; // default to system local
+ this.recordType = 'bib';
+ this.bibTrashGroups = [];
+
+ if (this.vandelay.importSelection) {
+
+ if (!this.vandelay.importSelection.queue) {
+ // Incomplete import selection, clear it.
+ this.vandelay.importSelection = null;
+ return;
+ }
+
+ const queue = this.vandelay.importSelection.queue;
+ this.recordType = queue.queue_type();
+ this.selectedMatchSet = queue.match_set();
+
+ // This will be propagated to selectedQueue as a combobox
+ // entry via the combobox
+ this.startQueueId = queue.id();
+
+ if (this.recordType === 'bib') {
+ this.selectedBucket = queue.match_bucket();
+ this.selectedHoldingsProfile = queue.item_attr_def();
+ }
+ }
+ }
+
+ ngOnInit() {}
+
+ ngAfterViewInit() {
+ this.loadStartupData();
+ }
+
+ ngOnDestroy() {
+ // If we successfully completed the most recent
+ // upload/import assume the importSelection can be cleared.
+ if (this.uploadComplete) {
+ this.clearSelection();
+ }
+ }
+
+ importSelection(): VandelayImportSelection {
+ return this.vandelay.importSelection;
+ }
+
+ loadStartupData(): Promise<any> {
+ // Note displaying and manipulating a progress dialog inside
+ // the AfterViewInit cycle leads to errors because the child
+ // component is modifed after dirty checking.
+
+ const promises = [
+ this.vandelay.getMergeProfiles(),
+ this.vandelay.getAllQueues('bib'),
+ this.vandelay.getAllQueues('authority'),
+ this.vandelay.getMatchSets('bib'),
+ this.vandelay.getMatchSets('authority'),
+ this.vandelay.getBibBuckets(),
+ this.vandelay.getBibSources(),
+ this.vandelay.getItemImportDefs(),
+ this.vandelay.getBibTrashGroups().then(
+ groups => this.bibTrashGroups = groups),
+ this.org.settings(['vandelay.default_match_set']).then(
+ s => this.defaultMatchSet = s['vandelay.default_match_set'])
+ ];
+
+ return Promise.all(promises);
+ }
+
+ // Format typeahead data sets
+ formatEntries(etype: string): ComboboxEntry[] {
+ const rtype = this.recordType;
+ let list;
+
+ switch (etype) {
+ case 'bibSources':
+ return (this.vandelay.bibSources || []).map(
+ s => { return {id: s.id(), label: s.source()}; });
+
+ case 'bibBuckets':
+ list = this.vandelay.bibBuckets;
+ break;
+
+ case 'allQueues':
+ list = this.vandelay.allQueues[rtype];
+ break;
+
+ case 'matchSets':
+ list = this.vandelay.matchSets[rtype];
+ break;
+
+ case 'importItemDefs':
+ list = this.vandelay.importItemAttrDefs;
+ break;
+
+ case 'mergeProfiles':
+ list = this.vandelay.mergeProfiles;
+ break;
+ }
+
+ return (list || []).map(item => {
+ return {id: item.id(), label: item.name()};
+ });
+ }
+
+ selectEntry($event: ComboboxEntry, etype: string) {
+ const id = $event ? $event.id : null;
+
+ switch (etype) {
+ case 'recordType':
+ this.recordType = id;
+
+ case 'bibSources':
+ this.selectedBibSource = id;
+ break;
+
+ case 'bibBuckets':
+ this.selectedBucket = id;
+ break;
+
+ case 'matchSets':
+ this.selectedMatchSet = id;
+ break;
+
+ case 'importItemDefs':
+ this.selectedHoldingsProfile = id;
+ break;
+
+ case 'mergeProfiles':
+ this.selectedMergeProfile = id;
+ break;
+
+ case 'FallThruMergeProfile':
+ this.selectedFallThruMergeProfile = id;
+ break;
+ }
+ }
+
+ fileSelected($event) {
+ this.selectedFile = $event.target.files[0];
+ }
+
+ // Required form data varies depending on context.
+ hasNeededData(): boolean {
+ if (this.vandelay.importSelection) {
+ return this.importActionSelected();
+ } else {
+ return this.selectedQueue
+ && Boolean(this.recordType) && Boolean(this.selectedFile)
+ }
+ }
+
+ importActionSelected(): boolean {
+ return this.importNonMatching
+ || this.mergeOnExact
+ || this.mergeOnSingleMatch
+ || this.mergeOnBestMatch;
+ }
+
+ // 1. create queue if necessary
+ // 2. upload MARC file
+ // 3. Enqueue MARC records
+ // 4. Import records
+ upload() {
+ this.sessionKey = null;
+ this.showProgress = true;
+ this.isUploading = true;
+ this.uploadComplete = false;
+ this.resetProgressBars();
+
+ this.resolveQueue()
+ .then(
+ queueId => {
+ this.activeQueueId = queueId;
+ return this.uploadFile();
+ },
+ err => Promise.reject('queue create failed')
+ ).then(
+ ok => this.processSpool(),
+ err => Promise.reject('process spool failed')
+ ).then(
+ ok => this.importRecords(),
+ err => Promise.reject('import records failed')
+ ).then(
+ ok => {
+ this.isUploading = false;
+ this.uploadComplete = true;
+ },
+ err => {
+ console.log('file upload failed: ', err);
+ this.isUploading = false;
+ this.resetProgressBars();
+
+ }
+ );
+ }
+
+ resetProgressBars() {
+ this.uploadProgress.update({value: 0, max: 1});
+ this.enqueueProgress.update({value: 0, max: 1});
+ this.importProgress.update({value: 0, max: 1});
+ }
+
+ // Extract selected queue ID or create a new queue when requested.
+ resolveQueue(): Promise<number> {
+
+ if (this.selectedQueue.freetext) {
+ // Free text queue selector means create a new entry.
+ // TODO: first check for name dupes
+
+ return this.vandelay.createQueue(
+ this.selectedQueue.label,
+ this.recordType,
+ this.selectedHoldingsProfile,
+ this.selectedMatchSet,
+ this.selectedBucket
+ );
+
+ } else {
+ return Promise.resolve(this.selectedQueue.id);
+ }
+ }
+
+ uploadFile(): Promise<any> {
+
+ if (this.vandelay.importSelection) {
+ // Nothing to upload when processing pre-queued records.
+ return Promise.resolve();
+ }
+
+ const formData: FormData = new FormData();
+
+ formData.append('ses', this.auth.token());
+ formData.append('marc_upload',
+ this.selectedFile, this.selectedFile.name);
+
+ if (this.selectedBibSource) {
+ formData.append('bib_source', ''+this.selectedBibSource);
+ }
+
+ const req = new HttpRequest('POST', VANDELAY_UPLOAD_PATH, formData,
+ {reportProgress: true, responseType: 'text'});
+
+ return this.http.request(req).pipe(tap(
+ evt => {
+ if (evt.type === HttpEventType.UploadProgress) {
+ this.uploadProgress.update(
+ {value: evt.loaded, max: evt.total});
+
+ } else if (evt instanceof HttpResponse) {
+ this.sessionKey = evt.body as string;
+ console.log(
+ 'Vandelay file uploaded OK with key '+this.sessionKey);
+ }
+ },
+
+ (err: HttpErrorResponse) => {
+ console.error(err);
+ this.toast.danger(err.error);
+ }
+ )).toPromise();
+ }
+
+ processSpool(): Promise<any> {
+
+ if (this.vandelay.importSelection) {
+ // Nothing to enqueue when processing pre-queued records
+ return Promise.resolve();
+ }
+
+ const method = `open-ils.vandelay.${this.recordType}.process_spool`;
+
+ return new Promise((resolve, reject) => {
+ this.net.request(
+ 'open-ils.vandelay', method,
+ this.auth.token(), this.sessionKey, this.activeQueueId,
+ null, null, this.selectedBibSource,
+ (this.sessionName || null), true
+ ).subscribe(
+ tracker => {
+ const e = this.evt.parse(tracker);
+ if (e) { console.error(e); return reject(); }
+
+ // Spooling is in progress, track the results.
+ this.vandelay.pollSessionTracker(tracker.id())
+ .subscribe(
+ trkr => {
+ this.enqueueProgress.update({
+ // enqueue API only tracks actions performed
+ max: null,
+ value: trkr.actions_performed()
+ });
+ },
+ err => { console.log(err); reject(); },
+ () => {
+ this.enqueueProgress.update({max: 1, value: 1});
+ resolve();
+ }
+ );
+ }
+ );
+ });
+ }
+
+ importRecords(): Promise<any> {
+
+ if (!this.importActionSelected()) {
+ return Promise.resolve();
+ }
+
+ const selection = this.vandelay.importSelection;
+
+ if (selection && !selection.importQueue) {
+ return this.importRecordQueue(selection.recordIds);
+ } else {
+ return this.importRecordQueue();
+ }
+ }
+
+ importRecordQueue(recIds?: number[]): Promise<any> {
+ const rtype = this.recordType === 'bib' ? 'bib' : 'auth';
+
+ let method = `open-ils.vandelay.${rtype}_queue.import`;
+ const options: ImportOptions = this.compileImportOptions();
+
+ let target: number | number[] = this.activeQueueId;
+ if (recIds && recIds.length) {
+ method = `open-ils.vandelay.${rtype}_record.list.import`;
+ target = recIds;
+ }
+
+ return new Promise((resolve, reject) => {
+ this.net.request('open-ils.vandelay',
+ method, this.auth.token(), target, options)
+ .subscribe(
+ tracker => {
+ const e = this.evt.parse(tracker);
+ if (e) { console.error(e); return reject(); }
+
+ // Spooling is in progress, track the results.
+ this.vandelay.pollSessionTracker(tracker.id())
+ .subscribe(
+ trkr => {
+ this.importProgress.update({
+ max: trkr.total_actions(),
+ value: trkr.actions_performed()
+ });
+ },
+ err => { console.log(err); reject(); },
+ () => {
+ this.importProgress.update({max: 1, value: 1});
+ resolve();
+ }
+ );
+ }
+ );
+ });
+ }
+
+ compileImportOptions(): ImportOptions {
+
+ const options: ImportOptions = {
+ session_key: this.sessionKey,
+ import_no_match: this.importNonMatching,
+ auto_overlay_exact: this.mergeOnExact,
+ auto_overlay_best_match: this.mergeOnBestMatch,
+ auto_overlay_1match: this.mergeOnSingleMatch,
+ opp_acq_copy_overlay: this.autoOverlayAcqCopies,
+ merge_profile: this.selectedMergeProfile,
+ fall_through_merge_profile: this.selectedFallThruMergeProfile,
+ strip_field_groups: this.selectedTrashGroups,
+ exit_early: true
+ };
+
+ if (this.vandelay.importSelection) {
+ options.overlay_map = this.vandelay.importSelection.overlayMap;
+ }
+
+ return options;
+ }
+
+ clearSelection() {
+ this.vandelay.importSelection = null;
+ this.startQueueId = null;
+ }
+
+ openQueue() {
+ console.log('opening queue ' + this.activeQueueId);
+ }
+}
+
--- /dev/null
+<ng-template #nodeStrTmpl let-point="point" let-showmatch="showmatch" i18n>
+ <ng-container *ngIf="point">
+ <span *ngIf="point.negate()">NOT </span>
+ <span *ngIf="point.heading()">Normalized Heading</span>
+ <span>{{point.bool_op()}}{{point.svf()}}{{point.tag()}}</span>
+ <span *ngIf="point.subfield()"> ‡{{point.subfield()}}</span>
+ <span *ngIf="showmatch && !point.bool_op()"> | Match score {{point.quality()}}</span>
+ </ng-container>
+</ng-template>
+<eg-string key="staff.cat.vandelay.matchpoint.label"
+ [template]="nodeStrTmpl"></eg-string>
+
+<div class="row mt-2">
+ <div class="col-lg-7">
+ <div class="row ml-2">
+ <span class="text-white bg-dark p-2" i18n>
+ Your Expression: {{expressionAsString()}}
+ </span>
+ </div>
+ <div class="row ml-2 mt-4">
+ <span class="mr-2" i18n>Add New:</span>
+ <button class="btn btn-outline-dark mr-2" *ngIf="matchSetType=='biblio'"
+ (click)="newPointType='attr'" i18n>Record Attribute</button>
+ <button class="btn btn-outline-dark mr-2"
+ (click)="newPointType='marc'" i18n>MARC Tag and Subfield</button>
+ <button class="btn btn-outline-dark mr-2" *ngIf="matchSetType=='authority'"
+ (click)="newPointType='heading'" i18n>Normalized Authority Heading</button>
+ <button class="btn btn-outline-dark mr-2"
+ (click)="newPointType='bool'" i18n>Boolean Operator</button>
+ </div>
+ <eg-match-set-new-point #newPoint [pointType]="newPointType">
+ </eg-match-set-new-point>
+ <div class="row mt-2 ml-2" *ngIf="newPointType">
+ <button class="btn btn-success" (click)="addChildNode()"
+ [disabled]="!selectedIsBool()" i18n>
+ Add To Selected Node
+ </button>
+ </div>
+ <div class="row mt-2 ml-2 font-italic" *ngIf="newPointType">
+ <ol i18n>
+ <li>Define a new match point using the above fields.</li>
+ <li>Select a boolean node in the tree.</li>
+ <li>Click the "Add..." button to add the new matchpoint
+ as a child of the selected node.</li>
+ </ol>
+ </div>
+ </div>
+ <div class="col-lg-5">
+ <ng-container *ngIf="tree">
+ <div class="d-flex">
+ <button class="btn btn-warning mr-1" (click)="deleteNode()"
+ [disabled]="!hasSelectedNode()" i18n>
+ Remove Selected Node
+ </button>
+ <button class="btn btn-success mr-1" (click)="saveTree()"
+ [disabled]="!changesMade" i18n>
+ Save Changes
+ </button>
+ </div>
+ <div class="pt-2">
+ <eg-tree
+ [tree]="tree"
+ (nodeClicked)="nodeClicked($event)">
+ </eg-tree>
+ </div>
+ </ng-container>
+ </div>
+</div>
+
--- /dev/null
+import {Component, OnInit, ViewChild, AfterViewInit, Input} from '@angular/core';
+import {IdlObject, IdlService} from '@eg/core/idl.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {OrgService} from '@eg/core/org.service';
+import {Tree, TreeNode} from '@eg/share/tree/tree';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+import {StringService} from '@eg/share/string/string.service';
+import {MatchSetNewPointComponent} from './match-set-new-point.component';
+
+@Component({
+ selector: 'eg-match-set-expression',
+ templateUrl: 'match-set-expression.component.html'
+})
+export class MatchSetExpressionComponent implements OnInit {
+
+ // Match set arrives from parent async.
+ matchSet_: IdlObject;
+ @Input() set matchSet(ms: IdlObject) {
+ this.matchSet_ = ms;
+ if (ms && !this.initDone) {
+ this.matchSetType = ms.mtype();
+ this.initDone = true;
+ this.refreshTree();
+ }
+ }
+
+ tree: Tree;
+ initDone: boolean;
+ matchSetType: string;
+ changesMade: boolean;
+
+ // Current type of new match point
+ newPointType: string;
+ newId: number;
+
+ @ViewChild('newPoint') newPoint: MatchSetNewPointComponent;
+
+ constructor(
+ private idl: IdlService,
+ private pcrud: PcrudService,
+ private net: NetService,
+ private auth: AuthService,
+ private org: OrgService,
+ private strings: StringService
+ ) {
+ this.newId = -1;
+ }
+
+ ngOnInit() {}
+
+ refreshTree(): Promise<any> {
+ if (!this.matchSet_) { return Promise.resolve(); }
+
+ return this.pcrud.search('vmsp',
+ {match_set: this.matchSet_.id()}, {},
+ {atomic: true, authoritative: true}
+ ).toPromise().then(points => this.ingestMatchPoints(points));
+ }
+
+ ingestMatchPoints(points: IdlObject[]) {
+ const nodes = [];
+ const idmap: any = {};
+
+ // massage data, create tree nodes
+ points.forEach(point => {
+
+ point.negate(point.negate() === 't' ? true : false);
+ point.heading(point.heading() === 't' ? true : false);
+ point.children([]);
+
+ const node = new TreeNode({
+ id: point.id(),
+ expanded: true,
+ callerData: {point: point}
+ });
+ idmap[node.id + ''] = node;
+ this.setNodeLabel(node, point).then(() => nodes.push(node));
+ });
+
+ // apply the tree parent/child relationships
+ points.forEach(point => {
+ const node = idmap[point.id() + ''];
+ if (point.parent()) {
+ idmap[point.parent() + ''].children.push(node);
+ } else {
+ this.tree = new Tree(node);
+ }
+ });
+ }
+
+ setNodeLabel(node: TreeNode, point: IdlObject): Promise<any> {
+ if (node.label) { return Promise.resolve(null); }
+ return Promise.all([
+ this.getPointLabel(point, true).then(txt => node.label = txt),
+ this.getPointLabel(point, false).then(
+ txt => node.callerData.slimLabel = txt)
+ ]);
+ }
+
+ getPointLabel(point: IdlObject, showmatch?: boolean): Promise<string> {
+ return this.strings.interpolate(
+ 'staff.cat.vandelay.matchpoint.label',
+ {point: point, showmatch: showmatch}
+ );
+ }
+
+ nodeClicked(node: TreeNode) {}
+
+ deleteNode() {
+ this.changesMade = true;
+ const node = this.tree.selectedNode()
+ this.tree.removeNode(node);
+ }
+
+ hasSelectedNode(): boolean {
+ return Boolean(this.tree.selectedNode());
+ }
+
+ selectedIsBool(): boolean {
+ if (this.tree) {
+ const node = this.tree.selectedNode();
+ return node && node.callerData.point.bool_op();
+ }
+ return false;
+ }
+
+ addChildNode() {
+ this.changesMade = true;
+
+ const pnode = this.tree.selectedNode();
+ const point = this.idl.create('vmsp');
+ point.id(this.newId--);
+ point.isnew(true);
+ point.parent(pnode.id);
+ point.match_set(this.matchSet_.id());
+ point.children([]);
+
+ const ptype = this.newPoint.values.pointType;
+
+ if (ptype === 'bool') {
+ point.bool_op(this.newPoint.values.boolOp);
+
+ } else {
+
+ if (ptype == 'attr') {
+ point.svf(this.newPoint.values.recordAttr);
+
+ } else if (ptype == 'marc') {
+ point.tag(this.newPoint.values.marcTag);
+ point.subfield(this.newPoint.values.marcSf);
+ } else if (ptype == 'heading') {
+ point.heading(true);
+ }
+
+ point.negate(this.newPoint.values.negate);
+ point.quality(this.newPoint.values.matchScore);
+ }
+
+ const node: TreeNode = new TreeNode({
+ id: point.id(),
+ callerData: {point: point}
+ });
+
+ // Match points are added to the DB only when the tree is saved.
+ this.setNodeLabel(node, point).then(() => pnode.children.push(node));
+ }
+
+ expressionAsString(): string {
+ if (!this.tree) { return ''; }
+
+ const renderNode = (node: TreeNode): string => {
+ if (!node) { return ''; }
+
+ if (node.children.length) {
+ return '(' + node.children.map(renderNode).join(
+ ' ' + node.callerData.slimLabel + ' ') + ')'
+ } else if (!node.callerData.point.bool_op()) {
+ return node.callerData.slimLabel;
+ } else {
+ return '()';
+ }
+ }
+
+ return renderNode(this.tree.rootNode);
+ }
+
+ // Server API deletes and recreates the tree on update.
+ // It manages parent/child relationships via the children array.
+ // We only need send the current tree in a form the API recognizes.
+ saveTree(): Promise<any> {
+
+
+ const compileTree = (node?: TreeNode) => {
+
+ if (!node) { node = this.tree.rootNode; }
+
+ const point = node.callerData.point;
+
+ node.children.forEach(child =>
+ point.children().push(compileTree(child)));
+
+ return point;
+ };
+
+ const rootPoint: IdlObject = compileTree();
+
+ return this.net.request(
+ 'open-ils.vandelay',
+ 'open-ils.vandelay.match_set.update',
+ this.auth.token(), this.matchSet_.id(), rootPoint
+ ).toPromise().then(
+ ok =>this.refreshTree(),
+ err => console.error(err)
+ );
+ }
+}
+
--- /dev/null
+
+<div class="d-flex mb-3">
+ <div>
+ <div class="input-group">
+ <div class="input-group-prepend">
+ <span class="input-group-text">Owner</span>
+ </div>
+ <eg-org-select
+ [initialOrg]="contextOrg"
+ (onChange)="orgOnChange($event)">
+ </eg-org-select>
+ </div>
+ </div>
+</div>
+
+<ng-template #nameTmpl let-row="row">
+ <a routerLink="/staff/cat/vandelay/match_sets/{{row.id()}}/editor">
+ {{row.name()}}
+ </a>
+</ng-template>
+
+<eg-grid #grid [dataSource]="gridSource"
+ persistKey="cat.vandelay.match_set.list"
+ idlClass="vms" [dataSource]="queueSource">
+ <eg-grid-toolbar-button label="New Match Set" i18n-label [action]="createNew">
+ </eg-grid-toolbar-button>
+ <eg-grid-toolbar-action label="Delete Selected" i18n-label
+ [action]="deleteSelected"></eg-grid-toolbar-action>
+ <eg-grid-column name="name" [cellTemplate]="nameTmpl">
+ </eg-grid-column>
+</eg-grid>
+
+<eg-fm-record-editor #editDialog idlClass="vms">
+</eg-fm-record-editor>
+
+
+
--- /dev/null
+import {Component, AfterViewInit, ViewChild} from '@angular/core';
+import {Router} from '@angular/router';
+import {Pager} from '@eg/share/util/pager';
+import {IdlObject} from '@eg/core/idl.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {OrgService} from '@eg/core/org.service';
+import {AuthService} from '@eg/core/auth.service';
+import {GridComponent} from '@eg/share/grid/grid.component';
+import {GridDataSource, GridColumn} from '@eg/share/grid/grid';
+import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component';
+
+@Component({
+ templateUrl: 'match-set-list.component.html'
+})
+export class MatchSetListComponent implements AfterViewInit {
+
+ contextOrg: IdlObject;
+ gridSource: GridDataSource;
+ deleteSelected: (rows: IdlObject[]) => void;
+ createNew: () => void;
+ @ViewChild('grid') grid: GridComponent;
+ @ViewChild('editDialog') editDialog: FmRecordEditorComponent;
+
+ constructor(
+ private router: Router,
+ private pcrud: PcrudService,
+ private auth: AuthService,
+ private org: OrgService) {
+
+ this.gridSource = new GridDataSource();
+ this.contextOrg = this.org.get(this.auth.user().ws_ou());
+
+ this.gridSource.getRows = (pager: Pager) => {
+ const orgs = this.org.ancestors(this.contextOrg, true);
+ return this.pcrud.search('vms', {owner: orgs}, {
+ order_by: {vms: ['name']},
+ limit: pager.limit,
+ offset: pager.offset
+ });
+ }
+
+ this.createNew = () => {
+ this.editDialog.mode = 'create';
+ this.editDialog.open({size: 'lg'}).then(
+ ok => this.grid.reload(),
+ err => {}
+ );
+ };
+
+ this.deleteSelected = (matchSets: IdlObject[]) => {
+ matchSets.forEach(matchSet => matchSet.isdeleted(true));
+ this.pcrud.autoApply(matchSets).subscribe(
+ val => console.debug('deleted: ' + val),
+ err => {},
+ () => this.grid.reload()
+ );
+ };
+ }
+
+ ngAfterViewInit() {
+ this.grid.onRowActivate.subscribe(
+ (matchSet: IdlObject) => {
+ this.editDialog.mode = 'update';
+ this.editDialog.recId = matchSet.id();
+ this.editDialog.open({size: 'lg'}).then(
+ ok => this.grid.reload(),
+ err => {}
+ );
+ }
+ );
+ }
+
+ orgOnChange(org: IdlObject) {
+ this.contextOrg = org;
+ this.grid.reload();
+ }
+}
+
--- /dev/null
+<div class="row ml-2 mt-4 p-2 border border-secondary" *ngIf="values.pointType">
+ <div class="col-lg-12 common-form striped-odd form-validated">
+ <ng-container *ngIf="values.pointType=='attr'">
+ <div class="row mb-1">
+ <div class="col-lg-3" i18n>Record Attribute:</div>
+ <div class="col-lg-4">
+ <eg-combobox [entries]="bibAttrDefEntries"
+ [required]="true"
+ (onChange)="values.recordAttr=$event ? $event.id : ''"
+ placeholder="Record Attribute..." i18n-placeholder>
+ </eg-combobox>
+ </div>
+ </div>
+ </ng-container>
+ <ng-container *ngIf="values.pointType=='marc'">
+ <div class="row mb-1">
+ <div class="col-lg-3" i18n>Tag:</div>
+ <div class="col-lg-2">
+ <input required type="text" class="form-control" [(ngModel)]="values.marcTag"/>
+ </div>
+ </div>
+ <div class="row mb-1">
+ <div class="col-lg-3" i18n>Subfield ‡:</div>
+ <div class="col-lg-2">
+ <input required type="text" class="form-control" [(ngModel)]="values.marcSf"/>
+ </div>
+ </div>
+ </ng-container>
+ <ng-container *ngIf="values.pointType=='heading'">
+ <div class="row mb-1">
+ <div class="col-lg-3" i18n>Normalized Heading:</div>
+ <div class="col-lg-2">
+ <input type="checkbox" class="form-check-input" checked disabled/>
+ </div>
+ </div>
+ </ng-container>
+ <ng-container *ngIf="values.pointType!='bool'">
+ <div class="row mb-1">
+ <div class="col-lg-3">Match Score:</div>
+ <div class="col-lg-2">
+ <input required type="number" class="form-control"
+ [(ngModel)]="values.matchScore" step="0.1"/>
+ </div>
+ </div>
+ <ng-container *ngIf="!isForQuality">
+ <div class="row mb-1">
+ <div class="col-lg-3">Negate:</div>
+ <div class="col-lg-2">
+ <input type="checkbox"
+ class="form-check-input" [(ngModel)]="values.negate"/>
+ </div>
+ </div>
+ </ng-container>
+ </ng-container>
+ <ng-container *ngIf="values.pointType=='bool'">
+ <div class="row mb-1">
+ <div class="col-lg-3">Operator:</div>
+ <div class="col-lg-2">
+ <select class="form-control" [(ngModel)]="values.boolOp">
+ <option value='AND' i18n>AND</option>
+ <option value='OR' i18n>OR</option>
+ </select>
+ </div>
+ </div>
+ </ng-container>
+ <ng-container *ngIf="isForQuality">
+ <div class="row mb-1">
+ <div class="col-lg-3" i18n>Value:</div>
+ <div class="col-lg-2">
+ <input type="text" class="form-control" required
+ [(ngModel)]="values.value"/>
+ </div>
+ </div>
+ </ng-container>
+ </div>
+</div>
+
--- /dev/null
+import {Component, OnInit, ViewChild, Output, Input} from '@angular/core';
+import {IdlObject, IdlService} from '@eg/core/idl.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+
+// Can be used to create match_set_point's and match_set_quality's
+export class MatchSetPointValues {
+ pointType: string;
+ recordAttr: string;
+ matchScore: number;
+ negate: boolean;
+ marcTag: string;
+ marcSf: string;
+ heading: string;
+ boolOp: string;
+ value: string;
+}
+
+@Component({
+ selector: 'eg-match-set-new-point',
+ templateUrl: 'match-set-new-point.component.html'
+})
+export class MatchSetNewPointComponent implements OnInit {
+
+ public values: MatchSetPointValues;
+
+ bibAttrDefs: IdlObject[];
+ bibAttrDefEntries: ComboboxEntry[];
+
+ // defining a new match_set_quality
+ @Input() isForQuality: boolean;
+
+ // biblio, authority, quality
+ @Input() set pointType(type_: string) {
+ this.values.pointType = type_;
+ this.values.recordAttr = '';
+ this.values.matchScore = 1;
+ this.values.negate = false;
+ this.values.marcTag = '';
+ this.values.marcSf = '';
+ this.values.boolOp = 'AND';
+ this.values.value = '';
+ }
+
+ constructor(
+ private idl: IdlService,
+ private pcrud: PcrudService
+ ) {
+ this.values = new MatchSetPointValues();
+ this.bibAttrDefs = [];
+ this.bibAttrDefEntries = [];
+ }
+
+ ngOnInit() {
+ this.pcrud.retrieveAll('crad', {order_by: {crad: 'label'}})
+ .subscribe(attr => {
+ this.bibAttrDefs.push(attr);
+ this.bibAttrDefEntries.push({id: attr.name(), label: attr.label()});
+ });
+ }
+
+ setNewPointType(type_: string) {
+ }
+}
+
--- /dev/null
+<div class="row mt-2">
+ <div class="col-lg-7">
+ <div class="row ml-2 mt-4">
+ <span class="mr-2" i18n>Add New:</span>
+ <button class="btn btn-outline-dark mr-2" *ngIf="matchSetType=='biblio'"
+ (click)="newPointType='attr'" i18n>Record Attribute</button>
+ <button class="btn btn-outline-dark mr-2"
+ (click)="newPointType='marc'" i18n>MARC Tag and Subfield</button>
+ </div>
+ <eg-match-set-new-point #newPoint
+ [pointType]="newPointType" [isForQuality]="true">
+ </eg-match-set-new-point>
+ <div class="row mt-2 ml-2" *ngIf="newPointType">
+ <button class="btn btn-success mr-2"
+ (click)="addQuality()" i18n>Add</button>
+ <button class="btn btn-warning"
+ (click)="newPointType=null" i18n>Cancel</button>
+ </div>
+ </div>
+</div>
+
+<eg-grid idlClass="vmsq" [dataSource]="dataSource" #grid
+ persistKey="staff.cat.vandelay.match_set.quality">
+ <eg-grid-toolbar-action label="Delete Selected" i18n-label
+ [action]="deleteSelected"></eg-grid-toolbar-action>
+</eg-grid>
+
--- /dev/null
+import {Component, OnInit, ViewChild, AfterViewInit, Input} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import 'rxjs/add/observable/of';
+import {IdlObject, IdlService} from '@eg/core/idl.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {OrgService} from '@eg/core/org.service';
+import {GridComponent} from '@eg/share/grid/grid.component';
+import {GridDataSource} from '@eg/share/grid/grid';
+import {Pager} from '@eg/share/util/pager';
+import {MatchSetNewPointComponent} from './match-set-new-point.component';
+
+@Component({
+ selector: 'eg-match-set-quality',
+ templateUrl: 'match-set-quality.component.html'
+})
+export class MatchSetQualityComponent implements OnInit {
+
+ // Match set arrives from parent async.
+ matchSet_: IdlObject;
+ @Input() set matchSet(ms: IdlObject) {
+ this.matchSet_ = ms;
+ if (ms) {
+ this.matchSetType = ms.mtype();
+ if (this.grid) {
+ this.grid.reload();
+ }
+ }
+ }
+
+ newPointType: string;
+ matchSetType: string;
+ dataSource: GridDataSource;
+ @ViewChild('newPoint') newPoint: MatchSetNewPointComponent;
+ @ViewChild('grid') grid: GridComponent;
+ deleteSelected: (rows: IdlObject[]) => void;
+
+ constructor(
+ private idl: IdlService,
+ private pcrud: PcrudService,
+ private net: NetService,
+ private auth: AuthService,
+ private org: OrgService
+ ) {
+
+ this.dataSource = new GridDataSource();
+ this.dataSource.getRows = (pager: Pager, sort: any[]) => {
+
+ if (!this.matchSet_) {
+ return Observable.of();
+ }
+
+ const orderBy: any = {};
+ if (sort.length) {
+ orderBy.vmsq = sort[0].name + ' ' + sort[0].dir;
+ }
+
+ const searchOps = {
+ offset: pager.offset,
+ limit: pager.limit,
+ order_by: orderBy
+ };
+
+ const search = {match_set: this.matchSet_.id()};
+ return this.pcrud.search('vmsq', search, searchOps);
+ }
+
+ this.deleteSelected = (rows: any[]) => {
+ this.pcrud.remove(rows).subscribe(
+ ok => console.log('deleted ', ok),
+ err => console.error(err),
+ () => this.grid.reload()
+ );
+ };
+ }
+
+ ngOnInit() {}
+
+ addQuality() {
+ const quality = this.idl.create('vmsq');
+ const values = this.newPoint.values;
+
+ quality.match_set(this.matchSet_.id());
+ quality.quality(values.matchScore);
+ quality.value(values.value);
+
+ if (values.recordAttr) {
+ quality.svf(values.recordAttr);
+ } else {
+ quality.tag(values.marcTag);
+ quality.subfield(values.marcSf);
+ }
+
+ this.pcrud.create(quality).subscribe(
+ ok => console.debug('created ', ok),
+ err => console.error(err),
+ () => {
+ this.newPointType = null;
+ this.grid.reload();
+ }
+ );
+ }
+}
+
--- /dev/null
+<div class="row pb-2" *ngIf="matchSet">
+ <div class="col-lg-4">
+ <div class="card tight-card">
+ <h5 class="card-header" i18n>Match Set Summary</h5>
+ <div class="card-body">
+ <div class="row">
+ <div class="col-lg-6" i18n>Match Set Name:</div>
+ <div class="col-lg-6">{{matchSet.name()}}</div>
+ </div>
+ <div class="row">
+ <div class="col-lg-6" i18n>Owning Library:</div>
+ <div class="col-lg-6">{{matchSet.owner().shortname()}}</div>
+ </div>
+ <div class="row">
+ <div class="col-lg-6" i18n>Type:</div>
+ <div class="col-lg-6">{{matchSet.mtype()}}</div>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
+
+<ngb-tabset [activeId]="matchSetTab" (tabChange)="onTabChange($event)">
+ <ngb-tab title="Match Set Editor" i18n-title id="editor">
+ <ng-template ngbTabContent>
+ <eg-match-set-expression [matchSet]="matchSet">
+ </eg-match-set-expression>
+ </ng-template>
+ </ngb-tab>
+ <ngb-tab title="Match Set Quality Metrics" i18n-title id="quality">
+ <ng-template ngbTabContent>
+ <eg-match-set-quality [matchSet]="matchSet">
+ </eg-match-set-quality>
+ </ng-template>
+ </ngb-tab>
+</ngb-tabset>
--- /dev/null
+import {Component, OnInit, ViewChild} from '@angular/core';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import {NgbTabset, NgbTabChangeEvent} from '@ng-bootstrap/ng-bootstrap';
+import {IdlObject} from '@eg/core/idl.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {OrgService} from '@eg/core/org.service';
+
+@Component({
+ templateUrl: 'match-set.component.html'
+})
+export class MatchSetComponent implements OnInit {
+
+ matchSet: IdlObject;
+ matchSetId: number;
+ matchSetTab: string;
+
+ constructor(
+ private router: Router,
+ private route: ActivatedRoute,
+ private pcrud: PcrudService,
+ private org: OrgService
+ ) {
+ this.route.paramMap.subscribe((params: ParamMap) => {
+ this.matchSetId = +params.get('id');
+ this.matchSetTab = params.get('matchSetTab');
+ });
+ }
+
+ ngOnInit() {
+ this.pcrud.retrieve('vms', this.matchSetId)
+ .toPromise().then(ms => {
+ ms.owner(this.org.get(ms.owner()));
+ this.matchSet = ms;
+ });
+ }
+
+ // Changing a tab in the UI means changing the route.
+ // Changing the route ultimately results in changing the tab.
+ onTabChange(evt: NgbTabChangeEvent) {
+ this.matchSetTab = evt.nextId;
+
+ // prevent tab changing until after route navigation
+ evt.preventDefault();
+
+ const url =
+ `/staff/cat/vandelay/match_sets/${this.matchSetId}/${this.matchSetTab}`;
+
+ this.router.navigate([url]);
+ }
+}
+
--- /dev/null
+import {Component} from '@angular/core';
+
+@Component({
+ template: `<eg-admin-page idlClass="vmp"></eg-admin-page>`
+})
+export class MergeProfilesComponent {
+ constructor() {}
+}
+
--- /dev/null
+<div class="row mb-3">
+ <div class="col-lg-2">
+ <button class="btn btn-info label-with-material-icon"
+ routerLink="/staff/cat/vandelay/queue/{{queueType}}/{{queueId}}">
+ <span class="material-icons">arrow_back</span>
+ <span i18n>Return to Queue</span>
+ </button>
+ </div>
+</div>
+
+<eg-grid #itemsGrid
+ showFields="record,import_error,imported_as,import_time,owning_lib,call_number,barcode"
+ persistKey="cat.vandelay.queue.items"
+ idlClass="vii" [dataSource]="gridSource">
+ <eg-grid-toolbar-checkbox [onChange]="limitToImportErrors"
+ i18n-label label="Limit to Import Failures"></eg-grid-toolbar-checkbox>
+
+</eg-grid>
+
--- /dev/null
+import {Component, OnInit, ViewChild} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import 'rxjs/add/observable/of';
+import {map} from 'rxjs/operators/map';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import {Pager} from '@eg/share/util/pager';
+import {IdlObject} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {GridComponent} from '@eg/share/grid/grid.component';
+import {GridDataSource} from '@eg/share/grid/grid';
+import {VandelayService} from './vandelay.service';
+
+@Component({
+ templateUrl: 'queue-items.component.html'
+})
+export class QueueItemsComponent {
+
+ queueType: string;
+ queueId: number;
+ filterImportErrors: boolean;
+ limitToImportErrors: (checked: boolean) => void;
+
+ gridSource: GridDataSource;
+ @ViewChild('itemsGrid') itemsGrid: GridComponent;
+
+ constructor(
+ private router: Router,
+ private route: ActivatedRoute,
+ private net: NetService,
+ private auth: AuthService,
+ private vandelay: VandelayService) {
+
+ this.route.paramMap.subscribe((params: ParamMap) => {
+ this.queueId = +params.get('id');
+ this.queueType = params.get('qtype');
+ });
+
+ this.gridSource = new GridDataSource();
+
+ // queue API does not support sorting
+ this.gridSource.getRows = (pager: Pager) => {
+ return this.net.request(
+ 'open-ils.vandelay',
+ 'open-ils.vandelay.import_item.queue.retrieve',
+ this.auth.token(), this.queueId, {
+ with_import_error: this.filterImportErrors,
+ offset: pager.offset,
+ limit: pager.limit
+ }
+ );
+ };
+
+ this.limitToImportErrors = (checked: boolean) => {
+ this.filterImportErrors = checked;
+ this.itemsGrid.reload();
+ }
+ }
+}
+
--- /dev/null
+<div class="import-form">
+ <h2 i18n>Select a Queue To Inspect</h2>
+ <div class="row flex">
+ <div>
+ <label i18n>Queue Type</label>
+ </div>
+ <div class="col-lg-3">
+ <eg-combobox (onChange)="queueTypeChanged($event)"
+ [startId]="queueType"
+ placeholder="Queue Type..." i18n-placeholder>
+ <eg-combobox-entry entryId="bib" entryLabel="Bibliographic Records"
+ i18n-entryLabel></eg-combobox-entry>
+ <eg-combobox-entry entryId="auth" entryLabel="Authority Records"
+ i18n-entryLabel></eg-combobox-entry>
+ <eg-combobox-entry entryId="bib-acq" entryLabel="Acquisitions Records"
+ i18n-entryLabel></eg-combobox-entry>
+ </eg-combobox>
+ </div>
+ </div>
+</div>
+
+<eg-grid *ngIf="queueType=='bib'" #bibQueueGrid
+ persistKey="cat.vandelay.queue.list.bib"
+ (onRowActivate)="rowActivated($event)"
+ idlClass="vbq" [dataSource]="queueSource">
+ <eg-grid-toolbar-action label="Delete Selected" i18n-label
+ [action]="deleteSelected"></eg-grid-toolbar-action>
+</eg-grid>
+
+<eg-grid *ngIf="queueType=='auth'" #authQueueGrid
+ persistKey="cat.vandelay.queue.list.auth"
+ (onRowActivate)="rowActivated($event)"
+ idlClass="vaq" [dataSource]="queueSource">
+</eg-grid>
+
+
--- /dev/null
+import {Component, OnInit, ViewChild} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import 'rxjs/add/observable/of';
+import {map} from 'rxjs/operators/map';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import {Pager} from '@eg/share/util/pager';
+import {IdlObject} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {GridComponent} from '@eg/share/grid/grid.component';
+import {GridDataSource, GridColumn} from '@eg/share/grid/grid';
+import {VandelayService} from './vandelay.service';
+
+@Component({
+ templateUrl: 'queue-list.component.html'
+})
+export class QueueListComponent {
+
+ queueType: string; // bib / auth / bib-acq
+ queueSource: GridDataSource;
+ deleteSelected: (rows: IdlObject[]) => void;
+
+ // points to the currently active grid.
+ queueGrid: GridComponent;
+
+ @ViewChild('bibQueueGrid') bibQueueGrid: GridComponent;
+ @ViewChild('authQueueGrid') authQueueGrid: GridComponent;
+
+ constructor(
+ private router: Router,
+ private route: ActivatedRoute,
+ private net: NetService,
+ private auth: AuthService,
+ private vandelay: VandelayService) {
+
+ this.queueType = 'bib';
+ this.queueSource = new GridDataSource();
+
+ // Reset queue grid offset
+ this.vandelay.queuePageOffset = 0;
+
+ // queue API does not support sorting
+ this.queueSource.getRows = (pager: Pager) => {
+ return this.loadQueues(pager);
+ }
+
+ this.deleteSelected = (queues: IdlObject[]) => {
+
+ // Serialize the deletes, especially if there are many of them
+ // because they can be bulky calls
+ const qtype = this.queueType;
+ const method = `open-ils.vandelay.${qtype}_queue.delete`;
+
+ const deleteNext = (queues: IdlObject[], idx: number) => {
+ const queue = queues[idx];
+ if (!queue) {
+ this.currentGrid().reload();
+ return Promise.resolve();
+ }
+
+ return this.net.request('open-ils.vandelay',
+ method, this.auth.token(), queue.id()
+ ).toPromise().then(() => deleteNext(queues, ++idx));
+ }
+
+ deleteNext(queues, 0);
+ };
+ }
+
+ currentGrid(): GridComponent {
+ // The active grid changes along with the queue type.
+ // The inactive grid will be set to null.
+ return this.bibQueueGrid || this.authQueueGrid;
+ }
+
+ rowActivated(queue) {
+ const url = `/staff/cat/vandelay/queue/${this.queueType}/${queue.id()}`;
+ this.router.navigate([url]);
+ }
+
+ queueTypeChanged($event) {
+ this.queueType = $event.id;
+ this.queueSource.reset();
+ }
+
+
+ loadQueues(pager: Pager): Observable<any> {
+
+ if (!this.queueType) {
+ return Observable.of();
+ }
+
+ const qtype = this.queueType.match(/bib/) ? 'bib' : 'authority';
+ const method = `open-ils.vandelay.${qtype}_queue.owner.retrieve`;
+
+ return this.net.request('open-ils.vandelay',
+ method, this.auth.token(), null, null,
+ {offset: pager.offset, limit: pager.limit}
+ );
+ }
+}
+
--- /dev/null
+
+<eg-progress-dialog #progressDlg
+ dialogTitle="Deleting Queue..." i18n-dialogTitle></eg-progress-dialog>
+
+<ng-container *ngIf="queueSummary && queueSummary.queue">
+
+ <eg-confirm-dialog
+ #confirmDelDlg
+ i18n-dialogTitle i18n-dialogBody
+ dialogTitle="Confirm Delete"
+ dialogBody="Delete Queue {{queueSummary.queue.name()}}?">
+ </eg-confirm-dialog>
+
+ <h2 i18n>Queue {{queueSummary.queue.name()}}</h2>
+ <div class="row pb-2">
+ <div class="col-lg-6">
+ <div class="card tight-card">
+ <h5 class="card-header" i18n>Queue Summary</h5>
+ <ul class="list-group list-group-flush">
+ <li class="list-group-item">
+ <div class="d-flex">
+ <div class="flex-3" i18n>Records in Queue:</div>
+ <div class="flex-1">{{queueSummary.total}}</div>
+ <div class="flex-3" i18n>Items in Queue:</div>
+ <div class="flex-1">{{queueSummary.total_items}}</div>
+ </div>
+ </li>
+ <li class="list-group-item">
+ <div class="d-flex">
+ <div class="flex-3" i18n>Records Imported:</div>
+ <div class="flex-1">{{queueSummary.imported}}</div>
+ <div class="flex-3" i18n>Items Imported:</div>
+ <div class="flex-1">{{queueSummary.total_items_imported}}</div>
+ </div>
+ </li>
+ <li class="list-group-item">
+ <div class="d-flex">
+ <div class="flex-3" i18n>Records Import Failures:</div>
+ <div class="flex-1">{{queueSummary.rec_import_errors}}</div>
+ <div class="flex-3" i18n>Item Import Failures:</div>
+ <div class="flex-1">{{queueSummary.item_import_errors}}</div>
+ </div>
+ </li>
+ </ul>
+ </div>
+ </div>
+ <div class="col-lg-6">
+ <div class="card tight-card">
+ <h5 class="card-header" i18n>Queue Actions</h5>
+ <ul class="list-group list-group-flush">
+ <li class="list-group-item">
+ <div class="d-flex">
+ <div class="flex-1">
+ <a [routerLink]="" (click)="importSelected()"
+ i18n>Import Selected Records</a>
+ </div>
+ <div class="flex-1">
+ <a [routerLink]="" (click)="importAll()" i18n>Import All Records</a>
+ </div>
+ </div>
+ </li>
+ <li class="list-group-item">
+ <div class="d-flex">
+ <div class="flex-1">
+ <a i18n
+ routerLink="/staff/cat/vandelay/queue/{{queueType}}/{{queueId}}/items">
+ View Import Items
+ </a>
+ </div>
+ <div class="flex-1">
+ <a [routerLink]="" (click)="exportNonImported()"
+ i18n>Export Non-Imported Records</a>
+ </div>
+ </div>
+ </li>
+ <li class="list-group-item">
+ <div class="d-flex">
+ <eg-record-bucket-dialog #bucketDialog [queueId]="queueId">
+ </eg-record-bucket-dialog>
+ <div class="flex-1">
+ <a [routerLink]="" (click)="bucketDialog.open({size:'lg'})" i18n>
+ Copy Queue To Bucket
+ </a>
+ </div>
+ <div class="flex-1">
+ <a [routerLink]="" (click)="deleteQueue()" i18n>Delete Queue</a>
+ </div>
+ </div>
+ </li>
+ </ul>
+ </div>
+ </div>
+ </div>
+</ng-container>
+
+<ng-template #matchesTmpl let-row="row">
+ <a i18n [ngClass]="{'font-weight-bold': hasOverlayTarget(row.id)}"
+ routerLink="/staff/cat/vandelay/queue/{{queueType}}/{{queueId}}/record/{{row.id}}/matches">
+ ({{row.matches.length}})
+ {{hasOverlayTarget(row.id) ? '*' : ''}}
+ </a>
+</ng-template>
+
+<ng-template #errorsTmpl let-row="row">
+ <div *ngIf="row.error_detail">
+ <b class="text-danger" title="{{row.error_detail}}">{{row.import_error}}</b>
+ </div>
+ <div *ngIf="row.error_items.length">
+ <b class="text-danger">Items ({{row.error_items.length}})</b>
+ </div>
+</ng-template>
+
+<ng-template #importedAsTmpl let-row="row">
+ <a routerLink="/staff/catalog/record/{{row.imported_as}}">
+ {{row.imported_as}}
+ </a>
+</ng-template>
+
+
+<!--
+Most columns are generated programmatically from queued record attribute
+definitions. Hide a number of stock record attributes by default
+because there are a lot of them.
+-->
+
+<eg-grid #queueGrid [dataSource]="queueSource"
+ persistKey="cat.vandelay.queue.{{queueType}}"
+ (onRowActivate)="openRecord($event)"
+ [pageOffset]="queuePageOffset()"
+ hideFields="language,pagination,price,rec_identifier,eg_tcn_source,eg_identifier,item_barcode,zsource">
+
+ <eg-grid-toolbar-checkbox i18n-label label="Records With Matches"
+ [onChange]="limitToMatches"></eg-grid-toolbar-checkbox>
+
+ <eg-grid-toolbar-checkbox i18n-label label="Non-Imported Records"
+ [onChange]="limitToNonImported"></eg-grid-toolbar-checkbox>
+
+ <eg-grid-toolbar-checkbox i18n-label label="Records with Import Errors"
+ [onChange]="limitToImportErrors"></eg-grid-toolbar-checkbox>
+
+ <eg-grid-column name="id" [index]="true"
+ [hidden]="true"></eg-grid-column>
+ <eg-grid-column i18n-label label="Matches"
+ name="+matches" [cellTemplate]="matchesTmpl"></eg-grid-column>
+ <eg-grid-column name="import_error" i18n-label
+ label="Import Errors" [cellTemplate]="errorsTmpl"></eg-grid-column>
+ <eg-grid-column name="import_time" i18n-label
+ label="Import Date" datatype="timestamp"></eg-grid-column>
+ <eg-grid-column name="imported_as" i18n-label
+ label="Imported As" [cellTemplate]="importedAsTmpl"></eg-grid-column>
+</eg-grid>
+
--- /dev/null
+import {Component, OnInit, AfterViewInit, ViewChild} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import 'rxjs/add/observable/of';
+import {map} from 'rxjs/operators/map';
+import {filter} from 'rxjs/operators/filter';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import {Pager} from '@eg/share/util/pager';
+import {IdlObject} from '@eg/core/idl.service';
+import {EventService} from '@eg/core/event.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
+import {ProgressDialogComponent} from '@eg/share/dialog/progress.component';
+import {GridComponent} from '@eg/share/grid/grid.component';
+import {GridDataSource, GridColumn} from '@eg/share/grid/grid';
+import {VandelayService, VandelayImportSelection,
+ VANDELAY_EXPORT_PATH} from './vandelay.service';
+
+@Component({
+ templateUrl: 'queue.component.html'
+})
+export class QueueComponent implements OnInit, AfterViewInit {
+
+ queueId: number;
+ queueType: string; // bib / authority
+ queueSource: GridDataSource;
+ queuedRecClass: string;
+ queueSummary: any;
+
+ filters = {
+ matches: false,
+ nonImported: false,
+ withErrors: false
+ };
+
+ limitToMatches: (checked: boolean) => void;
+ limitToNonImported: (checked: boolean) => void;
+ limitToImportErrors: (checked: boolean) => void;
+
+ // keep a local copy for convenience
+ attrDefs: IdlObject[];
+
+ @ViewChild('queueGrid') queueGrid: GridComponent;
+ @ViewChild('confirmDelDlg') confirmDelDlg: ConfirmDialogComponent;
+ @ViewChild('progressDlg') progressDlg: ProgressDialogComponent;
+
+ constructor(
+ private router: Router,
+ private route: ActivatedRoute,
+ private evt: EventService,
+ private net: NetService,
+ private auth: AuthService,
+ private vandelay: VandelayService) {
+
+ this.route.paramMap.subscribe((params: ParamMap) => {
+ this.queueType = params.get('qtype');
+ this.queueId = +params.get('id');
+ });
+
+ this.queueSource = new GridDataSource();
+ this.queueSource.getRows = (pager: Pager) => {
+ this.vandelay.queuePageOffset = pager.offset;
+ return this.loadQueueRecords(pager);
+ };
+
+ this.limitToMatches = (checked: boolean) => {
+ this.filters.matches = checked;
+ this.queueGrid.reload();
+ };
+
+ this.limitToNonImported = (checked: boolean) => {
+ this.filters.nonImported = checked;
+ this.queueGrid.reload();
+ };
+
+ this.limitToImportErrors = (checked: boolean) => {
+ this.filters.withErrors = checked;
+ this.queueGrid.reload();
+ };
+ }
+
+ ngOnInit() {
+ }
+
+ queuePageOffset(): number {
+ return this.vandelay.queuePageOffset;
+ }
+
+ ngAfterViewInit() {
+ if (this.queueType) {
+ this.applyQueueType();
+ if (this.queueId) {
+ this.loadQueueSummary();
+ }
+ }
+ }
+
+ openRecord(row: any) {
+ const url =
+ `/staff/cat/vandelay/queue/${this.queueType}/${this.queueId}/record/${row.id}/marc`;
+ this.router.navigate([url]);
+ }
+
+ applyQueueType() {
+ this.queuedRecClass = this.queueType.match(/bib/) ? 'vqbr' : 'vqar';
+ this.vandelay.getAttrDefs(this.queueType).then(
+ attrs => {
+ this.attrDefs = attrs;
+ // Add grid columns for record attributes
+ attrs.forEach(attr => {
+ const col = new GridColumn();
+ col.name = attr.code(),
+ col.label = attr.description(),
+ col.datatype = 'string';
+ this.queueGrid.context.columnSet.add(col);
+ });
+
+ // Reapply the grid configuration now that we've
+ // dynamically added columns.
+ this.queueGrid.context.applyGridConfig();
+ }
+ );
+ }
+
+ qtypeShort(): string {
+ return this.queueType === 'bib' ? 'bib' : 'auth';
+ }
+
+ loadQueueSummary(): Promise<any> {
+ const method =
+ `open-ils.vandelay.${this.qtypeShort()}_queue.summary.retrieve`;
+
+ return this.net.request(
+ 'open-ils.vandelay', method, this.auth.token(), this.queueId)
+ .toPromise().then(sum => this.queueSummary = sum);
+ }
+
+ loadQueueRecords(pager: Pager): Observable<any> {
+
+ const options = {
+ clear_marc: true,
+ offset: pager.offset,
+ limit: pager.limit,
+ flesh_import_items: true,
+ non_imported: this.filters.nonImported,
+ with_import_error: this.filters.withErrors
+ }
+
+ return this.vandelay.getQueuedRecords(
+ this.queueId, this.queueType, options, this.filters.matches).pipe(
+ filter(rec => {
+ // avoid sending mishapen data to the grid
+ // this happens (among other reasons) when the grid
+ // no longer exists
+ const e = this.evt.parse(rec);
+ if (e) { console.error(e); return false; }
+ return true;
+ }),
+ map(rec => {
+ const recHash: any = {
+ id: rec.id(),
+ import_error: rec.import_error(),
+ error_detail: rec.error_detail(),
+ import_time: rec.import_time(),
+ imported_as: rec.imported_as(),
+ import_items: rec.import_items(),
+ error_items: rec.import_items().filter(i => i.import_error()),
+ matches: rec.matches()
+ };
+
+ // Link the record attribute values to the root record
+ // object so the grid can find them.
+ rec.attributes().forEach(attr => {
+ const def =
+ this.attrDefs.filter(d => d.id() === attr.field())[0];
+ recHash[def.code()] = attr.attr_value();
+ });
+
+ return recHash;
+ }));
+ }
+
+ findOrCreateImportSelection() {
+ let selection = this.vandelay.importSelection;
+ if (!selection) {
+ selection = new VandelayImportSelection();
+ this.vandelay.importSelection = selection;
+ }
+ selection.queue = this.queueSummary.queue;
+ return selection;
+ }
+
+ hasOverlayTarget(rid: number): boolean {
+ return this.vandelay.importSelection &&
+ Boolean(this.vandelay.importSelection.overlayMap[rid]);
+ }
+
+ importSelected() {
+ const rows = this.queueGrid.context.getSelectedRows();
+ if (rows.length) {
+ const selection = this.findOrCreateImportSelection();
+ selection.recordIds = rows.map(row => row.id);
+ console.log('importing: ', this.vandelay.importSelection);
+ this.router.navigate(['/staff/cat/vandelay/import']);
+ }
+ }
+
+ importAll() {
+ const selection = this.findOrCreateImportSelection();
+ selection.importQueue = true;
+ this.router.navigate(['/staff/cat/vandelay/import']);
+ }
+
+ deleteQueue() {
+ this.confirmDelDlg.open().then(
+ yes => {
+ this.progressDlg.open();
+ return this.net.request(
+ 'open-ils.vandelay',
+ `open-ils.vandelay.${this.qtypeShort()}_queue.delete`,
+ this.auth.token(), this.queueId
+ ).toPromise();
+ },
+ no => {
+ this.progressDlg.close();
+ return Promise.reject('delete failed');
+ }
+ ).then(
+ resp => {
+ this.progressDlg.close();
+ const e = this.evt.parse(resp);
+ if (e) {
+ console.error(e);
+ alert(e);
+ } else {
+ // Jump back to the main queue page.
+ this.router.navigate(['/staff/cat/vandelay/queue']);
+ }
+ },
+ err => {
+ this.progressDlg.close();
+ }
+ );
+ }
+
+ exportNonImported() {
+ this.vandelay.exportQueue(this.queueSummary.queue, true);
+ }
+}
+
--- /dev/null
+
+<ng-template #bibIdTemplate let-row="row">
+ <a routerLink="/staff/catalog/record/{{row.eg_record}}/marc_view" i18n>
+ {{row.eg_record}}
+ </a>
+</ng-template>
+
+<ng-template #targetTemplate let-row="row">
+ <ng-container *ngIf="isOverlayTarget(row.id)">
+ <span i18n-title title="Selected Merge Target"
+ class="material-icons">check_circle</span>
+ </ng-container>
+</ng-template>
+
+<ng-container *ngIf="queueType == 'bib'">
+ <eg-grid #bibGrid [dataSource]="bibDataSource"
+ (onRowClick)="matchRowClick($event)"
+ [disableMultiSelect]="true">
+ <!--
+ <eg-grid-toolbar-action i18n-label label="Mark As Overlay Target"
+ [action]="markOverlayTarget">
+ </eg-grid-toolbar-action>
+ -->
+ <eg-grid-column name="id" [index]="true" [hidden]="true"
+ i18n-label label="Match ID">
+ </eg-grid-column>
+ <eg-grid-column name="selected" i18n-label label="Merge Target"
+ [cellTemplate]="targetTemplate">
+ </eg-grid-column>
+ <eg-grid-column name="eg_record" i18n-label label="Record ID"
+ [cellTemplate]="bibIdTemplate">
+ </eg-grid-column>
+ <eg-grid-column name="match_score" i18n-label label="Match Score">
+ </eg-grid-column>
+ <eg-grid-column name="bre_quality" i18n-label label="Matched Record Quality">
+ </eg-grid-column>
+ <eg-grid-column name="vqbr_quality" i18n-label label="Queued Record Quality">
+ </eg-grid-column>
+ <eg-grid-column path="bib_summary.display.title" i18n-label label="Title">
+ </eg-grid-column>
+ <eg-grid-column path="bib_summary.record.creator.usrname"
+ i18n-label label="Creator">
+ </eg-grid-column>
+ <eg-grid-column path="bib_summary.record.create_date" datatype="timestamp"
+ i18n-label label="Create Date">
+ </eg-grid-column>
+ <eg-grid-column path="bib_summary.record.editor.usrname"
+ i18n-label label="Editor">
+ </eg-grid-column>
+ <eg-grid-column path="bib_summary.record.edit_date" datatype="timestamp"
+ i18n-label label="Edit Date">
+ </eg-grid-column>
+ </eg-grid>
+</ng-container>
+<ng-container *ngIf="queueType == 'authority'">
+ <eg-grid #authGrid [dataSource]="authDataSource">
+ <eg-grid-column name="id" [index]="true" [hidden]="true"
+ i18n-label label="Match ID">
+ </eg-grid-column>
+ </eg-grid>
+</ng-container>
+
+
+<!--
+{
+name: '[% l('Merge Target') %]',
+get: vlGetOverlayTargetSelector,
+formatter : vlFormatOverlayTargetSelector,
+},
+{name: '[% l('ID') %]', field:'id'},
+{ name: '[% l('View MARC') %]',
+get: vlGetViewMARC,
+formatter : vlFormatViewMatchMARC
+},
+{name: '[% l('Match Score') %]', field:'match_score'},
+{name: '[% l('Queued Record Quality') %]', field:'rec_quality'},
+{name: '[% l('Matched Record Quality') %]', field:'match_quality'},
+{name: '[% l('Creator') %]', get: vlGetCreator},
+{name: '[% l('Create Date') %]', field:'create_date', get: vlGetDateTimeField},
+{name: '[% l('Last Edit Date') %]', field:'edit_date', get: vlGetDateTimeField},
+{name: '[% l('Source') %]', field:'source'},
+]]
+}];
+
+if (recordType == 'auth') {
+vlMatchGridLayout[0].cells[0].push(
+{name: '[% l("Heading") %]', field:'heading'}
+);
+} else {
+vlMatchGridLayout[0].cells[0].push(
+{name: '[% l('TCN Source') %]', field:'tcn_source'},
+{name: '[% l('TCN Value') %]', field:'tcn_value'}
+);
+}
+-->
+
--- /dev/null
+import {Component, Input, OnInit, ViewChild} from '@angular/core';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import {Observable} from 'rxjs/Observable';
+import 'rxjs/add/observable/of';
+import {map} from 'rxjs/operators/map';
+import {Pager} from '@eg/share/util/pager';
+import {GridComponent} from '@eg/share/grid/grid.component';
+import {GridDataSource, GridColumn} from '@eg/share/grid/grid';
+import {IdlObject} from '@eg/core/idl.service';
+import {EventService} from '@eg/core/event.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {BibRecordService, BibRecordSummary} from '@eg/share/catalog/bib-record.service';
+import {VandelayService, VandelayImportSelection} from './vandelay.service';
+
+@Component({
+ selector: 'eg-queued-record-matches',
+ templateUrl: 'queued-record-matches.component.html'
+})
+export class QueuedRecordMatchesComponent implements OnInit {
+
+ @Input() queueType: string;
+ @Input() recordId: number;
+ @ViewChild('bibGrid') bibGrid: GridComponent;
+ @ViewChild('authGrid') authGrid: GridComponent;
+
+ queuedRecord: IdlObject;
+ bibDataSource: GridDataSource;
+ authDataSource: GridDataSource;
+ markOverlayTarget: (rows: any[]) => any;
+ matchRowClick: (row: any) => void;
+ matchMap: {[id: number]: IdlObject};
+
+ constructor(
+ private router: Router,
+ private route: ActivatedRoute,
+ private evt: EventService,
+ private net: NetService,
+ private auth: AuthService,
+ private pcrud: PcrudService,
+ private bib: BibRecordService,
+ private vandelay: VandelayService) {
+
+ this.bibDataSource = new GridDataSource();
+ this.authDataSource = new GridDataSource();
+
+ this.bibDataSource.getRows = (pager: Pager) => {
+ return this.getBibMatchRows(pager);
+ }
+
+ /* TODO
+ this.authDataSource.getRows = (pager: Pager) => {
+ }
+ */
+
+ // Mark or un-mark as row as the merge target on row click
+ this.matchRowClick = (row: any) => {
+ this.toggleMergeTarget(row.id);
+ }
+ }
+
+ toggleMergeTarget(matchId: number) {
+
+ if (this.isOverlayTarget(matchId)) {
+
+ // clear selection on secondary click;
+ delete this.vandelay.importSelection.overlayMap[this.recordId];
+
+ } else {
+ // Add to selection.
+ // Start a new one if necessary, which will be adopted
+ // and completed by the queue UI before import.
+
+ let selection = this.vandelay.importSelection;
+ if (!selection) {
+ selection = new VandelayImportSelection();
+ this.vandelay.importSelection = selection;
+ }
+ const match = this.matchMap[matchId];
+ selection.overlayMap[this.recordId] = match.eg_record();
+ }
+ }
+
+ isOverlayTarget(matchId: number): boolean {
+ const selection = this.vandelay.importSelection;
+ if (selection) {
+ const match = this.matchMap[matchId];
+ return selection.overlayMap[this.recordId] === match.eg_record();
+ }
+ return false;
+ }
+
+ ngOnInit() {}
+
+ // This thing is a nesty beast -- clean it up
+ getBibMatchRows(pager: Pager): Observable<any> {
+
+ return new Observable(observer => {
+
+ this.getQueuedRecord().then(() => {
+
+ const matches = this.queuedRecord.matches();
+ const recIds = [];
+ this.matchMap = {};
+ matches.forEach(m => {
+ this.matchMap[m.id()] = m;
+ if (!recIds.includes(m.eg_record())) {
+ recIds.push(m.eg_record());
+ }
+ });
+
+ const bibSummaries: {[id: number]: BibRecordSummary} = {};
+ this.bib.getBibSummary(recIds).subscribe(
+ summary => bibSummaries[summary.id] = summary,
+ err => {},
+ () => {
+ this.bib.fleshBibUsers(
+ Object.values(bibSummaries).map(sum => sum.record)
+ ).then(() => {
+ matches.forEach(match => {
+ const row = {
+ id: match.id(),
+ eg_record: match.eg_record(),
+ bre_quality: match.quality(),
+ vqbr_quality: this.queuedRecord.quality(),
+ match_score: match.match_score(),
+ bib_summary: bibSummaries[match.eg_record()]
+ }
+
+ observer.next(row);
+ });
+
+ observer.complete();
+ });
+ }
+ );
+ });
+ });
+ }
+
+ getQueuedRecord(): Promise<any> {
+ if (this.queuedRecord) {
+ return Promise.resolve('');
+ }
+ let idlClass = this.queueType === 'bib' ? 'vqbr' : 'vqar';
+ const flesh = {flesh: 1, flesh_fields: {}};
+ flesh.flesh_fields[idlClass] = ['matches'];
+ return this.pcrud.retrieve(idlClass, this.recordId, flesh)
+ .toPromise().then(rec => this.queuedRecord = rec);
+ }
+}
+
--- /dev/null
+
+<div class="row mb-3">
+ <div class="col-lg-2">
+ <button class="btn btn-info label-with-material-icon"
+ routerLink="/staff/cat/vandelay/queue/{{queueType}}/{{queueId}}">
+ <span class="material-icons">arrow_back</span>
+ <span i18n>Return to Queue</span>
+ </button>
+ </div>
+</div>
+
+<ngb-tabset #recordTabs [activeId]="recordTab" (tabChange)="onTabChange($event)">
+ <ngb-tab title="Queued Record MARC" i18n-title id="marc">
+ <ng-template ngbTabContent>
+ <eg-marc-html [recordId]="recordId" [recordType]="'vandelay-'+queueType">
+ </eg-marc-html>
+ </ng-template>
+ </ngb-tab>
+ <ngb-tab title="Record Matches" i18n-title id="matches">
+ <ng-template ngbTabContent>
+ <eg-queued-record-matches [recordId]="recordId" [queueType]="queueType">
+ </eg-queued-record-matches>
+ </ng-template>
+ </ngb-tab>
+ <ngb-tab title="Import Items" i18n-title id="items">
+ <ng-template ngbTabContent>
+ <eg-queued-record-items [recordId]="recordId">
+ </eg-queued-record-items>
+ </ng-template>
+ </ngb-tab>
+</ngb-tabset>
--- /dev/null
+import {Component, OnInit, ViewChild} from '@angular/core';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import {NgbTabset, NgbTabChangeEvent} from '@ng-bootstrap/ng-bootstrap';
+
+@Component({
+ templateUrl: 'queued-record.component.html'
+})
+export class QueuedRecordComponent {
+
+ queueId: number;
+ queueType: string;
+ recordId: number;
+ recordTab: string;
+
+ constructor(
+ private router: Router,
+ private route: ActivatedRoute) {
+
+ this.route.paramMap.subscribe((params: ParamMap) => {
+ this.queueId = +params.get('id');
+ this.recordId = +params.get('recordId');
+ this.queueType = params.get('qtype');
+ this.recordTab = params.get('recordTab');
+ });
+ }
+
+ // Changing a tab in the UI means changing the route.
+ // Changing the route ultimately results in changing the tab.
+ onTabChange(evt: NgbTabChangeEvent) {
+ this.recordTab = evt.nextId;
+
+ // prevent tab changing until after route navigation
+ evt.preventDefault();
+
+ const url =
+ `/staff/cat/vandelay/queue/${this.queueType}/${this.queueId}` +
+ `/record/${this.recordId}/${this.recordTab}`;
+
+ this.router.navigate([url]);
+ }
+}
+
--- /dev/null
+<div class="row mb-2">
+ <div class="col-lg-6">
+ <div class="input-group">
+ <div class="input-group-prepend">
+ <span class="input-group-text" i18n>Show Sessions Since: </span>
+ </div>
+ <eg-date-select
+ [initialIso]="sinceDate"
+ (onChangeAsIso)="dateFilterChange($event)">
+ </eg-date-select>
+ </div>
+ </div>
+</div>
+
+<div *ngIf="trackers.length == 0">
+ <div class="row">
+ <div class="col-lg-6">
+ <div class="alert alert-info">
+ <span i18n>No Import Sessions To Display</span>
+ </div>
+ </div>
+ </div>
+</div>
+
+ <div class="row mb-4" *ngFor="let tracker of trackers">
+ <div class="col-lg-12">
+ <div class="card tight-card">
+ <div class="card-header">
+ <div class="panel-title">
+ <span i18n>
+ {{tracker.create_time() | date:'short'}} :
+ <span class="font-weight-bold">{{tracker.name()}}</span>
+ </span>
+ </div>
+ </div>
+ <div class="card-body">
+ <div class="row">
+ <div class="col-lg-6">
+ <!-- ensure the progress shows 100% when complete -->
+ <eg-progress-inline
+ [max]="tracker.state() == 'complete' ? tracker.actions_performed() : tracker.total_actions() || null"
+ [value]="tracker.actions_performed()">
+ </eg-progress-inline>
+ </div>
+ <div class="col-lg-6">
+ <!-- .id (not .id()) check to see if it's fleshed yet -->
+ <span i18n *ngIf="tracker.queue().id">
+ <a class="font-weight-bold"
+ routerLink="/staff/cat/vandelay/queue/{{tracker.record_type()}}/{{tracker.queue().id()}}">
+ Queue {{tracker.queue().name()}}
+ </a>
+ </span>
+ <span class="pl-2" *ngIf="tracker.action_type() == 'enqueue'" i18n>Enqueuing... </span>
+ <span class="pl-2" *ngIf="tracker.action_type() == 'import'" i18n>Importing... </span>
+ <span *ngIf="tracker.state() == 'active'" i18n>Active</span>
+ <span *ngIf="tracker.state() == 'complete'" i18n>Complete</span>
+ <span *ngIf="tracker.state() == 'error'" i18n>Error</span>
+ <span class='pl-3' *ngIf="tracker.state() == 'complete'">
+ <span class="material-icons text-success">thumb_up</span>
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
--- /dev/null
+import {Component, OnInit} from '@angular/core';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {AuthService} from '@eg/core/auth.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {VandelayService} from './vandelay.service';
+
+@Component({
+ templateUrl: 'recent-imports.component.html'
+})
+
+export class RecentImportsComponent implements OnInit {
+
+ trackers: IdlObject[];
+ refreshInterval = 2000; // ms
+ sinceDate: string;
+ pollTimeout: any;
+
+ constructor(
+ private idl: IdlService,
+ private auth: AuthService,
+ private pcrud: PcrudService,
+ private vandelay: VandelayService
+ ) {
+ this.trackers = [];
+ }
+
+ ngOnInit() {
+ // Default to showing all trackers created today.
+ const d = new Date();
+ d.setHours(0);
+ d.setMinutes(0);
+ d.setSeconds(0);
+ this.sinceDate = d.toISOString();
+
+ this.pollTrackers();
+ }
+
+ dateFilterChange(iso: string) {
+ if (iso) {
+ this.sinceDate = iso;
+ if (this.pollTimeout) {
+ clearTimeout(this.pollTimeout);
+ this.pollTimeout = null;
+ }
+ this.trackers = [];
+ this.pollTrackers();
+ }
+ }
+
+ pollTrackers() {
+
+ // Report on recent trackers for this workstation and for the
+ // logged in user. Always show active trackers regardless
+ // of sinceDate.
+ const query: any = {
+ '-and': [
+ {
+ '-or': [
+ {workstation: this.auth.user().wsid()},
+ {usr: this.auth.user().id()}
+ ],
+ }, {
+ '-or': [
+ {create_time: {'>=': this.sinceDate}},
+ {state: 'active'}
+ ]
+ }
+ ]
+ };
+
+ this.pcrud.search('vst', query, {order_by: {vst: 'create_time'}})
+ .subscribe(
+ tracker => {
+ // The screen flickers less if the tracker array is
+ // updated inline instead of rebuilt every time.
+
+ const existing =
+ this.trackers.filter(t => t.id() === tracker.id())[0];
+
+ if (existing) {
+ existing.update_time(tracker.update_time());
+ existing.state(tracker.state());
+ existing.total_actions(tracker.total_actions());
+ existing.actions_performed(tracker.actions_performed());
+ } else {
+
+ // Only show the import tracker when both an enqueue
+ // and import tracker exist for a given session.
+ const sameSes = this.trackers.filter(
+ t => t.session_key() === tracker.session_key())[0];
+
+ if (sameSes) {
+ if (sameSes.action_type() === 'enqueue') {
+ // Remove the enqueueu tracker
+
+ for (let idx = 0; idx < this.trackers.length; idx++) {
+ const trkr = this.trackers[idx];
+ if (trkr.id() === sameSes.id()) {
+ console.debug(
+ `removing tracker ${trkr.id()} from the list`);
+ this.trackers.splice(idx, 1);
+ break;
+ }
+ }
+ } else if (sameSes.action_type() === 'import') {
+ // Avoid adding the new enqueue tracker
+ return;
+ }
+ }
+
+ console.debug(`adding tracker ${tracker.id()} to list`);
+
+ this.trackers.unshift(tracker);
+ this.fleshTrackerQueue(tracker);
+ }
+ },
+ err => {},
+ () => {
+ const active =
+ this.trackers.filter(t => t.state() === 'active');
+
+ // Continue updating the display with updated tracker
+ // data as long as we have any active trackers.
+ if (active.length > 0) {
+ this.pollTimeout = setTimeout(
+ () => this.pollTrackers(), this.refreshInterval);
+ } else {
+ this.pollTimeout = null;
+ }
+ }
+ );
+ }
+
+ fleshTrackerQueue(tracker: IdlObject) {
+ const qClass = tracker.record_type() === 'bib' ? 'vbq' : 'vaq';
+ this.pcrud.retrieve(qClass, tracker.queue())
+ .subscribe(queue => tracker.queue(queue));
+ }
+
+}
--- /dev/null
+<eg-grid #itemsGrid
+ showFields="record,import_error,imported_as,import_time,owning_lib,call_number,barcode"
+ persistKey="cat.vandelay.queue.bib.items"
+ idlClass="vii" [dataSource]="gridSource">
+</eg-grid>
+
--- /dev/null
+import {Component, Input, ViewChild} from '@angular/core';
+import {Pager} from '@eg/share/util/pager';
+import {IdlObject} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {AuthService} from '@eg/core/auth.service';
+import {GridComponent} from '@eg/share/grid/grid.component';
+import {GridDataSource} from '@eg/share/grid/grid';
+import {VandelayService} from './vandelay.service';
+
+@Component({
+ selector: 'eg-queued-record-items',
+ templateUrl: 'record-items.component.html'
+})
+export class RecordItemsComponent {
+
+ @Input() recordId: number;
+
+ gridSource: GridDataSource;
+ @ViewChild('itemsGrid') itemsGrid: GridComponent;
+
+ constructor(
+ private net: NetService,
+ private auth: AuthService,
+ private pcrud: PcrudService,
+ private vandelay: VandelayService) {
+
+ this.gridSource = new GridDataSource();
+
+ // queue API does not support sorting
+ this.gridSource.getRows = (pager: Pager) => {
+ return this.pcrud.search('vii',
+ {record: this.recordId}, {order_by: {vii: ['id']}});
+ };
+ }
+}
+
--- /dev/null
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes} from '@angular/router';
+import {VandelayComponent} from './vandelay.component';
+import {ImportComponent} from './import.component';
+import {ExportComponent} from './export.component';
+import {QueueListComponent} from './queue-list.component';
+import {QueueComponent} from './queue.component';
+import {QueuedRecordComponent} from './queued-record.component';
+import {DisplayAttrsComponent} from './display-attrs.component';
+import {MergeProfilesComponent} from './merge-profiles.component';
+import {HoldingsProfilesComponent} from './holdings-profiles.component';
+import {QueueItemsComponent} from './queue-items.component';
+import {MatchSetListComponent} from './match-set-list.component';
+import {MatchSetComponent} from './match-set.component';
+import {RecentImportsComponent} from './recent-imports.component';
+
+const routes: Routes = [{
+ path: '',
+ component: VandelayComponent,
+ children: [{
+ path: '',
+ pathMatch: 'full',
+ redirectTo: 'import'
+ }, {
+ path: 'import',
+ component: ImportComponent
+ }, {
+ path: 'export',
+ component: ExportComponent
+ }, {
+ path: 'queue',
+ component: QueueListComponent
+ }, {
+ path: 'queue/:qtype/:id',
+ component: QueueComponent
+ }, {
+ path: 'queue/:qtype/:id/record/:recordId',
+ component: QueuedRecordComponent
+ }, {
+ path: 'queue/:qtype/:id/record/:recordId/:recordTab',
+ component: QueuedRecordComponent
+ }, {
+ path: 'queue/:qtype/:id/items',
+ component: QueueItemsComponent
+ }, {
+ path: 'display_attrs',
+ component: DisplayAttrsComponent
+ }, {
+ path: 'display_attrs/:atype',
+ component: DisplayAttrsComponent
+ }, {
+ path: 'merge_profiles',
+ component: MergeProfilesComponent
+ }, {
+ path: 'holdings_profiles',
+ component: HoldingsProfilesComponent
+ }, {
+ path: 'match_sets',
+ component: MatchSetListComponent
+ }, {
+ path: 'match_sets/:id/:matchSetTab',
+ component: MatchSetComponent
+ }, {
+ path: 'active_imports',
+ component: RecentImportsComponent
+ }]
+}];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule],
+ providers: []
+})
+
+export class VandelayRoutingModule {}
--- /dev/null
+
+<ul class="nav nav-pills nav-fill pb-4">
+ <li class="nav-item">
+ <a class="nav-link" [ngClass]="{active: tab=='export'}"
+ routerLink="/staff/cat/vandelay/export" i18n>Export</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" [ngClass]="{active: tab=='import'}"
+ routerLink="/staff/cat/vandelay/import" i18n>Import</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" [ngClass]="{active: tab=='queue'}"
+ routerLink="/staff/cat/vandelay/queue" i18n>Inspect Queue</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" [ngClass]="{active: tab=='display_attrs'}"
+ routerLink="/staff/cat/vandelay/display_attrs/bib"
+ i18n>Record Display Attributes</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" [ngClass]="{active: tab=='merge_profiles'}"
+ routerLink="/staff/cat/vandelay/merge_profiles"
+ i18n>Merge / Overlay Profiles</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" [ngClass]="{active: tab=='match_sets'}"
+ routerLink="/staff/cat/vandelay/match_sets"
+ i18n>Record Match Sets</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" [ngClass]="{active: tab=='holdings_profiles'}"
+ routerLink="/staff/cat/vandelay/holdings_profiles"
+ i18n>Holdings Import Profiles</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link" [ngClass]="{active: tab=='active_imports'}"
+ routerLink="/staff/cat/vandelay/active_imports"
+ i18n>Recent Imports</a>
+ </li>
+</ul>
+
+<!-- load nav-specific page -->
+<router-outlet></router-outlet>
+
--- /dev/null
+import {Component, OnInit, AfterViewInit, ViewChild} from '@angular/core';
+import {Router, ActivatedRoute, NavigationEnd} from "@angular/router";
+import {take} from 'rxjs/operators/take';
+import {VandelayService} from './vandelay.service';
+import {IdlObject} from '@eg/core/idl.service';
+
+@Component({
+ templateUrl: 'vandelay.component.html'
+})
+export class VandelayComponent implements OnInit, AfterViewInit {
+ tab: string;
+
+ constructor(
+ private router: Router,
+ private route: ActivatedRoute,
+ private vandelay: VandelayService) {
+
+ // As the parent component of the vandelay route tree, our
+ // activated route never changes. Instead, listen for global
+ // route events, then ask for the first segement of the first
+ // child, which will be the tab name.
+ this.router.events.subscribe(routeEvent => {
+ if (routeEvent instanceof NavigationEnd) {
+ this.route.firstChild.url.pipe(take(1))
+ .subscribe(segments => this.tab = segments[0].path);
+ }
+ });
+ }
+
+ ngOnInit() {}
+
+ ngAfterViewInit() {}
+}
+
--- /dev/null
+import {NgModule} from '@angular/core';
+import {StaffCommonModule} from '@eg/staff/common.module';
+import {CatalogCommonModule} from '@eg/share/catalog/catalog-common.module';
+import {HttpClientModule} from '@angular/common/http';
+import {TreeModule} from '@eg/share/tree/tree.module';
+import {VandelayRoutingModule} from './routing.module';
+import {VandelayService} from './vandelay.service';
+import {VandelayComponent} from './vandelay.component';
+import {ImportComponent} from './import.component';
+import {ExportComponent} from './export.component';
+import {QueueComponent} from './queue.component';
+import {QueueListComponent} from './queue-list.component';
+import {QueuedRecordComponent} from './queued-record.component';
+import {QueuedRecordMatchesComponent} from './queued-record-matches.component';
+import {DisplayAttrsComponent} from './display-attrs.component';
+import {MergeProfilesComponent} from './merge-profiles.component';
+import {HoldingsProfilesComponent} from './holdings-profiles.component';
+import {QueueItemsComponent} from './queue-items.component';
+import {RecordItemsComponent} from './record-items.component';
+import {MatchSetListComponent} from './match-set-list.component';
+import {MatchSetComponent} from './match-set.component';
+import {MatchSetExpressionComponent} from './match-set-expression.component';
+import {MatchSetQualityComponent} from './match-set-quality.component';
+import {MatchSetNewPointComponent} from './match-set-new-point.component';
+import {RecentImportsComponent} from './recent-imports.component';
+
+@NgModule({
+ declarations: [
+ VandelayComponent,
+ ImportComponent,
+ ExportComponent,
+ QueueComponent,
+ QueueListComponent,
+ QueuedRecordComponent,
+ QueuedRecordMatchesComponent,
+ DisplayAttrsComponent,
+ MergeProfilesComponent,
+ HoldingsProfilesComponent,
+ QueueItemsComponent,
+ RecordItemsComponent,
+ MatchSetListComponent,
+ MatchSetComponent,
+ MatchSetExpressionComponent,
+ MatchSetQualityComponent,
+ MatchSetNewPointComponent,
+ RecentImportsComponent
+ ],
+ imports: [
+ TreeModule,
+ StaffCommonModule,
+ CatalogCommonModule,
+ VandelayRoutingModule,
+ HttpClientModule,
+ ],
+ providers: [
+ VandelayService
+ ]
+})
+
+export class VandelayModule {
+}
--- /dev/null
+import {Injectable, EventEmitter} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import {tap} from 'rxjs/operators/tap';
+import {map} from 'rxjs/operators/map';
+import {HttpClient} from '@angular/common/http';
+import {saveAs} from 'file-saver/FileSaver';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {OrgService} from '@eg/core/org.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {PermService} from '@eg/core/perm.service';
+import {EventService} from '@eg/core/event.service';
+import {ProgressDialogComponent} from '@eg/share/dialog/progress.component';
+
+export const VANDELAY_EXPORT_PATH = '/exporter';
+export const VANDELAY_UPLOAD_PATH = '/vandelay-upload';
+
+export class VandelayImportSelection {
+ recordIds: number[];
+ queue: IdlObject;
+ importQueue: boolean; // import the whole queue
+ overlayMap: {[qrId: number]: /* breId */ number};
+
+ constructor() {
+ this.recordIds = [];
+ this.overlayMap = {};
+ }
+}
+
+@Injectable()
+export class VandelayService {
+
+ allQueues: {[qtype: string]: IdlObject[]};
+ activeQueues: {[qtype: string]: IdlObject[]};
+ attrDefs: {[atype: string]: IdlObject[]};
+ bibSources: IdlObject[];
+ bibBuckets: IdlObject[];
+ copyStatuses: IdlObject[];
+ matchSets: {[stype: string]: IdlObject[]};
+ importItemAttrDefs: IdlObject[];
+ bibTrashGroups: IdlObject[];
+ mergeProfiles: IdlObject[];
+
+ // Used for tracking records between the queue page and
+ // the import page. Fields managed externally.
+ importSelection: VandelayImportSelection;
+
+ // Track the last grid offset in the queue page so we
+ // can return the user to the same page of data after
+ // going to the matches page.
+ queuePageOffset: number;
+
+ constructor(
+ private http: HttpClient,
+ private idl: IdlService,
+ private org: OrgService,
+ private evt: EventService,
+ private net: NetService,
+ private auth: AuthService,
+ private pcrud: PcrudService,
+ private perm: PermService
+ ) {
+ this.attrDefs = {};
+ this.activeQueues = {};
+ this.allQueues = {};
+ this.matchSets = {};
+ this.importSelection = null;
+ this.queuePageOffset = 0;
+ }
+
+ getAttrDefs(dtype: string): Promise<IdlObject[]> {
+ if (this.attrDefs[dtype]) {
+ return Promise.resolve(this.attrDefs[dtype]);
+ }
+ const cls = (dtype === 'bib') ? 'vqbrad' : 'vqarad';
+ const orderBy = {};
+ orderBy[cls] = 'id'
+ return this.pcrud.retrieveAll(cls,
+ {order_by: orderBy}, {atomic: true}).toPromise()
+ .then(list => {
+ this.attrDefs[dtype] = list;
+ return list;
+ });
+ }
+
+ getMergeProfiles(): Promise<IdlObject[]> {
+ if (this.mergeProfiles) {
+ return Promise.resolve(this.mergeProfiles);
+ }
+
+ const owners = this.org.ancestors(this.auth.user().ws_ou(), true);
+ return this.pcrud.search('vmp',
+ {owner: owners}, {order_by: {vmp: ['name']}}, {atomic: true})
+ .toPromise().then(profiles => {
+ this.mergeProfiles = profiles;
+ return profiles;
+ });
+ }
+
+ // Returns a promise resolved with the list of queues.
+ // Also emits the onQueueListUpdate event so listeners
+ // can detect queue content changes.
+ getAllQueues(qtype: string): Promise<IdlObject[]> {
+ if (this.allQueues[qtype]) {
+ return Promise.resolve(this.allQueues[qtype]);
+ } else {
+ this.allQueues[qtype] = [];
+ }
+
+ // could be a big list, invoke in streaming mode
+ return this.net.request(
+ 'open-ils.vandelay',
+ `open-ils.vandelay.${qtype}_queue.owner.retrieve`,
+ this.auth.token()
+ ).pipe(tap(
+ queue => this.allQueues[qtype].push(queue)
+ )).toPromise().then(() => this.allQueues[qtype]);
+ }
+
+
+ // Returns a promise resolved with the list of queues.
+ // Also emits the onQueueListUpdate event so listeners
+ // can detect queue content changes.
+ getActiveQueues(qtype: string): Promise<IdlObject[]> {
+ if (this.activeQueues[qtype]) {
+ return Promise.resolve(this.activeQueues[qtype]);
+ } else {
+ this.activeQueues[qtype] = [];
+ }
+
+ // could be a big list, invoke in streaming mode
+ return this.net.request(
+ 'open-ils.vandelay',
+ `open-ils.vandelay.${qtype}_queue.owner.retrieve`,
+ this.auth.token(), null, {complete: 'f'}
+ ).pipe(tap(
+ queue => this.activeQueues[qtype].push(queue)
+ )).toPromise().then(() => this.activeQueues[qtype]);
+ }
+
+ getBibSources(): Promise<IdlObject[]> {
+ if (this.bibSources) {
+ return Promise.resolve(this.bibSources);
+ }
+
+ return this.pcrud.retrieveAll('cbs',
+ {order_by: {cbs: 'id'}},
+ {atomic: true}
+ ).toPromise().then(sources => {
+ this.bibSources = sources;
+ return sources;
+ });
+ }
+
+ getItemImportDefs(): Promise<IdlObject[]> {
+ if (this.importItemAttrDefs) {
+ return Promise.resolve(this.importItemAttrDefs);
+ }
+
+ const owners = this.org.ancestors(this.auth.user().ws_ou(), true);
+ return this.pcrud.search('viiad', {owner: owners}, {}, {atomic: true})
+ .toPromise().then(defs => {
+ this.importItemAttrDefs = defs;
+ return defs;
+ });
+ }
+
+ // todo: differentiate between biblio and authority a la queue api
+ getMatchSets(mtype: string): Promise<IdlObject[]> {
+
+ const mstype = mtype.match(/bib/) ? 'biblio' : 'authority';
+
+ if (this.matchSets[mtype]) {
+ return Promise.resolve(this.matchSets[mtype]);
+ } else {
+ this.matchSets[mtype] = [];
+ }
+
+ const owners = this.org.ancestors(this.auth.user().ws_ou(), true);
+
+ return this.pcrud.search('vms',
+ {owner: owners, mtype: mstype}, {}, {atomic: true})
+ .toPromise().then(sets => {
+ this.matchSets[mtype] = sets;
+ return sets;
+ });
+ }
+
+ getBibBuckets(): Promise<IdlObject[]> {
+ if (this.bibBuckets) {
+ return Promise.resolve(this.bibBuckets);
+ }
+
+ const bkts = [];
+ return this.net.request(
+ 'open-ils.actor',
+ 'open-ils.actor.container.retrieve_by_class',
+ this.auth.token(), this.auth.user().id(), 'biblio', 'staff_client'
+ //).pipe(tap(bkt => bkts.push(bkt))).toPromise().then(() => bkts);
+ ).toPromise().then(bkts => {
+ this.bibBuckets = bkts;
+ return bkts;
+ });
+ }
+
+ getCopyStatuses(): Promise<any> {
+ if (this.copyStatuses) {
+ return Promise.resolve(this.copyStatuses);
+ }
+ return this.pcrud.retrieveAll('ccs', {}, {atomic: true})
+ .toPromise().then(stats => {
+ this.copyStatuses = stats;
+ return stats;
+ });
+ }
+
+ getBibTrashGroups(): Promise<any> {
+ if (this.bibTrashGroups) {
+ return Promise.resolve(this.bibTrashGroups);
+ }
+
+ const owners = this.org.ancestors(this.auth.user().ws_ou(), true);
+
+ return this.pcrud.search('vibtg',
+ {always_apply : 'f', owner: owners},
+ {vibtg : ['label']},
+ {atomic: true}
+ ).toPromise().then(groups => {
+ this.bibTrashGroups = groups;
+ return groups;
+ });
+ }
+
+
+ // Create a queue and return the ID of the new queue via promise.
+ createQueue(
+ queueName: string,
+ recordType: string,
+ importDefId: number,
+ matchSet: number,
+ matchBucket: number): Promise<number> {
+
+ const method = `open-ils.vandelay.${recordType}_queue.create`;
+
+ let qType = recordType;
+ if (recordType.match(/acq/)) {
+ let qType = 'acq';
+ }
+
+ return new Promise((resolve, reject) => {
+ this.net.request(
+ 'open-ils.vandelay', method,
+ this.auth.token(), queueName, null, qType,
+ matchSet, importDefId, matchBucket
+ ).subscribe(queue => {
+ const e = this.evt.parse(queue);
+ if (e) {
+ alert(e);
+ reject(e);
+ } else {
+ resolve(queue.id());
+ }
+ });
+ });
+ }
+
+ getQueuedRecords(queueId: number, queueType: string,
+ options?: any, limitToMatches?: boolean): Observable<any> {
+
+ const qtype = queueType.match(/bib/) ? 'bib' : 'auth';
+
+ let method =
+ `open-ils.vandelay.${qtype}_queue.records.retrieve`;
+
+ if (limitToMatches) {
+ method =
+ `open-ils.vandelay.${qtype}_queue.records.matches.retrieve`;
+ }
+
+ return this.net.request('open-ils.vandelay',
+ method, this.auth.token(), queueId, options);
+ }
+
+ // Download a queue as a MARC file.
+ exportQueue(queue: IdlObject, nonImported?: boolean) {
+
+ const etype = queue.queue_type().match(/auth/) ? 'auth' : 'bib';
+
+ let url =
+ `${VANDELAY_EXPORT_PATH}?type=${etype}&queueid=${queue.id()}`
+
+ let saveName = queue.name();
+
+ if (nonImported) {
+ url += '&nonimported=1';
+ saveName += '_nonimported';
+ }
+
+ saveName += '.mrc';
+
+ this.http.get(url, {responseType: 'text'}).subscribe(
+ data => {
+ saveAs(
+ new Blob([data], {type: 'application/octet-stream'}),
+ saveName
+ );
+ },
+ err => {
+ console.error(err);
+ }
+ );
+ }
+
+ // Poll every 2 seconds for session tracker updates so long
+ // as the session tracker is active.
+ // Returns an Observable of tracker objects.
+ pollSessionTracker(id: number): Observable<IdlObject> {
+ return new Observable(observer => {
+ this.getNextSessionTracker(id, observer);
+ });
+ }
+
+ getNextSessionTracker(id: number, observer: any) {
+
+ // No need for this to be an authoritative call.
+ // It will complete eventually regardless.
+ this.pcrud.retrieve('vst', id).subscribe(
+ tracker => {
+ if (tracker && tracker.state() === 'active') {
+ observer.next(tracker);
+ setTimeout(() =>
+ this.getNextSessionTracker(id, observer), 2000);
+ } else {
+ console.debug(
+ `Vandelay session tracker ${id} is ${tracker.state()}`);
+ observer.complete();
+ }
+ }
+ );
+ }
+}
+
<span class="material-icons">cloud_download</span>
<span i18n>Import Record from Z39.50</span>
</a>
- <a href="/eg/staff/cat/catalog/vandelay" class="dropdown-item">
+ <a routerLink="/staff/cat/vandelay/import" class="dropdown-item">
<span class="material-icons">import_export</span>
<span i18n>MARC Batch Import/Export</span>
</a>
path: 'circ',
loadChildren : '@eg/staff/circ/routing.module#CircRoutingModule'
}, {
+ path: 'cat',
+ loadChildren : '@eg/staff/cat/routing.module#CatRoutingModule'
+ }, {
path: 'catalog',
loadChildren : '@eg/staff/catalog/catalog.module#CatalogModule'
}, {
<ng-template #dialogContent>
<div class="modal-header bg-info">
- <h4 class="modal-title" i18n>Add To Record #{{recId}} to Bucket</h4>
+ <h4 class="modal-title" *ngIf="recId" i18n>Add To Record #{{recId}} to Bucket</h4>
+ <h4 class="modal-title" *ngIf="qId" i18n>Add Records from queue #{{qId}} to Bucket</h4>
<button type="button" class="close"
i18n-aria-label aria-label="Close"
(click)="dismiss('cross_click')">
newBucketDesc: string;
buckets: any[];
+ @Input() bucketType: string;
+
recId: number;
@Input() set recordId(id: number) {
this.recId = id;
}
+ // Add items from a (vandelay) bib queue to a bucket
+ qId: number;
+ @Input() set queueId(id: number) {
+ this.qId = id;
+ }
+
constructor(
private modal: NgbModal, // required for passing to parent
private renderer: Renderer2,
}
ngOnInit() {
+
+ if (this.qId) {
+ this.bucketType = 'vandelay_queue';
+ } else {
+ this.bucketType = 'staff_client';
+ }
this.onOpen$.subscribe(ok => {
// Reset data on dialog open
'open-ils.actor',
'open-ils.actor.container.retrieve_by_class.authoritative',
this.auth.token(), this.auth.user().id(),
- 'biblio', 'staff_client'
+ 'biblio', this.bucketType
).subscribe(buckets => this.buckets = buckets);
});
}
bucket.owner(this.auth.user().id());
bucket.name(this.newBucketName);
bucket.description(this.newBucketDesc);
- bucket.btype('staff_client');
+ bucket.btype(this.bucketType);
this.net.request(
'open-ils.actor',
if (evt) {
this.toast.danger(evt.desc);
} else {
+ // make it find-able to the queue-add method which
+ // requires the bucket name.
+ bucket.id(bktId);
+ this.buckets.push(bucket);
+
this.addToBucket(bktId);
}
});
// Add the record to the selected existing bucket
addToBucket(id: number) {
+ if (this.recId) {
+ this.addRecordToBucket(id);
+ } else if (this.qId) {
+ this.addQueueToBucket(id);
+ }
+ }
+
+ addRecordToBucket(bucketId: number) {
const item = this.idl.create('cbrebi');
- item.bucket(id);
+ item.bucket(bucketId);
item.target_biblio_record_entry(this.recId);
this.net.request(
'open-ils.actor',
}
});
}
+
+ addQueueToBucket(bucketId: number) {
+ const bucket = this.buckets.filter(b => b.id() === bucketId)[0];
+ if (!bucket) { return; }
+
+ this.net.request(
+ 'open-ils.vandelay',
+ 'open-ils.vandelay.bib_queue.to_bucket',
+ this.auth.token(), this.qId, bucket.name()
+ ).toPromise().then(resp => {
+ const evt = this.evt.parse(resp);
+ if (evt) {
+ this.toast.danger(evt.toString());
+ } else {
+ this.close();
+ }
+ });
+ }
}
} elsif( $$options{with_item_import_error} and $type eq 'bib') {
- $query->{from} = {$class => 'vii'};
+ $query->{from} = {$class => {'vii' => {}}};
$query->{where}->{'+vii'} = {import_error => {'!=' => undef}};
}
}
if($self->api_name =~ /matches/) {
# find only records that have matches
- $query->{from} = {$class => {$mclass => {type => 'right'}}};
+ if (ref $query->{from}) {
+ $query->{from}{$class}{$mclass} = {type => 'right'};
+ } else {
+ $query->{from} = {$class => {$mclass => {type => 'right'}}};
+ }
} else {
# join to mclass for sorting (see below)
- $query->{from} = {$class => {$mclass => {type => 'left'}}};
+ if (ref $query->{from}) {
+ $query->{from}{$class}{$mclass} = {type => 'left'};
+ } else {
+ $query->{from} = {$class => {$mclass => {type => 'left'}}};
+ }
}
# order by the matched bib records to group like queued records
# if other trackers exist for this key, adopt the name
my $existing =
$e->search_vandelay_session_tracker({session_key => $key})->[0];
- $name = $existing->name if $name;
+ $name = $existing->name if $existing;
} else {
# anonymous tracker
);
sub owner_queue_retrieve {
- my($self, $conn, $auth, $owner_id, $filters) = @_;
+ my($self, $conn, $auth, $owner_id, $filters, $pager) = @_;
my $e = new_editor(authtoken => $auth, xact => 1);
return $e->die_event unless $e->checkauth;
$owner_id = $e->requestor->id; # XXX add support for viewing other's queues?
my $search = {owner => $owner_id};
$search->{$_} = $filters->{$_} for keys %$filters;
+ my %paging;
+ if ($pager) {
+ $paging{limit} = $pager->{limit} || 1000;
+ $paging{offset} = $pager->{offset} || 0;
+ }
+
if($self->{record_type} eq 'bib') {
$queues = $e->search_vandelay_bib_queue(
- [$search, {order_by => {vbq => 'evergreen.lowercase(name)'}}]);
+ [$search, {%paging, order_by => {vbq => 'evergreen.lowercase(name)'}}]);
} else {
$queues = $e->search_vandelay_authority_queue(
- [$search, {order_by => {vaq => 'evergreen.lowercase(name)'}}]);
+ [$search, {%paging, order_by => {vaq => 'evergreen.lowercase(name)'}}]);
}
$conn->respond($_) for @$queues;
$e->rollback;
)
);
+INSERT INTO config.workstation_setting_type (name, grp, datatype, label)
+VALUES (
+ 'eg.grid.cat.vandelay.queue.bib', 'gui', 'object',
+ oils_i18n_gettext(
+ 'eg.grid.cat.vandelay.queue.bib',
+ 'Grid Config: Vandelay Bib Queue',
+ 'cwst', 'label'
+ )
+), (
+ 'eg.grid.cat.vandelay.queue.auth', 'gui', 'object',
+ oils_i18n_gettext(
+ 'eg.grid.cat.vandelay.queue.auth',
+ 'Grid Config: Vandelay Authority Queue',
+ 'cwst', 'label'
+ )
+), (
+ 'cat.vandelay.match_set.list', 'gui', 'object',
+ oils_i18n_gettext(
+ 'cat.vandelay.match_set.list',
+ 'Grid Config: Vandelay Match Sets',
+ 'cwst', 'label'
+ )
+), (
+ 'staff.cat.vandelay.match_set.quality', 'gui', 'object',
+ oils_i18n_gettext(
+ 'staff.cat.vandelay.match_set.quality',
+ 'Grid Config: Vandelay Match Quality Metrics',
+ 'cwst', 'label'
+ )
+), (
+ 'cat.vandelay.queue.items', 'gui', 'object',
+ oils_i18n_gettext(
+ 'cat.vandelay.queue.items',
+ 'Grid Config: Vandelay Queue Import Items',
+ 'cwst', 'label'
+ )
+), (
+ 'cat.vandelay.queue.list.bib', 'gui', 'object',
+ oils_i18n_gettext(
+ 'cat.vandelay.queue.list.bib',
+ 'Grid Config: Vandelay Bib Queue List',
+ 'cwst', 'label'
+ )
+), (
+ 'cat.vandelay.queue.bib.items', 'gui', 'object',
+ oils_i18n_gettext(
+ 'cat.vandelay.queue.bib.items',
+ 'Grid Config: Vandelay Bib Items',
+ 'cwst', 'label'
+ )
+), (
+ 'cat.vandelay.queue.list.auth', 'gui', 'object',
+ oils_i18n_gettext(
+ 'cat.vandelay.queue.list.auth',
+ 'Grid Config: Vandelay Authority Queue List',
+ 'cwst', 'label'
+ )
+);
+
INSERT into config.org_unit_setting_type (name, label, description, datatype)
VALUES (
--- /dev/null
+BEGIN;
+
+--SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+
+INSERT INTO config.workstation_setting_type (name, grp, datatype, label)
+VALUES (
+ 'eg.grid.cat.vandelay.queue.bib', 'gui', 'object',
+ oils_i18n_gettext(
+ 'eg.grid.cat.vandelay.queue.bib',
+ 'Grid Config: Vandelay Bib Queue',
+ 'cwst', 'label'
+ )
+), (
+ 'eg.grid.cat.vandelay.queue.auth', 'gui', 'object',
+ oils_i18n_gettext(
+ 'eg.grid.cat.vandelay.queue.auth',
+ 'Grid Config: Vandelay Authority Queue',
+ 'cwst', 'label'
+ )
+), (
+ 'cat.vandelay.match_set.list', 'gui', 'object',
+ oils_i18n_gettext(
+ 'cat.vandelay.match_set.list',
+ 'Grid Config: Vandelay Match Sets',
+ 'cwst', 'label'
+ )
+), (
+ 'staff.cat.vandelay.match_set.quality', 'gui', 'object',
+ oils_i18n_gettext(
+ 'staff.cat.vandelay.match_set.quality',
+ 'Grid Config: Vandelay Match Quality Metrics',
+ 'cwst', 'label'
+ )
+), (
+ 'cat.vandelay.queue.items', 'gui', 'object',
+ oils_i18n_gettext(
+ 'cat.vandelay.queue.items',
+ 'Grid Config: Vandelay Queue Import Items',
+ 'cwst', 'label'
+ )
+), (
+ 'cat.vandelay.queue.list.bib', 'gui', 'object',
+ oils_i18n_gettext(
+ 'cat.vandelay.queue.list.bib',
+ 'Grid Config: Vandelay Bib Queue List',
+ 'cwst', 'label'
+ )
+), (
+ 'cat.vandelay.queue.bib.items', 'gui', 'object',
+ oils_i18n_gettext(
+ 'cat.vandelay.queue.bib.items',
+ 'Grid Config: Vandelay Bib Items',
+ 'cwst', 'label'
+ )
+), (
+ 'cat.vandelay.queue.list.auth', 'gui', 'object',
+ oils_i18n_gettext(
+ 'cat.vandelay.queue.list.auth',
+ 'Grid Config: Vandelay Authority Queue List',
+ 'cwst', 'label'
+ )
+);
+
+COMMIT;
+
+
</a>
</li>
<li>
- <a href="./cat/catalog/vandelay" target="_self">
+ <a href="/eg2/staff/cat/vandelay/import">
<span class="glyphicon glyphicon-transfer"></span>
[% l('MARC Batch Import/Export') %]
</a>