partkeepr

fork of partkeepr
git clone https://git.e1e0.net/partkeepr.git
Log | Files | Refs | Submodules | README | LICENSE

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 });