PartsManager.js (21568B)
1 /** 2 * @class PartKeepr.PartManager 3 * @todo Document the editor system a bit better 4 * 5 * The part manager encapsulates the category tree, the part display grid and the part detail view. 6 */ 7 Ext.define('PartKeepr.PartManager', { 8 extend: 'Ext.panel.Panel', 9 alias: 'widget.PartManager', 10 layout: 'border', 11 id: 'partkeepr-partmanager', 12 border: false, 13 padding: 5, 14 dragAndDrop: true, 15 16 /** 17 * Defines if the border layout should be compact or regular. 18 * 19 * Compact style stacks the tree panel and the part detail panel on top of each other to save space, which is a bit 20 * odd in terms of usability. Regular style means that the layout will be Category Tree->Part List->Part details. 21 * 22 * @var boolean True if compact layout should be used, false otherwise. 23 */ 24 compactLayout: false, 25 26 selectedCategory: null, 27 28 initComponent: function () { 29 30 /** 31 * Create the store with the default sorter "name ASC" 32 */ 33 this.createStore({ 34 model: 'PartKeepr.PartBundle.Entity.Part', 35 groupField: 'categoryPath', 36 sorters: [ 37 { 38 property: 'category.categoryPath', 39 direction: 'ASC' 40 }, 41 { 42 property: 'name', 43 direction: 'ASC' 44 } 45 ] 46 }); 47 48 var treeConfig = { 49 region: 'west', 50 ddGroup: 'CategoryTree' 51 }; 52 53 if (this.compactLayout) 54 { 55 treeConfig.region = 'center'; 56 } else 57 { 58 treeConfig.floatable = false; 59 treeConfig.split = true; 60 treeConfig.width = 300; // @todo Make this configurable 61 treeConfig.title = i18n("Categories"); 62 treeConfig.collapsible = true; // We want to collapse the tree panel on small screens 63 } 64 65 // Create the tree 66 this.tree = Ext.create("PartKeepr.PartCategoryTree", treeConfig); 67 68 // Trigger a grid reload on category change 69 this.tree.on("itemclick", this.onCategoryClick, this); 70 71 // Create the detail panel 72 this.detail = Ext.create("PartKeepr.PartDisplay", {title: i18n("Part Details")}); 73 this.detail.on("editPart", this.onEditPart, this); 74 75 var gridConfig = { 76 title: i18n("Parts List"), 77 region: 'center', 78 layout: 'fit', 79 store: this.getStore(), 80 itemId: "partsGrid" 81 }; 82 83 if (this.dragAndDrop) 84 { 85 gridConfig.viewConfig = { 86 plugins: { 87 ddGroup: 'PartTree', 88 ptype: 'gridviewdragdrop', 89 enableDrop: false 90 } 91 }; 92 93 gridConfig.enableDragDrop = true; 94 } 95 96 // Create the grid 97 this.grid = Ext.create("PartKeepr.PartsGrid", gridConfig); 98 this.grid.on("editPart", this.onEditPart, this); 99 100 // Create the grid listeners 101 this.grid.on("itemSelect", this.onItemSelect, this); 102 this.grid.on("itemDeselect", this.onItemSelect, this); 103 this.grid.on("itemAdd", this.onItemAdd, this); 104 this.grid.on("itemDelete", this.onItemDelete, this); 105 this.grid.on("addMetaPart", this.onAddMetaPart, this); 106 this.grid.on("duplicateItemWithBasicData", this.onDuplicateItemWithBasicData, this); 107 this.grid.on("duplicateItemWithAllData", this.onDuplicateItemWithAllData, this); 108 this.tree.on("syncCategory", this.onSyncCategory, this); 109 110 // Create the stock level panel 111 this.stockLevel = Ext.create("PartKeepr.PartStockHistory", {title: "Stock History"}); 112 113 var detailPanelConfig = { 114 title: i18n("Part Details"), 115 collapsed: true, 116 collapsible: true, 117 region: 'east', 118 floatable: false, 119 titleCollapse: true, 120 split: true, 121 animCollapse: false, 122 items: [this.detail, this.stockLevel] 123 }; 124 125 if (this.compactLayout) 126 { 127 detailPanelConfig.height = 300; 128 detailPanelConfig.region = 'south'; 129 } else 130 { 131 detailPanelConfig.width = 300; 132 } 133 134 this.detailPanel = Ext.create("Ext.tab.Panel", detailPanelConfig); 135 136 this.filterPanel = Ext.create("PartKeepr.PartFilterPanel", { 137 title: i18n("Filter"), 138 region: 'south', 139 height: 225, 140 animCollapse: false, 141 floatable: false, 142 titleCollapse: true, 143 split: true, 144 collapsed: true, 145 collapsible: true, 146 store: this.store, 147 partManager: this 148 }); 149 150 this.thumbnailViewTpl = new Ext.XTemplate( 151 '<tpl for=".">', 152 '<div class="dataview-multisort-item iclogo"><img src="{[values["@id"]]}/getImage?maxWidth=100&maxHeight=100"/></div>', 153 '</tpl>'); 154 155 this.thumbnailView = Ext.create("Ext.view.View", { 156 tpl: this.thumbnailViewTpl, 157 componentCls: 'manufacturer-ic-logos', 158 itemSelector: 'div.dataview-multisort-item', 159 store: { 160 model: PartKeepr.PartBundle.Entity.PartAttachment 161 } 162 }); 163 164 this.thumbnailView.on("selectionchange", function (selModel, selection) { 165 var parts = []; 166 167 for (var i = 0; i < selection.length; i++) 168 { 169 parts.push(selection[i].get("part")); 170 } 171 172 this.grid.getSelectionModel().select(parts); 173 }, this); 174 175 this.grid.store.on("update", function (store, record) { 176 if (this.detail.record !== null && this.detail.record.getId() == record.getId()) 177 { 178 this.detail.setValues(record); 179 } 180 }, this); 181 182 this.grid.store.on("load", function () { 183 this.thumbnailView.getStore().removeAll(); 184 185 var data = this.grid.store.getData(), 186 i, j, 187 attachments, attachment; 188 189 for (i = 0; i < data.getCount(); i++) 190 { 191 attachments = data.getAt(i).attachments().getData(); 192 193 for (j = 0; j < attachments.getCount(); j++) 194 { 195 attachment = attachments.getAt(j); 196 if (attachment.get("isImage")) 197 { 198 this.thumbnailView.getStore().add({ 199 "@id": attachment.get("@id"), 200 "part": data.getAt(i) 201 }); 202 203 } 204 } 205 } 206 207 var t = new Ext.Template(i18n("Displaying {0} image(s) from {1} part(s)")); 208 var q = t.apply([this.thumbnailView.getStore().getCount(), this.grid.store.getCount()]); 209 this.down("#thumbnailViewStatusMessage").setText(q); 210 }, this); 211 212 this.thumbnailViewToolbar = Ext.create("Ext.toolbar.Paging", { 213 store: this.grid.store, 214 enableOverflow: true, 215 dock: 'bottom', 216 displayInfo: false, 217 items: [ 218 {xtype: 'tbfill'}, { 219 xtype: 'tbtext', 220 itemId: "thumbnailViewStatusMessage" 221 } 222 ] 223 }); 224 225 this.thumbnailPanel = Ext.create("Ext.panel.Panel", { 226 title: i18n("Thumbnail View"), 227 scrollable: true, 228 bbar: this.thumbnailViewToolbar, 229 items: this.thumbnailView 230 }); 231 232 this.tabPanel = Ext.create("Ext.tab.Panel", { 233 region: 'center', 234 items: [this.grid, this.thumbnailPanel] 235 }); 236 237 this.thumbnailView.on("render", function () { 238 this.loadMask = Ext.create("Ext.LoadMask", { 239 store: this.grid.store, 240 target: this.thumbnailPanel 241 }); 242 243 this.thumbnailViewToolbar.onLoad(); 244 }, this); 245 246 if (this.compactLayout) 247 { 248 // Create two border layouts: One for the center panel and one for the left panel. Each border layout 249 // has two columns each, containing Categories+Part Details and Part List+Part Filter Panel. 250 this.items = [ 251 { 252 layout: 'border', 253 border: false, 254 region: 'west', 255 animCollapse: false, 256 width: 300, 257 split: true, 258 title: i18n("Categories / Part Details"), 259 titleCollapse: true, 260 collapsed: false, 261 collapsible: true, 262 items: [this.tree, this.detailPanel] 263 }, { 264 layout: 'border', 265 border: false, 266 region: 'center', 267 items: [this.tabPanel, this.filterPanel] 268 } 269 ]; 270 } else 271 { 272 // The regular 3-column layout. The tree, then the part list+part filter, then the part details. 273 this.items = [ 274 this.tree, { 275 layout: 'border', 276 border: false, 277 region: 'center', 278 items: [this.tabPanel, this.filterPanel] 279 }, this.detailPanel 280 ]; 281 } 282 283 this.callParent(); 284 }, 285 /** 286 * Applies the category filter to the store when a category is selected 287 * 288 * @param {Ext.tree.View} tree The tree view 289 * @param {Ext.data.Model} record the selected record 290 */ 291 onCategoryClick: function (tree, record) { 292 this.selectedCategory = record; 293 294 var filter = Ext.create("PartKeepr.util.Filter", { 295 id: 'categoryFilter', 296 property: 'category', 297 operator: 'IN', 298 value: this.getChildrenIds(record) 299 }); 300 301 if (record.get("description").trim() != "") 302 { 303 this.grid.setNotification(record.get("description")); 304 } else { 305 this.grid.removeNotification(); 306 } 307 308 309 if (record.parentNode.isRoot()) 310 { 311 // Workaround for big installations: Passing all child categories for the root node 312 // to the filter exceeds the HTTP URI length. See 313 // https://github.com/partkeepr/PartKeepr/issues/473 314 this.store.removeFilter(filter); 315 } else 316 { 317 this.store.addFilter(filter); 318 } 319 }, 320 getSelectedCategory: function () { 321 return this.selectedCategory; 322 }, 323 /** 324 * Returns the ID for this node and all child nodes 325 * 326 * @param {Ext.data.Model} node The node 327 * @return Array 328 */ 329 getChildrenIds: function (node) { 330 var childNodes = [node]; 331 332 if (node.hasChildNodes()) 333 { 334 for (var i = 0; i < node.childNodes.length; i++) 335 { 336 childNodes = childNodes.concat(this.getChildrenIds(node.childNodes[i])); 337 } 338 } 339 340 return childNodes; 341 }, 342 /** 343 * Called when the sync button was clicked. Highlights the category 344 * of the selected part for a short time. We can't select the category 345 * as this would affect the parts grid. 346 */ 347 onSyncCategory: function () { 348 var r = this.grid.getSelectionModel().getSelection(); 349 350 if (r.length != 1) 351 { 352 return; 353 } 354 355 var rootNode = this.tree.getRootNode(); 356 var cat = r[0].getCategory().getId(); 357 358 var node = rootNode.findChild("@id", cat, true); 359 360 if (node) 361 { 362 this.tree.getView().ensureVisible(node); 363 this.tree.getView().focusNode(node); 364 } 365 }, 366 /** 367 * Called when the delete button was clicked. 368 * 369 * Prompts the user if he really wishes to delete the part. If yes, it calls deletePart. 370 */ 371 onItemDelete: function () { 372 var r = this.grid.getSelectionModel().getLastSelected(); 373 374 Ext.Msg.confirm(i18n("Delete Part"), sprintf(i18n("Do you really wish to delete the part %s?"), r.get("name")), 375 this.deletePart, this); 376 }, 377 /** 378 * Creates a duplicate with the basic data only from the selected item. Loads the selected part and calls 379 * createPartDuplicate after the part was loaded. 380 */ 381 onDuplicateItemWithBasicData: function () { 382 var r = this.grid.getSelectionModel().getLastSelected(); 383 384 this.loadPart(r.getId(), Ext.bind(this.createPartDuplicate, this)); 385 }, 386 /** 387 * Creates a full duplicate from the selected item. Loads the selected part and calls createPartDuplicate 388 * after the part was loaded. 389 */ 390 onDuplicateItemWithAllData: function () { 391 var r = this.grid.getSelectionModel().getLastSelected(); 392 393 this.loadPart(r.getId(), Ext.bind(this.createFullPartDuplicate, this)); 394 }, 395 /** 396 * Creates a part duplicate from the given record and opens the editor window. 397 * @param rec The record to duplicate 398 */ 399 createPartDuplicate: function (rec) { 400 var data = rec.getData(); 401 var associationData = rec.getAssociationData(); 402 403 var newItem = Ext.create("PartKeepr.PartBundle.Entity.Part"); 404 newItem.set(data); 405 newItem.setAssociationData({ 406 category: associationData.category, 407 partUnit: associationData.partUnit, 408 storageLocation: associationData.storageLocation, 409 footprint: associationData.footprint 410 }); 411 412 var j = Ext.create("PartKeepr.PartEditorWindow", { 413 partMode: 'create' 414 }); 415 416 j.editor.on("partSaved", this.onNewPartSaved, this); 417 j.editor.editItem(newItem); 418 j.show(); 419 }, 420 onAddMetaPart: function () { 421 var defaults = {}; 422 var j = Ext.create("PartKeepr.Components.Part.Editor.MetaPartEditorWindow", {}); 423 424 var defaultPartUnit = PartKeepr.getApplication().getPartUnitStore().findRecord("default", true); 425 426 Ext.apply(defaults, {metaPart: true}); 427 428 var record = Ext.create("PartKeepr.PartBundle.Entity.Part", { 429 metaPart: true 430 }); 431 432 if (this.getSelectedCategory() !== null) 433 { 434 record.setCategory(this.getSelectedCategory()); 435 } else 436 { 437 record.setCategory(this.tree.getRootNode().firstChild); 438 } 439 440 record.setPartUnit(defaultPartUnit); 441 442 j.editor.editItem(record); 443 j.editor.on("partSaved", this.onNewPartSaved, this); 444 j.show(); 445 }, 446 /** 447 * Creates a part duplicate from the given record and opens the editor window. 448 * @param rec The record to duplicate 449 */ 450 createFullPartDuplicate: function (rec) { 451 var data = rec.getData(); 452 453 var newItem = Ext.create("PartKeepr.PartBundle.Entity.Part"); 454 newItem.set(data); 455 newItem.setAssociationData(rec.getAssociationData()); 456 457 var j = Ext.create("PartKeepr.PartEditorWindow", { 458 partMode: 'create' 459 }); 460 461 j.editor.on("partSaved", this.onNewPartSaved, this); 462 j.editor.editItem(newItem); 463 j.show(); 464 }, 465 /** 466 * Deletes the selected part. 467 * 468 * @param {String} btn The clicked button in the message box window. 469 * @todo We use the current selection of the grid. If for some reason the selection changes during the user is prompted, 470 * we delete the wrong part. Fix that to pass the selected item to the onItemDelete then to this function. 471 */ 472 deletePart: function (btn) { 473 var r = this.grid.getSelectionModel().getLastSelected(); 474 475 if (btn == "yes") 476 { 477 this.detailPanel.collapse(); 478 this.detail.clear(); 479 r.erase(); 480 } 481 }, 482 /** 483 * Creates a new, empty part editor window 484 */ 485 onItemAdd: function (defaults) { 486 var j = Ext.create("PartKeepr.PartEditorWindow", { 487 partMode: 'create' 488 }); 489 490 var defaultPartUnit = PartKeepr.getApplication().getPartUnitStore().findRecord("default", true); 491 492 Ext.apply(defaults, {}); 493 494 var record = Ext.create("PartKeepr.PartBundle.Entity.Part", defaults); 495 496 if (this.getSelectedCategory() !== null) 497 { 498 record.setCategory(this.getSelectedCategory()); 499 } else 500 { 501 record.setCategory(this.tree.getRootNode().firstChild); 502 } 503 504 record.setPartUnit(defaultPartUnit); 505 506 j.editor.editItem(record); 507 j.editor.on("partSaved", this.onNewPartSaved, this); 508 j.show(); 509 510 return j; 511 }, 512 /** 513 * Called when a part was edited. Refreshes the grid. 514 */ 515 onEditPart: function (part) { 516 var editorWindow; 517 518 if (part.get("metaPart") === true) 519 { 520 editorWindow = Ext.create("PartKeepr.Components.Part.Editor.MetaPartEditorWindow"); 521 } else 522 { 523 editorWindow = Ext.create("PartKeepr.PartEditorWindow"); 524 } 525 526 editorWindow.editor.on("partSaved", this.onPartSaved, this); 527 editorWindow.editor.editItem(part); 528 editorWindow.show(); 529 }, 530 onNewPartSaved: function () { 531 this.grid.getStore().reload(); 532 }, 533 onPartSaved: function (record) { 534 this.detail.setValues(record); 535 }, 536 /** 537 * Called when a part was selected in the grid. Displays the details for this part. 538 */ 539 onItemSelect: function () { 540 if (this.grid.getSelection().length > 1) 541 { 542 this.detailPanel.collapse(); 543 this.tree.syncButton.disable(); 544 } else 545 { 546 if (this.grid.getSelection().length == 1) 547 { 548 var selection = this.grid.getSelection(); 549 550 var r = selection[0]; 551 552 this.detailPanel.setActiveTab(this.detail); 553 this.detail.setValues(r); 554 this.detailPanel.expand(); 555 this.stockLevel.part = r.getId(); 556 557 this.tree.syncButton.enable(); 558 } else 559 { 560 this.tree.syncButton.disable(); 561 } 562 } 563 564 }, 565 /** 566 * Triggers loading of a part 567 * @param {Integer} id The ID of the part to load 568 * @param {Function} handler The callback to call when the part was loaded 569 */ 570 loadPart: function (id, handler) { 571 // @todo we have this method duplicated in PartEditor 572 573 PartKeepr.PartBundle.Entity.Part.load(id, { 574 scope: this, 575 success: handler 576 }); 577 }, 578 /** 579 * Creates the store 580 */ 581 createStore: function (config) { 582 this.store = Ext.create('PartKeepr.Data.Store.PartStore'); 583 }, 584 /** 585 * Returns the store 586 */ 587 getStore: function () { 588 return this.store; 589 }, 590 statics: { 591 formatParameter: function (partParameter) { 592 var minSiPrefix = "", siPrefix = "", maxSiPrefix = "", unit = "", minValue = "", maxValue = "", value = "", 593 minMaxCombined = ""; 594 595 if (partParameter.get("valueType") === "string") 596 { 597 return partParameter.get("stringValue"); 598 } 599 600 if (partParameter.getUnit() instanceof PartKeepr.UnitBundle.Entity.Unit) 601 { 602 unit = partParameter.getUnit().get("symbol"); 603 } 604 605 if (partParameter.getMinSiPrefix() instanceof PartKeepr.SiPrefixBundle.Entity.SiPrefix) 606 { 607 minSiPrefix = partParameter.getMinSiPrefix().get("symbol"); 608 } 609 610 if (partParameter.getSiPrefix() instanceof PartKeepr.SiPrefixBundle.Entity.SiPrefix) 611 { 612 siPrefix = partParameter.getSiPrefix().get("symbol"); 613 } 614 615 if (partParameter.getMaxSiPrefix() instanceof PartKeepr.SiPrefixBundle.Entity.SiPrefix) 616 { 617 maxSiPrefix = partParameter.getMaxSiPrefix().get("symbol"); 618 } 619 620 if (partParameter.get("value") !== null && partParameter.get("value") !== "") 621 { 622 value = partParameter.get("value"); 623 } 624 625 if (partParameter.get("minValue") !== null && partParameter.get("minValue") !== "") 626 { 627 minValue = partParameter.get("minValue"); 628 } 629 630 if (partParameter.get("maxValue") !== null && partParameter.get("maxValue") !== "") 631 { 632 maxValue = partParameter.get("maxValue"); 633 } 634 635 if (minValue !== "" && maxValue !== "") 636 { 637 minMaxCombined = minValue + " " + minSiPrefix + "…" + maxValue + " " + maxSiPrefix + unit; 638 } else 639 { 640 if (minValue !== "") 641 { 642 minMaxCombined = i18n("Min.") + minValue + " " + minSiPrefix + unit; 643 } 644 645 if (maxValue !== "") 646 { 647 minMaxCombined = i18n("Max.") + maxValue + " " + maxSiPrefix + unit; 648 } 649 } 650 651 if (value !== "") 652 { 653 if (minMaxCombined !== "") 654 { 655 return value + " " + siPrefix + unit + " (" + minMaxCombined + ")"; 656 } else 657 { 658 return value + " " + siPrefix + unit; 659 } 660 } else 661 { 662 return minMaxCombined; 663 } 664 } 665 } 666 });