From: Zavier Banks Date: Fri, 11 Oct 2019 21:17:40 +0000 (+0000) Subject: Lp#1846552: Port Local Admin Shelving Location Order Editor to Angular X-Git-Url: https://old-git.evergreen-ils.org/?a=commitdiff_plain;h=ab2283791624f0ff94d9c01aedb04b44a0de5ff9;p=working%2FEvergreen.git Lp#1846552: Port Local Admin Shelving Location Order Editor to Angular 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 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 --- diff --git a/Open-ILS/src/eg2/src/app/staff/admin/local/admin-local-splash.component.html b/Open-ILS/src/eg2/src/app/staff/admin/local/admin-local-splash.component.html index e051d37c42..17fe909842 100644 --- a/Open-ILS/src/eg2/src/app/staff/admin/local/admin-local-splash.component.html +++ b/Open-ILS/src/eg2/src/app/staff/admin/local/admin-local-splash.component.html @@ -23,6 +23,12 @@ url="/eg/staff/admin/local/config/circ_matrix_matchpoint"> + + + @@ -53,7 +59,7 @@ + routerLink="/staff/admin/local/asset/shelving_location_order"> + keyboard_arrow_right + + + 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 index 0000000000..6e3885963d --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/admin/local/share/card.component.ts @@ -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(); + @Output() detectMouseDownEvents = new EventEmitter(); + @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 index 0000000000..bcf746fc2b --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/admin/local/share/sortable-list.component.html @@ -0,0 +1,21 @@ +
+
+ +
    +
    + + + {{content.name}} ({{content.shortname}}) + + +
    +
+
+
+
\ 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 index 0000000000..0b2b79e302 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/admin/local/share/sortable-list.component.ts @@ -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(); + @Output() detectMousePosition = new EventEmitter(); + @Output() nodeIsSelected = new EventEmitter(); + @Output() toggleDragAndDrop = new EventEmitter(); + @Output() tableRender = new EventEmitter(); + @Output() toggleKeyboardSelection = new EventEmitter(); + @Output() haveDetectedChanges = new EventEmitter(); + 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 index 0000000000..9c84c0dece --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/admin/local/shelving-location/shelving-location-order.component.html @@ -0,0 +1,49 @@ +
+ + +
+
+
+
+
Context Org Unit
+ +
+
+
+
+ +
+
+

To move an item, drag it up or down with the mouse.

+

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. +

+
+ + + + +
+
+ {{selected_node.name}} ({{selected_node.shortname}}) +
+
+ {{selected_node.name}} ({{selected_node.shortname}}) +
+
\ 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 index 0000000000..1b7f9cd015 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/admin/local/shelving-location/shelving-location-order.component.ts @@ -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 index 0000000000..e66f486c90 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/admin/local/shelving-location/shelving-location-routing.module.ts @@ -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 index 0000000000..60c7a737b7 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/admin/local/shelving-location/shelving-location.module.ts @@ -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