* string. For example: Control and 't' becomes 'ctrl+t'.
*/
compressKeys(evt: KeyboardEvent): string {
-
+ if (!evt.key) {
+ return null;
+ }
let s = '';
if (evt.ctrlKey || evt.metaKey) { s += 'ctrl+'; }
if (evt.altKey) { s += 'alt+'; }
}
}
+ if (context.cnBrowseSearch.isSearchable()) {
+ params.cnBrowseTerm = context.cnBrowseSearch.value;
+ params.cnBrowsePage = context.cnBrowseSearch.offset;
+ }
+
return params;
}
}
}
+ if (params.get('cnBrowseTerm')) {
+ context.cnBrowseSearch.value = params.get('cnBrowseTerm');
+ context.cnBrowseSearch.offset = Number(params.get('cnBrowsePage'));
+ }
+
const ts = context.termSearch;
// browseEntry and query searches may be facet-limited
ctx.searchState = CatalogSearchState.COMPLETE;
}));
}
+
+ cnBrowse(ctx: CatalogSearchContext): Observable<any> {
+ ctx.searchState = CatalogSearchState.SEARCHING;
+ const cbs = ctx.cnBrowseSearch;
+
+ return this.net.request(
+ 'open-ils.supercat',
+ 'open-ils.supercat.call_number.browse',
+ cbs.value, ctx.searchOrg.shortname(), ctx.pager.limit, cbs.offset
+ ).pipe(tap(result => ctx.searchState = CatalogSearchState.COMPLETE));
+ }
}
}
+export class CatalogCnBrowseContext {
+ value: string;
+ // offset in pages from base browse term
+ // e.g. -2 means 2 pages back (alphabetically) from the original search.
+ offset: number;
+
+ reset() {
+ this.value = '';
+ this.offset = 0;
+ }
+
+ isSearchable() {
+ return this.value !== '';
+ }
+}
+
export class CatalogTermContext {
fieldClass: string[];
query: string[];
marcSearch: CatalogMarcContext;
identSearch: CatalogIdentContext;
browseSearch: CatalogBrowseContext;
+ cnBrowseSearch: CatalogCnBrowseContext;
// Result from most recent search.
result: CatalogSearchResults;
this.marcSearch = new CatalogMarcContext();
this.identSearch = new CatalogIdentContext();
this.browseSearch = new CatalogBrowseContext();
+ this.cnBrowseSearch = new CatalogCnBrowseContext();
this.reset();
}
<eg-catalog-search-form #searchForm></eg-catalog-search-form>
-<eg-catalog-browse-results><eg-catalog-browse-results>
+<eg-catalog-browse-results></eg-catalog-browse-results>
import {Component, OnInit, ViewChild} from '@angular/core';
import {StaffCatalogService} from './catalog.service';
-import {BasketService} from '@eg/share/catalog/basket.service';
import {SearchFormComponent} from './search-form.component';
@Component({
@ViewChild('searchForm') searchForm: SearchFormComponent;
constructor(
- private staffCat: StaffCatalogService,
- private basket: BasketService
+ private staffCat: StaffCatalogService
) {}
ngOnInit() {
// A SearchContext provides all the data needed for browse.
this.staffCat.createContext();
-
- // Cache the basket on page load.
- this.basket.getRecordIds();
-
this.searchForm.searchTab = 'browse';
}
}
-import {Component, OnInit, Input} from '@angular/core';
-import {Observable, Subscription} from 'rxjs';
-import {map, switchMap, distinctUntilChanged} from 'rxjs/operators';
+import {Component, OnInit, OnDestroy} from '@angular/core';
import {ActivatedRoute, ParamMap} from '@angular/router';
+import {Subscription} from 'rxjs';
import {CatalogService} from '@eg/share/catalog/catalog.service';
-import {BibRecordService} from '@eg/share/catalog/bib-record.service';
import {CatalogUrlService} from '@eg/share/catalog/catalog-url.service';
import {CatalogSearchContext, CatalogSearchState} from '@eg/share/catalog/search-context';
-import {PcrudService} from '@eg/core/pcrud.service';
import {StaffCatalogService} from '../catalog.service';
-import {IdlObject} from '@eg/core/idl.service';
@Component({
selector: 'eg-catalog-browse-results',
templateUrl: 'results.component.html'
})
-export class BrowseResultsComponent implements OnInit {
+export class BrowseResultsComponent implements OnInit, OnDestroy {
searchContext: CatalogSearchContext;
results: any[];
+ routeSub: Subscription;
constructor(
private route: ActivatedRoute,
- private pcrud: PcrudService,
private cat: CatalogService,
- private bib: BibRecordService,
private catUrl: CatalogUrlService,
private staffCat: StaffCatalogService
) {}
ngOnInit() {
this.searchContext = this.staffCat.searchContext;
- this.route.queryParamMap.subscribe((params: ParamMap) => {
- this.browseByUrl(params);
- });
+ this.routeSub = this.route.queryParamMap.subscribe(
+ (params: ParamMap) => this.browseByUrl(params)
+ );
+ }
+
+ ngOnDestroy() {
+ this.routeSub.unsubscribe();
}
browseByUrl(params: ParamMap): void {
// child components. After initial creation, the context is
// reset and updated as needed to apply new search parameters.
this.staffCat.createContext();
-
- // Cache the basket on page load.
- this.basket.getRecordIds();
}
}
import {BrowseResultsComponent} from './browse/results.component';
import {HoldingsMaintenanceComponent} from './record/holdings.component';
import {ConjoinedComponent} from './record/conjoined.component';
+import {CnBrowseComponent} from './cnbrowse.component';
+import {CnBrowseResultsComponent} from './cnbrowse/results.component';
@NgModule({
declarations: [
BrowseComponent,
BrowseResultsComponent,
ConjoinedComponent,
- HoldingsMaintenanceComponent
+ HoldingsMaintenanceComponent,
+ CnBrowseComponent,
+ CnBrowseResultsComponent
],
imports: [
StaffCommonModule,
*/
browse(): void {
if (!this.searchContext.browseSearch.isSearchable()) { return; }
-
const params = this.catUrl.toUrlParams(this.searchContext);
// Force a new browse every time this method is called, even if
params.ridx = '' + this.routeIndex++;
this.router.navigate(
- ['/staff/catalog/browse'], {queryParams: params});
+ ['/staff/catalog/browse'], {queryParams: params});
+ }
+
+ // Call number browse.
+ // Redirect to cn browse page and let its component perform the search
+ cnBrowse(): void {
+ if (!this.searchContext.cnBrowseSearch.isSearchable()) { return; }
+ const params = this.catUrl.toUrlParams(this.searchContext);
+ params.ridx = '' + this.routeIndex++; // see comments above
+ this.router.navigate(['/staff/catalog/cnbrowse'], {queryParams: params});
}
}
--- /dev/null
+
+<eg-catalog-search-form #searchForm></eg-catalog-search-form>
+
+<eg-catalog-cn-browse-results></eg-catalog-cn-browse-results>
+
--- /dev/null
+import {Component, OnInit, ViewChild} from '@angular/core';
+import {StaffCatalogService} from './catalog.service';
+import {SearchFormComponent} from './search-form.component';
+
+@Component({
+ templateUrl: 'cnbrowse.component.html'
+})
+export class CnBrowseComponent implements OnInit {
+
+ @ViewChild('searchForm') searchForm: SearchFormComponent;
+
+ constructor(
+ private staffCat: StaffCatalogService,
+ ) {}
+
+ ngOnInit() {
+ // A SearchContext provides all the data needed for browse.
+ this.staffCat.createContext();
+ this.searchForm.searchTab = 'cnbrowse';
+ }
+}
+
--- /dev/null
+<!-- search results progress bar -->
+<div class="row" *ngIf="browseIsActive()">
+ <div class="col-lg-6 offset-lg-3 pt-3">
+ <div class="progress">
+ <div class="progress-bar progress-bar-striped active w-100"
+ role="progressbar" aria-valuenow="100"
+ aria-valuemin="0" aria-valuemax="100">
+ <span class="sr-only" i18n>Searching..</span>
+ </div>
+ </div>
+ </div>
+</div>
+
+<!-- no items found -->
+<div *ngIf="browseIsDone() && !browseHasResults()">
+ <div class="row pt-3">
+ <div class="col-lg-6 offset-lg-3">
+ <div class="alert alert-warning">
+ <span i18n>No Maching Items Were Found</span>
+ </div>
+ </div>
+ </div>
+</div>
+
+<!-- header, pager, and list of records -->
+<div id="staff-catalog-browse-results-container" *ngIf="browseHasResults()">
+
+ <div class="row mb-2">
+ <div class="col-lg-3">
+ <button class="btn btn-primary" (click)="prevPage()">Back</button>
+ <button class="btn btn-primary ml-3" (click)="nextPage()">Next</button>
+ </div>
+ </div>
+
+ <div class="row" *ngFor="let result of results; let idx = index">
+ <div class="col-lg-12" *ngIf="result._bibSummary">
+ <eg-catalog-result-record [summary]="result._bibSummary"
+ [index]="idx" [callNumber]="result">
+ </eg-catalog-result-record>
+ </div>
+ </div>
+
+ <div class="row mb-2">
+ <div class="col-lg-3">
+ <button class="btn btn-primary" (click)="prevPage()">Back</button>
+ <button class="btn btn-primary ml-3" (click)="nextPage()">Next</button>
+ </div>
+ </div>
+
+</div>
+
+
--- /dev/null
+import {Component, OnInit, OnDestroy} from '@angular/core';
+import {ActivatedRoute, Router, ParamMap} from '@angular/router';
+import {Subscription} from 'rxjs';
+import {IdlObject} from '@eg/core/idl.service';
+import {CatalogService} from '@eg/share/catalog/catalog.service';
+import {BibRecordService} from '@eg/share/catalog/bib-record.service';
+import {CatalogUrlService} from '@eg/share/catalog/catalog-url.service';
+import {CatalogSearchContext, CatalogSearchState} from '@eg/share/catalog/search-context';
+import {StaffCatalogService} from '../catalog.service';
+import {BibRecordSummary} from '@eg/share/catalog/bib-record.service';
+
+@Component({
+ selector: 'eg-catalog-cn-browse-results',
+ templateUrl: 'results.component.html'
+})
+export class CnBrowseResultsComponent implements OnInit, OnDestroy {
+
+ searchContext: CatalogSearchContext;
+ results: any[];
+ routeSub: Subscription;
+
+ constructor(
+ private router: Router,
+ private route: ActivatedRoute,
+ private cat: CatalogService,
+ private bib: BibRecordService,
+ private catUrl: CatalogUrlService,
+ private staffCat: StaffCatalogService
+ ) {}
+
+ ngOnInit() {
+ this.searchContext = this.staffCat.searchContext;
+ this.routeSub = this.route.queryParamMap.subscribe(
+ (params: ParamMap) => this.browseByUrl(params)
+ );
+ }
+
+ ngOnDestroy() {
+ this.routeSub.unsubscribe();
+ }
+
+ browseByUrl(params: ParamMap): void {
+ this.catUrl.applyUrlParams(this.searchContext, params);
+ const cbs = this.searchContext.cnBrowseSearch;
+
+ if (cbs.isSearchable()) {
+ this.results = [];
+ this.cat.cnBrowse(this.searchContext)
+ .subscribe(results => this.processResults(results));
+ }
+ }
+
+ processResults(results: any[]) {
+ this.results = results;
+
+ const depth = this.searchContext.global ?
+ this.searchContext.org.root().ou_type().depth() :
+ this.searchContext.searchOrg.ou_type().depth();
+
+ const bibIds = this.results.map(r => r.record().id());
+ const distinct = (value: any, index: number, self: Array<number>) => {
+ return self.indexOf(value) === index;
+ };
+
+ const bres: IdlObject[] = [];
+ this.bib.getBibSummary(
+ bibIds.filter(distinct),
+ this.searchContext.searchOrg.id(), depth
+ ).subscribe(
+ summary => {
+ // Response order not guaranteed. Match the summary
+ // object up with its response object. A bib may be
+ // linked to multiple call numbers
+ const bibResults = this.results.filter(
+ r => Number(r.record().id()) === summary.id);
+
+ bres.push(summary.record);
+
+ // Use _ since result is an 'acn' object.
+ bibResults.forEach(r => r._bibSummary = summary);
+ },
+ err => {},
+ () => {
+ this.bib.fleshBibUsers(bres);
+ }
+ );
+ }
+
+ browseIsDone(): boolean {
+ return this.searchContext.searchState === CatalogSearchState.COMPLETE;
+ }
+
+ browseIsActive(): boolean {
+ return this.searchContext.searchState === CatalogSearchState.SEARCHING;
+ }
+
+ browseHasResults(): boolean {
+ return this.browseIsDone() && this.results.length > 0;
+ }
+
+ prevPage() {
+ this.searchContext.cnBrowseSearch.offset--;
+ this.staffCat.cnBrowse();
+ }
+
+ nextPage() {
+ this.searchContext.cnBrowseSearch.offset++;
+ this.staffCat.cnBrowse();
+ }
+
+ /**
+ * Propagate the search params along when navigating to each record.
+ */
+ navigateToRecord(summary: BibRecordSummary) {
+ const params = this.catUrl.toUrlParams(this.searchContext);
+
+ this.router.navigate(
+ ['/staff/catalog/record/' + summary.id], {queryParams: params});
+ }
+}
+
+
import {Injectable} from '@angular/core';
-import {Observable, Observer} from 'rxjs';
import {Router, Resolve, RouterStateSnapshot,
ActivatedRouteSnapshot} from '@angular/router';
import {ServerStoreService} from '@eg/core/server-store.service';
import {NetService} from '@eg/core/net.service';
import {OrgService} from '@eg/core/org.service';
import {AuthService} from '@eg/core/auth.service';
-import {PcrudService} from '@eg/core/pcrud.service';
import {CatalogService} from '@eg/share/catalog/catalog.service';
import {StaffCatalogService} from './catalog.service';
+import {BasketService} from '@eg/share/catalog/basket.service';
+
@Injectable()
export class CatalogResolver implements Resolve<Promise<any[]>> {
private net: NetService,
private auth: AuthService,
private cat: CatalogService,
- private staffCat: StaffCatalogService
+ private staffCat: StaffCatalogService,
+ private basket: BasketService
) {}
resolve(
return Promise.all([
this.cat.fetchCcvms(),
this.cat.fetchCmfs(),
- this.fetchSettings()
+ this.fetchSettings(),
+ this.basket.getRecordIds()
]);
}
<img src="/opac/extras/ac/jacket/small/r/{{summary.id}}"/>
</a>
</div>
+ <!-- for call number browse display -->
+ <ng-container *ngIf="callNumber">
+ <div class="pl-2 font-weight-bold">
+ {{callNumber.prefix().label()}}
+ {{callNumber.label()}}
+ {{callNumber.suffix().label()}}
+ @ {{orgName(callNumber.owning_lib())}}
+ </div>
+ </ng-container>
<div class="flex-1 pl-2">
<div class="row">
<div class="col-lg-12 font-weight-bold">
import {Router} from '@angular/router';
import {OrgService} from '@eg/core/org.service';
import {NetService} from '@eg/core/net.service';
+import {IdlObject} from '@eg/core/idl.service';
import {CatalogService} from '@eg/share/catalog/catalog.service';
import {BibRecordService, BibRecordSummary} from '@eg/share/catalog/bib-record.service';
import {CatalogSearchContext} from '@eg/share/catalog/search-context';
@Input() index: number; // 0-index display row
@Input() summary: BibRecordSummary;
+
+ // Optional call number (acn) object to highlight
+ // Assumed prefix/suffix are fleshed
+ // Used by call number browse.
+ @Input() callNumber: IdlObject;
+
searchContext: CatalogSearchContext;
isRecordSelected: boolean;
basketSub: Subscription;
import {CatalogResolver} from './resolver.service';
import {HoldComponent} from './hold/hold.component';
import {BrowseComponent} from './browse.component';
+import {CnBrowseComponent} from './cnbrowse.component';
const routes: Routes = [{
path: '',
path: 'record/:id/:tab',
component: RecordComponent
}]}, {
- // Browse is a top-level UI
- path: 'browse',
- component: BrowseComponent,
- resolve: {catResolver : CatalogResolver},
-}];
+ // Browse is a top-level UI
+ path: 'browse',
+ component: BrowseComponent,
+ resolve: {catResolver : CatalogResolver}
+ }, {
+ path: 'cnbrowse',
+ component: CnBrowseComponent,
+ resolve: {catResolver : CatalogResolver}
+ }
+];
@NgModule({
imports: [RouterModule.forChild(routes)],
</div>
</ng-template>
</ngb-tab>
+ <ngb-tab title="Shelf Browse" i18n-title id="cnbrowse">
+ <ng-template ngbTabContent>
+ <div class="row mt-4">
+ <div class="col-lg-12 form-inline">
+ <label for="cnbrowse-term-input" i18n>
+ Browse Call Numbers starting with
+ </label>
+ <input type="text" class="form-control ml-2"
+ id='cnbrowse-term-input' name="query"
+ [(ngModel)]="context.cnBrowseSearch.value"
+ (keyup.enter)="searchByForm()"
+ placeholder="Browse Call Numbers..."/>
+ </div>
+ </div>
+ </ng-template>
+ </ngb-tab>
</ngb-tabset>
</div>
<div class="col-lg-4">
focusTabInput() {
// Select a DOM node to focus when the tab changes.
- let selector;
+ let selector: string;
switch (this.searchTab) {
case 'ident':
selector = '#ident-query-input';
case 'browse':
selector = '#browse-term-input';
break;
+ case 'cnbrowse':
+ selector = '#cnbrowse-term-input';
+ break;
default:
this.refreshCopyLocations();
selector = '#first-query-input';
this.context.marcSearch.reset();
this.context.browseSearch.reset();
this.context.identSearch.reset();
+ this.context.cnBrowseSearch.reset();
this.context.termSearch.hasBrowseEntry = '';
this.context.termSearch.browseEntry = null;
this.context.termSearch.fromMetarecord = null;
this.context.marcSearch.reset();
this.context.browseSearch.reset();
this.context.termSearch.reset();
+ this.context.cnBrowseSearch.reset();
this.staffCat.search();
break;
this.context.browseSearch.reset();
this.context.termSearch.reset();
this.context.identSearch.reset();
+ this.context.cnBrowseSearch.reset();
this.staffCat.search();
break;
this.context.marcSearch.reset();
this.context.termSearch.reset();
this.context.identSearch.reset();
+ this.context.cnBrowseSearch.reset();
this.context.browseSearch.pivot = null;
this.staffCat.browse();
break;
+
+ case 'cnbrowse':
+ this.context.marcSearch.reset();
+ this.context.termSearch.reset();
+ this.context.identSearch.reset();
+ this.context.browseSearch.reset();
+ this.context.cnBrowseSearch.offset = 0;
+ this.staffCat.cnBrowse();
+ break;
}
}
searchIsActive(): boolean {
return this.context.searchState === CatalogSearchState.SEARCHING;
}
-
- goToBrowse() {
- this.router.navigate(['/staff/catalog/browse']);
- }
}