LP1806087 Metarecord holds formats/lang selectors
authorBill Erickson <berickxx@gmail.com>
Fri, 28 Dec 2018 17:16:13 +0000 (12:16 -0500)
committerBill Erickson <berickxx@gmail.com>
Mon, 7 Jan 2019 14:58:44 +0000 (09:58 -0500)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Open-ILS/src/eg2/src/app/share/catalog/catalog.service.ts
Open-ILS/src/eg2/src/app/share/catalog/search-context.ts
Open-ILS/src/eg2/src/app/staff/catalog/hold/hold.component.html
Open-ILS/src/eg2/src/app/staff/catalog/hold/hold.component.ts
Open-ILS/src/eg2/src/app/staff/catalog/result/record.component.html
Open-ILS/src/eg2/src/app/staff/catalog/result/record.component.ts
Open-ILS/src/eg2/src/app/staff/share/hold.service.ts

index 86a014d..068fef7 100644 (file)
@@ -189,13 +189,7 @@ export class CatalogService {
             ctx.org.root().ou_type().depth() :
             ctx.searchOrg.ou_type().depth();
 
-        // Term search, looking for metarecords, but no 
-        // specific metarecord has been selected for display.
-        const isMeta = (
-            ctx.termSearch.isSearchable() && 
-            ctx.termSearch.groupByMetarecord &&
-            !ctx.termSearch.fromMetarecord
-        );
+        const isMeta = ctx.termSearch.isMetarecordSearch();
 
         let observable: Observable<BibRecordSummary>;
         
index 11e5c89..d34d711 100644 (file)
@@ -154,6 +154,16 @@ export class CatalogTermContext {
         CATALOG_CCVM_FILTERS.forEach(code => this.ccvmFilters[code] = ['']);
     }
 
+    // True when grouping by metarecord but not when displaying the
+    // contents of a metarecord.
+    isMetarecordSearch(): boolean {
+        return (
+            this.isSearchable() && 
+            this.groupByMetarecord && 
+            this.fromMetarecord === null
+        );
+    }
+
     isSearchable(): boolean {
         return (
             this.query[0] !== ''
index e4f2433..1ef096c 100644 (file)
     <div class="col-lg-1" i18n>Override</div>
   </div>
   <div class="row mt-1 ml-1 mr-1" *ngFor="let ctx of holdContexts">
-    <ng-container *ngIf="ctx.holdMeta">
-      <div class="col-lg-1">
-        <ng-container *ngFor="let code of ctx.holdMeta.bibSummary.attributes.icon_format">
-          <img class="pr-1" 
-            alt="{{iconFormatLabel(code)}}"
-            title="{{iconFormatLabel(code)}}"
-            src="/images/format_icons/icon_format/{{code}}.png"/>
-        </ng-container>
-      </div>
-      <div class="col-lg-3">
-        <a routerLink="/staff/catalog/record/{{ctx.holdMeta.bibId}}">
-          {{ctx.holdMeta.bibSummary.display.title}}
-        </a>
-      </div>
-      <div class="col-lg-2">{{ctx.holdMeta.bibSummary.display.author}}</div>
-      <div class="col-lg-2">
-        <ng-container *ngIf="ctx.holdMeta.volume; else anyValue">
-          {{ctx.holdMeta.volume.label()}}
-        </ng-container>
-      </div>
-      <div class="col-lg-1">
-        <ng-container *ngIf="ctx.holdMeta.copy; else anyValue">
-          {{ctx.holdMeta.copy.barcode()}}
-        </ng-container>
-      </div>
-      <div class="col-lg-2">
-        <ng-container *ngIf="!ctx.lastRequest && !ctx.processing">
-          <div class="alert alert-info" i18n>Hold Pending</div>
-        </ng-container>
-        <ng-container *ngIf="ctx.processing">
-          <div class="alert alert-primary" i18n>Hold Processing...</div>
-        </ng-container>
-        <ng-container *ngIf="ctx.lastRequest">
-          <ng-container *ngIf="ctx.lastRequest.result.success">
-            <div class="alert alert-success" i18n>Hold Succeeded</div>
+    <div class="col-lg-12" *ngIf="ctx.holdMeta">
+      <div class="row">
+        <div class="col-lg-1">
+          <ng-container 
+            *ngFor="let code of ctx.holdMeta.bibSummary.attributes.icon_format">
+            <img class="pr-1" 
+              alt="{{iconFormatLabel(code)}}"
+              title="{{iconFormatLabel(code)}}"
+              src="/images/format_icons/icon_format/{{code}}.png"/>
+          </ng-container>
+        </div>
+        <!-- TODO: link for a metarecord should 
+            jump to constituent bib list search page? -->
+        <div class="col-lg-3">
+          <a routerLink="/staff/catalog/record/{{ctx.holdMeta.bibId}}">
+            {{ctx.holdMeta.bibSummary.display.title}}
+          </a>
+        </div>
+        <div class="col-lg-2">{{ctx.holdMeta.bibSummary.display.author}}</div>
+        <div class="col-lg-2">
+          <ng-container *ngIf="ctx.holdMeta.volume; else anyValue">
+            {{ctx.holdMeta.volume.label()}}
+          </ng-container>
+        </div>
+        <div class="col-lg-1">
+          <ng-container *ngIf="ctx.holdMeta.copy; else anyValue">
+            {{ctx.holdMeta.copy.barcode()}}
+          </ng-container>
+        </div>
+        <div class="col-lg-2">
+          <ng-container *ngIf="!ctx.lastRequest && !ctx.processing">
+            <div class="alert alert-info" i18n>Hold Pending</div>
+          </ng-container>
+          <ng-container *ngIf="ctx.processing">
+            <div class="alert alert-primary" i18n>Hold Processing...</div>
           </ng-container>
-          <ng-container *ngIf="!ctx.lastRequest.result.success">
-            <div class="alert alert-danger">
-              {{ctx.lastRequest.result.evt.textcode}}
+          <ng-container *ngIf="ctx.lastRequest">
+            <ng-container *ngIf="ctx.lastRequest.result.success">
+              <div class="alert alert-success" i18n>Hold Succeeded</div>
+            </ng-container>
+            <ng-container *ngIf="!ctx.lastRequest.result.success">
+              <div class="alert alert-danger">
+                {{ctx.lastRequest.result.evt.textcode}}
+              </div>
+            </ng-container>
+          </ng-container>
+        </div>
+        <div class="col-lg-1">
+          <ng-container *ngIf="canOverride(ctx)">
+            <button class="btn btn-info" (click)="override(ctx)">Override</button>
+          </ng-container>
+        </div>
+      </div>
+      <!-- note: using inline style since class-level styling for rows
+          is superseded by the striped-even styling of the container -->
+      <div class="row" *ngIf="hasMetaFilters(ctx)" 
+        style="background-color:inherit; border:none">
+        <div class="col-lg-1"><label i18n>Formats: </label></div>
+        <div class="col-lg-11 d-flex">
+          <ng-container 
+            *ngFor="let ccvm of ctx.holdMeta.metarecord_filters.formats">
+            <div class="form-check ml-3">
+              <input class="form-check-input" type="checkbox" 
+                [disabled]="ctx.holdMeta.metarecord_filters.formats.length == 1"
+                [(ngModel)]="ctx.selectedFormats.formats[ccvm.code()]"/>
+              <img class="ml-1" 
+                alt="{{iconFormatLabel(ccvm.code())}}"
+                title="{{iconFormatLabel(ccvm.code())}}"
+                src="/images/format_icons/icon_format/{{ccvm.code()}}.png"/>
+              <label class="form-check-label ml-1">
+                {{ccvm.search_label() || ccvm.value()}}
+              </label>
             </div>
           </ng-container>
-        </ng-container>
+        </div>
       </div>
-      <div class="col-lg-1">
-        <ng-container *ngIf="canOverride(ctx)">
-          <button class="btn btn-info" (click)="override(ctx)">Override</button>
-        </ng-container>
+      <div class="row" *ngIf="hasMetaFilters(ctx)" 
+        style="background-color:inherit; border:none">
+        <div class="col-lg-1"><label i18n>Languages: </label></div>
+        <div class="col-lg-11 d-flex">
+          <ng-container 
+            *ngFor="let ccvm of ctx.holdMeta.metarecord_filters.langs">
+            <div class="form-check ml-3">
+              <input class="form-check-input" type="checkbox" 
+                [disabled]="ctx.holdMeta.metarecord_filters.langs.length == 1"
+                [(ngModel)]="ctx.selectedFormats.langs[ccvm.code()]"/>
+              <label class="form-check-label ml-1">
+                {{ccvm.search_label() || ccvm.value()}}
+              </label>
+            </div>
+          </ng-container>
+        </div>
       </div>
-    </ng-container>
+    </div>
   </div>
 </div>
 
index b7f341d..a0a0dc2 100644 (file)
@@ -23,6 +23,17 @@ class HoldContext {
     lastRequest: HoldRequest;
     canOverride?: boolean;
     processing: boolean;
+    selectedFormats: any;
+
+    constructor(target: number) {
+        this.holdTarget = target;
+        this.processing = false;
+        this.selectedFormats = {
+           // code => selected-boolean
+           formats: {},
+           langs: {}
+        }
+    }
 }
 
 @Component({
@@ -89,8 +100,7 @@ export class HoldComponent implements OnInit {
         this.pickupLib = this.auth.user().ws_ou();
 
         this.holdContexts = this.holdTargets.map(target => {
-            const ctx = new HoldContext();
-            ctx.holdTarget = target;
+            const ctx = new HoldContext(target);
             return ctx;
         });
 
@@ -118,10 +128,82 @@ export class HoldComponent implements OnInit {
         this.holds.getHoldTargetMeta(this.holdType, this.holdTargets)
         .subscribe(meta => {
             this.holdContexts.filter(ctx => ctx.holdTarget === meta.target)
-            .forEach(ctx => ctx.holdMeta = meta);
+            .forEach(ctx => {
+                ctx.holdMeta = meta;
+                this.mrFiltersToSelectors(ctx);
+            });
         });
     }
 
+    // By default, all metarecord filters options are enabled.
+    mrFiltersToSelectors(ctx: HoldContext) {
+        if (this.holdType !== 'M') { return; }
+
+        const meta = ctx.holdMeta;
+        if (meta.metarecord_filters) {
+            if (meta.metarecord_filters.formats) {
+                meta.metarecord_filters.formats.forEach(
+                    ccvm => ctx.selectedFormats.formats[ccvm.code()] = true);
+            }
+            if (meta.metarecord_filters.langs) {
+                meta.metarecord_filters.langs.forEach(
+                    ccvm => ctx.selectedFormats.langs[ccvm.code()] = true);
+            }
+        }
+    }
+
+    // Map the selected metarecord filters optoins to a JSON-encoded
+    // list of attr filters as required by the API.
+    // Compiles a blob of 
+    // {target: JSON({"0": [{_attr: ctype, _val: code}, ...], "1": [...]})}
+    // TODO: this should live in the hold service, not in the UI code.
+    mrSelectorsToFilters(ctx: HoldContext): {[target: number]: string} {
+
+        const meta = ctx.holdMeta;
+        const slf = ctx.selectedFormats;
+        const result: any = {};
+
+        const formats = Object.keys(slf.formats)
+            .filter(code => Boolean(slf.formats[code])); // user-selected
+
+        const langs = Object.keys(slf.langs)
+            .filter(code => Boolean(slf.langs[code])); // user-selected
+
+        const compiled: any = {};
+
+        if (formats.length > 0) {
+            compiled['0'] = [];
+            formats.forEach(code => {
+                const ccvm = meta.metarecord_filters.formats.filter(
+                    format => format.code() === code)[0];
+                compiled['0'].push({
+                    _attr: ccvm.ctype(),
+                    _val: ccvm.code()
+                });
+            });
+        }
+
+        if (langs.length > 0) {
+            compiled['1'] = [];
+            langs.forEach(code => {
+                const ccvm = meta.metarecord_filters.langs.filter(
+                    format => format.code() === code)[0];
+                compiled['1'].push({
+                    _attr: ccvm.ctype(),
+                    _val: ccvm.code()
+                });
+            });
+        }
+
+        if (Object.keys(compiled).length > 0) {
+            const result = {};
+            result[ctx.holdTarget] = JSON.stringify(compiled);
+            return result;
+        }
+
+        return null;
+    }
+
     holdForChanged() {
         this.user = null;
 
@@ -248,6 +330,7 @@ export class HoldComponent implements OnInit {
     placeOneHold(ctx: HoldContext, override?: boolean): Promise<any> {
 
         ctx.processing = true;
+        const selectedFormats = this.mrSelectorsToFilters(ctx);
 
         return this.holds.placeHold({
             holdTarget: ctx.holdTarget,
@@ -261,7 +344,8 @@ export class HoldComponent implements OnInit {
             notifySms: this.notifySms ? this.smsValue : null,
             smsCarrier: this.notifySms ? this.smsCarrier : null,
             thawDate: this.suspend ? this.activeDate : null,
-            frozen: this.suspend
+            frozen: this.suspend,
+            holdableFormats: selectedFormats
 
         }).toPromise().then(
             request => {
@@ -301,10 +385,16 @@ export class HoldComponent implements OnInit {
         return this.cat.iconFormatLabel(code);
     }
 
+    // TODO: for now, only show meta filters for meta holds.
+    // Add an "advanced holds" option to display these for T hold.
     hasMetaFilters(ctx: HoldContext): boolean {
-        return ctx.holdMeta.metarecord_filters && (
-            ctx.holdMeta.metarecord_filters.langs.length > 1 ||
-            ctx.holdMeta.metarecord_filters.formats.length > 1); 
+        return (
+            this.holdType === 'M' && // TODO
+            ctx.holdMeta.metarecord_filters && (
+                ctx.holdMeta.metarecord_filters.langs.length > 1 ||
+                ctx.holdMeta.metarecord_filters.formats.length > 1
+            )
+        );
     }
 }
 
index 41dd3bc..90f066b 100644 (file)
             <div class="float-right">
               <span>
                 <button (click)="placeHold()"
-                  [disabled]="summary.metabibId"
                   class="btn btn-sm btn-success label-with-material-icon small-text-1">
                   <span class="material-icons">check</span>
                   <span i18n>Place Hold</span>
index e4d78a5..7510b3d 100644 (file)
@@ -58,8 +58,17 @@ export class ResultRecordComponent implements OnInit, OnDestroy {
     }
 
     placeHold(): void {
-        this.router.navigate(['/staff/catalog/hold/T'], 
-            {queryParams: {target: this.summary.id}});
+        let holdType = 'T';
+        let holdTarget = this.summary.id;
+
+        const ts = this.searchContext.termSearch;
+        if (ts.isMetarecordSearch()) {
+            holdType = 'M';
+            holdTarget = this.summary.metabibId;
+        }
+
+        this.router.navigate([`/staff/catalog/hold/${holdType}`], 
+            {queryParams: {target: holdTarget}});
     }
 
     addToList(): void {
index 0bbfd63..3d89c20 100644 (file)
@@ -34,7 +34,7 @@ export interface HoldRequest {
     smsCarrier?: string;
     thawDate?: string; // ISO date
     frozen?: boolean;
-    holdableFormats?: {[id: number]: string[]};
+    holdableFormats?: {[target: number]: string};
     result?: HoldRequestResult
 };