diff --git a/.gitignore b/.gitignore index 5f9d4708c..67677767a 100644 --- a/.gitignore +++ b/.gitignore @@ -50,6 +50,9 @@ node_modules .DS_Store .idea +# .sfdx files +.sfdx + hardis-report/ megalinter-reports/ config/user/ diff --git a/src/force-app/main/default/classes/ConfigBuilderExport.cls b/src/force-app/main/default/classes/ConfigBuilderExport.cls new file mode 100644 index 000000000..5bacaa28d --- /dev/null +++ b/src/force-app/main/default/classes/ConfigBuilderExport.cls @@ -0,0 +1,138 @@ +public class ConfigBuilderExport { + + + static String filter = '';// where LastModifiedDate >= LAST_N_WEEKS:2'; + static Map objectsExtId = new Map(); + static Map objectsExtIdExcep = new Map(); + //List of object which record to be exported + static List objects = new List{ + 'SBQQ__QuoteTemplate__c','SBQQ__TemplateContent__c' + }; + //Fields which are self/system generated, no need to export/import + static Set blacklistedFields = new Set{'CompletedDateTime','RecurrenceRegeneratedType','RecurrenceMonthOfYear', + 'RecurrenceInstance','RecurrenceDayOfMonth','RecurrenceDayOfWeekMask','RecurrenceType','RecurrenceTimeZoneSidKey', + 'RecurrenceEndDateOnly','RecurrenceStartDateOnly ','RecurrenceActivityId','ReminderDateTime','ActivityOriginType', + 'ArchivedDate','IsArchived','IsClosed','PrioritySortOrder','IsHighPriority','JigsawContactId', + 'ReportsToName','EmailBouncedReason','EmailBouncedDate','IsEmailBounced','JigsawContactId', + 'Id','IsDeleted','CreatedDate','LastModifiedDate','SystemModstamp','LastActivityDate','LastViewedDate', + 'LastReferencedDate','UserRecordAccessId','MasterRecordId','AccountSource','IsCssEnabled', + 'CssLastLoginDate','CompareName','PhotoUrl','CompareSite','OwnerAlias','JigSawCompanyId', + 'ConnectionReceivedDate','ConnectionSentDate','AccountRollupId','ProductIsArchived', 'OwnerId', 'Id', 'SetupOwnerId'}; + + //This is to filter out reference/lookup field before export + Static Set unUsedObject = new Set{'dummy__C'}; + + + + + @AuraEnabled(cacheable=true) + public static String exportConfigData(String filterString){ + filterString = String.escapeSingleQuotes(filterString); + Map objectsQuery = new Map{}; + Map> objectsFields = new Map>{}; + + String jsonConfigData = ''; + try{ + + Map schemaMap = Schema.getGlobalDescribe(); + + //Collecting externalId first for all objects + for(String obj: objects){ + Map fieldMap = schemaMap.get(obj).getDescribe().fields.getMap(); + + for(Schema.SObjectField sfield : fieldMap.Values()) + { + Schema.DescribeFieldResult des = sfield.getDescribe(); + + if(des.isExternalID() && ){ + objectsExtId.put(obj, sfield); + + //Adding external Id for object which has exception like multiple ExternalIds + // this can be improved by having input and managed dynamically + if(obj=='Product__c' && des.getName()== 'External_Id__c'){ + objectsExtIdExcep.put(obj,sfield); + } + } + + } + } + + for(String obj: objects){ + Map lookupFieldToRefField = new Map(); + Map refFieldToObject = new Map(); + Set objLookupFields = new Set(); + Set objLookupRefExtIdFields = new Set(); + Set fields = new Set(); + //Map schemaMap = Schema.getGlobalDescribe(); + Map fieldMap = schemaMap.get(obj).getDescribe().fields.getMap(); + + + for(Schema.SObjectField sfield : fieldMap.Values()) + { + Schema.DescribeFieldResult des = sfield.getDescribe(); + if(!des.isCalculated() && des.isCreateable() ) + //&& !des.isDefaultedOnCreate()) + { + if(des.getName() == 'RecordTypeId'){ + fields.add('RecordType.DeveloperName'); + } + else{ + fields.add(des.getName()); + } + } + + + if( des.isCustom() && des.getReferenceTo() !=null && des.getRelationshipName() != null){ + + if(!unUsedObject.contains(des.getReferenceTo()[0].getDescribe().getName())){ + lookupFieldToRefField.put(des.getName(), des.getRelationshipName()); + refFieldToObject.put(des.getName(), des.getReferenceTo()[0].getDescribe().getName()); + } + } + + } + + //replacing existing key with below info + //once object with multiple externalId are managed dynmically, below piece of code will undergo changes + objectsExtId.put('Product__c',objectsExtIdExcep.get('Product__c')); + + for(String lookupField :lookupFieldToRefField.keySet()){ + + String refExtIdField = lookupFieldToRefField.get(lookupField)+ '.' + + (objectsExtId.get(refFieldToObject.get(lookupField))).getDescribe().getName(); + objLookupRefExtIdFields.add(refExtIdField); + } + + fields.removeAll(blacklistedFields); + fields.removeAll(lookupFieldToRefField.keyset()); + fields.addAll(objLookupRefExtIdFields); + objectsFields.put(obj, fields); + } + + for(String obj: objects){ + + String query=''; + query = 'Select '+String.join(new List(objectsFields.get(obj)), ',')+ ' from '+obj +' '+ filterString + + ' Order by CreatedDate'; + objectsQuery.put(obj, query); + } + + Map> objRecords = new Map>(); + for(String obj : objects){ + List records = database.query(objectsQuery.get(obj)); + if(records != null && records.size()>0 ){ + objRecords.put(obj,records); + + } + + } + jsonConfigData = JSON.serializePretty(objRecords, true); + return jsonConfigData; + } + catch(Exception ex){ + system.debug('::ex:'+ex.getMessage()+'::line:'+ex.getLineNumber()); + return ex.getMessage(); + } + + } +} \ No newline at end of file diff --git a/src/force-app/main/default/classes/ConfigBuilderExport.cls-meta.xml b/src/force-app/main/default/classes/ConfigBuilderExport.cls-meta.xml new file mode 100644 index 000000000..4b0bc9f38 --- /dev/null +++ b/src/force-app/main/default/classes/ConfigBuilderExport.cls-meta.xml @@ -0,0 +1,5 @@ + + + 55.0 + Active + diff --git a/src/force-app/main/default/classes/ConfigBuilderImport.cls b/src/force-app/main/default/classes/ConfigBuilderImport.cls new file mode 100644 index 000000000..51e31385a --- /dev/null +++ b/src/force-app/main/default/classes/ConfigBuilderImport.cls @@ -0,0 +1,153 @@ +public class ConfigBuilderImport { + + static Map objectsExtId = new Map(); + + //This might be needed if code is improved, it can be decided by uploaded file itself + static List objects = new List{ + 'SBQQ__QuoteTemplate__c','SBQQ__TemplateContent__c' + }; + Static Set unUsedObject = new Set{'Product__c','BusinessHours'}; + + public ConfigBuilderImport(){ + + } + + //Accept text file having JSON content + @AuraEnabled(cacheable=false) + public static String importConfigData(String base64){ + // system.debug(':::'+base64); + Blob textBlob = EncodingUtil.base64Decode(base64); + String configData = textBlob.toString(); + Map> objToLookupFields = new Map>(); + Map refFieldToObject = new Map(); + Map recordsByObj = (Map) JSON.deserializeUntyped(configData); + + Map objSelfLookUpFields = new Map(); + for(String obj: objects){ + Set fields = new Set(); + Map schemaMap = Schema.getGlobalDescribe(); + Map fieldMap = schemaMap.get(obj).getDescribe().fields.getMap(); + + Set objLookupFields = new Set(); + Set objLookupRefExtIdFields = new Set(); + + Set lookupFields = new Set(); + for(Schema.SObjectField sfield : fieldMap.Values()) + { + + + Schema.DescribeFieldResult des = sfield.getDescribe(); + + if(des.isExternalID()){ + objectsExtId.put(obj, sfield); + + } + + if( des.isCustom() && des.getReferenceTo() !=null && des.getRelationshipName() != null){ + + if(!unUsedObject.contains(des.getReferenceTo()[0].getDescribe().getName())){ + + //lookupFieldToRefField.put(des.getName(), des.getRelationshipName()); + lookupFields.add(des.getName()); + system.debug('::lookupFields::'+lookupFields); + if(des.getReferenceTo()[0].getDescribe().getName() == obj){ + objSelfLookUpFields.put(obj, des.getName()); + } + //Removing lookup fields which has relationship to own object + lookupFields.removeAll(objSelfLookUpFields.values()); + } + + } + + } + objToLookupFields.put(obj,lookupFields); + //Adding external Id which has exception like multiple ExternalIds + //Can be improvised by accpeting such objects name as input and dynamically handling below code instead + // objectsExtId.put('Product__c', + // Schema.getGlobalDescribe().get('Product__c').getDescribe().fields.getMap().get('External_Id__c')); + } + + Map> objToRecords = new Map>(); + Map> objToRecordsFirst = new Map>(); + + for(String obj :objects){ + if(recordsByObj.containsKey(obj)){ + + List sobjRecords = new List(); + List sobjRecordFirstInsert = new List(); + List sobjRecordLaterInsert = new List(); + + List recordsJson = (List)recordsByObj.get(obj); + for(Object jsonRec : recordsJson){ + jsonRec = removeAttributes((Map)jsonRec, objToLookupFields.get(obj)); + String objSer = JSON.serializePretty(jsonRec); + SObject sObj = (SObject)JSON.deserialize(objSer, SObject.class); + sobjRecords.add(sObj); + } + //Order the records in case of self reference lookup + if(objSelfLookUpFields.containsKey(obj)) + { + for(SObject sobj: sobjRecords){ + if(sobj.get(objSelfLookUpFields.get(obj)) == null){ + sobjRecordFirstInsert.add(sobj); + } + } + for(SObject sobj: sobjRecords){ + if(sobj.get(objSelfLookUpFields.get(obj)) != null){ + sobj.put(objSelfLookUpFields.get(obj), null); + //Removing the lookup field from sobj. + String jsonStr = JSON.serialize(sobj); + Map objJson = (Map)JSON.deserializeUntyped(jsonStr); + objJson.remove(objSelfLookUpFields.get(obj)); + String objSer = JSON.serializePretty(objJson); + sobj = (SObject)JSON.deserialize(objSer, SObject.class); + sobjRecordLaterInsert.add(sobj); + } + + } + } + else{ + sobjRecordLaterInsert.addAll(sobjRecords); + } + //creating 2 collection to upsert separtiely due to dependecy on records + objToRecordsFirst.put(obj,sobjRecordFirstInsert ); + objToRecords.put(obj,sobjRecordLaterInsert ); + + + } + } + + for(String objName : objects){ + if(objToRecords.containsKey(objName)){ + + //inserting records first which has self lookup empty and are dependent for rest of records + if(objToRecordsFirst.containsKey(objName)){ + Database.upsert(objToRecordsFirst.get(objName), objectsExtId.get(objName)); + } + Database.upsert(objToRecords.get(objName), objectsExtId.get(objName)); + } + } + + return 'Config Imported Successfully'; + } + + //Removes the Key/fields which are not needed for next org + public static Object removeAttributes(Object jsonObj, Set extraKeyToRemove){ + Map jsonObjMap = (Map)jsonObj; + for(String key : jsonObjMap.keySet()) { + if(key == 'Id' || key == 'RecordTypeId' || key=='RecordType' || extraKeyToRemove.contains(key)) { + jsonObjMap.remove(key); + } else { + if(jsonObjMap.get(key) instanceof Map) { + removeAttributes((Map)jsonObjMap.get(key), extraKeyToRemove); + } + } + } + return jsonObjMap; + } + + + + + +} \ No newline at end of file diff --git a/src/force-app/main/default/classes/ConfigBuilderImport.cls-meta.xml b/src/force-app/main/default/classes/ConfigBuilderImport.cls-meta.xml new file mode 100644 index 000000000..fbbad0af5 --- /dev/null +++ b/src/force-app/main/default/classes/ConfigBuilderImport.cls-meta.xml @@ -0,0 +1,5 @@ + + + 56.0 + Active + diff --git a/src/force-app/main/default/classes/FileUploaderClass.cls b/src/force-app/main/default/classes/FileUploaderClass.cls new file mode 100644 index 000000000..3657aad1a --- /dev/null +++ b/src/force-app/main/default/classes/FileUploaderClass.cls @@ -0,0 +1,74 @@ +public with sharing class FileUploaderClass { + /* + * @method uploadFile() + * @desc Creates a content version from a given file's base64 and name + * + * @param {String} base64 - base64 string that represents the file + * @param {String} filename - full file name with extension, i.e. 'products.csv' + * @param {String} recordId - Id of the record you want to attach this file to + * + * @return {ContentVersion} - returns the created ContentDocumentLink Id if the + * upload was successful, otherwise returns null + */ + @AuraEnabled + public static String uploadFile(String base64, String filename, String recordId) { + ContentVersion cv = createContentVersion(base64, filename); + ContentDocumentLink cdl = createContentLink(cv.Id, recordId); + if (cv == null || cdl == null) { return null; } + return cdl.Id; + } + /* + * @method createContentVersion() [private] + * @desc Creates a content version from a given file's base64 and name + * + * @param {String} base64 - base64 string that represents the file + * @param {String} filename - full file name with extension, i.e. 'products.csv' + * + * @return {ContentVersion} - returns the newly created ContentVersion, or null + * if there was an error inserting the record + */ + private static ContentVersion createContentVersion(String base64, String filename) { + ContentVersion cv = new ContentVersion(); + cv.VersionData = EncodingUtil.base64Decode(base64); + cv.Title = filename; + cv.PathOnClient = filename; + try { + insert cv; + return cv; + } catch(DMLException e) { + System.debug(e); + return null; + } + } + + /* + * @method createContentLink() [private] + * @desc Creates a content link for a given ContentVersion and record + * + * @param {String} contentVersionId - Id of the ContentVersion of the file + * @param {String} recordId - Id of the record you want to attach this file to + * + * @return {ContentDocumentLink} - returns the newly created ContentDocumentLink, + * or null if there was an error inserting the record + */ + private static ContentDocumentLink createContentLink(String contentVersionId, String recordId) { + if (contentVersionId == null || recordId == null) { return null; } + ContentDocumentLink cdl = new ContentDocumentLink(); + cdl.ContentDocumentId = [ + SELECT ContentDocumentId + FROM ContentVersion + WHERE Id =: contentVersionId + ].ContentDocumentId; + cdl.LinkedEntityId = recordId; + // ShareType is either 'V', 'C', or 'I' + // V = Viewer, C = Collaborator, I = Inferred + cdl.ShareType = 'V'; + try { + insert cdl; + return cdl; + } catch(DMLException e) { + System.debug(e); + return null; + } + } + } \ No newline at end of file diff --git a/src/force-app/main/default/classes/FileUploaderClass.cls-meta.xml b/src/force-app/main/default/classes/FileUploaderClass.cls-meta.xml new file mode 100644 index 000000000..4b0bc9f38 --- /dev/null +++ b/src/force-app/main/default/classes/FileUploaderClass.cls-meta.xml @@ -0,0 +1,5 @@ + + + 55.0 + Active + diff --git a/src/force-app/main/default/lwc/configExport/__tests__/configExport.test.js b/src/force-app/main/default/lwc/configExport/__tests__/configExport.test.js new file mode 100644 index 000000000..595a96fc5 --- /dev/null +++ b/src/force-app/main/default/lwc/configExport/__tests__/configExport.test.js @@ -0,0 +1,25 @@ +import { createElement } from 'lwc'; +import ConfigExport from 'c/configExport'; + +describe('c-config-export', () => { + afterEach(() => { + // The jsdom instance is shared across test cases in a single file so reset the DOM + while (document.body.firstChild) { + document.body.removeChild(document.body.firstChild); + } + }); + + it('TODO: test case generated by CLI command, please fill in test logic', () => { + // Arrange + const element = createElement('c-config-export', { + is: ConfigExport + }); + + // Act + document.body.appendChild(element); + + // Assert + // const div = element.shadowRoot.querySelector('div'); + expect(1).toBe(1); + }); +}); \ No newline at end of file diff --git a/src/force-app/main/default/lwc/configExport/configExport.html b/src/force-app/main/default/lwc/configExport/configExport.html new file mode 100644 index 000000000..cf3f5e7a8 --- /dev/null +++ b/src/force-app/main/default/lwc/configExport/configExport.html @@ -0,0 +1,26 @@ + \ No newline at end of file diff --git a/src/force-app/main/default/lwc/configExport/configExport.js b/src/force-app/main/default/lwc/configExport/configExport.js new file mode 100644 index 000000000..17e392db6 --- /dev/null +++ b/src/force-app/main/default/lwc/configExport/configExport.js @@ -0,0 +1,58 @@ +import { LightningElement, api, track } from 'lwc'; +//import { getRecord} from 'lightning/uiRecordApi'; +import exportConfigData from '@salesforce/apex/ConfigBuilderExport.exportConfigData'; + +export default class TextFileLauncher extends LightningElement { + + @api recordId; + @api isLoaded = false; + text = ''; + @track filterCritera = ''; + + connectedCallback() { + //this.classList.add('new-class'); + } + + handleFilterChange(event){ + + this.filterCritera = event.target.value; + + } + + handleClick() { + console.log(':::,',this.filterCritera); + this.isLoaded = !this.isLoaded; + exportConfigData({ filterString: this.filterCritera }) + .then(result => { + let config = result; + this.text = `data:text/plain,${encodeURIComponent(`${config}`)}`; + + const downloadContainer = this.template.querySelector('.slds-m-left_x-small .downloadArea'); + const downloadUrl = this.text; + const fileName = 'Config file.txt'; + + let a = document.createElement('a'); + a.href = downloadUrl; + a.target = '_parent'; + // Use a.download if available, it prevents plugins from opening. + a.download = fileName; + // Add a to the doc for click to work. + if (downloadContainer) { + downloadContainer.appendChild(a); + } + if (a.click) { + a.click(); // The click method is supported by most browsers. + this.isLoaded = !this.isLoaded; + } + // Delete the temporary link. + downloadContainer.removeChild(a); + + }) + .catch(error => { + this.error = error; + }); + + } + + +} \ No newline at end of file diff --git a/src/force-app/main/default/lwc/configExport/configExport.js-meta.xml b/src/force-app/main/default/lwc/configExport/configExport.js-meta.xml new file mode 100644 index 000000000..c33aceb74 --- /dev/null +++ b/src/force-app/main/default/lwc/configExport/configExport.js-meta.xml @@ -0,0 +1,9 @@ + + + 51.0 + true + + lightning__RecordPage + lightning__HomePage + + \ No newline at end of file diff --git a/src/force-app/main/default/lwc/fileupload/fileupload.css b/src/force-app/main/default/lwc/fileupload/fileupload.css new file mode 100644 index 000000000..a1e1045df --- /dev/null +++ b/src/force-app/main/default/lwc/fileupload/fileupload.css @@ -0,0 +1,4 @@ +.customRadioCls .slds-form-element__control .slds-radio +{ + display: inline-block !important; +} \ No newline at end of file diff --git a/src/force-app/main/default/lwc/fileupload/fileupload.html b/src/force-app/main/default/lwc/fileupload/fileupload.html new file mode 100644 index 000000000..aa546b45b --- /dev/null +++ b/src/force-app/main/default/lwc/fileupload/fileupload.html @@ -0,0 +1,54 @@ + \ No newline at end of file diff --git a/src/force-app/main/default/lwc/fileupload/fileupload.js b/src/force-app/main/default/lwc/fileupload/fileupload.js new file mode 100644 index 000000000..cbc3fe5a0 --- /dev/null +++ b/src/force-app/main/default/lwc/fileupload/fileupload.js @@ -0,0 +1,61 @@ +import { LightningElement,track,api,wire } from 'lwc'; +import { ShowToastEvent } from 'lightning/platformShowToastEvent'; +import importConfigData from '@salesforce/apex/ConfigBuilderImport.importConfigData' + +export default class Fileupload extends LightningElement { + RadioValue = 'Import File'; + + get options() { + return [ + { label: 'Import File', value: 'Import File' }, + { label: 'Export File', value: 'Export File' }, + ]; + } + @track importFileFieldValue = true; + @track exportFileFieldValue = false; + + /* @api recordId;*/ + fileData; + openfileUpload(event) { + const file = event.target.files[0]; + var reader = new FileReader(); + reader.onload = () => { + var base64 = reader.result.split(',')[1] + this.fileData = { + 'filename': file.name, + 'base64': base64, + } + console.log(base64) + } + reader.readAsDataURL(file) + + } + + radioOptionOnChnage(){ + if (RadioValue == 'Import File'){ + this.importFileFieldValue = true; + this.exportFileFieldValue = false; + }else{ + this.importFileFieldValue = false; + this.exportFileFieldValue = true; + } + } + + handleClick(){ + //const {base64, filename, recordId} = this.fileData + + importConfigData({ base64:this.fileData.base64 }).then(result=>{ + this.fileData = null + //let title = 'uploaded successfully!!' + this.toast(result) + }) + } + + toast(title){ + const toastEvent = new ShowToastEvent({ + title, + variant:"success" + }) + this.dispatchEvent(toastEvent) + } +} \ No newline at end of file diff --git a/src/force-app/main/default/lwc/fileupload/fileupload.js-meta.xml b/src/force-app/main/default/lwc/fileupload/fileupload.js-meta.xml new file mode 100644 index 000000000..316cabe5b --- /dev/null +++ b/src/force-app/main/default/lwc/fileupload/fileupload.js-meta.xml @@ -0,0 +1,9 @@ + + + 55.0 + true + + lightning__RecordPage + lightning__HomePage + + \ No newline at end of file