LP#1357037: add sorting option to Angular line item lists
authorGalen Charlton <gmc@equinoxOLI.org>
Mon, 29 Nov 2021 21:04:16 +0000 (16:04 -0500)
committerJane Sandberg <js7389@princeton.edu>
Sun, 2 Oct 2022 15:02:49 +0000 (08:02 -0700)
This applies to the line item lists in the following new
Angular interfaces:

- purchase order
- selection list
- list of line items related to a bib record

The available sort options are:

- line item ID
- title
- author
- publisher
- order identifier (i.e., ISBN, ISSN, and/or UPC)

The method open-ils.acq.lineitem.unified_search is now used to retrieve
line items to make use of existing sorting functionality.

The last sort order used is persistant via a workstation setting

Signed-off-by: Galen Charlton <gmc@equinoxOLI.org>
Signed-off-by: Ruth Frasur <rfrasur@library.in.gov>
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Signed-off-by: Jane Sandberg <js7389@princeton.edu>
Open-ILS/src/eg2/src/app/staff/acq/lineitem/lineitem-list.component.html
Open-ILS/src/eg2/src/app/staff/acq/lineitem/lineitem-list.component.ts

index c471ade..c6413c1 100644 (file)
       </span>
     </div>
 
+    <div class="d-flex ml-3 justify-content-center flex-column h-100">
+      <button type="button" (click)="toggleFilterSort()"
+        class="btn btn-sm btn-outline-dark mr-1">
+        <span *ngIf="showFilterSort"  i18n>Hide Filter &amp; Sort Options</span>
+        <span *ngIf="!showFilterSort" i18n>Show Filter &amp; Sort Options</span>
+      </button>
+    </div>
+
     <div class="flex-1"></div>
 
     <div class="btn-toolbar">
       </div>
     </div><!-- buttons -->
   </div>
+  <div [hidden]="!showFilterSort">
+    <div class="col-lg-12 d-flex">
+      <div class="d-flex justify-content-center flex-column h-100">
+        <div class="form-group form-inline">
+          <label for="sort-order-select" class="form-check-label mr-1">Sort by:</label>
+          <select name="sort-order-select" id="sort-order-select"
+            [ngModel]="sortOrder" (ngModelChange)="sortOrderChange($event)"
+            class="form-control">
+            <option value="li_id_asc" i18n>Lineitem ID Ascending</option>
+            <option value="li_id_desc" i18n>Lineitem ID Descending</option>
+            <option value="title_asc" i18n>Title Ascending</option>
+            <option value="title_desc" i18n>Title Descending</option>
+            <option value="author_asc" i18n>Author Ascending</option>
+            <option value="author_desc" i18n>Author Descending</option>
+            <option value="publisher_asc" i18n>Publisher Ascending</option>
+            <option value="publisher_desc" i18n>Publisher Descending</option>
+            <option value="order_ident_asc" i18n>Order Identifier Ascending</option>
+            <option value="order_ident_desc" i18n>Order Identifier Descending</option>
+          </select>
+        </div>
+      </div>
+    </div>
+  </div>
 </div>
 
 <!-- LINEITEM LIST -->
index 941bc9f..0e2caac 100644 (file)
@@ -17,6 +17,22 @@ const DELETABLE_STATES = [
     'new', 'selector-ready', 'order-ready', 'approved', 'pending-order'
 ];
 
+const DEFAULT_SORT_ORDER = 'li_id_asc';
+const SORT_ORDER_MAP = {
+    li_id_asc:  { 'order_by': [{'class': 'jub', 'field': 'id', 'direction': 'ASC'}] },
+    li_id_desc: { 'order_by': [{'class': 'jub', 'field': 'id', 'direction': 'DESC'}] },
+    title_asc:  { 'order_by': [{'class': 'acqlia', 'field': 'attr_value', 'direction': 'ASC'}], 'order_by_attr': 'title' },
+    title_desc: { 'order_by': [{'class': 'acqlia', 'field': 'attr_value', 'direction': 'DESC'}], 'order_by_attr': 'title' },
+    author_asc:  { 'order_by': [{'class': 'acqlia', 'field': 'attr_value', 'direction': 'ASC'}], 'order_by_attr': 'author' },
+    author_desc: { 'order_by': [{'class': 'acqlia', 'field': 'attr_value', 'direction': 'DESC'}], 'order_by_attr': 'author' },
+    publisher_asc:  { 'order_by': [{'class': 'acqlia', 'field': 'attr_value', 'direction': 'ASC'}], 'order_by_attr': 'publisher' },
+    publisher_desc: { 'order_by': [{'class': 'acqlia', 'field': 'attr_value', 'direction': 'DESC'}], 'order_by_attr': 'publisher' },
+    order_ident_asc:  { 'order_by': [{'class': 'acqlia', 'field': 'attr_value', 'direction': 'ASC'}],
+                        'order_by_attr': ['isbn', 'issn', 'upc'] },
+    order_ident_desc: { 'order_by': [{'class': 'acqlia', 'field': 'attr_value', 'direction': 'DESC'}],
+                        'order_by_attr': ['isbn', 'issn', 'upc'] },
+};
+
 @Component({
   templateUrl: 'lineitem-list.component.html',
   selector: 'eg-lineitem-list',
@@ -46,6 +62,10 @@ export class LineitemListComponent implements OnInit {
     // a lot of repetitive looping.
     liMarcAttrs: {[id: number]: {[name: string]: IdlObject[]}} = {};
 
+    // sorting and filtering
+    sortOrder = DEFAULT_SORT_ORDER;
+    showFilterSort = false;
+
     batchNote: string;
     noteIsPublic = false;
     batchSelectPage = false;
@@ -56,6 +76,9 @@ export class LineitemListComponent implements OnInit {
     action = '';
     batchFailure: EgEvent;
     focusLi: number;
+    firstLoad = true; // using this to ensure that we avoid loading the LI table
+                      // until the page size and sort order WS settings have been fetched
+                      // TODO: route guard might be better
 
     @ViewChild('cancelDialog') cancelDialog: CancelDialogComponent;
 
@@ -75,7 +98,9 @@ export class LineitemListComponent implements OnInit {
         this.route.queryParamMap.subscribe((params: ParamMap) => {
             this.pager.offset = +params.get('offset');
             this.pager.limit = +params.get('limit');
-            this.load();
+            if (!this.firstLoad) {
+                this.load();
+            }
         });
 
         this.route.fragment.subscribe((fragment: string) => {
@@ -87,13 +112,24 @@ export class LineitemListComponent implements OnInit {
             this.picklistId = +params.get('picklistId');
             this.poId = +params.get('poId');
             this.recordId = +params.get('recordId');
-            this.load();
+            if (!this.firstLoad) {
+                this.load();
+            }
         });
 
         this.store.getItem('acq.lineitem.page_size').then(count => {
             this.pager.setLimit(count || 20);
-            this.load();
+            this.store.getItem('acq.lineitem.sort_order').then(sortOrder => {
+                if (sortOrder && (sortOrder in SORT_ORDER_MAP)) {
+                    this.sortOrder = sortOrder;
+                } else {
+                    this.sortOrder = DEFAULT_SORT_ORDER;
+                }
+                this.load();
+                this.firstLoad = false;
+            });
         });
+
     }
 
     pageSizeChange(count: number) {
@@ -104,6 +140,18 @@ export class LineitemListComponent implements OnInit {
         });
     }
 
+    sortOrderChange(sortOrder: string) {
+        this.store.setItem('acq.lineitem.sort_order', sortOrder).then(_ => {
+            this.sortOrder = sortOrder;
+            if (this.pager.isFirstPage()) {
+                this.load();
+            } else {
+                this.pager.toFirst();
+                this.goToPage();
+            }
+        });
+    }
+
     // Focus the selected lineitem, which may not yet exist in the
     // DOM for focusing.
     focusLineitem(id?: number) {
@@ -136,44 +184,33 @@ export class LineitemListComponent implements OnInit {
     loadIds(): Promise<any> {
         this.lineitemIds = [];
 
-        let id = this.poId;
-        let options: any = {flesh_lineitem_ids: true, li_limit: 10000};
-        let method = 'open-ils.acq.purchase_order.retrieve';
-        let handler = (po) => po.lineitems();
-        let sort = true;
+        const searchTerms = {};
+        const opts = { id_list: true, limit: 1000 };
 
         if (this.picklistId) {
-
-            id = this.picklistId;
-            options = {idlist: true, limit: 1000};
-            method = 'open-ils.acq.lineitem.picklist.retrieve.atomic';
-            handler = (ids) => ids;
-
+            Object.assign(searchTerms, { jub: [ { picklist: this.picklistId } ] });
         } else if (this.recordId) {
+            Object.assign(searchTerms, { jub: [ { eg_bib_id: this.recordId } ] });
+        } else {
+            Object.assign(searchTerms, { jub: [ { purchase_order: this.poId } ] });
+        }
 
-            id = this.recordId;
-            method = 'open-ils.acq.lineitems_for_bib.by_bib_id.atomic';
-            options = {idlist: true, limit: 1000};
-            handler = (ids) => ids;
-            // The API sorts the newest to oldest, which is what
-            // we want here.
-            sort = false;
+        if (!(this.sortOrder in SORT_ORDER_MAP)) {
+            this.sortOrder = DEFAULT_SORT_ORDER;
         }
+        Object.assign(opts, SORT_ORDER_MAP[this.sortOrder]);
 
         return this.net.request(
-            'open-ils.acq', method, this.auth.token(), id, options
+            'open-ils.acq',
+            'open-ils.acq.lineitem.unified_search.atomic',
+            this.auth.token(),
+            searchTerms, // "and" terms
+            {},          // "or" terms
+            null,
+            opts
         ).toPromise().then(resp => {
-            const ids = handler(resp);
-
-            if (sort) {
-                this.lineitemIds = ids
-                    .map(i => Number(i))
-                    .sort((id1, id2) => id1 < id2 ? -1 : 1);
-            } else {
-                this.lineitemIds = ids.map(i => Number(i));
-            }
-
-            this.pager.resultCount = ids.length;
+            this.lineitemIds = resp.map(i => Number(i));
+            this.pager.resultCount = resp.length;
         });
     }
 
@@ -423,6 +460,10 @@ export class LineitemListComponent implements OnInit {
         this.expandAll = !this.expandAll;
     }
 
+    toggleFilterSort() {
+        this.showFilterSort = !this.showFilterSort;
+    }
+
     liHasAlerts(li: IdlObject): boolean {
         return li.lineitem_notes().filter(n => n.alert_text()).length > 0;
     }