PartsGrid.js (19245B)
1 /** 2 * This class is the main part list grid. 3 * 4 */ 5 Ext.define('PartKeepr.PartsGrid', { 6 extend: 'PartKeepr.EditorGrid', 7 alias: 'widget.PartsGrid', 8 9 /** 10 * Display button texts by default 11 */ 12 buttonTextMode: 'show', 13 14 /** 15 * @cfg {String} Defines the text of the "Add" button 16 */ 17 addButtonText: i18n("Add Part"), 18 19 /** 20 * @cfg {String} Defines the icon of the "Add" button 21 */ 22 addButtonIconCls: 'web-icon brick_add', 23 24 /** 25 * @cfg {String} Defines the text of the "Delete" button 26 */ 27 deleteButtonText: i18n("Delete Part"), 28 29 /** 30 * @cfg {String} Defines the icon of the "Add" button 31 */ 32 deleteButtonIconCls: 'web-icon brick_delete', 33 34 /** 35 * @cfg {String} Defines the icon of the "Expand Row" button 36 */ 37 expandRowButtonIconCls: 'partkeepr-icon group-expand', 38 39 /** 40 * @cfg {String} Defines the icon of the "Collapse Row" button 41 */ 42 collapseRowButtonIconCls: 'partkeepr-icon group-collapse', 43 44 /** 45 * Configure drag'n'drop. 46 * @todo Check if this messes up with the Part drop down in the project view 47 */ 48 viewConfig: { 49 plugins: { 50 ddGroup: 'CategoryTree', 51 ptype: 'gridviewdragdrop', 52 enableDrop: false 53 } 54 }, 55 enableDragDrop: true, 56 stripeRows: true, 57 multiSelect: true, 58 autoScroll: false, 59 invalidateScrollerOnRefresh: true, 60 titleProperty: 'name', 61 searchFieldSystemPreference: "partkeepr.part.search.field", 62 searchFieldSystemPreferenceDefaults: ["name", "description", "comment", "internalPartNumber"], 63 splitSearchTermSystemPreference: "partkeepr.part.search.split", 64 splitSearchTermSystemPreferenceDefaults: true, 65 66 initComponent: function () 67 { 68 69 this.groupingFeature = Ext.create('Ext.grid.feature.Grouping', { 70 //enableGroupingMenu: false, 71 groupHeaderTpl: '{name} ({rows.length} ' + i18n("Part(s)") + ")" 72 }); 73 74 // Create the columns 75 this.defineColumns(); 76 77 78 this.features = [this.groupingFeature]; 79 80 this.on("itemdblclick", this.onDoubleClick, this); 81 82 // Bugfix for scroller becoming detached. 83 // @todo Remove with ExtJS 4.1 84 this.on('scrollershow', function (scroller) 85 { 86 if (scroller && scroller.scrollEl) { 87 scroller.clearManagedListeners(); 88 scroller.mon(scroller.scrollEl, 'scroll', scroller.onElScroll, scroller); 89 } 90 }); 91 92 if (this.enableEditing) { 93 this.editing = Ext.create('Ext.grid.plugin.CellEditing', { 94 clicksToEdit: 1 95 }); 96 97 this.editing.on("edit", this.onEdit, this); 98 99 this.plugins = [this.editing]; 100 } 101 102 // Initialize the panel 103 this.callParent(); 104 105 this.bottomToolbar.add({ 106 xtype: 'button', 107 tooltip: i18n("Expand all Groups"), 108 iconCls: this.expandRowButtonIconCls, 109 listeners: { 110 scope: this.groupingFeature, 111 click: this.groupingFeature.expandAll 112 } 113 114 }); 115 116 this.bottomToolbar.add({ 117 xtype: 'button', 118 tooltip: i18n("Collapse all Groups"), 119 iconCls: this.collapseRowButtonIconCls, 120 listeners: { 121 scope: this.groupingFeature, 122 click: this.groupingFeature.collapseAll 123 } 124 125 }); 126 127 var insertPosition = this.bottomToolbar.items.indexOf(this.bottomToolbar.down("#addFilter")); 128 129 this.bottomToolbar.insert(insertPosition, { 130 xtype: 'button', 131 tooltip: i18n("Filter by Part Parameter"), 132 iconCls: "fugue-icon table--plus", 133 listeners: { 134 scope: this, 135 click: this.addParameterFilter 136 } 137 }); 138 139 var duplicateBasicData = i18n( 140 "Duplicates the selected part with the data found in the \"basic\" tab and opens the editor. Doesn't immediately saves the duplicate, in order to allow editing."); 141 var duplicateAllData = i18n( 142 "Duplicates the selected part with all data including attachments, distributors etc. Doesn't immediately saves the duplicate, in order to allow editing."); 143 144 this.addFromTemplateButton = Ext.create("Ext.button.Split", { 145 disabled: true, 146 handler: Ext.bind(function () 147 { 148 this.fireEvent("duplicateItemWithBasicData"); 149 }, this), 150 tooltip: duplicateBasicData, 151 text: i18n("Duplicate"), 152 iconCls: 'web-icon brick_link', 153 menu: new Ext.menu.Menu({ 154 items: [ 155 { 156 text: i18n("Duplicate with all data"), 157 tooltip: duplicateAllData, 158 handler: function () 159 { 160 this.fireEvent("duplicateItemWithAllData"); 161 }, 162 scope: this 163 }, { 164 text: i18n("Duplicate basic data only"), 165 tooltip: duplicateBasicData, 166 handler: function () 167 { 168 this.fireEvent("duplicateItemWithBasicData"); 169 }, 170 scope: this 171 } 172 ] 173 }) 174 }); 175 176 if (this.enableEditing) { 177 this.topToolbar.insert(2, this.addFromTemplateButton); 178 } 179 180 this.createMetaPartButton = Ext.create("Ext.button.Button", { 181 iconCls: 'web-icon bricks', 182 text: i18n("Add Meta-Part"), 183 handler: function () 184 { 185 this.fireEvent("addMetaPart"); 186 }, 187 scope: this 188 }); 189 190 this.topToolbar.insert(1, this.createMetaPartButton); 191 192 this.mapSearchHotkey(); 193 }, 194 /** 195 * Maps a search hotkey to the search box. 196 * 197 * Right now, this is hardcoded to alt+x. 198 * 199 */ 200 mapSearchHotkey: function () 201 { 202 this.searchKey = new Ext.util.KeyMap(Ext.get(document), { 203 key: 'x', 204 ctrl: false, 205 alt: true, 206 fn: function () 207 { 208 var searchBox = this.searchField; 209 if (Ext.get(document).activeElement !== searchBox) { 210 searchBox.focus('', 10); 211 } 212 searchBox.setValue(''); 213 }, 214 scope: this, 215 stopEvent: true 216 }); 217 }, 218 /** 219 * Called when an item was selected. Enables/disables the delete button. 220 */ 221 _updateAddTemplateButton: function () 222 { 223 /* Right now, we support delete on a single record only */ 224 if (this.getSelectionModel().getCount() === 1) { 225 this.addFromTemplateButton.enable(); 226 } else { 227 this.addFromTemplateButton.disable(); 228 } 229 }, 230 /** 231 * Called when an item was selected 232 */ 233 _onItemSelect: function (selectionModel, record) 234 { 235 this._updateAddTemplateButton(selectionModel, record); 236 this.callParent(arguments); 237 }, 238 /** 239 * Called when an item was deselected 240 */ 241 _onItemDeselect: function (selectionModel, record) 242 { 243 this._updateAddTemplateButton(selectionModel, record); 244 this.callParent(arguments); 245 }, 246 /** 247 * Called when the record was double-clicked 248 */ 249 onDoubleClick: function (view, record) 250 { 251 if (record) { 252 this.fireEvent("editPart", record); 253 } 254 }, 255 /** 256 * Defines the columns used in this grid. 257 */ 258 defineColumns: function () 259 { 260 this.columns = [ 261 { 262 header: '<span class="web-icon fugue-icon paper-clip"></span>', 263 dataIndex: "", 264 width: 30, 265 tooltip: i18n("Has attachments?"), 266 renderers: [{ 267 rtype: 'partAttachment' 268 }] 269 }, { 270 text: '<span class="web-icon flag_orange"></span>', 271 dataIndex: "needsReview", 272 width: 30, 273 tooltip: i18n("Needs Review?"), 274 renderers: [{ 275 rtype: 'icon', 276 rendererConfig: { 277 iconCls: 'web-icon flag_orange' 278 } 279 }] 280 }, { 281 text: '<span class="web-icon bricks"></span>', 282 dataIndex: "metaPart", 283 width: 30, 284 tooltip: i18n("Meta Part"), 285 renderers: [{ 286 rtype: 'icon', 287 rendererConfig: { 288 iconCls: 'web-icon bricks' 289 } 290 }] 291 }, { 292 header: i18n("Name"), 293 dataIndex: 'name', 294 flex: 1, 295 minWidth: 150 296 }, { 297 header: i18n("Description"), 298 dataIndex: 'description', 299 flex: 2, 300 minWidth: 150 301 }, { 302 header: i18n("Storage Location"), 303 dataIndex: 'storageLocation.name' 304 }, { 305 header: i18n("Status"), 306 dataIndex: "status"}, 307 { 308 header: i18n("Condition"), 309 dataIndex: "partCondition" 310 }, { 311 header: i18n("Stock"), 312 dataIndex: 'stockLevel', 313 renderers: [{ 314 rtype: "stockLevel" 315 }], 316 editor: { 317 xtype: 'textfield', 318 allowBlank: false 319 } 320 }, { 321 header: i18n("Min. Stock"), 322 dataIndex: 'minStockLevel', 323 renderers: [{ 324 rtype: "stockLevel" 325 }] 326 }, { 327 header: i18n("Avg. Price"), 328 dataIndex: 'averagePrice', 329 align: 'right', 330 renderers: [{ 331 rtype: "currency" 332 }] 333 }, { 334 header: i18n("Footprint"), 335 dataIndex: 'footprint.name' 336 }, { 337 header: i18n("Category"), 338 dataIndex: "category.categoryPath", 339 hidden: true 340 }, { 341 header: i18n("Create Date"), 342 dataIndex: 'createDate', 343 hidden: true 344 }, { 345 header: i18n("Internal ID"), 346 dataIndex: '@id', 347 renderers: [{ 348 rtype: "internalID" 349 }] 350 } 351 352 ]; 353 }, 354 /** 355 * Sets the category. Triggers a store reload with a category filter. 356 */ 357 setCategory: function (category) 358 { 359 var proxy = this.store.getProxy(); 360 361 proxy.extraParams.category = category; 362 363 this.store.currentPage = 1; 364 this.store.load({ 365 start: 0 366 }); 367 }, 368 /** 369 * Handles editing of the grid fields. Right now, only the stock level editing is supported. 370 * 371 * @param editor Not used 372 * @param e An edit event, as documented in 373 * http://docs.sencha.com/ext-js/4-0/#!/api/Ext.grid.plugin.CellEditing-event-edit 374 */ 375 onEdit: function (editor, e) 376 { 377 switch (e.field) { 378 case "stockLevel": 379 if (e.value !== e.originalValue.toString()) { 380 this.handleStockFieldEdit(e); 381 } 382 break; 383 default: 384 break; 385 } 386 }, 387 addParameterFilter: function () { 388 this.addFilterWindow = Ext.create("PartKeepr.Components.Widgets.PartParameterSearchWindow", { 389 390 sourceModel: this.getStore().getModel(), 391 listeners: { 392 "apply": this.onAddParameterFilter, 393 scope: this 394 } 395 }); 396 397 this.addFilterWindow.show(); 398 }, 399 /** 400 * @todo Refactor this function as well as the one in DataApplicator to a single central function 401 * 402 * Note that this function takes the input and multiplies it by the si prefix 403 * @param value 404 * @param siPrefix 405 * @returns {*} 406 */ 407 applySiPrefix: function (value, siPrefix) 408 { 409 if (siPrefix instanceof PartKeepr.SiPrefixBundle.Entity.SiPrefix) { 410 var fractionValue = value * Math.pow(siPrefix.get("base"), siPrefix.get("exponent")); 411 412 if (siPrefix.get("exponent") < 0) 413 { 414 return fractionValue.toFixed(Math.abs(siPrefix.get("exponent"))); 415 } else 416 { 417 return fractionValue; 418 } 419 } else { 420 return value; 421 } 422 }, 423 onAddParameterFilter: function (rec) { 424 var subFilters = []; 425 426 427 subFilters.push( Ext.create("PartKeepr.util.Filter", { 428 property: "parameters.name", 429 operator: "=", 430 value: rec.get("partParameterName") 431 })); 432 433 var value; 434 435 if (rec.get("valueType") === "numeric") { 436 value = this.applySiPrefix(rec.get("value"), rec.getSiPrefix()); 437 438 subFilters.push( Ext.create("PartKeepr.util.Filter", { 439 property: "parameters.normalizedValue", 440 operator: rec.get("operator"), 441 value: value 442 })); 443 } else { 444 value = rec.get("stringValue"); 445 446 subFilters.push( Ext.create("PartKeepr.util.Filter", { 447 property: "parameters.stringValue", 448 operator: rec.get("operator"), 449 value: value 450 })); 451 } 452 453 454 var filter = Ext.create("PartKeepr.util.Filter", { 455 type: "AND", 456 subfilters: subFilters 457 }); 458 this.getStore().addFilter(filter); 459 }, 460 461 /** 462 * Handles the editing of the stock level field. Checks if the user has opted in to skip the 463 * online stock edit confirm window, and runs the changes afterwards. 464 * 465 * @param e An edit event, as documented in 466 * http://docs.sencha.com/ext-js/4-0/#!/api/Ext.grid.plugin.CellEditing-event-edit 467 */ 468 handleStockFieldEdit: function (e) 469 { 470 if (PartKeepr.getApplication().getUserPreference("partkeepr.inline-stock-change.confirm", true) === false) { 471 this.handleStockChange(e); 472 } else { 473 this.confirmStockChange(e); 474 } 475 }, 476 getStockChangeMode: function (value) 477 { 478 var n = value.indexOf("+"); 479 480 if (n !== -1) { 481 return "addition"; 482 } 483 484 n = value.indexOf("-"); 485 486 if (n !== -1) { 487 return "removal"; 488 } 489 490 return "fixed"; 491 }, 492 /** 493 * Opens the confirm dialog 494 * 495 * @param e An edit event, as documented in 496 * http://docs.sencha.com/ext-js/4-0/#!/api/Ext.grid.plugin.CellEditing-event-edit 497 */ 498 confirmStockChange: function (e) 499 { 500 var mode = this.getStockChangeMode(e.value); 501 var value = Math.abs(parseInt(e.value)); 502 var confirmText = ""; 503 var headerText = ""; 504 505 switch (mode) { 506 case "removal": 507 confirmText = sprintf( 508 i18n("You wish to remove <b>%s %s</b> of the part <b>%s</b>. Is this correct?"), 509 value, e.record.getPartUnit().get("name"), e.record.get("name")); 510 511 // Set the stock level to a temporary calculated value. 512 e.record.set("stockLevel", (e.originalValue - value)); 513 headerText = i18n("Remove Part(s)"); 514 break; 515 case "addition": 516 confirmText = sprintf( 517 i18n("You wish to add <b>%s %s</b> of part <b>%s</b>. Is this correct?"), 518 value, e.record.getPartUnit().get("name"), e.record.get("name")); 519 520 e.record.set("stockLevel", (e.originalValue + value)); 521 headerText = i18n("Add Part(s)"); 522 break; 523 case "fixed": 524 confirmText = sprintf( 525 i18n("You wish to set the stock level to <b>%s %s</b> for part <b>%s</b>. Is this correct?"), 526 value, e.record.getPartUnit().get("name"), e.record.get("name")); 527 528 e.record.set("stockLevel", value); 529 headerText = i18n("Set Stock Level for Part(s)"); 530 break; 531 } 532 533 534 var j = Ext.create("PartKeepr.RememberChoiceMessageBox", { 535 escButtonAction: "cancel", 536 dontAskAgainProperty: "partkeepr.inline-stock-change.confirm", 537 dontAskAgainValue: false 538 }); 539 540 j.show({ 541 title: headerText, 542 msg: confirmText, 543 buttons: Ext.Msg.OKCANCEL, 544 fn: this.afterConfirmStockChange, 545 scope: this, 546 originalOnEdit: e, 547 dialog: j 548 }); 549 }, 550 /** 551 * Callback for the stock removal confirm window. 552 * 553 * The parameters are documented on: 554 * http://docs.sencha.com/ext-js/4-0/#!/api/Ext.window.MessageBox-method-show 555 */ 556 afterConfirmStockChange: function (buttonId, text, opts) 557 { 558 if (buttonId === "cancel") { 559 opts.originalOnEdit.record.set("stockLevel", opts.originalOnEdit.originalValue); 560 return; 561 } 562 563 this.handleStockChange(opts.originalOnEdit); 564 }, 565 /** 566 * Handles the stock change. Automatically figures out which method to call (deleteStock or addStock) and 567 * sets the correct quantity. 568 * 569 * @param e An edit event, as documented in 570 * http://docs.sencha.com/ext-js/4-0/#!/api/Ext.grid.plugin.CellEditing-event-edit 571 */ 572 handleStockChange: function (e) 573 { 574 var mode = this.getStockChangeMode(e.value); 575 var value = Math.abs(parseInt(e.value)); 576 var call; 577 578 if (e.value === 0) { 579 return; 580 } 581 582 switch (mode) { 583 case "removal": 584 call = "removeStock"; 585 break; 586 case "addition": 587 call = "addStock"; 588 break; 589 case "fixed": 590 call = "setStock"; 591 break; 592 default: 593 return; 594 } 595 596 e.record.callPutAction(call, { 597 quantity: value 598 }, Ext.bind(this.reloadPart, this, [e])); 599 }, 600 /** 601 * Reloads the current part 602 */ 603 reloadPart: function (opts) 604 { 605 this.loadPart(opts.record.getId(), opts); 606 }, 607 /** 608 * Load the part from the database. 609 */ 610 loadPart: function (id) 611 { 612 PartKeepr.PartBundle.Entity.Part.load(id, { 613 scope: this, 614 success: this.onPartLoaded 615 }); 616 }, 617 /** 618 * Callback after the part is loaded 619 */ 620 onPartLoaded: function (record) 621 { 622 var rec = this.store.findRecord("id", record.getId()); 623 if (rec) { 624 rec.set("stockLevel", record.get("stockLevel")); 625 } 626 } 627 });