</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="auth" entryLabel="Authority Records"
+ <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>
</div>
<div class="col-lg-3">
<eg-combobox [entries]="formatEntries('activeQueues')"
+ [startId]="startQueueId"
+ [startIdFiresOnChange]="true"
+ [disabled]="startQueueId"
(onChange)="selectedQueue=$event" i18n-placeholder
[required]="true"
[allowFreeText]="true" placeholder="Select or Create a Queue...">
</div>
<div class="col-lg-3">
<eg-combobox [entries]="formatEntries('bibBuckets')"
- [disabled]="selectedQueue && !selectedQueue.freetext"
+ [startId]="selectedBucket"
+ [disabled]="(selectedQueue && !selectedQueue.freetext) || importSelection()"
(onChange)="selectEntry($event, 'bibBuckets')"
placeholder="Buckets..." i18n-placeholder></eg-combobox>
</div>
</div>
<div class="col-lg-3">
<eg-combobox [entries]="formatEntries('matchSets')"
- [disabled]="selectedQueue && !selectedQueue.freetext"
- [startId]="defaultMatchSet"
+ [disabled]="(selectedQueue && !selectedQueue.freetext) || importSelection()"
+ [startId]="selectedMatchSet || defaultMatchSet"
(onChange)="selectEntry($event, 'matchSets')"
placeholder="Match Set..." i18n-placeholder></eg-combobox>
</div>
</div>
<div class="col-lg-3"> <!-- TODO disable for authority -->
<eg-combobox [entries]="formatEntries('importItemDefs')"
- [disabled]="selectedQueue && !selectedQueue.freetext"
+ [startId]="selectedHoldingsProfile"
+ [disabled]="(selectedQueue && !selectedQueue.freetext) || importSelection()"
(onChange)="selectEntry($event, 'importItemDefs')"
placeholder="Holdings Import Profile..." i18n-placeholder>
</eg-combobox>
[(ngModel)]="autoOverlayAcqCopies">
</div>
</div>
- <div class="row">
+ <div class="row" *ngIf="!importSelection()">
<div class="col-lg-3">
<label i18n>File to Upload:</label>
</div>
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>
+ <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"
</div>
</div>
<!-- hide instead of *ngIf so ViewChild can find the progress bars -->
- <div class="row" [hidden]="!showProgress">
+ <div class="row" [hidden]="!showProgress || importSelection()">
<div class="col-lg-3">
<label i18n>Upload Progress</label>
</div>
<eg-progress-inline #uploadProgress></eg-progress-inline>
</div>
</div>
- <div class="row" [hidden]="!showProgress">
+ <div class="row" [hidden]="!showProgress || importSelection()">
<div class="col-lg-3">
<label i18n>Enqueue Progress</label>
</div>
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} from './vandelay.service';
+import {VandelayService, VandelayImportSelection} 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';
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;
+
activeQueueId: number;
selectedBucket: number;
selectedBibSource: number;
selectedFallThruMergeProfile: number;
selectedFile: File;
- attrDefs: {[atype: string]: IdlObject[]};
defaultMatchSet: string;
importNonMatching: boolean;
private org: OrgService,
private vandelay: VandelayService
) {
- this.recordType = 'bib';
- this.selectedBibSource = 2; // default to system local
- this.attrDefs = {};
+ this.applyDefaults();
+ }
+
+ applyDefaults() {
+
this.minQualityRatio = 0;
+ this.selectedBibSource = 1; // default to system local
+ this.recordType = 'bib';
+
+ if (!this.vandelay.importSelection) {
+ return;
+ }
+
+ const queue = this.vandelay.importSelection.queue;
+ this.recordType = queue.queue_type();
+ this.selectedMatchSet = queue.match_set();
+ this.startQueueId = queue.id();
+
+ if (this.recordType === 'bib') {
+ this.selectedBucket = queue.match_bucket();
+ this.selectedHoldingsProfile = queue.item_attr_def();
+ }
}
ngOnInit() {}
this.loadStartupData();
}
+ 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.getAttrDefs('bib')
- .then(defs => this.attrDefs.bib = defs),
- this.vandelay.getAttrDefs('auth')
- .then(defs => this.attrDefs.auth = defs),
this.vandelay.getMergeProfiles(),
this.vandelay.getActiveQueues('bib'),
- this.vandelay.getActiveQueues('auth'),
+ this.vandelay.getActiveQueues('authority'),
this.vandelay.getMatchSets('bib'),
- this.vandelay.getMatchSets('auth'),
+ this.vandelay.getMatchSets('authority'),
this.vandelay.getBibBuckets(),
this.vandelay.getBibSources(),
this.vandelay.getItemImportDefs(),
// Required form data varies depending on context.
hasNeededData(): boolean {
- return this.selectedQueue
- && Boolean(this.recordType)
- && Boolean(this.selectedFile);
+ 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
}
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());
}
processSpool(): Promise<any> {
- const rtype = this.recordType === 'bib' ? 'bib' : 'authority';
- const method = `open-ils.vandelay.${rtype}.process_spool`;
+
+ 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 this.net.request(
'open-ils.vandelay', method,
- this.auth.token(), this.sessionKey, this.activeQueueId
+ this.auth.token(), this.sessionKey, this.activeQueueId,
+ null, null, this.selectedBibSource
).pipe(tap(
resp => {
const e = this.evt.parse(resp);
importRecords(): Promise<any> {
- // Confirm an import action was selected
- if (this.importNonMatching
- || this.mergeOnExact
- || this.mergeOnSingleMatch
- || this.mergeOnBestMatch) {
-
- return this.importRecordQueue()
+ if (!this.importActionSelected()) {
+ return Promise.resolve();
}
- return Promise.resolve();
+ const selection = this.vandelay.importSelection;
+
+ if (selection && !selection.importQueue) {
+ return this.importRecordQueue(selection.recordIds);
+ } else {
+ return this.importRecordQueue();
+ }
}
- importRecordQueue(): Promise<any> {
- const method = `open-ils.vandelay.${this.recordType}_queue.import`;
+ 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 this.net.request('open-ils.vandelay',
- method, this.auth.token(), this.activeQueueId, options
+ method, this.auth.token(), target, options
).pipe(tap(
resp => {
const e = this.evt.parse(resp);
)).toPromise();
}
- // TODO
- //importRecordList() {}
-
compileImportOptions(): ImportOptions {
const options: ImportOptions = {
return options;
}
+ clearSelection() {
+ this.vandelay.importSelection = null;
+ this.startQueueId = null;
+ }
+
openQueue() {
console.log('opening queue ' + this.activeQueueId);
}
<ng-container *ngIf="queueSummary">
<h2 i18n>Queue {{queueSummary.queue.name()}}</h2>
-
<div class="row pb-2">
- <div class="col-lg-3">
- <div class="card tight-card">
- <h5 class="card-header" i18n>Queue Actions</h5>
- <ul class="list-group list-group-flush">
- </ul>
- </div>
- </div>
- <div class="col-lg-3">
+ <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="row">
- <div class="col-lg-10" i18n>Records in Queue:</div>
- <div class="col-lg-2">{{queueSummary.total}}</div>
- </div>
- </li>
- <li class="list-group-item">
- <div class="row">
- <div class="col-lg-10" i18n>Records Imported:</div>
- <div class="col-lg-2">{{queueSummary.imported}}</div>
- </div>
- </li>
- <li class="list-group-item">
- <div class="row">
- <div class="col-lg-10" i18n>Records Import Failures:</div>
- <div class="col-lg-2">{{queueSummary.rec_import_errors}}</div>
- </div>
- </li>
- <li class="list-group-item">
- <div class="row">
- <div class="col-lg-10" i18n>Items in Queue:</div>
- <div class="col-lg-2">{{queueSummary.total_items}}</div>
+ <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="row">
- <div class="col-lg-10" i18n>Items Imported:</div>
- <div class="col-lg-2">{{queueSummary.total_items_imported}}</div>
+ <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="row">
- <div class="col-lg-10" i18n>Item Import Failures:</div>
- <div class="col-lg-2">{{queueSummary.item_import_errors}}</div>
+ <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-3">
+ <div class="col-lg-6">
<div class="card tight-card">
- <h5 class="card-header" i18n>Queue Filters</h5>
+ <h5 class="card-header" i18n>Queue Actions</h5>
<ul class="list-group list-group-flush">
<li class="list-group-item">
- <div class="row">
- <div class="col-lg-10" i18n>Limit to Records with Matches:</div>
- <div class="col-lg-2">
- <input type="checkbox" class="form-control"
- [(ngModel)]="limitToMatches"/>
+ <div class="d-flex">
+ <div class="flex-1">
+ <a [routerLink]="" (click)="importSelected()"
+ i18n>Import Selected Records</a>
+ </div>
+ <div class="flex-1">
+ <a i18n>Import All Records</a>
</div>
</div>
</li>
<li class="list-group-item">
- <div class="row">
- <div class="col-lg-10" i18n>Limit to Non-Imported Records:</div>
- <div class="col-lg-2">
- <input type="checkbox" class="form-control"
- [(ngModel)]="limitToNonImported"/>
+ <div class="d-flex">
+ <div class="flex-1">
+ <a i18n>View Import Items</a>
+ </div>
+ <div class="flex-1">
+ <a i18n>Export Non-Imported Records</a>
</div>
</div>
</li>
<li class="list-group-item">
- <div class="row">
- <div class="col-lg-10" i18n>
- Limit to Records with Import Errors</div>
- <div class="col-lg-2">
- <input type="checkbox" class="form-control"
- [(ngModel)]="limitToImportErrors"/>
+ <div class="d-flex">
+ <div class="flex-1">
+ <a i18n>Copy Queue To Bucket</a>
+ </div>
+ <div class="flex-1">
+ <a i18n>Delete Queue</a>
</div>
</div>
</li>
<eg-grid #queueGrid [dataSource]="queueSource"
hideFields="language,pagination,price,rec_identifier,eg_tcn_source,eg_identifier,item_barcode,zsource">
+
+ <eg-grid-toolbar-checkbox i18n-label label="Limit to Records With Matches"
+ [onChange]="limitToMatches"></eg-grid-toolbar-checkbox>
+
+ <eg-grid-toolbar-checkbox i18n-label label="Limit to Non-Imported Records"
+ [onChange]="limitToNonImported"></eg-grid-toolbar-checkbox>
+
+ <eg-grid-toolbar-checkbox i18n-label label="Limit to 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="View MARC"
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';
+import {VandelayService, VandelayImportSelection} from './vandelay.service';
@Component({
templateUrl: 'queue.component.html'
export class QueueComponent implements AfterViewInit {
queueId: number;
- queueType: string; // bib / auth / bib-acq
+ queueType: string; // bib / authority
queueSource: GridDataSource;
queuedRecClass: string;
queueSummary: any;
-
- // Filters
- limitToMatches: boolean;
- limitToNonImported: boolean;
- limitToImportErrors: boolean;
+
+ 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[];
});
this.queueSource = new GridDataSource();
-
- // queue API does not support sorting
this.queueSource.getRows = (pager: Pager) => {
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();
+ };
}
ngAfterViewInit() {
}
loadQueueSummary(): Promise<any> {
+ const qtype = this.queueType === 'bib' ? 'bib' : 'auth';
const method =
- `open-ils.vandelay.${this.queueType}_queue.summary.retrieve`;
+ `open-ils.vandelay.${qtype}_queue.summary.retrieve`;
return this.net.request(
'open-ils.vandelay', method, this.auth.token(), this.queueId)
clear_marc: true,
offset: pager.offset,
limit: pager.limit,
- flesh_import_items: true
+ 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.queueId, this.queueType, options, this.filters.matches)
.pipe(map(rec => {
const recHash: any = {
return recHash;
}));
}
+
+ importSelected() {
+ const rows = this.queueGrid.context.getSelectedRows();
+ if (rows.length === 0) { return; }
+ const selection = new VandelayImportSelection();
+ selection.recordIds = rows.map(row => row.id);
+ selection.queue = this.queueSummary.queue;
+ this.vandelay.importSelection = selection;
+ this.router.navigate(['/staff/cat/vandelay/import']);
+ }
}
import {EventService} from '@eg/core/event.service';
import {ProgressDialogComponent} from '@eg/share/dialog/progress.component';
+export class VandelayImportSelection {
+ recordIds: number[];
+ queue: IdlObject;
+ importQueue: boolean; // import the whole queue
+}
+
@Injectable()
export class VandelayService {
bibTrashGroups: IdlObject[];
mergeProfiles: IdlObject[];
+ // Used for tracking records between the queue page and
+ // the import page. Fields managed externally.
+ importSelection: VandelayImportSelection;
+
constructor(
private idl: IdlService,
private org: OrgService,
}
// could be a big list, invoke in streaming mode
- const qt = (qtype === 'bib') ? qtype : 'authority';
return this.net.request(
'open-ils.vandelay',
- `open-ils.vandelay.${qt}_queue.owner.retrieve`,
+ `open-ils.vandelay.${qtype}_queue.owner.retrieve`,
this.auth.token(), null, {complete: 'f'}
).pipe(tap(
queue => this.activeQueues[qtype].push(queue)
}
const owners = this.org.ancestors(this.auth.user().ws_ou(), true);
- const mt = (mtype === 'bib') ? 'biblio' : 'authority';
return this.pcrud.search('vms',
- {owner: owners, mtype: mt}, {}, {atomic: true})
+ {owner: owners, mtype: mtype}, {}, {atomic: true})
.toPromise().then(sets => {
this.matchSets[mtype] = sets;
return sets;
matchSet: number,
matchBucket: number): Promise<number> {
- const name = recordType === 'bib' ? 'bib' : 'authority';
- const method = `open-ils.vandelay.${name}_queue.create`;
+ const method = `open-ils.vandelay.${recordType}_queue.create`;
- let qType = name;
+ let qType = recordType;
if (recordType.match(/acq/)) {
let qType = 'acq';
}
});
}
- getQueuedRecords(queueId: number,
- queueType: string, options?: any): Observable<any> {
+ getQueuedRecords(queueId: number, queueType: string,
+ options?: any, limitToMatches?: boolean): Observable<any> {
- const method =
- `open-ils.vandelay.${queueType}_queue.records.retrieve`;
+ const qtype = queueType === '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);