Lp#1846552: Port Local Admin Shelving Location Order Editor to Angular collab/khuckins/lp1846552-port-local-admin-shelving-location-order-editor
authorZavier Banks <zbanks@catalyte.io>
Fri, 11 Oct 2019 21:17:40 +0000 (21:17 +0000)
committerKyle Huckins <khuckins@catalyte.io>
Tue, 18 Aug 2020 17:31:10 +0000 (17:31 +0000)
I ported the admin shelving location from dojo, into Angular, with all the
same bells and whistles. Using the Angular framework, the user can drag and
drop through a list of different org units, and save said list to
a database. I also added keyboard functionality and changed the naming conventions to
be inline with the current meta. Additionally, there is a confirm message if
the user tries to leave without saving data.

Signed-off-by: Zavier Banks <zbanks@catalyte.io>
 Changes to be committed:
modified:   Open-ILS/src/eg2/src/app/staff/admin/local/admin-local.module.ts
new file:   Open-ILS/src/eg2/src/app/staff/admin/local/copy-location/copy-location-order.component.html
new file:   Open-ILS/src/eg2/src/app/staff/admin/local/copy-location/copy-location-order.component.ts
new file:   Open-ILS/src/eg2/src/app/staff/admin/local/copy-location/copy-location-routing.module.ts
new file:   Open-ILS/src/eg2/src/app/staff/admin/local/copy-location/copy-location.module.ts
modified:   Open-ILS/src/eg2/src/app/staff/admin/local/routing.module.ts
new file:   Open-ILS/src/eg2/src/app/staff/admin/local/share/card.component.html
new file:   Open-ILS/src/eg2/src/app/staff/admin/local/share/card.component.ts
new file:   Open-ILS/src/eg2/src/app/staff/admin/local/share/table-list.component.html
new file:   Open-ILS/src/eg2/src/app/staff/admin/local/share/table-list.component.ts
modified:   Open-ILS/src/eg2/src/app/staff/admin/local/admin-local-splash.component.html

Open-ILS/src/eg2/src/app/staff/admin/local/admin-local-splash.component.html
Open-ILS/src/eg2/src/app/staff/admin/local/admin-local.module.ts
Open-ILS/src/eg2/src/app/staff/admin/local/routing.module.ts
Open-ILS/src/eg2/src/app/staff/admin/local/share/card.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/local/share/card.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/local/share/sortable-list.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/local/share/sortable-list.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/local/shelving-location/shelving-location-order.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/local/shelving-location/shelving-location-order.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/local/shelving-location/shelving-location-routing.module.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/local/shelving-location/shelving-location.module.ts [new file with mode: 0644]

index 223d181..10d8c59 100644 (file)
       url="/eg/staff/admin/local/config/circ_matrix_matchpoint"></eg-link-table-link>
     <eg-link-table-link i18n-label label="Closed Dates Editor" 
       url="/eg/staff/admin/local/actor/closed_dates"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Item Alert Types" 
+      url="/eg/staff/admin/local/config/copy_alert_types"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Item Alert Suppression" 
+      routerLink="/staff/admin/local/actor/copy_alert_suppress"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Item Tags" 
+      routerLink="/staff/admin/local/asset/copy_tag"></eg-link-table-link>
     <!-- do-able with a list of IDL classes to add to the edit dialog -->
     <eg-link-table-link i18n-label label="Field Documentation" 
       url="/eg/staff/admin/local/config/idl_field_doc"></eg-link-table-link>
@@ -53,7 +59,7 @@
     <eg-link-table-link i18n-label label="Shelving Location Groups" 
       url="/eg/staff/admin/local/asset/copy_location_group"></eg-link-table-link>
     <eg-link-table-link i18n-label label="Shelving Location Order" 
-      url="/eg/staff/admin/local/asset/copy_location_order"></eg-link-table-link>
+      routerLink="/staff/admin/local/asset/shelving_location_order"></eg-link-table-link>
     <eg-link-table-link i18n-label label="Shelving Locations Editor" 
       routerLink="/staff/admin/local/asset/copy_location"></eg-link-table-link>
     <eg-link-table-link i18n-label label="Standing Penalties" 
index 9f70ab7..3bbdcea 100644 (file)
@@ -13,7 +13,7 @@ import {StandingPenaltyComponent} from './standing-penalty.component';
       AdminLocalSplashComponent,
       AddressAlertComponent,
       AdminCarouselComponent,
-      StandingPenaltyComponent
+      StandingPenaltyComponent,
   ],
   imports: [
     AdminCommonModule,
index c29e7e6..269bee2 100644 (file)
@@ -6,7 +6,10 @@ import {AddressAlertComponent} from './address-alert.component';
 import {AdminCarouselComponent} from './admin-carousel.component';
 import {StandingPenaltyComponent} from './standing-penalty.component';
 
-const routes: Routes = [{
+const routes: Routes = [ {
+    path: 'asset/shelving_location_order',
+    loadChildren: '@eg/staff/admin/local/shelving-location/shelving-location.module#ShelvingLocationModule'
+},{
     path: 'splash',
     component: AdminLocalSplashComponent
 }, {
@@ -29,7 +32,9 @@ const routes: Routes = [{
 }, {
     path: ':schema/:table',
     component: BasicAdminPageComponent
-}];
+},
+
+];
 
 @NgModule({
   imports: [RouterModule.forChild(routes)],
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/share/card.component.html b/Open-ILS/src/eg2/src/app/staff/admin/local/share/card.component.html
new file mode 100644 (file)
index 0000000..a6aa2fd
--- /dev/null
@@ -0,0 +1,10 @@
+<div (mouseenter) = "onMouseEnter()" (mouseleave) = "onMouseLeave()"
+  [ngStyle] = "{display:'flex', flexDirection:'row'}">
+  <span *ngIf = "displaySelection()"
+    [ngStyle] = "divider_style" class = "material-icons">keyboard_arrow_right
+  </span>
+  <button  class="btn btn-lg" (mousedown) = "onMouseDownEvent()"
+    (mouseup) = "onMouseUpEvent()" [style.fontSize.px] = "13" aria-grabbed="displaySelection()" #button>
+    {{index+1}}. <ng-content></ng-content>
+  </button>
+</div>
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/share/card.component.ts b/Open-ILS/src/eg2/src/app/staff/admin/local/share/card.component.ts
new file mode 100644 (file)
index 0000000..6e38859
--- /dev/null
@@ -0,0 +1,59 @@
+import {Component, OnInit, Input,Output, EventEmitter, ViewChild, ElementRef} from '@angular/core';
+
+@Component({
+    selector:'card',
+    templateUrl: './card.component.html'
+})
+
+export class CardComponent implements OnInit {
+    @Input() content:any;
+    @Input() index:number;
+    @Input() content_length:number;
+    @Input() is_mouse_down:boolean;
+    @Input() index_for_card:number;
+    @Output() detectMouseUpEvents = new EventEmitter<any>();
+    @Output() detectMouseDownEvents = new EventEmitter<any>();
+    @ViewChild('button', { static: false }) button: ElementRef;
+    detect_drag = false;
+
+    card_style = {
+        display:"flex",
+        flexDirection:"column"
+    }
+
+    divider_style = {
+        marginTop:"4px",
+        width:"2px"
+    }
+
+    constructor() {
+    }
+
+    ngOnInit() {};
+
+    onMouseEnter() {
+        this.detect_drag = true;
+        this.button.nativeElement.focus()
+    }
+    onMouseLeave() {
+        this.detect_drag = false;
+    }
+
+    onMouseUpEvent() {
+        this.is_mouse_down = false;
+        this.detectMouseUpEvents.emit(this.index);
+    }
+    
+    onMouseDownEvent() {
+        this.is_mouse_down = true;
+        this.detectMouseDownEvents.emit(this.index);
+    }
+
+    displaySelection() {
+        if ((this.is_mouse_down == true && this.detect_drag == true) || this.index_for_card == this.index) {
+            this.button.nativeElement.focus()
+            return true;
+        }
+        return false;
+    }
+}
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/share/sortable-list.component.html b/Open-ILS/src/eg2/src/app/staff/admin/local/share/sortable-list.component.html
new file mode 100644 (file)
index 0000000..bcf746f
--- /dev/null
@@ -0,0 +1,21 @@
+<div [ngStyle]="table_container" (keydown)="onKeyDown($event.key)">
+  <div [ngStyle]="card_container" (mouseleave)="onMouseLeave()" (mouseenter)="onMouseEnter()">
+    <ng-container *ngIf="mutable_content_list && mutable_content_list.length > 0">
+      <ul aria-dropeffect="move">
+        <div *ngFor="let content of mutable_content_list; let i = index">
+            <ng-container *ngIf="content">
+              <card [content]="content" [index]="i"
+                [content_length]="mutable_content_list.length"
+                [is_mouse_down]="is_mouse_down"
+                [index_for_card]="index_for_card"
+                (detectMouseDownEvents)="onMouseDown($event)" 
+                (detectMouseUpEvents)="onMouseUp($event)"
+                >
+                {{content.name}} ({{content.shortname}})
+              </card>
+            </ng-container>
+        </div>
+      </ul>
+    </ng-container>
+  </div>
+</div>
\ No newline at end of file
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/share/sortable-list.component.ts b/Open-ILS/src/eg2/src/app/staff/admin/local/share/sortable-list.component.ts
new file mode 100644 (file)
index 0000000..0b2b79e
--- /dev/null
@@ -0,0 +1,238 @@
+import {Component, OnInit, Input,Output,EventEmitter, SimpleChanges} from '@angular/core';
+
+/**
+ * Creates a class for the pairs.
+ */
+class Pair {
+    1:number;
+    2:number;
+}
+
+@Component({
+    selector:'sortable-list',
+    templateUrl: './sortable-list.component.html'
+})
+
+export class SortableListComponent implements OnInit {
+    @Input() content_list:any;
+    @Input() is_mouse_down = false;
+    @Input() is_submit_button_pressed = false;
+    @Output() saveContent = new EventEmitter<any>();
+    @Output() detectMousePosition = new EventEmitter<boolean>();
+    @Output() nodeIsSelected =  new EventEmitter<any>();
+    @Output() toggleDragAndDrop = new EventEmitter<any>();
+    @Output() tableRender = new EventEmitter<any>();
+    @Output() toggleKeyboardSelection = new EventEmitter<boolean>();
+    @Output() haveDetectedChanges = new EventEmitter<void>();
+    detect_mouse_leave = false;
+    mutable_content_list:any;
+    mutable_content_dictionary = {};
+    card_pairs:Pair;
+    index_for_card:number;
+
+    /**
+     * The Following are style variables to style to tabs and cards in the html file.
+     */
+
+    table_container = {
+        display:"flex",
+        flexDirection:"row",
+        justifyContent:"flex-start"
+    }
+
+    card_container = {
+        display:"flex",
+        flexDirection:"column",
+        justifyContent:"center",
+        width:"300px",
+        marginRight:"10px"
+    }
+
+    button_style = {
+        margin:"auto",
+    }
+
+    constructor() {};
+
+    ngOnInit() {
+        this.onRender();
+    };
+
+    ngOnChanges(changes: SimpleChanges){
+        if (changes['is_submit_button_pressed'] && changes['is_submit_button_pressed'].currentValue){
+            this.applyChanges();
+        }
+    }
+
+    onRender(){
+        this.tableRender.emit();
+        this.card_pairs = new Pair();
+        this.mutable_content_dictionary = this.content_list;
+        //Retains immutability when saving string to another variable. 
+        this.mutable_content_list = this.convertDictionaryToList(this.content_list).slice(0);
+    }
+
+    ngOnChange() {}
+
+    ngAfterViewInit() {}
+
+    applyChanges() {
+        this.saveContent.emit(this.mutable_content_list);
+    }
+
+    onKeyDown(keyType) {
+        if(this.card_pairs[1]) this.toggleKeyboardSelection.emit(true);
+        if(keyType == "Control") this.onCtrlDown();
+        if(keyType == "ArrowDown") this.onDownKeyDown();
+        if(keyType == "ArrowUp") this.onUpKeyDown();
+        if(keyType == "Enter") this.onEnterDown();
+        if(keyType == "Shift") this.onShiftDown();
+    }
+
+    onUpKeyDown() {
+        if(this.index_for_card == 0) {
+            this.index_for_card = this.mutable_content_list.length-1
+        } else {this.index_for_card--};
+    }
+
+    onDownKeyDown() {
+        if(!this.index_for_card) {
+            this.index_for_card = 0;
+        }
+        if(this.index_for_card == this.mutable_content_list.length-1) {
+            this.index_for_card = 0;
+        } else {this.index_for_card++};
+    }
+
+    onCtrlDown() {
+        this.index_for_card = 0;
+    }
+
+    onShiftDown() {
+        this.index_for_card = 0;
+        this.card_pairs = new Pair();
+        this.toggleKeyboardSelection.emit(false);
+    }
+
+    onEnterDown() {
+        if(this.card_pairs[1] == undefined && this.card_pairs[1] == null) {
+            this.card_pairs[1] = this.index_for_card;
+            this.nodeIsSelected.emit(this.mutable_content_list[this.index_for_card]);
+            this.toggleKeyboardSelection.emit(true);
+        } else {
+            this.card_pairs[2] = this.index_for_card;
+            if ((this.card_pairs[1] != this.card_pairs[2])
+                && this.card_pairs[2] != undefined && this.card_pairs[2] != null) {
+                this.toggleKeyboardSelection.emit(false);
+                this.haveDetectedChanges.emit();
+                this.reorderContentListFromCards(this.card_pairs, this.mutable_content_list);
+            }
+        }
+    }
+
+    onMouseLeave() {
+        this.toggleDragAndDrop.emit(false);
+    }
+
+    onMouseEnter() {
+        this.toggleDragAndDrop.emit(true);
+    }
+
+    onMouseDown(index) {
+        this.detect_mouse_leave = true;
+        this.is_mouse_down = true;
+        this.card_pairs[1] = index;
+        this.detectMousePosition.emit(true);
+        this.toggleKeyboardSelection.emit(false);
+        this.toggleDragAndDrop.emit(true);
+        this.nodeIsSelected.emit(this.mutable_content_list[index]);
+        this.index_for_card = index
+    }
+
+    onMouseUp(index) {
+        this.toggleDragAndDrop.emit(false);
+        this.detectMousePosition.emit(false);
+        if (this.card_pairs[1] != undefined && this.card_pairs[1] != null) {
+            this.card_pairs[2] = index;
+            this.is_mouse_down = false;
+            if ((this.card_pairs[1] != this.card_pairs[2])
+                && this.card_pairs[2] != undefined
+                && this.card_pairs[2] != null) {
+                this.haveDetectedChanges.emit()
+                this.reorderContentListFromCards(this.card_pairs, this.mutable_content_list);
+            }
+        } else {
+            this.is_mouse_down = false;
+        }
+        this.index_for_card = index
+    }
+
+    /**
+     * Converts a list into a dictionary to decrease the time complexity of multiple rearrangements.
+     * @param list The list that is to be converted
+     */
+    convertListToDictionary(list) {
+        var dictionary = {}
+        list.forEach(function (element, index) {
+            dictionary[index] = element;
+        });
+        this.mutable_content_dictionary = dictionary;
+    }
+
+    convertDictionaryToList(dictionary) {
+        var new_list = [];
+        for(var x = 0; x < Object.keys(dictionary).length; x++) {
+            if (dictionary[x]) new_list.push(dictionary[x]);
+        }
+        return new_list;
+    }
+
+    /**
+     * Reorders the content list using the pairs provided by the user.
+     * @param pairs The pairs provided by the user.
+     * @param list The List that is to be reordered.
+     */
+    reorderContentListFromCards(pairs, list) {
+        this.mutable_content_list =
+            this.convertDictionaryToList(this.reorderDictionary(this.mutable_content_dictionary, pairs));
+        this.card_pairs = new Pair();
+    }
+
+    reorderDictionary(dictionary, paired_values) {
+        var new_dictionary = new Object();
+        new_dictionary = Object.assign({...dictionary, [paired_values[2]]:dictionary[paired_values[1]]});
+        new_dictionary = this.alternateLoopWithHighestValueInPair(dictionary, new_dictionary, paired_values);
+        this.mutable_content_dictionary = Object.assign({...new_dictionary});
+        return this.mutable_content_dictionary;
+    }
+
+    alternateLoopWithHighestValueInPair(dictionary, new_dictionary, paired_values) {
+        if(paired_values[2] > paired_values[1]) {
+            for(var y = 0; y < paired_values[1]; y++) {
+                new_dictionary[y] = dictionary[y];
+            }
+            for(var z = paired_values[1]; z < paired_values[2]; z++) {
+                new_dictionary[z] = dictionary[z+1];
+            }
+
+            for(var x = Object.keys(dictionary).length-1; x > paired_values[2]; x--) {
+                new_dictionary[x] = dictionary[x];
+            }
+        } else {
+
+            for(var y = 0; y < paired_values[2]; y++) {
+                new_dictionary[y] = dictionary[y];
+            }
+
+            for(var z = paired_values[2]; z < paired_values[1]; z++) {
+                new_dictionary[z+1] = dictionary[z];
+            }
+            
+            for(var x = Object.keys(dictionary).length-1; x > paired_values[1]; x--) {
+                new_dictionary[x] = dictionary[x];
+            }
+        }
+        return Object.assign({...new_dictionary});
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/shelving-location/shelving-location-order.component.html b/Open-ILS/src/eg2/src/app/staff/admin/local/shelving-location/shelving-location-order.component.html
new file mode 100644 (file)
index 0000000..9c84c0d
--- /dev/null
@@ -0,0 +1,49 @@
+<div (mouseup)="disableMouseUp()" (mouseleave)="disableMouseUp()" [style.height.vh]="100">
+    <eg-staff-banner bannerText="Shelving Location Order" i18n-bannerText>
+    </eg-staff-banner>
+    <div class="row mt-3">
+      <div class="col-md-3">
+        <div class="input-group">
+          <div class="input-group-prepend">
+            <div class="input-group-text" i18n>Context Org Unit</div>
+            <eg-org-select domId="acpl" [applyDefault]="true" (onChange)="setOrg($event)"></eg-org-select>
+          </div>
+        </div>
+      </div>
+      <div class="col-md-3">
+        <button class="btn btn-lg btn-primary" (click)="is_submit_button_pressed = true" i18n>Apply Changes?</button>
+      </div>
+    </div>
+    <p>To move an item, drag it up or down with the mouse.</p>
+    <p>To use the keyboard selection, use the up and down arrow keys to select, 
+      press control key to return to the top of the list,
+      press the shift key to reset the selection process.
+    </p>
+    <div>
+        <ng-container *ngIf="orders && locations && locations[0]">
+            <sortable-list
+              #sortableTable
+              [is_submit_button_pressed]="is_submit_button_pressed"
+              [content_list]="ordered_locations"
+              [is_mouse_down]="detect_mouse_down"
+              (saveContent)="applyChanges($event)"
+              (tableRender)="findLocation(locations)"
+              (toggleDragAndDrop)="toggleDragAndDrop($event)"
+              (nodeIsSelected)="displaySelectedNode($event)"
+              (detectMousePosition)="onMouseDown($event)"
+              (toggleKeyboardSelection)="toggleKeyboardSelection($event)"
+              (haveDetectedChanges)="haveDetectedChanges()">
+            </sortable-list>
+        </ng-container>
+    </div>
+    <div *ngIf="detect_mouse_down && detect_mouse"
+      [ngStyle]="position_styling" 
+      [style.top.px]="mouse_y_position+25" 
+      [style.left.px]="mouse_x_position-75">
+      {{selected_node.name}} ({{selected_node.shortname}})
+    </div>
+    <div *ngIf="toggle_keyboard_display"
+      [ngStyle]="keyboard_position_styling">
+      {{selected_node.name}} ({{selected_node.shortname}})
+    </div>
+</div>
\ No newline at end of file
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/shelving-location/shelving-location-order.component.ts b/Open-ILS/src/eg2/src/app/staff/admin/local/shelving-location/shelving-location-order.component.ts
new file mode 100644 (file)
index 0000000..1b7f9cd
--- /dev/null
@@ -0,0 +1,301 @@
+import { Component, HostListener, OnInit, ViewChild } from '@angular/core';
+import {SortableListComponent} from '../share/sortable-list.component';
+import { IdlService, IdlObject } from '@eg/core/idl.service';
+import { OrgService } from '@eg/core/org.service';
+import { PcrudService } from '@eg/core/pcrud.service';
+
+class Category {
+    name: string;
+    owning_lib: string;
+    shortname: string;
+    unhashed_location: any;
+    unhashed_order: any;
+    location: any;
+    position: any;
+}
+
+@Component({
+    templateUrl: './shelving-location-order.component.html'
+})
+
+export class ShelvingLocationOrderComponent implements OnInit {
+
+    @ViewChild('sortableTable', { static: false }) sortableTable: SortableListComponent;
+
+    contextOrg: IdlObject;
+    orders: any;
+    locations: any;
+    finding_orders = false;
+    ordered_locations = {};
+    complete_order_list = [];
+    id_log = [];
+    lib_tree = [];
+    active_libraries = [];
+    mouse_x_position: number;
+    mouse_y_position: number;
+    detect_mouse_down = false;
+    detect_mouse = false;
+    selected_node: any;
+    is_submit_button_pressed = false;
+    toggle_keyboard_display = false;
+    detectChanges: boolean;
+
+    /**
+     * Styles the little box that indicates something is selected.
+     */
+    position_styling = {
+        backgroundColor: "white",
+        boxShadow: "5px 4px 10px 2px #5B5A5A",
+        textAlign: "center",
+        padding: "3px",
+        width: '80px',
+        position: 'fixed',
+    }
+
+    keyboard_position_styling = {
+        backgroundColor: "white",
+        boxShadow: "5px 4px 10px 2px #5B5A5A",
+        textAlign: "center",
+        padding: "10px",
+        fontSize: "30px",
+        width: '200px',
+        top:'200px',
+        left:'300px',
+        position: 'fixed',
+    }
+
+    constructor(
+        private pcrud: PcrudService,
+        private idl: IdlService,
+        private org: OrgService,
+    ) {
+    }
+
+    ngOnInit() {//Use the root org unit to create a library tree
+        if (this.lib_tree.length == 0) {
+            this.createBranch(this.idl.toHash(this.org.root(), false));
+        }
+    }
+     
+    checkObjectLength(object) {
+        return Object.keys(object).length;
+    }
+    
+    detectMouseLeave() {
+        this.detect_mouse = false;
+    }
+
+    toggleDragAndDrop(toggle) {
+        this.detect_mouse = toggle;
+    }
+
+    disableMouseUp() {
+        this.detect_mouse_down = false;
+    }
+
+    onMouseDown(is_mouse_down) {
+        this.detect_mouse_down = is_mouse_down;
+    }
+
+    toggleKeyboardSelection(isDisplayed) {
+        this.toggle_keyboard_display = isDisplayed;
+    }
+
+    displaySelectedNode(node) {
+        this.selected_node = node;
+    }
+
+    /**
+     * Maps the values of the specified category to an array of objects, whose keys hold the necessary values.
+     */
+    mappingLocation() {
+        this.locations = {};
+        this.orders = {};
+        this.pcrud.search('acplo', {org : this.contextOrg.id()}, {order_by : {acplo : 'position'}})
+        .subscribe(element => {
+            var categ = new Category();
+            categ.unhashed_order = element;
+            this.orders[element.location()] = categ;
+        });
+        var location_index = 0;
+        this.pcrud.search('acpl', {owning_lib : this.id_log, deleted : 'f'}).subscribe(element => {
+            var categ = new Category();
+            var unhashed_element = element;
+            var hashed_element = this.idl.toHash(element, false);
+            var found_library = this.findLibrary(hashed_element.owning_lib)
+            categ.name = hashed_element.name;
+            categ.owning_lib = found_library.name;
+            categ.shortname = found_library.shortname;
+            categ.unhashed_location = unhashed_element;
+            this.locations[location_index] = categ;
+            location_index++;
+        });
+    }
+
+    /**
+     * Take the existing locations and orders them using the existing orders.
+     *  If one doesn't exist, then append them to the end of the object
+     * @param locations The locations that are to be ordered.
+     */
+    findLocation(locations) {
+        var unordered_locations = [];
+        var greatest_order_position = 0;
+        for(var x = 0; x < Object.keys(locations).length; x++ ) {
+            var location_identification = this.orders[
+                locations[x].unhashed_location.id()
+            ];
+            if(!location_identification) {
+                //If there isn't an order for the location, then add to list of unordered locations.
+                unordered_locations.push((Object.assign({...locations[x]})));
+            }else {
+                var order_position = location_identification.unhashed_order.position();
+                if (this.ordered_locations[order_position - 1]) {
+                    unordered_locations.push(Object.assign({...locations[x], ...location_identification}));
+                } else {
+                    if(order_position > greatest_order_position) greatest_order_position = order_position; //Find the highest order position
+                    this.ordered_locations[order_position - 1] = Object.assign({...locations[x], ...location_identification}); 
+                }
+            }
+        }
+        //After the ordered locations have been ordered, then add in the unordered ones. 
+        unordered_locations.forEach((location, index) => {
+            this.ordered_locations[greatest_order_position + index] = location;
+        });
+    }
+
+    /**
+     * Finds the Library name, using the library id.
+     * @param lib_id The id that is to be used to find the name.
+     */
+    findLibrary(lib_id) {
+        var lib_name;
+        this.active_libraries.forEach(library => {
+            if (library.id == lib_id){
+                lib_name = {
+                    name: library.name,
+                    shortname: library.shortname,
+                };
+            }
+        });
+        return lib_name;
+    }
+
+    /**
+     * Starts from parent, logs id, looks for a specific key, checks to see if it has any children. If so
+     * rinse repeat.
+     * @param parent_node The uppermost parent node. 
+     */
+    nodeTrailing(parent_node) {
+        this.findParent(parent_node);
+    }
+
+    /**
+     * Creates a tree of the different library branches
+     * @param node The parent node, of which, the branches 'branch out'
+     */
+    createBranch(node) {
+        this.lib_tree.push(node);
+        node.children.forEach(child => {
+            this.createBranch(child);
+        });
+    }
+
+    /**
+     * Checks if the node passed in has a parent node. If so, follow that branch, while saving the child nodes,
+     *  until the root node is found.
+     * @param node The node that is to be checked for a parent.
+     */
+    findParent(node) {
+        this.id_log.push(node.id);
+        this.active_libraries.push(node);
+        var filteredTree = this.lib_tree.filter(branch => branch.ou_type.depth <= (node.ou_type.depth - 1)); //Creates a filtered branch
+        filteredTree.forEach(branch => {
+            if((branch.id == node.parent_ou) &&
+              this.isChild(branch.children, node)) {
+                this.findParent(branch);
+            }
+        });
+    }
+
+    /**
+     * Using the given parameters, checks if the 'node' passed is the parent of the 'children' parameter passed in.
+     * @param children The array of children nodes that is to be checked.
+     * @param node The assumed parent node.
+     */
+    isChild(children, node) {
+        var names_are_equal = false;
+        children.forEach(child => {
+            if (child.shortname == node.shortname){
+                names_are_equal= true;
+            }
+        });
+        return names_are_equal;
+    }
+
+
+    /**
+     * When the org unit changes, then wipe the id log, locations, and orders. Then retrieve new data
+     * @param object The new org unit
+     */
+    setOrg(org_unit) {
+        this.id_log = [];
+        this.locations = {};
+        this.orders = {};
+        this.ordered_locations = {};
+        this.contextOrg = org_unit;
+        this.nodeTrailing(this.idl.toHash(org_unit, false))
+        this.mappingLocation();
+    }
+
+    /**
+     * Creates Order if one doesn't yet exist.
+     * @param unfinished_order The order without an id.
+     * @param order_index The index of the unfinished order, used for the position.
+     */
+    createOrder(unfinished_order, order_index){
+        var new_order = this.idl.create('acplo');
+        new_order.id(unfinished_order.unhashed_order.id());
+        new_order.location(unfinished_order.unhashed_location.id());
+        new_order.org(this.contextOrg.id());
+        new_order.position(order_index + 1);
+        return this.pcrud.create(new_order);
+    }
+
+    buildOrder(unfinished_order, order_index) {
+        var new_order = this.idl.create('acplo');
+        if (unfinished_order.unhashed_order) {
+            new_order.id(unfinished_order.unhashed_order.id());
+            new_order.location(unfinished_order.unhashed_location.id());
+            new_order.org(this.contextOrg.id());
+            new_order.position(order_index + 1);
+            return this.pcrud.update(new_order);
+        } else {
+           return this.createOrder(unfinished_order, order_index);
+        }
+    }
+
+    haveDetectedChanges() {
+        this.detectChanges = true;
+    }
+
+    applyChanges(new_locations) {
+        this.detectChanges = false;
+        this.is_submit_button_pressed = false;
+        new_locations.forEach((location, index) => {
+             this.buildOrder(location,index).subscribe(order => {});
+        });
+    }
+
+    @HostListener('mousemove', ['$event'])
+        mouseMove($event: MouseEvent) {
+            this.mouse_y_position=$event.clientY;
+            this.mouse_x_position=$event.clientX;
+        }
+    @HostListener('window:beforeunload', ['$event'])
+        onWindowClose(event: any) {
+            if (this.detectChanges) {
+                return window.confirm("Are You Sure You Want To Leave Without Saving?")
+            }
+            return true;
+        };
+}
\ No newline at end of file
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/shelving-location/shelving-location-routing.module.ts b/Open-ILS/src/eg2/src/app/staff/admin/local/shelving-location/shelving-location-routing.module.ts
new file mode 100644 (file)
index 0000000..e66f486
--- /dev/null
@@ -0,0 +1,15 @@
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes} from '@angular/router';
+import {ShelvingLocationOrderComponent} from './shelving-location-order.component';
+
+const routes: Routes = [{
+    path: '',
+    component: ShelvingLocationOrderComponent
+}];
+
+@NgModule({
+    imports: [RouterModule.forChild(routes)],
+    exports: [RouterModule]
+})
+
+ export class ShelvingLocationRoutingModule {}
\ No newline at end of file
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/shelving-location/shelving-location.module.ts b/Open-ILS/src/eg2/src/app/staff/admin/local/shelving-location/shelving-location.module.ts
new file mode 100644 (file)
index 0000000..60c7a73
--- /dev/null
@@ -0,0 +1,24 @@
+import {NgModule} from '@angular/core';
+import {AdminCommonModule} from '@eg/staff/admin/common.module';
+import {TreeModule} from '@eg/share/tree/tree.module';
+import {ShelvingLocationRoutingModule} from './shelving-location-routing.module';
+import {ShelvingLocationOrderComponent} from './shelving-location-order.component';
+import {SortableListComponent} from '@eg/staff/admin/local/share/sortable-list.component';
+import {CardComponent} from '@eg/staff/admin/local/share/card.component';
+
+ @NgModule({
+    declarations: [
+        ShelvingLocationOrderComponent,
+        SortableListComponent,
+        CardComponent
+    ],
+    imports: [
+        AdminCommonModule,
+        ShelvingLocationRoutingModule,
+        TreeModule
+    ],
+    exports: [
+    ],
+    providers: []
+    })
+ export class ShelvingLocationModule {}
\ No newline at end of file