LP#1863387: multi-select now allows filtering shelving locations by owner
authorGalen Charlton <gmc@equinoxOLI.org>
Fri, 9 Sep 2022 16:55:22 +0000 (12:55 -0400)
committerGalen Charlton <gmc@equinoxOLI.org>
Wed, 26 Apr 2023 19:41:22 +0000 (19:41 +0000)
The Angular multi-select component now has a special case for
shelving locations: when the IDL class of "acpl" is selected,
rather than just displaying a combobox, the item-location-select
component followed by an org selector is displayed and checkbox.

The org selector defaults to workstation OU and is used to restrict
the list of shelving locations displayed in the shelving location
combobox to the context org unit and its ancestors. If the checkbox
is also selected, descendants of the context OU are included as well.

The effect of this is to allow large consortial to more efficiently
select the shelving locations to be used by a carousel.

To test
-------
[1] Apply the patch.
[2] Create or edit carousel definitions. Verify that the widget
    for the carousel's shelving locations now displays both a
    combobox for the location selector as well as one for the
    location owning library. Further verify that when the OU
    selector for the owning library is changed, that the list
    of available shelving locations reflects the locations available
    at the ancestors of the filter OU. Also verify that the
    "Include descendants?" checkbox updates the list of available
    locations as well.

Signed-off-by: Galen Charlton <gmc@equinoxOLI.org>
fix

Signed-off-by: Galen Charlton <gmc@equinoxOLI.org>
Signed-off-by: Jeff Davis <jdavis@sitka.bclibraries.ca>
Open-ILS/src/eg2/src/app/share/item-location-select/item-location-select.component.ts
Open-ILS/src/eg2/src/app/share/multi-select/multi-select.component.html
Open-ILS/src/eg2/src/app/share/multi-select/multi-select.component.ts
Open-ILS/src/eg2/src/app/staff/common.module.ts

index 809043e..3800b1a 100644 (file)
@@ -44,12 +44,25 @@ export class ItemLocationSelectComponent
         this.ngOnInit();
     }
 
+    // ... though if includeDescendants is true, shelving
+    // locations at the descendants of the context OU are
+    // also included; this is a special case for the
+    // carousels editor
+    @Input() set includeDescendants(value: boolean) {
+        this._includeDescendants = value;
+        this.ngOnInit();
+    }
+    get includeDescendants(): boolean {
+        return this._includeDescendants;
+    }
+
     get contextOrgId(): number {
         return this._contextOrgId;
     }
 
     // Load locations for multiple context org units.
     private _contextOrgIds = [];
+    private _includeDescendants = false;
     @Input() set contextOrgIds(value: number[]) {
         this._contextOrgIds = value;
     }
@@ -62,6 +75,8 @@ export class ItemLocationSelectComponent
 
     // Emits an acpl object or null on combobox value change
     @Output() valueChange: EventEmitter<IdlObject>;
+    // Emits the combobox entry or null on value change
+    @Output() entryChange: EventEmitter<ComboboxEntry>;
 
     @Input() required: boolean;
 
@@ -106,6 +121,7 @@ export class ItemLocationSelectComponent
         private loc: ItemLocationService
     ) {
         this.valueChange = new EventEmitter<IdlObject>();
+        this.entryChange = new EventEmitter<ComboboxEntry>();
     }
 
     ngOnInit() {
@@ -235,6 +251,7 @@ export class ItemLocationSelectComponent
         const id = entry ? entry.id : null;
         this.propagateChange(id);
         this.valueChange.emit(id ? this.loc.locationCache[id] : null);
+        this.entryChange.emit(entry ? entry : null);
     }
 
     writeValue(id: number) {
@@ -277,6 +294,9 @@ export class ItemLocationSelectComponent
 
         let orgIds = [];
         contextOrgIds.forEach(id => orgIds = orgIds.concat(this.org.ancestors(id, true)));
+        if (this.includeDescendants) {
+            contextOrgIds.forEach(id => orgIds = orgIds.concat(this.org.descendants(id, true)));
+        }
 
         this.filterOrgsApplied = true;
 
@@ -300,6 +320,9 @@ export class ItemLocationSelectComponent
             permOrgIds.forEach(orgId => {
                 if (orgIds.includes(orgId)) {
                     trimmedOrgIds = trimmedOrgIds.concat(this.org.ancestors(orgId, true));
+                    if (this.includeDescendants) {
+                        trimmedOrgIds = trimmedOrgIds.concat(this.org.descendants(orgId, true));
+                    }
                 }
             });
 
index 1926abf..7ec8cf3 100644 (file)
@@ -1,9 +1,34 @@
 <div>
   <div class="row">
-    <eg-combobox [idlBaseQuery]="idlBaseQuery" [idlClass]="idlClass" 
-      [idlIncludeLibraryInLabel]="linkedLibraryLabel" [asyncSupportsEmptyTermClick]="true"
-      (onChange)="valueSelected($event)">
-    </eg-combobox>
+    <ng-container *ngIf="idlClass === 'acpl'">
+      <div class="col-6">
+        <eg-item-location-select (entryChange)="valueSelected($event)"
+          [contextOrgId]="acplContextOrgId"
+          [includeDescendants]="acplIncludeDescendants"
+          domId='location-input'>
+        </eg-item-location-select>
+      </div>
+      <div class="col-6">
+        <label for="context_library" class="form-label" i18n>Owned by</label>
+        <eg-org-select
+          domId="context_library"
+          [applyDefault]="true"
+          (onChange)="acplContextOrgId = $event.id()">
+        </eg-org-select>
+        <label class="form-check-label" for="acplIncludeDescendants" i18n>Include descendants?</label>
+        <input type="checkbox"
+          domId="acplIncludeDescendants"
+          id="acplIncludeDescendants"
+          [(ngModel)]="acplIncludeDescendants"
+          class="ml-1">
+      </div>
+    </ng-container>
+    <ng-container *ngIf="idlClass !== 'acpl'">
+      <eg-combobox [idlBaseQuery]="idlBaseQuery" [idlClass]="idlClass"
+        [idlIncludeLibraryInLabel]="linkedLibraryLabel" [asyncSupportsEmptyTermClick]="true"
+        (onChange)="valueSelected($event)">
+      </eg-combobox>
+    </ng-container>
     <button class="btn btn-outline-dark" (click)="addSelectedValue()" [disabled]="!this.selected" i18n>Add</button>
   </div>
   <div class="row" *ngFor="let entry of entrylist">
index 3496f55..7f502d3 100644 (file)
@@ -7,7 +7,9 @@ import {map} from 'rxjs/operators';
 import {Observable, of, Subject} from 'rxjs';
 import {StoreService} from '@eg/core/store.service';
 import {PcrudService} from '@eg/core/pcrud.service';
+import {OrgService} from '@eg/core/org.service';
 import {ComboboxComponent, ComboboxEntry} from '@eg/share/combobox/combobox.component';
+import {ItemLocationSelectComponent} from '@eg/share/item-location-select/item-location-select.component';
 
 @Component({
   selector: 'eg-multi-select',
@@ -30,9 +32,13 @@ export class MultiSelectComponent implements OnInit {
 
     @Output() onChange: EventEmitter<string>;
 
+    acplContextOrgId: number;
+    acplIncludeDescendants: boolean;
+
     constructor(
       private store: StoreService,
       private pcrud: PcrudService,
+      private org: OrgService,
     ) {
         this.entrylist = [];
         this.onChange = new EventEmitter<string>();
@@ -45,7 +51,22 @@ export class MultiSelectComponent implements OnInit {
             this.selected = null;
         }
     }
+
+    getOrgShortname(ou: any) {
+        if (typeof ou === 'object') {
+            return ou.shortname();
+        } else {
+            return this.org.get(ou).shortname();
+        }
+    }
+
     addSelectedValue() {
+        // special case to format the label
+        if (this.idlClass === 'acpl' && this.selected.userdata) {
+            this.selected.label =
+                this.selected.userdata.name() + ' (' +
+                this.getOrgShortname(this.selected.userdata.owning_lib()) + ')';
+        }
         this.entrylist.push(this.selected);
         this.onChange.emit(this.compileCurrentValue());
     }
index 7802455..853576f 100644 (file)
@@ -24,6 +24,7 @@ import {BroadcastService} from '@eg/share/util/broadcast.service';
 import {CourseService} from './share/course.service';
 import {FileExportService} from '@eg/share/util/file-export.service';
 import {OfflineService} from '@eg/staff/share/offline.service';
+import {ItemLocationSelectModule} from '@eg/share/item-location-select/item-location-select.module';
 
 /**
  * Imports the EG common modules and adds modules common to all staff UI's.
@@ -51,7 +52,8 @@ import {OfflineService} from '@eg/staff/share/offline.service';
     EgCommonModule,
     CommonWidgetsModule,
     GridModule,
-    CatalogCommonModule
+    CatalogCommonModule,
+    ItemLocationSelectModule
   ],
   exports: [
     EgCommonModule,