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