|
| 1 | +/* |
| 2 | + * Copyright (c) 2018-2025 LabKey Corporation |
| 3 | + * |
| 4 | + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 |
| 5 | + */ |
| 6 | +/** |
| 7 | + * @cfg targetStore |
| 8 | + */ |
| 9 | +Ext4.define('EHR.window.FormBulkAddWindow', { |
| 10 | + extend: 'Ext.window.Window', |
| 11 | + |
| 12 | + modal: true, |
| 13 | + closeAction: 'destroy', |
| 14 | + title: 'Bulk Add Data', |
| 15 | + bodyStyle: 'padding: 5px;', |
| 16 | + width: 1000, |
| 17 | + defaults: { |
| 18 | + border: false |
| 19 | + }, |
| 20 | + |
| 21 | + fieldConfigs: [], |
| 22 | + fieldNames: [], |
| 23 | + requiredFieldConfigs: [], |
| 24 | + requiredFieldNames: [], |
| 25 | + upperCaseAnimalId: false, |
| 26 | + |
| 27 | + initComponent: function(){ |
| 28 | + const section = this.targetStore?.sectionCfg; |
| 29 | + if (!section) { |
| 30 | + Ext4.Msg.alert('Error', 'Unable to find form section. Check that the form is configured correctly.'); |
| 31 | + return; |
| 32 | + } |
| 33 | + const allConfigs = EHR.model.DefaultClientModel.getFieldConfigs(section.fieldConfigs, section.configSources, this.extraMetaData); |
| 34 | + this.fieldConfigs = allConfigs.filter((f) => { |
| 35 | + return !f.hidden && !f.isHidden && f.name.toLowerCase() !== 'taskid' && f.name.toLowerCase() !== "qcstate"; |
| 36 | + }); |
| 37 | + if (!this.fieldConfigs) { |
| 38 | + Ext4.Msg.alert('Error', 'Unable to find fields in target store. Check that the form is configured correctly.'); |
| 39 | + return; |
| 40 | + } |
| 41 | + |
| 42 | + this.fieldNames = this.fieldConfigs.map((f) => { |
| 43 | + const alias = f.importAliases?.[0]; // If the import alias matches the label, use that in the template |
| 44 | + if (alias?.toLowerCase() === f.label?.toLowerCase()) { |
| 45 | + return alias; |
| 46 | + } |
| 47 | + return f.name; |
| 48 | + }); |
| 49 | + |
| 50 | + this.requiredFieldConfigs = this.fieldConfigs.filter((f) => { |
| 51 | + return f.required; |
| 52 | + }); |
| 53 | + |
| 54 | + this.requiredFieldNames = this.requiredFieldConfigs.map((f) => { |
| 55 | + return f.name; |
| 56 | + }); |
| 57 | + |
| 58 | + this.items = [{ |
| 59 | + html : 'This allows you to import data using a simple Excel or TSV file. To import, cut/paste the contents of the Excel or TSV file (Ctl + A is a good way to select all) into the box below and hit submit. The limit for import is 250 rows.', |
| 60 | + style: 'padding-bottom: 10px;' |
| 61 | + },{ |
| 62 | + xtype: 'ldk-linkbutton', |
| 63 | + text: '[Download Template]', |
| 64 | + scope: this, |
| 65 | + linkTarget: '_blank', |
| 66 | + handler: function(){ |
| 67 | + LABKEY.Utils.convertToExcel({ |
| 68 | + fileName: this.targetStore.sectionCfg.label + '.xlsx', |
| 69 | + sheets: [{ |
| 70 | + name: 'Requests', |
| 71 | + data: [ |
| 72 | + this.fieldNames |
| 73 | + ] |
| 74 | + }] |
| 75 | + }); |
| 76 | + } |
| 77 | + },{ |
| 78 | + xtype: 'textarea', |
| 79 | + width: 970, |
| 80 | + height: 400, |
| 81 | + itemId: 'textField' |
| 82 | + }]; |
| 83 | + |
| 84 | + this.buttons = [{ |
| 85 | + text: 'Submit', |
| 86 | + scope: this, |
| 87 | + handler: this.onSubmit |
| 88 | + },{ |
| 89 | + text: 'Cancel', |
| 90 | + handler: function(btn){ |
| 91 | + btn.up('window').close(); |
| 92 | + } |
| 93 | + }]; |
| 94 | + |
| 95 | + this.projectStore = EHR.DataEntryUtils.getProjectStore(); |
| 96 | + |
| 97 | + this.callParent(arguments); |
| 98 | + }, |
| 99 | + |
| 100 | + onSubmit: function(){ |
| 101 | + const text = this.down('#textField').getValue(); |
| 102 | + if (!text){ |
| 103 | + Ext4.Msg.alert('Error', 'Must paste the records into the text area'); |
| 104 | + return; |
| 105 | + } |
| 106 | + |
| 107 | + const parsed = LDK.Utils.CSVToArray(Ext4.String.trim(text), '\t'); |
| 108 | + if (!parsed){ |
| 109 | + Ext4.Msg.alert('Error', 'There was an error parsing the data.'); |
| 110 | + return; |
| 111 | + } |
| 112 | + |
| 113 | + if (parsed.length < 2){ |
| 114 | + Ext4.Msg.alert('Error', 'There are not enough rows in the text, there was an error parsing the data.'); |
| 115 | + return; |
| 116 | + } |
| 117 | + |
| 118 | + this.doParse(parsed); |
| 119 | + }, |
| 120 | + |
| 121 | + doParse: function(parsed){ |
| 122 | + const errors = []; |
| 123 | + const records = []; |
| 124 | + |
| 125 | + //first get global values: |
| 126 | + Ext4.Msg.wait('Processing...'); |
| 127 | + |
| 128 | + const dataRowCount = parsed.length - 1; |
| 129 | + if (dataRowCount > 250) { |
| 130 | + errors.push('Row Count - ' + dataRowCount + ': Import maximum is 250 rows. Please split your import into multiple uploads and submit the form between each upload.'); |
| 131 | + } |
| 132 | + else { |
| 133 | + |
| 134 | + for (let i = 1; i < parsed.length; i++) { |
| 135 | + const row = parsed[i]; |
| 136 | + if (!row || row.length < this.requiredFieldNames.length) { |
| 137 | + errors.push('Row ' + i + ': not enough items in row'); |
| 138 | + continue; |
| 139 | + } |
| 140 | + |
| 141 | + const newRow = this.processRow(parsed[0], row, errors, i); |
| 142 | + if (newRow) { |
| 143 | + records.push(this.targetStore.createModel(newRow)); |
| 144 | + } |
| 145 | + } |
| 146 | + |
| 147 | + Ext4.Msg.hide(); |
| 148 | + } |
| 149 | + |
| 150 | + if (errors.length){ |
| 151 | + Ext4.Msg.alert('Error', 'There following errors were found:<p>' + errors.join('<br>')); |
| 152 | + return; |
| 153 | + } |
| 154 | + |
| 155 | + if (records.length){ |
| 156 | + this.targetStore.add(records); |
| 157 | + } |
| 158 | + |
| 159 | + this.close(); |
| 160 | + }, |
| 161 | + |
| 162 | + resolveLookup: function(field, value, errors){ |
| 163 | + if (!field || !field.lookup) |
| 164 | + return value; |
| 165 | + |
| 166 | + if (!field.lookup.store) { |
| 167 | + const storeId = LABKEY.ext4.Util.getLookupStoreId(field); |
| 168 | + |
| 169 | + field.lookup.store = Ext4.StoreMgr.get(storeId); |
| 170 | + if (!field.lookup.store) { |
| 171 | + console.log('Unable to find lookup store for ' + storeId); |
| 172 | + |
| 173 | + return value; |
| 174 | + } |
| 175 | + } |
| 176 | + |
| 177 | + const lookupRecord = field.lookup.store.findRecord(field.lookup.displayColumn, value); |
| 178 | + if (lookupRecord) { |
| 179 | + return lookupRecord.data[field.lookup.keyColumn]; |
| 180 | + } |
| 181 | + |
| 182 | + return value; |
| 183 | + }, |
| 184 | + |
| 185 | + processRow: function(headers, row, errors, rowIdx){ |
| 186 | + const obj = { |
| 187 | + Id: this.upperCaseAnimalId ? row[headers.indexOf('Id')].toUpperCase() : row[headers.indexOf('Id')], |
| 188 | + date: LDK.ConvertUtils.parseDate(row[headers.indexOf('date')]), |
| 189 | + project: this.resolveProjectByName(row[headers.indexOf('project')], errors, rowIdx) |
| 190 | + } |
| 191 | + |
| 192 | + Ext4.each(this.fieldConfigs, function(field) { |
| 193 | + if (!obj[field.name]) { |
| 194 | + let index = headers.indexOf(field.name); |
| 195 | + if (index === -1 && field.importAliases?.[0]) { |
| 196 | + index = headers.indexOf(field.importAliases?.[0]); |
| 197 | + } |
| 198 | + if (index !== -1) { |
| 199 | + obj[field.name] = this.resolveLookup(field, row[index], errors); |
| 200 | + } |
| 201 | + } |
| 202 | + }, this); |
| 203 | + |
| 204 | + if (!this.checkRequired(this.requiredFieldNames, obj, errors, rowIdx)){ |
| 205 | + return obj; |
| 206 | + } |
| 207 | + }, |
| 208 | + |
| 209 | + checkRequired: function(fields, row, errors, rowIdx){ |
| 210 | + let hasErrors = false, fieldName; |
| 211 | + |
| 212 | + for (let i=0;i<fields.length;i++){ |
| 213 | + fieldName = fields[i]; |
| 214 | + if (Ext4.isEmpty(row[fieldName])){ |
| 215 | + errors.push('Row ' + rowIdx + ': missing required field ' + fieldName); |
| 216 | + hasErrors = true; |
| 217 | + } |
| 218 | + } |
| 219 | + |
| 220 | + return hasErrors; |
| 221 | + }, |
| 222 | + |
| 223 | + resolveProjectByName: function(projectName, errors, rowIdx){ |
| 224 | + if (!projectName){ |
| 225 | + return null; |
| 226 | + } |
| 227 | + |
| 228 | + projectName = Ext4.String.leftPad(projectName, 4, '0'); |
| 229 | + |
| 230 | + const recIdx = this.projectStore.find('name', projectName); |
| 231 | + if (recIdx === -1){ |
| 232 | + errors.push('Row ' + rowIdx + ': unknown project ' + projectName); |
| 233 | + return null; |
| 234 | + } |
| 235 | + |
| 236 | + return this.projectStore.getAt(recIdx).get('project'); |
| 237 | + } |
| 238 | +}); |
| 239 | + |
| 240 | +EHR.DataEntryUtils.registerGridButton('FORM_BULK_ADD', function(config){ |
| 241 | + return Ext4.Object.merge({ |
| 242 | + text: 'Add From File', |
| 243 | + tooltip: 'Click to bulk import records from an Excel or TSV file.', |
| 244 | + handler: function(btn){ |
| 245 | + const grid = btn.up('grid'); |
| 246 | + LDK.Assert.assertNotEmpty('Unable to find grid in FORM_BULK_ADD button', grid); |
| 247 | + |
| 248 | + Ext4.create('EHR.window.FormBulkAddWindow', { |
| 249 | + targetStore: grid.store |
| 250 | + }).show(); |
| 251 | + } |
| 252 | + }); |
| 253 | +}); |
0 commit comments