partkeepr

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

Importer.js (19790B)


      1 Ext.define("PartKeepr.Importer.Importer", {
      2     extend: "Ext.panel.Panel",
      3     layout: 'border',
      4     bbar: [
      5         {
      6             xtype: 'button',
      7             itemId: "selectImportFile",
      8             text: i18n("Select CSV file for import…")
      9         },
     10         {
     11             xtype: 'button',
     12             itemId: "executeImport",
     13             text: i18n("Execute import")
     14         },
     15         {
     16             xtype: 'tbseparator',
     17         },
     18         {
     19             xtype: 'presetcombo',
     20             model: 'PartKeepr.ImportBundle.Entity.ImportPreset',
     21             itemId: 'importerPresetCombo',
     22             displayField: 'name',
     23             width: 300
     24 
     25         }
     26     ],
     27     items: [
     28         {
     29             title: i18n("Mapping"),
     30             xtype: 'treepanel',
     31             region: 'west',
     32             width: 280,
     33             split: true,
     34             itemId: 'fieldTree',
     35             columns: [
     36                 {
     37                     xtype: 'treecolumn',
     38                     text: i18n("Field"),
     39                     dataIndex: 'text',
     40                     width: 200
     41                 }, {
     42                     xtype: 'checkcolumn',
     43                     disabled: true,
     44                     disabledCls: '',
     45                     header: i18n("Required"),
     46                     dataIndex: 'required',
     47                     width: 70
     48                 }
     49             ],
     50             store: {
     51                 folderSort: true,
     52                 sorters: [
     53                     {
     54                         property: 'text',
     55                         direction: 'ASC'
     56                     }
     57                 ]
     58             },
     59             useArrows: true
     60         }, {
     61             title: i18n("Configuration"),
     62             region: 'center',
     63             itemId: 'configurationCards',
     64             layout: 'card',
     65             items: [
     66                 {
     67                     html: "Select a field to begin",
     68                     getImporterConfig: function ()
     69                     {
     70                         return {};
     71                     },
     72                     setImporterConfig: function ()
     73                     {
     74                     }
     75                 },
     76                 {
     77                     xtype: 'importerEntityConfiguration',
     78                     itemId: 'importerEntityConfiguration'
     79                 }, {
     80                     xtype: 'importerFieldConfiguration',
     81                     itemId: 'importerFieldConfiguration'
     82                 },
     83                 {
     84                     xtype: 'importerOneToManyConfiguration',
     85                     itemId: 'importerOneToManyConfiguration'
     86                 },
     87                 {
     88                     xtype: 'importerManyToOneConfiguration',
     89                     itemId: 'importerManyToOneConfiguration'
     90                 }
     91             ]
     92         },
     93         {
     94             xtype: 'tabpanel',
     95             region: 'south',
     96             height: 265,
     97             split: true,
     98             items: [
     99                 {
    100                     title: i18n("Source File"),
    101                     itemId: 'sourceFileGrid',
    102                     xtype: 'grid'
    103                 }, {
    104                     title: i18n("Preview"),
    105                     itemId: 'preview',
    106                     bodyStyle: "overflow: scroll;"
    107                 }, {
    108                     title: i18n("Errors"),
    109                     itemId: 'errorsGrid',
    110                     xtype: 'grid',
    111                     columns: [
    112                         {
    113                             text: i18n("Path"),
    114                             flex: 1,
    115                             dataIndex: "node",
    116                             renderer: function (val,p,rec)
    117                             {
    118                                 return rec.get("node").getPath("text", "/");
    119                             }
    120                         }, {
    121                             text: i18n("Error"),
    122                             flex: 1,
    123                             dataIndex: "error"
    124                         }
    125                     ],
    126                     store: {
    127                         fields: [{
    128                             name: "node",
    129                             type: 'auto'
    130                         }, {
    131                             name: "error",
    132                             type: 'auto'
    133                         }]
    134                     }
    135                 }
    136             ]
    137         }
    138     ],
    139 
    140     /**
    141      * @var {String} The model to use
    142      */
    143     model: null,
    144 
    145     importConfiguration: {},
    146 
    147     importColumnsStore: null,
    148 
    149     initComponent: function ()
    150     {
    151         this.callParent(arguments);
    152 
    153         this.importConfiguration = {};
    154 
    155         this.applyConfiguration();
    156 
    157         this.importColumnsStore = Ext.create("Ext.data.Store", {
    158             fields: ["headerIndex", "headerName"],
    159             storeId: "importColumns"
    160         });
    161 
    162         this.down("#importerPresetCombo").getStore().addFilter({
    163             property: "baseEntity",
    164             operator: "=",
    165             value: this.model.getName()
    166         });
    167 
    168         this.down("#importerEntityConfiguration").setModel(this.model);
    169 
    170         this.down("#fieldTree").on("selectionchange", this.onFieldChange, this);
    171         this.down("#fieldTree").on("beforeselect", this.onBeforeSelect, this);
    172         this.down("#selectImportFile").on("click", this.uploadCSVFile, this);
    173         this.down("#executeImport").on("click", this.executeImport, this);
    174         this.down("#importerEntityConfiguration").on("configChanged", this.onConfigChange, this);
    175         this.down("#importerFieldConfiguration").on("configChanged", this.onConfigChange, this);
    176         this.down("#importerOneToManyConfiguration").on("configChanged", this.onConfigChange, this);
    177         this.down("#importerManyToOneConfiguration").on("configChanged", this.onConfigChange, this);
    178 
    179         this.down("#importerPresetCombo").on("selectPreset", this.onPresetSelect, this);
    180         this.down("#importerPresetCombo").setAdditionalFields([
    181             {
    182                 fieldName: "baseEntity",
    183                 value: this.model.getName()
    184             }
    185         ]);
    186 
    187         this.down("#preview").on("afterrender", this.refreshPreview, this);
    188         this.down("#importerPresetCombo").setConfiguration(this.importConfiguration);
    189         this.validateConfig();
    190 
    191     },
    192     applyConfiguration: function ()
    193     {
    194         var rootNode = this.down("#fieldTree").getRootNode();
    195 
    196         rootNode.removeAll();
    197 
    198         rootNode.set("text", this.model.getName());
    199         rootNode.set("data", {
    200             name: "",
    201             type: "relation",
    202             reference: this.model,
    203             configuration: this.importConfiguration
    204         });
    205 
    206         var treeMaker = Ext.create("PartKeepr.ModelTreeMaker.ModelTreeMaker");
    207         treeMaker.addIgnoreField("@id");
    208         treeMaker.setCustomFieldIgnorer(this.customFieldIgnorer);
    209 
    210         treeMaker.make(rootNode, this.model, "", Ext.bind(this.appendFieldData, this));
    211         rootNode.expand();
    212     },
    213     onPresetSelect: function (configuration)
    214     {
    215         this.importConfiguration = configuration;
    216 
    217         this.applyConfiguration();
    218         this.refreshPreview();
    219         this.down("#importerPresetCombo").setConfiguration(configuration);
    220         this.down("#fieldTree").getSelectionModel().select(this.down("#fieldTree").getRootNode());
    221     },
    222     executeImport: function ()
    223     {
    224         //@todo Implement warning dialog
    225 
    226         Ext.Ajax.request({
    227             url: PartKeepr.getBasePath() + '/executeImport/?file=' + this.temporaryFile,
    228             method: 'POST',
    229             params: {
    230                 configuration: Ext.encode(this.importConfiguration),
    231                 baseEntity: this.model.getName()
    232             },
    233             success: function (response)
    234             {
    235                 var responseData = Ext.decode(response.responseText);
    236 
    237                 var j = Ext.create("Ext.window.Window", {
    238                     width: 800,
    239                     height: 400,
    240                     layout: "fit",
    241                     scrollable: true,
    242                     title: i18n("Import Results"),
    243                     items: [
    244                         {
    245                             xtype: 'textareafield',
    246                             itemId: 'resultPanel',
    247                             listeners: {
    248                                 render: function (p)
    249                                 {
    250                                     p.getEl().dom.innerHTML = "<pre><strong>Import Results</strong>\n\n" + responseData.logs + "</pre>";
    251                                     p.getEl().dom.style.overflow = "auto";
    252                                     p.getEl().dom.style.userSelect = "initial";
    253                                 }
    254                             }
    255                         }
    256                     ]
    257                 });
    258 
    259                 j.show();
    260             },
    261             scope: this
    262         });
    263 
    264     },
    265     uploadCSVFile: function ()
    266     {
    267         var j = Ext.create("PartKeepr.FileUploadDialog");
    268         j.on("fileUploaded", this.onFileUploaded, this);
    269         j.show();
    270     },
    271     onFileUploaded: function (data)
    272     {
    273         var uploadedFile = Ext.create("PartKeepr.UploadedFileBundle.Entity.TempUploadedFile", data);
    274         this.loadData(uploadedFile.getId());
    275     },
    276     onBeforeSelect: function ()
    277     {
    278         this.onConfigChange();
    279     },
    280     onConfigChange: function ()
    281     {
    282         this.down("#importerPresetCombo").setConfiguration(this.importConfiguration);
    283         Ext.Function.defer(this.refreshPreview, 100, this);
    284     },
    285     refreshPreview: function ()
    286     {
    287         this.validateConfig();
    288 
    289         Ext.Ajax.request({
    290 
    291             url: PartKeepr.getBasePath() + '/getPreview/?file=' + this.temporaryFile,
    292             method: 'POST',
    293             params: {
    294                 configuration: Ext.encode(this.importConfiguration),
    295                 baseEntity: this.model.getName()
    296             },
    297             success: function (response)
    298             {
    299                 var responseData = Ext.decode(response.responseText);
    300 
    301                 if (this.down("#preview").body !== undefined) {
    302                     this.down("#preview").body.dom.innerHTML = "<pre>" + responseData.logs + "</pre>";
    303                 }
    304             },
    305             scope: this
    306         });
    307     },
    308     onFieldChange: function (selectionModel, selected)
    309     {
    310         if (selected.length == 1) {
    311             if (selected[0].data.data.type == "field") {
    312                 this.down("#configurationCards").setActiveItem(this.down("#importerFieldConfiguration"));
    313             } else {
    314                 if (selected[0].data.data.name === "") {
    315                     this.down("#configurationCards").setActiveItem(this.down("#importerEntityConfiguration"));
    316                     this.down("#importerEntityConfiguration").setModel(selected[0].data.data.reference);
    317                 } else {
    318                     if (selected[0].data.data.type === "onetomany") {
    319                         this.down("#configurationCards").setActiveItem(this.down("#importerOneToManyConfiguration"));
    320                     } else {
    321                         this.down("#configurationCards").setActiveItem(this.down("#importerManyToOneConfiguration"));
    322                         this.down("#importerManyToOneConfiguration").setModel(selected[0].data.data.reference);
    323                     }
    324                 }
    325             }
    326 
    327             this.down("#configurationCards").getLayout().getActiveItem().setImporterConfig(
    328                 selected[0].data.data.configuration);
    329 
    330             if (Ext.isFunction(this.down("#configurationCards").getLayout().getActiveItem().reconfigureColumns)) {
    331                 this.down("#configurationCards").getLayout().getActiveItem().reconfigureColumns(
    332                     this.importColumnsStore);
    333             }
    334         }
    335     },
    336     customFieldIgnorer: function (field)
    337     {
    338         return !field.persist;
    339     },
    340     /**
    341      * Appends default configuration data while populating the tree
    342      * @param {Ext.data.field.Field} The model
    343      */
    344     appendFieldData: function (field, node)
    345     {
    346         var fieldData = {};
    347         fieldData.data = node.get("data");
    348 
    349         if (!node.parentNode.data.data.hasOwnProperty("configuration")) {
    350             node.parentNode.data.data.configuration = {};
    351         }
    352 
    353         if (!node.parentNode.data.data.configuration.hasOwnProperty("fields")) {
    354             node.parentNode.data.data.configuration.fields = {};
    355         }
    356 
    357         if (!node.parentNode.data.data.configuration.hasOwnProperty("onetomany")) {
    358             node.parentNode.data.data.configuration.onetomany = {};
    359         }
    360 
    361         if (!node.parentNode.data.data.configuration.hasOwnProperty("manytoone")) {
    362             node.parentNode.data.data.configuration.manytoone = {};
    363         }
    364 
    365         switch (node.data.data.type) {
    366             case "manytoone":
    367                 if (!node.parentNode.data.data.configuration.manytoone.hasOwnProperty(node.data.text)) {
    368                     node.parentNode.data.data.configuration.manytoone[node.data.text] = {};
    369                 }
    370                 fieldData.data.configuration = node.parentNode.data.data.configuration.manytoone[node.data.text];
    371 
    372                 if (typeof field.reference !== "undefined" && field.reference !== null) {
    373                     fieldData.data.reference = Ext.ClassManager.get(field.reference.type);
    374                 } else {
    375                     fieldData.data.reference = this.model;
    376                 }
    377 
    378                 if (field.allowBlank === false) {
    379                     fieldData.required = true;
    380                 }
    381                 return fieldData;
    382             case "onetomany":
    383                 if (!node.parentNode.data.data.configuration.onetomany.hasOwnProperty(node.data.text)) {
    384                     node.parentNode.data.data.configuration.onetomany[node.data.text] = {};
    385                 }
    386                 fieldData.data.configuration = node.parentNode.data.data.configuration.onetomany[node.data.text];
    387                 break;
    388             default:
    389                 if (!node.parentNode.data.data.configuration.fields.hasOwnProperty(node.data.text)) {
    390                     node.parentNode.data.data.configuration.fields[node.data.text] = {};
    391                 }
    392                 fieldData.data.configuration = node.parentNode.data.data.configuration.fields[node.data.text];
    393 
    394                 field.compileValidators();
    395 
    396                 for (var i = 0; i < field._validators.length; i++) {
    397                     if (field._validators[i].type === "presence") {
    398                         fieldData.required = true;
    399                     } else {
    400                         fieldData.required = false;
    401                     }
    402 
    403                 }
    404 
    405                 return fieldData;
    406         }
    407     },
    408     loadData: function (temporaryFile)
    409     {
    410         this.temporaryFile = temporaryFile;
    411 
    412         Ext.Ajax.request({
    413             url: PartKeepr.getBasePath() + '/getSource/?file=' + temporaryFile,
    414             success: function (response)
    415             {
    416                 var responseData = Ext.decode(response.responseText);
    417 
    418                 this.reconfigureGrid(responseData);
    419             },
    420             scope: this
    421         });
    422     },
    423     reconfigureGrid: function (data)
    424     {
    425         var columns = [];
    426         var fieldConfig = [];
    427         var header = data[0];
    428 
    429         this.importColumnsStore.removeAll();
    430 
    431         for (var i = 0; i < header.length; i++) {
    432             columns.push({
    433                 text: header[i],
    434                 dataIndex: "field" + i
    435             });
    436 
    437             this.importColumnsStore.add({"headerIndex": i, "headerName": header[i]});
    438 
    439             fieldConfig.push({
    440                 name: "field" + i,
    441                 type: "string"
    442             });
    443         }
    444 
    445         var store = Ext.create("Ext.data.Store", fieldConfig);
    446 
    447         var recordData = [];
    448         for (i = 1; i < data.length; i++) {
    449             var row = {};
    450             for (var j = 0; j < data[i].length; j++) {
    451                 row["field" + j] = data[i][j];
    452             }
    453 
    454             recordData.push(row);
    455         }
    456 
    457         store.add(recordData);
    458 
    459         this.down("#sourceFileGrid").reconfigure(store, columns);
    460         this.validateConfig();
    461     },
    462     /**
    463      * Recursively validates all nodes within the importer configuration and populates the errors grid
    464      */
    465     validateConfig: function ()
    466     {
    467         this.down("#errorsGrid").setTitle(i18n("Errors"));
    468         this.down("#errorsGrid").getStore().removeAll();
    469         this.validateConfigNode(this.down("#fieldTree").getRootNode());
    470     },
    471     /**
    472      * Validates a given node in the tree
    473      * @param {Ext.data.NodeInterface} node The node to validate
    474      */
    475     validateConfigNode: function (node)
    476     {
    477         var configuration = node.data.data.configuration;
    478         var recurse = false;
    479 
    480         switch (node.data.data.type) {
    481             case "field":
    482                 if (configuration.fieldConfiguration && configuration.fieldConfiguration === "copyFrom") {
    483                     if (this.down("#sourceFileGrid").getColumns().length - 1 < configuration.copyFromField) {
    484                         this.appendError(node, i18n(
    485                             "The selected CSV file does not contain enough columns to fulfill the configuration"
    486                         ));
    487                     }
    488                 }
    489 
    490                 if (node.data.required) {
    491                     if (configuration.fieldConfiguration) {
    492                         switch (configuration.fieldConfiguration) {
    493                             case "ignore":
    494                                 this.appendError(node, i18n("The field must be set to a value, but it is ignored"));
    495                                 break;
    496                             case "copyFrom":
    497                                 if (configuration.copyFromField === "") {
    498                                     this.appendError(node, i18n(
    499                                         "The field is configured to copy a value from the source file, but no source file field was configured"));
    500                                 }
    501                                 break;
    502                             default:
    503                                 break;
    504                         }
    505                     } else {
    506                         this.appendError(node, i18n("The field is required, but it is not configured"));
    507                     }
    508                 }
    509                 break;
    510             case "manytoone":
    511                 if (node.data.required) {
    512                     switch (configuration.importBehaviour) {
    513                         case "dontSet":
    514                         case undefined:
    515                             this.appendError(node, i18n("The entity is required, but it is not configured"));
    516                             break;
    517                         case "matchData":
    518                             if (configuration.notFoundBehaviour === "createEntity") {
    519                                 recurse = true;
    520                             }
    521                             break;
    522 
    523                     }
    524                 } else {
    525                     recurse = false;
    526                 }
    527                 break;
    528             case "onetomany":
    529                 recurse = false;
    530                 break;
    531             default:
    532                 recurse = true;
    533                 break;
    534         }
    535 
    536         if (recurse) {
    537             for (var i = 0; i < node.childNodes.length; i++) {
    538                 this.validateConfigNode(node.childNodes[i]);
    539             }
    540         }
    541     },
    542     /**
    543      * Appends a specific error for a given node
    544      *
    545      * @param {Ext.data.NodeInterface} node The node to append an error message
    546      * @param {String} error The error message to append
    547      */
    548     appendError: function (node, error)
    549     {
    550         this.down("#errorsGrid").getStore().add({node: node, error: error});
    551 
    552         var title = i18n("Errors") + " (" +
    553             this.down("#errorsGrid").getStore().getCount() + ")";
    554 
    555         this.down("#errorsGrid").setTitle(title);
    556     }
    557 });