From 63b5bf93cc4484b8a248d7058eb3547d10b9a6b9 Mon Sep 17 00:00:00 2001 From: Bill Erickson Date: Fri, 15 Mar 2019 17:01:13 -0400 Subject: [PATCH] Angular holdings maintenance wip Signed-off-by: Bill Erickson --- .../app/share/grid/grid-body-cell.component.html | 4 +- .../src/app/share/grid/grid-column.component.ts | 3 + .../src/eg2/src/app/share/grid/grid.component.ts | 6 + Open-ILS/src/eg2/src/app/share/grid/grid.ts | 1 + .../eg2/src/app/staff/catalog/catalog.module.ts | 2 + .../staff/catalog/record/holdings.component.html | 73 ++++++ .../app/staff/catalog/record/holdings.component.ts | 273 +++++++++++++++++++++ .../app/staff/catalog/record/record.component.html | 9 +- 8 files changed, 362 insertions(+), 9 deletions(-) create mode 100644 Open-ILS/src/eg2/src/app/staff/catalog/record/holdings.component.html create mode 100644 Open-ILS/src/eg2/src/app/staff/catalog/record/holdings.component.ts diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-body-cell.component.html b/Open-ILS/src/eg2/src/app/share/grid/grid-body-cell.component.html index 578bef5e9c..6dc4a99b56 100644 --- a/Open-ILS/src/eg2/src/app/share/grid/grid-body-cell.component.html +++ b/Open-ILS/src/eg2/src/app/share/grid/grid-body-cell.component.html @@ -1,6 +1,6 @@ @@ -8,7 +8,7 @@ ; + @Input() disableTooltip: boolean; + // get a reference to our container grid. constructor(@Host() private grid: GridComponent) {} @@ -50,6 +52,7 @@ export class GridColumnComponent implements OnInit { col.isIndex = this.index === true; col.cellTemplate = this.cellTemplate; col.cellContext = this.cellContext; + col.disableTooltip = this.disableTooltip; col.isSortable = this.sortable; col.isMultiSortable = this.multiSortable; col.datatype = this.datatype; diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid.component.ts b/Open-ILS/src/eg2/src/app/share/grid/grid.component.ts index d48028b938..7cebaf70f1 100644 --- a/Open-ILS/src/eg2/src/app/share/grid/grid.component.ts +++ b/Open-ILS/src/eg2/src/app/share/grid/grid.component.ts @@ -79,6 +79,8 @@ export class GridComponent implements OnInit, AfterViewInit, OnDestroy { // Allow the caller to jump directly to a specific page of // grid data. @Input() pageOffset: number; + // Pass in a default page size. May be overridden by settings. + @Input() pageSize: number; context: GridContext; @@ -126,6 +128,10 @@ export class GridComponent implements OnInit, AfterViewInit, OnDestroy { this.context.pager.offset = this.pageOffset; } + if (this.pageSize) { + this.context.pager.limit = this.pageSize; + } + // TS doesn't seem to like: let foo = bar || () => ''; this.context.rowClassCallback = this.rowClassCallback || function () { return ''; }; 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 d39454ff7e..0259cdfbbd 100644 --- a/Open-ILS/src/eg2/src/app/share/grid/grid.ts +++ b/Open-ILS/src/eg2/src/app/share/grid/grid.ts @@ -30,6 +30,7 @@ export class GridColumn { isDragTarget: boolean; isSortable: boolean; isMultiSortable: boolean; + disableTooltip: boolean; comparator: (valueA: any, valueB: any) => number; // True if the column was automatically generated. diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/catalog.module.ts b/Open-ILS/src/eg2/src/app/staff/catalog/catalog.module.ts index b158ac1442..1855000f60 100644 --- a/Open-ILS/src/eg2/src/app/staff/catalog/catalog.module.ts +++ b/Open-ILS/src/eg2/src/app/staff/catalog/catalog.module.ts @@ -21,6 +21,7 @@ import {PartsComponent} from './record/parts.component'; import {PartMergeDialogComponent} from './record/part-merge-dialog.component'; import {BrowseComponent} from './browse.component'; import {BrowseResultsComponent} from './browse/results.component'; +import {HoldingsMaintenanceComponent} from './record/holdings.component'; @NgModule({ declarations: [ @@ -40,6 +41,7 @@ import {BrowseResultsComponent} from './browse/results.component'; PartMergeDialogComponent, BrowseComponent, BrowseResultsComponent, + HoldingsMaintenanceComponent ], imports: [ StaffCommonModule, 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 new file mode 100644 index 0000000000..3f0d5c71cb --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/catalog/record/holdings.component.html @@ -0,0 +1,73 @@ + + + + + + + +
+ + + + + + + + + + + + + + + + + + +
+ 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 new file mode 100644 index 0000000000..92da8f917e --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/catalog/record/holdings.component.ts @@ -0,0 +1,273 @@ +import {Component, OnInit, Input, ViewChild} from '@angular/core'; +import {Observable, Observer, of} from 'rxjs'; +import {Pager} from '@eg/share/util/pager'; +import {IdlObject} from '@eg/core/idl.service'; +import {NetService} from '@eg/core/net.service'; +import {StaffCatalogService} from '../catalog.service'; +import {OrgService} from '@eg/core/org.service'; +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'; + + +// The holdings grid models a single HoldingsTree, composed of HoldingsTreeNodes +// flattened on-demand into a list of HoldingEntry objects. +class HoldingsTreeNode { + children: HoldingsTreeNode[]; + nodeType: 'org' | 'volume' | 'copy'; + target: any; + parentNode: HoldingsTreeNode; + expanded: boolean; + constructor() { + this.children = []; + } +} + +class HoldingsTree { + root: HoldingsTreeNode; + constructor() { + this.root = new HoldingsTreeNode(); + } +} + +class HoldingsEntry { + index: number; + // org unit shortname, call number label, or copy barcode + locationLabel: string; + // location label indentation depth + locationDepth: number | null; + volumeCount: number | null; + copyCount: number | null; + callNumberLabel: string; + copy: IdlObject; + volume: IdlObject; + treeNode: HoldingsTreeNode; +} + +@Component({ + selector: 'eg-holdings-maintenance', + templateUrl: 'holdings.component.html' +}) +export class HoldingsMaintenanceComponent implements OnInit { + + recId: number; + initDone = false; + gridDataSource: GridDataSource; + gridTemplateContext: any; + @ViewChild('holdingsGrid') holdingsGrid: GridComponent; + + showVolumes: boolean; + showCopies: boolean; + showEmptyVolumes: boolean; + showEmptyLibs: boolean; + contextOrg: IdlObject; + holdingsTree: HoldingsTree; + holdingsTreeOrgCache: {[id: number]: HoldingsTreeNode}; + refreshHoldings: boolean; + rowClassCallback: (row: any) => string; + + @Input() set recordId(id: number) { + this.recId = id; + // Only force new data collection when recordId() + // is invoked after ngInit() has already run. + if (this.initDone) { + this.refreshHoldings = true; + this.holdingsGrid.reload(); + } + } + + constructor( + private net: NetService, + private org: OrgService, + private auth: AuthService, + private pcrud: PcrudService, + private staffCat: StaffCatalogService, + ) { + // 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.rowClassCallback = (row: any): string => { + if (row.volume && !row.copy) { + return 'border border-info'; + } + } + + this.gridTemplateContext = { + toggleExpandRow: (row: HoldingsEntry) => { + row.treeNode.expanded = !row.treeNode.expanded; + + if (!row.treeNode.expanded) { + // When collapsing a node, all child nodes should be + // collapsed as well. + const traverse = (node: HoldingsTreeNode) => { + node.expanded = false; + node.children.forEach(traverse); + } + traverse(row.treeNode); + } + + this.holdingsGrid.reload(); + } + } + } + + ngOnInit() { + this.initDone = true; + + this.gridDataSource.getRows = (pager: Pager, sort: any[]) => { + return this.fetchHoldings(pager); + }; + } + + initHoldingsTree() { + + // The initial tree simply matches the org unit tree + const traverseOrg = (node: HoldingsTreeNode) => { + node.expanded = true; + node.target.children().forEach((org: IdlObject) => { + const nodeChild = new HoldingsTreeNode(); + nodeChild.nodeType = 'org'; + nodeChild.target = org; + nodeChild.parentNode = node; + node.children.push(nodeChild); + this.holdingsTreeOrgCache[org.id()] = nodeChild; + traverseOrg(nodeChild); + }); + } + + this.holdingsTree = new HoldingsTree(); + this.holdingsTree.root.nodeType = 'org'; + this.holdingsTree.root.target = this.org.root(); + + this.holdingsTreeOrgCache = {}; + this.holdingsTreeOrgCache[this.org.root().id()] = this.holdingsTree.root; + + traverseOrg(this.holdingsTree.root); + } + + flattenHoldingsTree(observer: Observer) { + + let index = 0; + + const traverseTree = (node: HoldingsTreeNode) => { + + const entry = new HoldingsEntry(); + entry.treeNode = node; + entry.index = index++; + + switch(node.nodeType) { + case 'org': + entry.locationLabel = node.target.shortname(); + entry.locationDepth = node.target.ou_type().depth(); + + // Org node children are sorted with any child org nodes + // pushed to the front, followed by the call number nodes + // sorted alphabetcially by label. + // TODO: prefix/suffix + node.children = node.children.sort((a, b) => { + if (a.nodeType === 'org') { + if (b.nodeType === 'org') { + return a.target.shortname() < b.target.shortname() ? -1 : 1; + } else { + return -1; + } + } else if (b.nodeType === 'org') { + return 1; + } else { + return a.target.label() < b.target.label() ? -1 : 1; + } + }); + + break; + + case 'volume': + entry.locationLabel = node.target.label(); // TODO prefix/suffix + entry.locationDepth = node.parentNode.target.ou_type().depth() + 1; + entry.callNumberLabel = entry.locationLabel; + entry.volume = node.target; + break; + + case 'copy': + entry.locationLabel = node.target.barcode(); + entry.locationDepth = node.parentNode.parentNode.target.ou_type().depth() + 2; + entry.callNumberLabel = node.parentNode.target.label() // TODO + entry.volume = node.parentNode.target; + entry.copy = node.target; + } + + observer.next(entry); + if (node.expanded) { + node.children.forEach(traverseTree); + } + } + + traverseTree(this.holdingsTree.root); + observer.complete(); + } + + fetchHoldings(pager: Pager): Observable { + if (!this.recId) { return of([]); } + + return new Observable(observer => { + + if (!this.refreshHoldings) { + this.flattenHoldingsTree(observer); + return; + } + + this.initHoldingsTree(); + + this.pcrud.search('acn', + { record: this.recId, + owning_lib: this.org.ancestors(this.contextOrg, true), + deleted: 'f', + label: {'!=' : '##URI##'} + }, { + flesh: 3, + flesh_fields: { + acp: ['status', 'location', 'circ_lib', 'parts', + 'age_protect', 'copy_alerts', 'latest_inventory'], + acn: ['prefix', 'suffix', 'copies'], + acli: ['inventory_workstation'] + } + } + ).subscribe( + vol => this.appendVolume(vol), + err => {}, + () => { + this.refreshHoldings = false; + this.flattenHoldingsTree(observer); + } + ); + }); + } + + appendVolume(volume: IdlObject) { + + const volNode = new HoldingsTreeNode(); + volNode.parentNode = this.holdingsTreeOrgCache[volume.owning_lib()]; + 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) + .forEach((copy: IdlObject) => { + const copyNode = new HoldingsTreeNode(); + copyNode.parentNode = volNode; + volNode.children.push(copyNode); + copyNode.nodeType = 'copy'; + copyNode.target = copy; + copyNode.expanded = true; // TODO if show copies + }); + + } +} + + diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/record/record.component.html b/Open-ILS/src/eg2/src/app/staff/catalog/record/record.component.html index ad75118182..b583cf7e71 100644 --- a/Open-ILS/src/eg2/src/app/staff/catalog/record/record.component.html +++ b/Open-ILS/src/eg2/src/app/staff/catalog/record/record.component.html @@ -66,13 +66,8 @@ -
- Holdings not yet implemented. See the - - AngularJS Holdings Tab. - -
+ +
-- 2.11.0