LP1821382 Conjoined items grid
authorBill Erickson <berickxx@gmail.com>
Tue, 26 Mar 2019 20:51:54 +0000 (16:51 -0400)
committerDan Wells <dbw2@calvin.edu>
Wed, 29 May 2019 19:30:51 +0000 (15:30 -0400)
Record detail conjoined items grid, with actions for batch-changing the
peer type and anctions for unlinking selected rows.

Signed-off-by: Bill Erickson <berickxx@gmail.com>
Signed-off-by: Dan Wells <dbw2@calvin.edu>
Open-ILS/examples/fm_IDL.xml
Open-ILS/src/eg2/src/app/share/grid/grid-toolbar-action.component.ts
Open-ILS/src/eg2/src/app/share/grid/grid.ts
Open-ILS/src/eg2/src/app/staff/catalog/catalog.module.ts
Open-ILS/src/eg2/src/app/staff/catalog/record/conjoined.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/catalog/record/conjoined.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/catalog/record/record.component.html
Open-ILS/src/eg2/src/app/staff/share/holdings/conjoined-items-dialog.component.html
Open-ILS/src/eg2/src/app/staff/share/holdings/conjoined-items-dialog.component.ts

index 1f51073..7270ee3 100644 (file)
@@ -7476,7 +7476,7 @@ SELECT  usr,
                        <field reporter:label="Last Editing User" name="editor" reporter:datatype="link"/>
                        <field reporter:label="Fine Level" name="fine_level" reporter:datatype="int"/>
                        <field reporter:label="Is Holdable" name="holdable" reporter:datatype="bool" />
-                       <field reporter:label="Copy ID" name="id" reporter:datatype="id"/>
+                       <field reporter:label="Copy ID" name="id" reporter:selector="barcode" reporter:datatype="id"/>
                        <field reporter:label="Loan Duration" name="loan_duration" reporter:datatype="int"/>
                        <field reporter:label="Shelving Location" name="location" reporter:datatype="link"/>
                        <field reporter:label="OPAC Visible" name="opac_visible" reporter:datatype="bool" />
index 4f85554..c84867f 100644 (file)
@@ -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<any []>;
 
+    // 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<any []>();
+        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);
     }
 }
index 7cf5f53..fc50f59 100644 (file)
@@ -1021,6 +1021,7 @@ export class GridToolbarAction {
     onClick: EventEmitter<any []>;
     action: (rows: any[]) => any; // DEPRECATED
     group: string;
+    disabled: boolean;
     isGroup: boolean; // used for group placeholder entries
     disableOnRows: (rows: any[]) => boolean;
 }
index 46d25d7..aab101d 100644 (file)
@@ -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 (file)
index 0000000..30b8aa5
--- /dev/null
@@ -0,0 +1,27 @@
+
+
+<eg-conjoined-items-dialog #conjoinedDialog 
+  [peerRecord]="recordId" [modifyAll]="true">
+</eg-conjoined-items-dialog>
+
+<eg-confirm-dialog #confirmUnlink 
+  i18n-dialogTitle dialogTitle="Confirm Unlink"
+  i18n-dialogBody dialogBody="Unlink {{idsToUnlink.length}} Items?">
+</eg-confirm-dialog>
+
+<div class="mt-3">
+
+  <eg-grid #conjoinedGrid idlClass="bpbcm" [dataSource]="gridDataSource"
+      [sortable]="true" persistKey="catalog.record.conjoined" class="mt-3">
+    
+    <eg-grid-toolbar-button [disabled]="!hasPerm" i18n-label 
+      label="Change Type" (onClick)="openConjoinedDialog()">
+    </eg-grid-toolbar-button>
+    
+    <eg-grid-toolbar-action 
+      [disabled]="!hasPerm"
+      label="Unlink" i18n-label (onClick)="unlink($event)">
+    </eg-grid-toolbar-action>
+
+  </eg-grid>
+</div>
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 (file)
index 0000000..560214f
--- /dev/null
@@ -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 => {}
+        );
+    }
+}
+
index 4508870..8be4524 100644 (file)
       </ngb-tab>
       <ngb-tab title="Conjoined Items" i18n-title id="conjoined">
         <ng-template ngbTabContent>
-          <div class="alert alert-info mt-3" i18n>
-            Conjoined Items not yet implemented.  See the
-            <a target="_blank"
-              href="/eg/staff/cat/catalog/record/{{recordId}}/conjoined">
-              AngularJS Conjoined Items Tab.
-            </a>
-          </div>
+          <eg-catalog-record-conjoined [recordId]="recordId">
+          </eg-catalog-record-conjoined>
         </ng-template>
       </ngb-tab>
     </ngb-tabset>
index 906ce24..9801c3e 100644 (file)
@@ -1,14 +1,14 @@
 
 
 <eg-string #successMsg
-    text="Successfully Attached Conjoined Item(s)" i18n-text></eg-string>
+    text="Successfully Attached/Modified Conjoined Item(s)" i18n-text></eg-string>
 <eg-string #errorMsg 
-    text="Failed To Attach Conjoined Item(s)" i18n-text></eg-string>
+    text="Failed To Attach/Modify Conjoined Item(s)" i18n-text></eg-string>
 
 <ng-template #dialogContent>
     <div class="modal-header bg-info">
       <h4 class="modal-title">
-        <span i18n>Attach {{copyIds.length}} Conjoined Item(s)</span>
+        <span i18n>Attach/Modify {{copyIds.length}} Conjoined Item(s)</span>
       </h4>
       <button type="button" class="close" 
         i18n-aria-label aria-label="Close" (click)="dismiss('cross_click')">
@@ -34,7 +34,7 @@
           (click)="dismiss('canceled')" i18n>Cancel</button>
         <button type="button" class="btn btn-success" 
           (click)="linkCopies()" [disabled]="!peerType" i18n>
-          Attach
+          Attach/Modify
         </button>
       </ng-container>
     </div>
index 69ff7e7..98bd462 100644 (file)
@@ -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<any> {
         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<any> {
-
-        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<any>;
-        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);
             }
         );
     }