1 import {Component, OnInit, ViewChild, HostListener} from '@angular/core';
2 import {NgbNav, NgbNavChangeEvent} from '@ng-bootstrap/ng-bootstrap';
3 import {Router, ActivatedRoute, ParamMap} from '@angular/router';
4 import {IdlObject} from '@eg/core/idl.service';
5 import {AuthService} from '@eg/core/auth.service';
6 import {CatalogSearchContext} from '@eg/share/catalog/search-context';
7 import {BibRecordService, BibRecordSummary} from '@eg/share/catalog/bib-record.service';
8 import {StaffCatalogService} from '../catalog.service';
9 import {AddedContentComponent} from '@eg/staff/catalog/content/added-content.component';
10 import {StoreService} from '@eg/core/store.service';
11 import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
12 import {MarcEditorComponent} from '@eg/staff/share/marc-edit/editor.component';
13 import {HoldingsMaintenanceComponent} from './holdings.component';
14 import {HoldingsService} from '@eg/staff/share/holdings/holdings.service';
15 import { ServerStoreService } from '@eg/core/server-store.service';
18 selector: 'eg-catalog-record',
19 templateUrl: 'record.component.html',
20 styleUrls: ['record.component.css']
22 export class RecordComponent implements OnInit {
26 added_content_activated = false;
27 added_content_sources: string[] = [];
28 summary: BibRecordSummary;
29 searchContext: CatalogSearchContext;
30 @ViewChild('recordTabs', { static: true }) recordTabs: NgbNav;
31 @ViewChild('marcEditor', {static: false}) marcEditor: MarcEditorComponent;
32 @ViewChild('addedContent', { static: true }) addedContent: AddedContentComponent;
34 @ViewChild('holdingsMaint', {static: false})
35 holdingsMaint: HoldingsMaintenanceComponent;
37 defaultTab: string; // eg.cat.default_record_tab
39 @ViewChild('pendingChangesDialog', {static: false})
40 pendingChangesDialog: ConfirmDialogComponent;
43 private router: Router,
44 private route: ActivatedRoute,
45 private auth: AuthService,
46 private bib: BibRecordService,
47 private staffCat: StaffCatalogService,
48 private holdings: HoldingsService,
49 private store: StoreService,
50 private serverStore: ServerStoreService,
54 this.searchContext = this.staffCat.searchContext;
57 this.store.getLocalItem('eg.cat.default_record_tab')
60 // Watch for URL record ID changes
61 // This includes the initial route.
62 // When applying the default configured tab, no navigation occurs
63 // to apply the tab name to the URL, it displays as the default.
64 // This is done so no intermediate redirect is required, which
65 // messes with browser back/forward navigation.
66 this.route.paramMap.subscribe((params: ParamMap) => {
67 this.recordTab = params.get('tab');
68 this.recordId = +params.get('id');
69 this.searchContext = this.staffCat.searchContext;
71 this.store.setLocalItem('eg.cat.last_record_retrieved', this.recordId);
73 if (!this.recordTab) {
74 this.recordTab = this.defaultTab || 'item_table';
82 this.defaultTab = this.recordTab;
83 this.store.setLocalItem('eg.cat.default_record_tab', this.recordTab);
86 // Changing a tab in the UI means changing the route.
87 // Changing the route ultimately results in changing the tab.
88 beforeNavChange(evt: NgbNavChangeEvent) {
90 // prevent tab changing until after route navigation
93 // Protect against tab changes with dirty data.
94 this.canDeactivate().then(ok => {
96 this.recordTab = evt.nextId;
103 * Handle 3 types of navigation which can cause loss of data.
104 * 1. Record detail tab navigation (see also beforeTabChange())
105 * 2. Intra-Angular route navigation away from the record detail page
106 * 3. Browser page unload/reload
108 * For the #1, and #2, display a eg confirmation dialog.
109 * For #3 use the stock browser onbeforeunload dialog.
111 * Note in this case a tab change is a route change, but it's one
112 * which does not cause RecordComponent to unload, so it has to be
113 * manually tracked in beforeTabChange().
115 @HostListener('window:beforeunload', ['$event'])
116 canDeactivate($event?: Event): Promise<boolean> {
118 if (this.marcEditor && this.marcEditor.changesPending()) {
120 // Each warning dialog clears the current "changes are pending"
121 // flag so the user is not presented with the dialog again
122 // unless new changes are made.
123 this.marcEditor.clearPendingChanges();
125 if ($event) { // window.onbeforeunload
126 $event.preventDefault();
127 $event.returnValue = true;
129 } else { // tab OR route change.
130 return this.pendingChangesDialog.open().toPromise();
134 return Promise.resolve(true);
140 `/staff/catalog/record/${this.recordId}/${this.recordTab}`;
142 // Retain search parameters
143 this.router.navigate([url], {queryParamsHandling: 'merge'});
148 // Avoid re-fetching the same record summary during tab navigation.
149 if (this.staffCat.currentDetailRecordSummary &&
150 this.recordId === this.staffCat.currentDetailRecordSummary.id) {
151 this.summary = this.staffCat.currentDetailRecordSummary;
152 this.activateAddedContent();
157 this.bib.getBibSummary(
159 this.searchContext.searchOrg.id(),
160 this.searchContext.isStaff).toPromise()
163 this.staffCat.currentDetailRecordSummary = summary;
164 this.activateAddedContent();
168 // Lets us intercept the summary object and augment it with
169 // search highlight data if/when it becomes available from
170 // an externally executed search.
171 summaryForDisplay(): BibRecordSummary {
172 if (!this.summary) { return null; }
173 const sum = this.summary;
174 const ctx = this.searchContext;
176 if (Object.keys(sum.displayHighlights).length === 0) {
177 if (ctx.highlightData[sum.id]) {
178 sum.displayHighlights = ctx.highlightData[sum.id];
185 currentSearchOrg(): IdlObject {
186 if (this.staffCat && this.staffCat.searchContext) {
187 return this.staffCat.searchContext.searchOrg;
192 handleMarcRecordSaved() {
193 this.staffCat.currentDetailRecordSummary = null;
197 // Our actions component broadcast a request to add holdings.
198 // If our Holdings Maintenance component is active/visible, ask
199 // it to figure out what data to pass to the holdings editor.
200 // Otherwise, just tell it to create a new call number and
201 // copy at the current working location.
202 addHoldingsRequested() {
203 if (this.holdingsMaint && this.holdingsMaint.holdingsGrid) {
204 this.holdingsMaint.openHoldingAdd(
205 this.holdingsMaint.holdingsGrid.context.getSelectedRows(),
211 this.holdings.spawnAddHoldingsUi(
212 this.recordId, null, [{owner: this.auth.user().ws_ou()}]);
216 // This just sets the record component-level flag. choices about /if/ to
217 // gather AC should go in here and set this.added_content_activated as needed.
218 activateAddedContent(): void {
219 // NovelistSelect settings
220 this.serverStore.getItemBatch([
221 'staff.added_content.novelistselect.profile',
222 'staff.added_content.novelistselect.passwd'
223 ]).then(settings => {
224 // eslint-disable-next-line eqeqeq
225 const activate = !!(Object.values(settings).filter(v => !!v).length == 2);
226 this.added_content_activated ||= activate;
229 if (!this.added_content_sources.includes('novelist')) {
230 this.added_content_sources.push('novelist');