From: Bill Erickson Date: Mon, 18 Mar 2019 17:49:59 +0000 (-0400) Subject: Angular holdings maintenance wip X-Git-Url: https://old-git.evergreen-ils.org/?a=commitdiff_plain;h=e9c6c33b787a3e69cca3f3381faa0b479a0a7aff;p=working%2FEvergreen.git Angular holdings maintenance wip Signed-off-by: Bill Erickson --- diff --git a/Open-ILS/src/eg2/src/app/core/server-store.service.ts b/Open-ILS/src/eg2/src/app/core/server-store.service.ts index ea2d93da36..b54a4c9a87 100644 --- a/Open-ILS/src/eg2/src/app/core/server-store.service.ts +++ b/Open-ILS/src/eg2/src/app/core/server-store.service.ts @@ -60,6 +60,22 @@ export class ServerStoreService { ); } + // Sync call for items known to be cached locally. + getItemCached(key: string): any { + return this.cache[key]; + } + + // Sync batch call for items known to be cached locally + getItemBatchCached(keys: string[]): {[key: string]: any} { + const values: any = {}; + keys.forEach(key => { + if (key in this.cache) { + values[key] = this.cache[key]; + } + }); + return values; + } + // Returns a set of key/value pairs for the requested settings getItemBatch(keys: string[]): Promise { diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar-checkbox.component.ts b/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar-checkbox.component.ts index 7ee3019477..24b7616e86 100644 --- a/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar-checkbox.component.ts +++ b/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar-checkbox.component.ts @@ -16,29 +16,48 @@ export class GridToolbarCheckboxComponent implements OnInit { // This does NOT fire the onChange handler. @Input() initialValue: boolean; - // This is an input instead of an Output because the handler is - // passed off to the grid context for maintenance -- events - // are not fired directly from this component. @Output() onChange: EventEmitter; + private cb: GridToolbarCheckbox; + // get a reference to our container grid. constructor(@Host() private grid: GridComponent) { this.onChange = new EventEmitter(); + + // Create in constructor so we can accept values before the + // grid is fully rendered. + this.cb = new GridToolbarCheckbox(); + this.cb.isChecked = null; + this.initialValue = null; } ngOnInit() { - if (!this.grid) { console.warn('GridToolbarCheckboxComponent needs a [grid]'); return; } - const cb = new GridToolbarCheckbox(); - cb.label = this.label; - cb.onChange = this.onChange; - cb.isChecked = this.initialValue; + this.cb.label = this.label; + this.cb.onChange = this.onChange; + + if (this.cb.isChecked === null && this.initialValue !== null) { + this.cb.isChecked = this.initialValue; + } + + this.grid.context.toolbarCheckboxes.push(this.cb); + } - this.grid.context.toolbarCheckboxes.push(cb); + // Toggle the value. onChange is not fired. + toggle() { + this.cb.isChecked = !this.cb.isChecked; + } + + // Set/get the value. onChange is not fired. + checked(value?: boolean): boolean { + if (value === true || value === false) { + this.cb.isChecked = value; + } + return this.cb.isChecked; } } diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid.ts b/Open-ILS/src/eg2/src/app/share/grid/grid.ts index 0259cdfbbd..d717c428fd 100644 --- a/Open-ILS/src/eg2/src/app/share/grid/grid.ts +++ b/Open-ILS/src/eg2/src/app/share/grid/grid.ts @@ -672,7 +672,7 @@ export class GridContext { for (let i = 0; i < steps.length; i++) { const step = steps[i]; - if (typeof obj !== 'object') { + if (typeof obj !== 'object' || obj === null) { // We have run out of data to step through before // reaching the end of the path. Conclude fleshing via // callback if provided then exit. diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/record/holdings.component.html b/Open-ILS/src/eg2/src/app/staff/catalog/record/holdings.component.html index 4ae2f66c05..db65e99930 100644 --- a/Open-ILS/src/eg2/src/app/staff/catalog/record/holdings.component.html +++ b/Open-ILS/src/eg2/src/app/staff/catalog/record/holdings.component.html @@ -6,10 +6,10 @@ - arrow_drop_down - arrow_drop_down + arrow_right {{row.locationLabel}} @@ -27,10 +27,18 @@ - - + + + + + + + + @@ -48,36 +56,29 @@ - - + diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/record/holdings.component.ts b/Open-ILS/src/eg2/src/app/staff/catalog/record/holdings.component.ts index 804ad04dff..ebead26e96 100644 --- a/Open-ILS/src/eg2/src/app/staff/catalog/record/holdings.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/catalog/record/holdings.component.ts @@ -1,5 +1,6 @@ import {Component, OnInit, Input, ViewChild} from '@angular/core'; import {Observable, Observer, of} from 'rxjs'; +import {map} from 'rxjs/operators'; import {Pager} from '@eg/share/util/pager'; import {IdlObject} from '@eg/core/idl.service'; import {NetService} from '@eg/core/net.service'; @@ -9,6 +10,7 @@ import {AuthService} from '@eg/core/auth.service'; import {PcrudService} from '@eg/core/pcrud.service'; import {GridDataSource} from '@eg/share/grid/grid'; import {GridComponent} from '@eg/share/grid/grid.component'; +import {GridToolbarCheckboxComponent} from '@eg/share/grid/grid-toolbar-checkbox.component'; import {ServerStoreService} from '@eg/core/server-store.service'; @@ -45,6 +47,7 @@ class HoldingsEntry { callNumberLabel: string; copy: IdlObject; volume: IdlObject; + circ: IdlObject; treeNode: HoldingsTreeNode; } @@ -60,20 +63,26 @@ export class HoldingsMaintenanceComponent implements OnInit { gridTemplateContext: any; @ViewChild('holdingsGrid') holdingsGrid: GridComponent; - showVolumes: boolean; - showCopies: boolean; - showEmptyVolumes: boolean; - showEmptyLibs: boolean; + // Manage visibility of various sub-sections + @ViewChild('volsCheckbox') volsCheckbox: GridToolbarCheckboxComponent; + @ViewChild('copiesCheckbox') copiesCheckbox: GridToolbarCheckboxComponent; + @ViewChild('emptyVolsCheckbox') emptyVolsCheckbox: GridToolbarCheckboxComponent; + @ViewChild('emptyLibsCheckbox') emptyLibsCheckbox: GridToolbarCheckboxComponent; + contextOrg: IdlObject; holdingsTree: HoldingsTree; holdingsTreeOrgCache: {[id: number]: HoldingsTreeNode}; refreshHoldings: boolean; gridIndex: number; - rowClassCallback: (row: any) => string; - // TODO XXXX The checkbox toggles will change to @Output handlers - // in a pending catalog working branch. - toggleShowVolumes: (value: boolean) => void; + // List of copies whose due date we need to retrieve. + itemCircsNeeded: IdlObject[]; + + // When true draw the grid based on the stored preferences. + // When not true, render based on the current "expanded" state of each node. + // Rendering from prefs happens on initial load and when any prefs change. + renderFromPrefs: boolean; + rowClassCallback: (row: any) => string; @Input() set recordId(id: number) { this.recId = id; @@ -94,11 +103,10 @@ export class HoldingsMaintenanceComponent implements OnInit { private store: ServerStoreService ) { // Set some sane defaults before settings are loaded. - this.showVolumes = true; - this.showCopies = true; this.contextOrg = this.org.get(this.auth.user().ws_ou()); this.gridDataSource = new GridDataSource(); this.refreshHoldings = true; + this.renderFromPrefs = true; this.rowClassCallback = (row: any): string => { if (row.volume && !row.copy) { @@ -123,47 +131,77 @@ export class HoldingsMaintenanceComponent implements OnInit { this.holdingsGrid.reload(); } } - - - // TODO XXXX The checkbox toggles will change to @Output handlers - // in a pending catalog working branch. - this.toggleShowVolumes = (value: boolean) => { - this.showVolumes = value; - this.store.setItem('cat.holdings_show_vols', value); - this.holdingsGrid.reload(); - } - } - - onRowActivate(row: any) { - if (row.copy) { - // Launch copy editor? - } else { - this.gridTemplateContext.toggleExpandRow(row); - } } ngOnInit() { this.initDone = true; // These are pre-cached via the resolver. - this.store.getItemBatch([ + const settings = this.store.getItemBatchCached([ 'cat.holdings_show_empty_org', 'cat.holdings_show_empty', 'cat.holdings_show_copies', 'cat.holdings_show_vols' - ]).then(settings => { - this.showCopies = settings['cat.holdings_show_copies']; - console.log('show copies = ', this.showCopies); - this.showVolumes = settings['cat.holdings_show_vols']; - this.showEmptyVolumes = settings['cat.holdings_show_empty']; - this.showEmptyLibs = settings['cat.holdings_show_empty_org']; - }); + ]); + + this.volsCheckbox.checked(settings['cat.holdings_show_vols']); + this.copiesCheckbox.checked(settings['cat.holdings_show_copies']); + this.emptyVolsCheckbox.checked(settings['cat.holdings_show_empty']); + this.emptyLibsCheckbox.checked(settings['cat.holdings_show_empty_org']); this.gridDataSource.getRows = (pager: Pager, sort: any[]) => { return this.fetchHoldings(pager); }; } + ngAfterViewInit() { + + } + + toggleShowCopies(value: boolean) { + this.store.setItem('cat.holdings_show_copies', value); + if (value) { + // Showing copies implies showing volumes + this.volsCheckbox.checked(true); + } + this.renderFromPrefs = true; + this.holdingsGrid.reload(); + } + + toggleShowVolumes(value: boolean) { + this.store.setItem('cat.holdings_show_vols', value); + if (!value) { + // Hiding volumes implies hiding empty vols and copies. + this.copiesCheckbox.checked(false); + this.emptyVolsCheckbox.checked(false); + } + this.renderFromPrefs = true; + this.holdingsGrid.reload(); + } + + toggleShowEmptyVolumes(value: boolean) { + this.store.setItem('cat.holdings_show_empty', value); + if (value) { + this.volsCheckbox.checked(true); + } + this.renderFromPrefs = true; + this.holdingsGrid.reload(); + } + + toggleShowEmptyLibs(value: boolean) { + this.store.setItem('cat.holdings_show_empty_org', value); + this.renderFromPrefs = true; + this.holdingsGrid.reload(); + } + + onRowActivate(row: any) { + if (row.copy) { + // Launch copy editor? + } else { + this.gridTemplateContext.toggleExpandRow(row); + } + } + initHoldingsTree() { // The initial tree simply matches the org unit tree @@ -210,6 +248,7 @@ export class HoldingsMaintenanceComponent implements OnInit { } // Sets call number and copy count sums to nodes that need it. + // Applies the initial expansed state of each container node. setTreeCounts(node: HoldingsTreeNode) { if (node.nodeType === 'org') { @@ -219,6 +258,8 @@ export class HoldingsMaintenanceComponent implements OnInit { node.copyCount = 0; } + let hasChildOrgWithData = false; + let hasChildOrgSansData = false; node.children.forEach(child => { this.setTreeCounts(child); if (node.nodeType === 'org') { @@ -226,12 +267,31 @@ export class HoldingsMaintenanceComponent implements OnInit { if (child.nodeType === 'volume') { node.volumeCount++; } else { + hasChildOrgWithData = child.volumeCount > 0; + hasChildOrgSansData = child.volumeCount === 0; node.volumeCount += child.volumeCount; } } else if (node.nodeType === 'volume') { node.copyCount = node.children.length; + if (this.renderFromPrefs) { + node.expanded = this.copiesCheckbox.checked(); + } } }); + + if (this.renderFromPrefs && node.nodeType === 'org') { + if (node.copyCount > 0 && this.volsCheckbox.checked()) { + node.expanded = true; + } else if (node.volumeCount > 0 && this.emptyVolsCheckbox.checked()) { + node.expanded = true; + } else if (hasChildOrgWithData) { + node.expanded = true; + } else if (hasChildOrgSansData && this.emptyLibsCheckbox.checked()) { + node.expanded = true; + } else { + node.expanded = false; + } + } } // Create HoldingsEntry objects for tree nodes that should be displayed @@ -243,11 +303,9 @@ export class HoldingsMaintenanceComponent implements OnInit { switch(node.nodeType) { case 'org': - // Confirm the user wants to see this node - if (!this.showEmptyLibs) { - if (node.volumeCount === 0) { - return; - } + if (this.renderFromPrefs && node.volumeCount === 0 + && !this.emptyLibsCheckbox.checked()) { + return; } entry.locationLabel = node.target.shortname(); entry.locationDepth = node.target.ou_type().depth(); @@ -257,14 +315,6 @@ export class HoldingsMaintenanceComponent implements OnInit { break; case 'volume': - // Confirm the user wants to see this node - if (!this.showVolumes) { - return; - } - if (!this.showEmptyVolumes && node.copyCount === 0) { - return; - } - entry.locationLabel = node.target.label(); // TODO prefix/suffix entry.locationDepth = node.parentNode.target.ou_type().depth() + 1; entry.callNumberLabel = entry.locationLabel; @@ -278,6 +328,7 @@ export class HoldingsMaintenanceComponent implements OnInit { entry.callNumberLabel = node.parentNode.target.label() // TODO entry.volume = node.parentNode.target; entry.copy = node.target; + entry.circ = node.target._circ; break; } @@ -297,6 +348,7 @@ export class HoldingsMaintenanceComponent implements OnInit { this.setTreeCounts(this.holdingsTree.root); this.propagateTreeEntries(observer, this.holdingsTree.root); observer.complete(); + this.renderFromPrefs = false; } fetchHoldings(pager: Pager): Observable { @@ -310,6 +362,7 @@ export class HoldingsMaintenanceComponent implements OnInit { } this.initHoldingsTree(); + this.itemCircsNeeded = []; this.pcrud.search('acn', { record: this.recId, @@ -330,12 +383,29 @@ export class HoldingsMaintenanceComponent implements OnInit { err => {}, () => { this.refreshHoldings = false; - this.flattenHoldingsTree(observer); + this.fetchCircs().then( + ok => this.flattenHoldingsTree(observer) + ); } ); }); } + // Retrieve circulation objects for checked out items. + fetchCircs(): Promise { + const copyIds = this.itemCircsNeeded.map(copy => copy.id()); + if (copyIds.length === 0) { return Promise.resolve(); } + + return this.pcrud.search('circ', { + target_copy: copyIds, + checkin_time: null + }).pipe(map(circ => { + const copy = this.itemCircsNeeded.filter( + c => Number(c.id()) === Number(circ.target_copy()))[0]; + copy._circ = circ; + })).toPromise(); + } + appendVolume(volume: IdlObject) { const volNode = new HoldingsTreeNode(); @@ -343,7 +413,6 @@ export class HoldingsMaintenanceComponent implements OnInit { volNode.parentNode.children.push(volNode); volNode.nodeType = 'volume'; volNode.target = volume; - volNode.expanded = true; // TODO if show volumes volume.copies() .sort((a: IdlObject, b: IdlObject) => a.barcode() < b.barcode() ? -1 : 1) @@ -353,9 +422,11 @@ export class HoldingsMaintenanceComponent implements OnInit { volNode.children.push(copyNode); copyNode.nodeType = 'copy'; copyNode.target = copy; - copyNode.expanded = true; // TODO if show copies + const stat = Number(copy.status().id()); + if (stat === 1 /* checked out */ || stat === 16 /* long overdue */) { + this.itemCircsNeeded.push(copy); + } }); - } }