From: Bill Erickson Date: Tue, 26 Mar 2019 20:51:54 +0000 (-0400) Subject: LP1821382 Conjoined items grid X-Git-Url: https://old-git.evergreen-ils.org/?a=commitdiff_plain;h=6534a4e20037fcfd69c89039c17396be00e4cc77;p=working%2FEvergreen.git LP1821382 Conjoined items grid Record detail conjoined items grid, with actions for batch-changing the peer type and anctions for unlinking selected rows. Signed-off-by: Bill Erickson Signed-off-by: Dan Wells --- diff --git a/Open-ILS/examples/fm_IDL.xml b/Open-ILS/examples/fm_IDL.xml index 1f510735fb..7270ee3be6 100644 --- a/Open-ILS/examples/fm_IDL.xml +++ b/Open-ILS/examples/fm_IDL.xml @@ -7476,7 +7476,7 @@ SELECT usr, - + diff --git a/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar-action.component.ts b/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar-action.component.ts index 4f8555404f..c84867fcf5 100644 --- a/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar-action.component.ts +++ b/Open-ILS/src/eg2/src/app/share/grid/grid-toolbar-action.component.ts @@ -9,17 +9,26 @@ import {GridComponent} from './grid.component'; export class GridToolbarActionComponent implements OnInit { + toolbarAction: GridToolbarAction; + // Note most input fields should match class fields for GridColumn @Input() label: string; // Register to click events @Output() onClick: EventEmitter; + // When present, actions will be grouped by the provided label. + @Input() group: string; + // DEPRECATED: Pass a reference to a function that is called on click. @Input() action: (rows: any[]) => any; - // When present, actions will be grouped by the provided label. - @Input() group: string; + @Input() set disabled(d: boolean) { + this.toolbarAction.disabled = d; + } + get disabled(): boolean { + return this.toolbarAction.disabled; + } // Optional: add a function that returns true or false. // If true, this action will be disabled; if false @@ -30,6 +39,7 @@ export class GridToolbarActionComponent implements OnInit { // get a reference to our container grid. constructor(@Host() private grid: GridComponent) { this.onClick = new EventEmitter(); + this.toolbarAction = new GridToolbarAction(); } ngOnInit() { @@ -39,12 +49,16 @@ export class GridToolbarActionComponent implements OnInit { return; } - const action = new GridToolbarAction(); - action.label = this.label; - action.action = this.action; - action.onClick = this.onClick; - action.group = this.group; - action.disableOnRows = this.disableOnRows; - this.grid.context.toolbarActions.push(action); + if (this.action) { + console.debug('toolbar [action] is deprecated. use (onClick) instead.') + } + + this.toolbarAction.label = this.label; + this.toolbarAction.onClick = this.onClick; + this.toolbarAction.group = this.group; + this.toolbarAction.action = this.action; + this.toolbarAction.disabled = this.disabled; + this.toolbarAction.disableOnRows = this.disableOnRows; + this.grid.context.toolbarActions.push(this.toolbarAction); } } 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 7cf5f53487..fc50f59332 100644 --- a/Open-ILS/src/eg2/src/app/share/grid/grid.ts +++ b/Open-ILS/src/eg2/src/app/share/grid/grid.ts @@ -1021,6 +1021,7 @@ export class GridToolbarAction { onClick: EventEmitter; action: (rows: any[]) => any; // DEPRECATED group: string; + disabled: boolean; isGroup: boolean; // used for group placeholder entries disableOnRows: (rows: any[]) => boolean; } 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 46d25d7d1c..aab101d743 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 @@ -22,6 +22,7 @@ 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'; +import {ConjoinedComponent} from './record/conjoined.component'; @NgModule({ declarations: [ @@ -41,6 +42,7 @@ import {HoldingsMaintenanceComponent} from './record/holdings.component'; PartMergeDialogComponent, BrowseComponent, BrowseResultsComponent, + ConjoinedComponent, HoldingsMaintenanceComponent ], imports: [ diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/record/conjoined.component.html b/Open-ILS/src/eg2/src/app/staff/catalog/record/conjoined.component.html new file mode 100644 index 0000000000..30b8aa5d61 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/catalog/record/conjoined.component.html @@ -0,0 +1,27 @@ + + + + + + + + +
+ + + + + + + + + + +
diff --git a/Open-ILS/src/eg2/src/app/staff/catalog/record/conjoined.component.ts b/Open-ILS/src/eg2/src/app/staff/catalog/record/conjoined.component.ts new file mode 100644 index 0000000000..560214f99b --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/catalog/record/conjoined.component.ts @@ -0,0 +1,109 @@ +import {Component, OnInit, Input, ViewChild} from '@angular/core'; +import {IdlService, IdlObject} from '@eg/core/idl.service'; +import {PcrudService} from '@eg/core/pcrud.service'; +import {Pager} from '@eg/share/util/pager'; +import {OrgService} from '@eg/core/org.service'; +import {PermService} from '@eg/core/perm.service'; +import {GridDataSource} from '@eg/share/grid/grid'; +import {GridComponent} from '@eg/share/grid/grid.component'; +import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component'; +import {ConjoinedItemsDialogComponent + } from '@eg/staff/share/holdings/conjoined-items-dialog.component'; + +/** Conjoined items per record grid */ + +@Component({ + selector: 'eg-catalog-record-conjoined', + templateUrl: 'conjoined.component.html' +}) +export class ConjoinedComponent implements OnInit { + + @Input() recordId: number; + + hasPerm: boolean; + gridDataSource: GridDataSource; + idsToUnlink: number[]; + + @ViewChild('conjoinedGrid') private grid: GridComponent; + + @ViewChild('conjoinedDialog') + private conjoinedDialog: ConjoinedItemsDialogComponent; + + @ViewChild('confirmUnlink') + private confirmUnlink: ConfirmDialogComponent; + + constructor( + private idl: IdlService, + private org: OrgService, + private pcrud: PcrudService, + private perm: PermService + ) { + this.gridDataSource = new GridDataSource(); + this.idsToUnlink = []; + } + + ngOnInit() { + // Load edit perms + this.perm.hasWorkPermHere(['UPDATE_COPY']) + .then(perms => this.hasPerm = perms.UPDATE_COPY); + + this.gridDataSource.getRows = (pager: Pager, sort: any[]) => { + const orderBy: any = {}; + + if (sort.length) { // Sort provided by grid. + orderBy.bmp = sort[0].name + ' ' + sort[0].dir; + } else { + orderBy.bmp = 'id'; + } + + const searchOps = { + offset: pager.offset, + limit: pager.limit, + order_by: orderBy + }; + + return this.pcrud.search('bpbcm', + {peer_record: this.recordId}, searchOps, {fleshSelectors: true}); + }; + } + + async unlink(rows: any) { + + this.idsToUnlink = rows.map(r => r.target_copy().id()); + if (this.idsToUnlink.length === 0) { return; } + + try { // rejects on dismiss, which results in an Error + await this.confirmUnlink.open({size: 'sm'}); + } catch (dismissed) {return;} + + const maps = []; + this.pcrud.search('bpbcm', + {target_copy: this.idsToUnlink, peer_record: this.recordId}) + .subscribe( + map => maps.push(map), + err => {}, + () => { + this.pcrud.remove(maps).subscribe( + ok => console.debug('deleted map ', ok), + err => console.error(err), + () => { + this.idsToUnlink = []; + this.grid.reload(); + } + ) + } + ); + } + + openConjoinedDialog() { + this.conjoinedDialog.open({size: 'sm'}).then( + modified => { + if (modified) { + this.grid.reload(); + } + }, + notOk => {} + ); + } +} + 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 450887034d..8be4524985 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 @@ -72,13 +72,8 @@ -
- Conjoined Items not yet implemented. See the - - AngularJS Conjoined Items Tab. - -
+ +
diff --git a/Open-ILS/src/eg2/src/app/staff/share/holdings/conjoined-items-dialog.component.html b/Open-ILS/src/eg2/src/app/staff/share/holdings/conjoined-items-dialog.component.html index 906ce24765..9801c3ee77 100644 --- a/Open-ILS/src/eg2/src/app/staff/share/holdings/conjoined-items-dialog.component.html +++ b/Open-ILS/src/eg2/src/app/staff/share/holdings/conjoined-items-dialog.component.html @@ -1,14 +1,14 @@ + text="Successfully Attached/Modified Conjoined Item(s)" i18n-text> + text="Failed To Attach/Modify Conjoined Item(s)" i18n-text> diff --git a/Open-ILS/src/eg2/src/app/staff/share/holdings/conjoined-items-dialog.component.ts b/Open-ILS/src/eg2/src/app/staff/share/holdings/conjoined-items-dialog.component.ts index 69ff7e79bb..98bd462bf1 100644 --- a/Open-ILS/src/eg2/src/app/staff/share/holdings/conjoined-items-dialog.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/share/holdings/conjoined-items-dialog.component.ts @@ -1,6 +1,6 @@ import {Component, OnInit, OnDestroy, Input, ViewChild, Renderer2} from '@angular/core'; import {Subscription} from 'rxjs'; -import {IdlService} from '@eg/core/idl.service'; +import {IdlService, IdlObject} from '@eg/core/idl.service'; import {PcrudService} from '@eg/core/pcrud.service'; import {ToastService} from '@eg/share/toast/toast.service'; import {StoreService} from '@eg/core/store.service'; @@ -23,22 +23,23 @@ export class ConjoinedItemsDialogComponent extends DialogComponent implements OnInit, OnDestroy { @Input() copyIds: number[]; - ids: number[]; // copy of list so we can pop() + + // If true, ignore the provided copyIds array and fetch all of + // the linked copies to work on. + @Input() modifyAll: boolean; + + // If peerRecord is not set, the localStorage value will be used. + @Input() peerRecord: number; peerType: number; numSucceeded: number; numFailed: number; peerTypes: ComboboxEntry[]; - peerRecord: number; existingMaps: any; - onOpenSub: Subscription; - @ViewChild('successMsg') - private successMsg: StringComponent; - - @ViewChild('errorMsg') - private errorMsg: StringComponent; + @ViewChild('successMsg') private successMsg: StringComponent; + @ViewChild('errorMsg') private errorMsg: StringComponent; constructor( private modal: NgbModal, // required for passing to parent @@ -48,25 +49,33 @@ export class ConjoinedItemsDialogComponent private localStore: StoreService) { super(modal); // required for subclassing this.peerTypes = []; + this.copyIds = []; } ngOnInit() { this.onOpenSub = this.onOpen$.subscribe(() => { - this.ids = [].concat(this.copyIds); + if (this.modifyAll) { + // This will be set once the list of copies to + // modify has been fetched. + this.copyIds = []; + } this.numSucceeded = 0; this.numFailed = 0; - this.peerRecord = - this.localStore.getLocalItem('eg.cat.marked_conjoined_record'); if (!this.peerRecord) { - this.close(false); + this.peerRecord = + this.localStore.getLocalItem('eg.cat.marked_conjoined_record'); + + if (!this.peerRecord) { + this.close(false); + } } if (this.peerTypes.length === 0) { this.getPeerTypes(); } - this.fetchExisting(); + this.fetchExistingMaps(); }); } @@ -74,17 +83,29 @@ export class ConjoinedItemsDialogComponent this.onOpenSub.unsubscribe(); } - fetchExisting() { + fetchExistingMaps() { this.existingMaps = {}; - this.pcrud.search('bpbcm', - {target_copy: this.copyIds, peer_record: this.peerRecord}) - .subscribe(map => this.existingMaps[map.target_copy()] = map); + const search: any = { + peer_record: this.peerRecord + }; + + if (!this.modifyAll) { + search.target_copy = this.copyIds; + } + + this.pcrud.search('bpbcm', search) + .subscribe(map => { + this.existingMaps[map.target_copy()] = map; + if (this.modifyAll) { + this.copyIds.push(map.target_copy()); + } + }); } + // Fetch and map peer types to combobox entries getPeerTypes(): Promise { return this.pcrud.retrieveAll('bpt', {}, {atomic: true}).toPromise() .then(types => - // Map types to ComboboxEntry's this.peerTypes = types.map(t => ({id: t.id(), label: t.name()})) ); } @@ -97,37 +118,36 @@ export class ConjoinedItemsDialogComponent } } - linkCopies(): Promise { - - if (this.ids.length === 0) { - this.close(this.numSucceeded > 0); - return Promise.resolve(); - } - - const id = this.ids.pop(); - const map = this.existingMaps[id] || this.idl.create('bpbcm'); - map.peer_record(this.peerRecord); - map.target_copy(id); - map.peer_type(this.peerType); + // Create or update peer copy links. + linkCopies() { + + const maps: IdlObject[] = []; + this.copyIds.forEach(id => { + let map: IdlObject; + if (this.existingMaps[id]) { + map = this.existingMaps[id]; + map.ischanged(true); + } else { + map = this.idl.create('bpbcm'); + map.isnew(true); + } - let promise: Promise; - if (this.existingMaps[id]) { - promise = this.pcrud.update(map).toPromise(); - } else { - promise = this.pcrud.create(map).toPromise(); - } + map.peer_record(this.peerRecord); + map.target_copy(id); + map.peer_type(this.peerType); + maps.push(map); + }); - return promise.then( - ok => { - this.successMsg.current().then(msg => this.toast.success(msg)); - this.numSucceeded++; - return this.linkCopies(); - }, + return this.pcrud.autoApply(maps).subscribe( + ok => this.numSucceeded++, err => { this.numFailed++; console.error(err); this.errorMsg.current().then(msg => this.toast.warning(msg)); - return this.linkCopies(); + }, + () => { + this.successMsg.current().then(msg => this.toast.success(msg)); + this.close(this.numSucceeded > 0); } ); }