diff --git a/CHANGES.md b/CHANGES.md index ab814abb..f054646e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,7 @@ ## Version 1.21 +- Undelete records from data import page [feature 193](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/193) - Create new records from SObject tab ("New" button) [feature 226](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/226) - Enhance shortcut search to include any part of the shortcut title, not only the beginning (contribution by [Joshua Yarmak](https://github.com/toly11)) - Org instance in not correct with after Hyperforce migration: store org instance in sessionStorage to retrieve it once per session [issue 167](https://github.com/tprouvot/Salesforce-Inspector-reloaded/issues/167) diff --git a/addon/data-import-test.js b/addon/data-import-test.js index 2ee27bed..64f78d0e 100644 --- a/addon/data-import-test.js +++ b/addon/data-import-test.js @@ -1,10 +1,10 @@ -import { sfConn } from "./inspector.js"; +import {sfConn} from "./inspector.js"; export async function dataImportTest(test) { console.log("TEST data-import"); - let { assertEquals, assertNotEquals, assert, loadPage, anonApex } = test; + let {assertEquals, assertNotEquals, assert, loadPage, anonApex} = test; - let { model } = await loadPage("data-import.html"); + let {model} = await loadPage("data-import.html"); let vm = model; function waitForSpinner() { @@ -60,18 +60,18 @@ export async function dataImportTest(test) { vm.didUpdate(); assertEquals(null, vm.confirmPopup); assertEquals(0, vm.activeBatches); - assertEquals({ Queued: 0, Processing: 0, Succeeded: 0, Failed: 0 }, vm.importCounts()); + assertEquals({Queued: 0, Processing: 0, Succeeded: 0, Failed: 0}, vm.importCounts()); assertEquals(null, vm.importTableResult); vm.setData('"Name","Checkbox__c","Number__c","Lookup__r:Inspector_Test__c:Name"\r\n"test3","false","300.03",""\r\ntest4,false,400.04,test1\r\ntest5,true,500.05,\r\n"test6","true","600.06","notfound"'); - assertEquals({ Queued: 4, Processing: 0, Succeeded: 0, Failed: 0 }, vm.importCounts()); + assertEquals({Queued: 4, Processing: 0, Succeeded: 0, Failed: 0}, vm.importCounts()); assertEquals([["Name", "Checkbox__c", "Number__c", "Lookup__r:Inspector_Test__c:Name"], ["test3", "false", "300.03", ""], ["test4", "false", "400.04", "test1"], ["test5", "true", "500.05", ""], ["test6", "true", "600.06", "notfound"]], vm.importTableResult.table); assertEquals(false, vm.importTableResult.isTooling); assertEquals([true, true, true, true, true], vm.importTableResult.rowVisibilities); assertEquals([true, true, true, true], vm.importTableResult.colVisibilities); vm.doImport(); - assertEquals({ text: "4 records will be imported.", action: undefined }, vm.confirmPopup); + assertEquals({text: "4 records will be created.", action: undefined}, vm.confirmPopup); assertEquals(0, vm.activeBatches); - assertEquals({ Queued: 4, Processing: 0, Succeeded: 0, Failed: 0 }, vm.importCounts()); + assertEquals({Queued: 4, Processing: 0, Succeeded: 0, Failed: 0}, vm.importCounts()); assertEquals([["Name", "Checkbox__c", "Number__c", "Lookup__r:Inspector_Test__c:Name"], ["test3", "false", "300.03", ""], ["test4", "false", "400.04", "test1"], ["test5", "true", "500.05", ""], ["test6", "true", "600.06", "notfound"]], vm.importTableResult.table); assertEquals(false, vm.importTableResult.isTooling); assertEquals([true, true, true, true, true], vm.importTableResult.rowVisibilities); @@ -79,15 +79,15 @@ export async function dataImportTest(test) { vm.confirmPopupNo(); assertEquals(null, vm.confirmPopup); assertEquals(0, vm.activeBatches); - assertEquals({ Queued: 4, Processing: 0, Succeeded: 0, Failed: 0 }, vm.importCounts()); + assertEquals({Queued: 4, Processing: 0, Succeeded: 0, Failed: 0}, vm.importCounts()); assertEquals([["Name", "Checkbox__c", "Number__c", "Lookup__r:Inspector_Test__c:Name"], ["test3", "false", "300.03", ""], ["test4", "false", "400.04", "test1"], ["test5", "true", "500.05", ""], ["test6", "true", "600.06", "notfound"]], vm.importTableResult.table); assertEquals(false, vm.importTableResult.isTooling); assertEquals([true, true, true, true, true], vm.importTableResult.rowVisibilities); assertEquals([true, true, true, true], vm.importTableResult.colVisibilities); vm.doImport(); - assertEquals({ text: "4 records will be imported.", action: undefined }, vm.confirmPopup); + assertEquals({text: "4 records will be created.", action: undefined}, vm.confirmPopup); assertEquals(0, vm.activeBatches); - assertEquals({ Queued: 4, Processing: 0, Succeeded: 0, Failed: 0 }, vm.importCounts()); + assertEquals({Queued: 4, Processing: 0, Succeeded: 0, Failed: 0}, vm.importCounts()); assertEquals([["Name", "Checkbox__c", "Number__c", "Lookup__r:Inspector_Test__c:Name"], ["test3", "false", "300.03", ""], ["test4", "false", "400.04", "test1"], ["test5", "true", "500.05", ""], ["test6", "true", "600.06", "notfound"]], vm.importTableResult.table); assertEquals(false, vm.importTableResult.isTooling); assertEquals([true, true, true, true, true], vm.importTableResult.rowVisibilities); @@ -95,7 +95,7 @@ export async function dataImportTest(test) { vm.confirmPopupYes(); assertEquals(null, vm.confirmPopup); assertEquals(1, vm.activeBatches); - assertEquals({ Queued: 0, Processing: 4, Succeeded: 0, Failed: 0 }, vm.importCounts()); + assertEquals({Queued: 0, Processing: 4, Succeeded: 0, Failed: 0}, vm.importCounts()); assertEquals([["Name", "Checkbox__c", "Number__c", "Lookup__r:Inspector_Test__c:Name", "__Status", "__Id", "__Action", "__Errors"], ["test3", "false", "300.03", "", "Processing", "", "", ""], ["test4", "false", "400.04", "test1", "Processing", "", "", ""], ["test5", "true", "500.05", "", "Processing", "", "", ""], ["test6", "true", "600.06", "notfound", "Processing", "", "", ""]], vm.importTableResult.table); assertEquals(false, vm.importTableResult.isTooling); assertEquals([true, true, true, true, true], vm.importTableResult.rowVisibilities); @@ -103,15 +103,15 @@ export async function dataImportTest(test) { await waitForSpinner(); assertEquals(null, vm.confirmPopup); assertEquals(0, vm.activeBatches); - assertEquals({ Queued: 0, Processing: 0, Succeeded: 3, Failed: 1 }, vm.importCounts()); + assertEquals({Queued: 0, Processing: 0, Succeeded: 3, Failed: 1}, vm.importCounts()); assertEquals([["Name", "Checkbox__c", "Number__c", "Lookup__r:Inspector_Test__c:Name", "__Status", "__Id", "__Action", "__Errors"], ["test3", "false", "300.03", "", "Succeeded", "--id--", "Inserted", ""], ["test4", "false", "400.04", "test1", "Succeeded", "--id--", "Inserted", ""], ["test5", "true", "500.05", "", "Succeeded", "--id--", "Inserted", ""], ["test6", "true", "600.06", "notfound", "Failed", "", "", "INVALID_FIELD: Foreign key external ID: notfound not found for field Name in entity Inspector_Test__c []"]], vm.importTableResult.table.map(row => row.map(cell => /^[a-zA-Z0-9]{18}$/.test(cell) ? "--id--" : cell))); records = getRecords(await sfConn.rest("/services/data/v35.0/query/?q=" + encodeURIComponent("select Name, Checkbox__c, Number__c, Lookup__r.Name from Inspector_Test__c order by Name"))); assertEquals([ - { Name: "test1", Checkbox__c: false, Number__c: 100.01, Lookup__r: null }, - { Name: "test2", Checkbox__c: true, Number__c: 200.02, Lookup__r: null }, - { Name: "test3", Checkbox__c: false, Number__c: 300.03, Lookup__r: null }, - { Name: "test4", Checkbox__c: false, Number__c: 400.04, Lookup__r: { Name: "test1" } }, - { Name: "test5", Checkbox__c: true, Number__c: 500.05, Lookup__r: null } + {Name: "test1", Checkbox__c: false, Number__c: 100.01, Lookup__r: null}, + {Name: "test2", Checkbox__c: true, Number__c: 200.02, Lookup__r: null}, + {Name: "test3", Checkbox__c: false, Number__c: 300.03, Lookup__r: null}, + {Name: "test4", Checkbox__c: false, Number__c: 400.04, Lookup__r: {Name: "test1"}}, + {Name: "test5", Checkbox__c: true, Number__c: 500.05, Lookup__r: null} ], records); // Create excel @@ -121,23 +121,23 @@ export async function dataImportTest(test) { vm.didUpdate(); vm.setData('"Name"\t"Number__c"\r\ntest6\t600.06\r\n"test7"\t"700.07"\r\n'); vm.doImport(); - assertEquals({ text: "2 records will be imported.", action: undefined }, vm.confirmPopup); + assertEquals({text: "2 records will be created.", action: undefined}, vm.confirmPopup); vm.confirmPopupYes(); assertEquals(null, vm.confirmPopup); - assertEquals({ Queued: 0, Processing: 2, Succeeded: 0, Failed: 0 }, vm.importCounts()); + assertEquals({Queued: 0, Processing: 2, Succeeded: 0, Failed: 0}, vm.importCounts()); assertEquals([["Name", "Number__c", "__Status", "__Id", "__Action", "__Errors"], ["test6", "600.06", "Processing", "", "", ""], ["test7", "700.07", "Processing", "", "", ""]], vm.importTableResult.table); await waitForSpinner(); - assertEquals({ Queued: 0, Processing: 0, Succeeded: 2, Failed: 0 }, vm.importCounts()); + assertEquals({Queued: 0, Processing: 0, Succeeded: 2, Failed: 0}, vm.importCounts()); assertEquals([["Name", "Number__c", "__Status", "__Id", "__Action", "__Errors"], ["test6", "600.06", "Succeeded", "--id--", "Inserted", ""], ["test7", "700.07", "Succeeded", "--id--", "Inserted", ""]], vm.importTableResult.table.map(row => row.map(cell => /^[a-zA-Z0-9]{18}$/.test(cell) ? "--id--" : cell))); records = getRecords(await sfConn.rest("/services/data/v35.0/query/?q=" + encodeURIComponent("select Name, Checkbox__c, Number__c, Lookup__r.Name from Inspector_Test__c order by Name"))); assertEquals([ - { Name: "test1", Checkbox__c: false, Number__c: 100.01, Lookup__r: null }, - { Name: "test2", Checkbox__c: true, Number__c: 200.02, Lookup__r: null }, - { Name: "test3", Checkbox__c: false, Number__c: 300.03, Lookup__r: null }, - { Name: "test4", Checkbox__c: false, Number__c: 400.04, Lookup__r: { Name: "test1" } }, - { Name: "test5", Checkbox__c: true, Number__c: 500.05, Lookup__r: null }, - { Name: "test6", Checkbox__c: false, Number__c: 600.06, Lookup__r: null }, - { Name: "test7", Checkbox__c: false, Number__c: 700.07, Lookup__r: null } + {Name: "test1", Checkbox__c: false, Number__c: 100.01, Lookup__r: null}, + {Name: "test2", Checkbox__c: true, Number__c: 200.02, Lookup__r: null}, + {Name: "test3", Checkbox__c: false, Number__c: 300.03, Lookup__r: null}, + {Name: "test4", Checkbox__c: false, Number__c: 400.04, Lookup__r: {Name: "test1"}}, + {Name: "test5", Checkbox__c: true, Number__c: 500.05, Lookup__r: null}, + {Name: "test6", Checkbox__c: false, Number__c: 600.06, Lookup__r: null}, + {Name: "test7", Checkbox__c: false, Number__c: 700.07, Lookup__r: null} ], records); // Update csv @@ -148,21 +148,21 @@ export async function dataImportTest(test) { vm.didUpdate(); vm.setData("Id,Name,Number__c\r\n" + records[4].Id + ",test5update,500.50\r\n" + records[5].Id + ",test6update,600.60\r\n"); vm.doImport(); - assertEquals({ text: "2 records will be imported.", action: undefined }, vm.confirmPopup); + assertEquals({text: "2 records will be updated.", action: undefined}, vm.confirmPopup); vm.confirmPopupYes(); assertEquals([["Id", "Name", "Number__c", "__Status", "__Id", "__Action", "__Errors"], [records[4].Id, "test5update", "500.50", "Processing", "", "", ""], [records[5].Id, "test6update", "600.60", "Processing", "", "", ""]], vm.importTableResult.table); await waitForSpinner(); - assertEquals({ Queued: 0, Processing: 0, Succeeded: 2, Failed: 0 }, vm.importCounts()); + assertEquals({Queued: 0, Processing: 0, Succeeded: 2, Failed: 0}, vm.importCounts()); assertEquals([["Id", "Name", "Number__c", "__Status", "__Id", "__Action", "__Errors"], [records[4].Id, "test5update", "500.50", "Succeeded", records[4].Id, "Updated", ""], [records[5].Id, "test6update", "600.60", "Succeeded", records[5].Id, "Updated", ""]], vm.importTableResult.table); records = getRecords(await sfConn.rest("/services/data/v35.0/query/?q=" + encodeURIComponent("select Name, Checkbox__c, Number__c, Lookup__r.Name from Inspector_Test__c order by Name"))); assertEquals([ - { Name: "test1", Checkbox__c: false, Number__c: 100.01, Lookup__r: null }, - { Name: "test2", Checkbox__c: true, Number__c: 200.02, Lookup__r: null }, - { Name: "test3", Checkbox__c: false, Number__c: 300.03, Lookup__r: null }, - { Name: "test4", Checkbox__c: false, Number__c: 400.04, Lookup__r: { Name: "test1" } }, - { Name: "test5update", Checkbox__c: true, Number__c: 500.50, Lookup__r: null }, - { Name: "test6update", Checkbox__c: false, Number__c: 600.60, Lookup__r: null }, - { Name: "test7", Checkbox__c: false, Number__c: 700.07, Lookup__r: null } + {Name: "test1", Checkbox__c: false, Number__c: 100.01, Lookup__r: null}, + {Name: "test2", Checkbox__c: true, Number__c: 200.02, Lookup__r: null}, + {Name: "test3", Checkbox__c: false, Number__c: 300.03, Lookup__r: null}, + {Name: "test4", Checkbox__c: false, Number__c: 400.04, Lookup__r: {Name: "test1"}}, + {Name: "test5update", Checkbox__c: true, Number__c: 500.50, Lookup__r: null}, + {Name: "test6update", Checkbox__c: false, Number__c: 600.60, Lookup__r: null}, + {Name: "test7", Checkbox__c: false, Number__c: 700.07, Lookup__r: null} ], records); // Delete csv (with ignored column and status column) @@ -173,23 +173,23 @@ export async function dataImportTest(test) { vm.importActionSelected = true; vm.didUpdate(); vm.setData("Id,_foo*,__Status\r\n" + records[5].Id + ",foo,Queued\r\n" + records[6].Id + ",foo,Succeeded"); - assertEquals({ Queued: 1, Processing: 0, Succeeded: 1, Failed: 0 }, vm.importCounts()); + assertEquals({Queued: 1, Processing: 0, Succeeded: 1, Failed: 0}, vm.importCounts()); vm.doImport(); - assertEquals({ text: "1 records will be imported. 1 records will be skipped because they have __Status Succeeded or Failed.", action: undefined }, vm.confirmPopup); + assertEquals({text: "1 records will be deleted. 1 records will be skipped because they have __Status Succeeded or Failed.", action: undefined}, vm.confirmPopup); vm.confirmPopupYes(); - assertEquals({ Queued: 0, Processing: 1, Succeeded: 1, Failed: 0 }, vm.importCounts()); + assertEquals({Queued: 0, Processing: 1, Succeeded: 1, Failed: 0}, vm.importCounts()); assertEquals([["Id", "_foo*", "__Status", "__Id", "__Action", "__Errors"], [records[5].Id, "foo", "Processing", "", "", ""], [records[6].Id, "foo", "Succeeded", "", "", ""]], vm.importTableResult.table); await waitForSpinner(); - assertEquals({ Queued: 0, Processing: 0, Succeeded: 2, Failed: 0 }, vm.importCounts()); + assertEquals({Queued: 0, Processing: 0, Succeeded: 2, Failed: 0}, vm.importCounts()); assertEquals([["Id", "_foo*", "__Status", "__Id", "__Action", "__Errors"], [records[5].Id, "foo", "Succeeded", records[5].Id, "Deleted", ""], [records[6].Id, "foo", "Succeeded", "", "", ""]], vm.importTableResult.table); records = getRecords(await sfConn.rest("/services/data/v35.0/query/?q=" + encodeURIComponent("select Name, Checkbox__c, Number__c, Lookup__r.Name from Inspector_Test__c order by Name"))); assertEquals([ - { Name: "test1", Checkbox__c: false, Number__c: 100.01, Lookup__r: null }, - { Name: "test2", Checkbox__c: true, Number__c: 200.02, Lookup__r: null }, - { Name: "test3", Checkbox__c: false, Number__c: 300.03, Lookup__r: null }, - { Name: "test4", Checkbox__c: false, Number__c: 400.04, Lookup__r: { Name: "test1" } }, - { Name: "test5update", Checkbox__c: true, Number__c: 500.50, Lookup__r: null }, - { Name: "test7", Checkbox__c: false, Number__c: 700.07, Lookup__r: null } + {Name: "test1", Checkbox__c: false, Number__c: 100.01, Lookup__r: null}, + {Name: "test2", Checkbox__c: true, Number__c: 200.02, Lookup__r: null}, + {Name: "test3", Checkbox__c: false, Number__c: 300.03, Lookup__r: null}, + {Name: "test4", Checkbox__c: false, Number__c: 400.04, Lookup__r: {Name: "test1"}}, + {Name: "test5update", Checkbox__c: true, Number__c: 500.50, Lookup__r: null}, + {Name: "test7", Checkbox__c: false, Number__c: 700.07, Lookup__r: null} ], records); // Upsert csv @@ -201,21 +201,21 @@ export async function dataImportTest(test) { vm.didUpdate(); vm.setData("Name,Number__c\r\ntest2,222\r\ntest6,666\r\n"); vm.doImport(); - assertEquals({ text: "2 records will be imported.", action: undefined }, vm.confirmPopup); + assertEquals({text: "2 records will be upserted.", action: undefined}, vm.confirmPopup); vm.confirmPopupYes(); assertEquals([["Name", "Number__c", "__Status", "__Id", "__Action", "__Errors"], ["test2", "222", "Processing", "", "", ""], ["test6", "666", "Processing", "", "", ""]], vm.importTableResult.table); await waitForSpinner(); - assertEquals({ Queued: 0, Processing: 0, Succeeded: 2, Failed: 0 }, vm.importCounts()); + assertEquals({Queued: 0, Processing: 0, Succeeded: 2, Failed: 0}, vm.importCounts()); assertEquals([["Name", "Number__c", "__Status", "__Id", "__Action", "__Errors"], ["test2", "222", "Succeeded", "--id--", "Updated", ""], ["test6", "666", "Succeeded", "--id--", "Inserted", ""]], vm.importTableResult.table.map(row => row.map(cell => /^[a-zA-Z0-9]{18}$/.test(cell) ? "--id--" : cell))); records = getRecords(await sfConn.rest("/services/data/v35.0/query/?q=" + encodeURIComponent("select Name, Checkbox__c, Number__c, Lookup__r.Name from Inspector_Test__c order by Name"))); assertEquals([ - { Name: "test1", Checkbox__c: false, Number__c: 100.01, Lookup__r: null }, - { Name: "test2", Checkbox__c: true, Number__c: 222, Lookup__r: null }, - { Name: "test3", Checkbox__c: false, Number__c: 300.03, Lookup__r: null }, - { Name: "test4", Checkbox__c: false, Number__c: 400.04, Lookup__r: { Name: "test1" } }, - { Name: "test5update", Checkbox__c: true, Number__c: 500.50, Lookup__r: null }, - { Name: "test6", Checkbox__c: false, Number__c: 666, Lookup__r: null }, - { Name: "test7", Checkbox__c: false, Number__c: 700.07, Lookup__r: null } + {Name: "test1", Checkbox__c: false, Number__c: 100.01, Lookup__r: null}, + {Name: "test2", Checkbox__c: true, Number__c: 222, Lookup__r: null}, + {Name: "test3", Checkbox__c: false, Number__c: 300.03, Lookup__r: null}, + {Name: "test4", Checkbox__c: false, Number__c: 400.04, Lookup__r: {Name: "test1"}}, + {Name: "test5update", Checkbox__c: true, Number__c: 500.50, Lookup__r: null}, + {Name: "test6", Checkbox__c: false, Number__c: 666, Lookup__r: null}, + {Name: "test7", Checkbox__c: false, Number__c: 700.07, Lookup__r: null} ], records); // Save import options @@ -237,7 +237,7 @@ export async function dataImportTest(test) { assertEquals("Inspector_Test__c", vm.importType); assertEquals("200", vm.batchSize); assertEquals("6", vm.batchConcurrency); - assertEquals({ Queued: 2, Processing: 0, Succeeded: 0, Failed: 0 }, vm.importCounts()); + assertEquals({Queued: 2, Processing: 0, Succeeded: 0, Failed: 0}, vm.importCounts()); assertEquals([["Name", "Number__c"], ["test", "100"], ["test", "200"]], vm.importTableResult.table); // Create multiple batches @@ -254,17 +254,17 @@ export async function dataImportTest(test) { vm.didUpdate(); vm.setData("Name\r\ntest10\r\ntest11\r\ntest12\r\ntest13\r\ntest14\r\ntest15\r\ntest16\r\ntest17\r\ntest18\r\ntest19\r\ntest20\r\ntest21\r\ntest22\r\ntest23\r\ntest24\r\ntest25"); vm.doImport(); - assertEquals({ text: "16 records will be imported.", action: undefined }, vm.confirmPopup); + assertEquals({text: "16 records will be created.", action: undefined}, vm.confirmPopup); vm.confirmPopupYes(); assertEquals(null, vm.confirmPopup); - assertEquals({ Queued: 13, Processing: 3, Succeeded: 0, Failed: 0 }, vm.importCounts()); + assertEquals({Queued: 13, Processing: 3, Succeeded: 0, Failed: 0}, vm.importCounts()); assertEquals([["Name", "__Status", "__Id", "__Action", "__Errors"], ["test10", "Processing", "", "", ""], ["test11", "Processing", "", "", ""], ["test12", "Processing", "", "", ""], ["test13", "Queued", "", "", ""], ["test14", "Queued", "", "", ""], ["test15", "Queued", "", "", ""], ["test16", "Queued", "", "", ""], ["test17", "Queued", "", "", ""], ["test18", "Queued", "", "", ""], ["test19", "Queued", "", "", ""], ["test20", "Queued", "", "", ""], ["test21", "Queued", "", "", ""], ["test22", "Queued", "", "", ""], ["test23", "Queued", "", "", ""], ["test24", "Queued", "", "", ""], ["test25", "Queued", "", "", ""]], vm.importTableResult.table); await waitForSpinner(); - assertEquals({ Queued: 0, Processing: 0, Succeeded: 16, Failed: 0 }, vm.importCounts()); + assertEquals({Queued: 0, Processing: 0, Succeeded: 16, Failed: 0}, vm.importCounts()); assertEquals([["Name", "__Status", "__Id", "__Action", "__Errors"], ["test10", "Succeeded", "--id--", "Inserted", ""], ["test11", "Succeeded", "--id--", "Inserted", ""], ["test12", "Succeeded", "--id--", "Inserted", ""], ["test13", "Succeeded", "--id--", "Inserted", ""], ["test14", "Succeeded", "--id--", "Inserted", ""], ["test15", "Succeeded", "--id--", "Inserted", ""], ["test16", "Succeeded", "--id--", "Inserted", ""], ["test17", "Succeeded", "--id--", "Inserted", ""], ["test18", "Succeeded", "--id--", "Inserted", ""], ["test19", "Succeeded", "--id--", "Inserted", ""], ["test20", "Succeeded", "--id--", "Inserted", ""], ["test21", "Succeeded", "--id--", "Inserted", ""], ["test22", "Succeeded", "--id--", "Inserted", ""], ["test23", "Succeeded", "--id--", "Inserted", ""], ["test24", "Succeeded", "--id--", "Inserted", ""], ["test25", "Succeeded", "--id--", "Inserted", ""]], vm.importTableResult.table.map(row => row.map(cell => /^[a-zA-Z0-9]{18}$/.test(cell) ? "--id--" : cell))); records = getRecords(await sfConn.rest("/services/data/v35.0/query/?q=" + encodeURIComponent("select Name from Inspector_Test__c order by Name"))); assertEquals([ - { Name: "test10" }, { Name: "test11" }, { Name: "test12" }, { Name: "test13" }, { Name: "test14" }, { Name: "test15" }, { Name: "test16" }, { Name: "test17" }, { Name: "test18" }, { Name: "test19" }, { Name: "test20" }, { Name: "test21" }, { Name: "test22" }, { Name: "test23" }, { Name: "test24" }, { Name: "test25" } + {Name: "test10"}, {Name: "test11"}, {Name: "test12"}, {Name: "test13"}, {Name: "test14"}, {Name: "test15"}, {Name: "test16"}, {Name: "test17"}, {Name: "test18"}, {Name: "test19"}, {Name: "test20"}, {Name: "test21"}, {Name: "test22"}, {Name: "test23"}, {Name: "test24"}, {Name: "test25"} ], records); // Stop import @@ -281,25 +281,25 @@ export async function dataImportTest(test) { vm.didUpdate(); vm.setData("Name\r\ntest10\r\ntest11\r\ntest12\r\ntest13\r\ntest14\r\ntest15\r\ntest16\r\ntest17\r\ntest18\r\ntest19\r\ntest20\r\ntest21\r\ntest22\r\ntest23\r\ntest24\r\ntest25"); vm.doImport(); - assertEquals({ text: "16 records will be imported.", action: undefined }, vm.confirmPopup); + assertEquals({text: "16 records will be created.", action: undefined}, vm.confirmPopup); vm.confirmPopupYes(); assertEquals(null, vm.confirmPopup); assertNotEquals(0, vm.activeBatches); - assertEquals({ Queued: 13, Processing: 3, Succeeded: 0, Failed: 0 }, vm.importCounts()); + assertEquals({Queued: 13, Processing: 3, Succeeded: 0, Failed: 0}, vm.importCounts()); assertEquals([["Name", "__Status", "__Id", "__Action", "__Errors"], ["test10", "Processing", "", "", ""], ["test11", "Processing", "", "", ""], ["test12", "Processing", "", "", ""], ["test13", "Queued", "", "", ""], ["test14", "Queued", "", "", ""], ["test15", "Queued", "", "", ""], ["test16", "Queued", "", "", ""], ["test17", "Queued", "", "", ""], ["test18", "Queued", "", "", ""], ["test19", "Queued", "", "", ""], ["test20", "Queued", "", "", ""], ["test21", "Queued", "", "", ""], ["test22", "Queued", "", "", ""], ["test23", "Queued", "", "", ""], ["test24", "Queued", "", "", ""], ["test25", "Queued", "", "", ""]], vm.importTableResult.table); vm.isProcessingQueue = !vm.isProcessingQueue; vm.executeBatch(); vm.didUpdate(); assertNotEquals(0, vm.activeBatches); - assertEquals({ Queued: 13, Processing: 3, Succeeded: 0, Failed: 0 }, vm.importCounts()); + assertEquals({Queued: 13, Processing: 3, Succeeded: 0, Failed: 0}, vm.importCounts()); assertEquals([["Name", "__Status", "__Id", "__Action", "__Errors"], ["test10", "Processing", "", "", ""], ["test11", "Processing", "", "", ""], ["test12", "Processing", "", "", ""], ["test13", "Queued", "", "", ""], ["test14", "Queued", "", "", ""], ["test15", "Queued", "", "", ""], ["test16", "Queued", "", "", ""], ["test17", "Queued", "", "", ""], ["test18", "Queued", "", "", ""], ["test19", "Queued", "", "", ""], ["test20", "Queued", "", "", ""], ["test21", "Queued", "", "", ""], ["test22", "Queued", "", "", ""], ["test23", "Queued", "", "", ""], ["test24", "Queued", "", "", ""], ["test25", "Queued", "", "", ""]], vm.importTableResult.table); await waitForSpinner(); assertEquals(0, vm.activeBatches); - assertEquals({ Queued: 13, Processing: 0, Succeeded: 3, Failed: 0 }, vm.importCounts()); + assertEquals({Queued: 13, Processing: 0, Succeeded: 3, Failed: 0}, vm.importCounts()); assertEquals([["Name", "__Status", "__Id", "__Action", "__Errors"], ["test10", "Succeeded", "--id--", "Inserted", ""], ["test11", "Succeeded", "--id--", "Inserted", ""], ["test12", "Succeeded", "--id--", "Inserted", ""], ["test13", "Queued", "", "", ""], ["test14", "Queued", "", "", ""], ["test15", "Queued", "", "", ""], ["test16", "Queued", "", "", ""], ["test17", "Queued", "", "", ""], ["test18", "Queued", "", "", ""], ["test19", "Queued", "", "", ""], ["test20", "Queued", "", "", ""], ["test21", "Queued", "", "", ""], ["test22", "Queued", "", "", ""], ["test23", "Queued", "", "", ""], ["test24", "Queued", "", "", ""], ["test25", "Queued", "", "", ""]], vm.importTableResult.table.map(row => row.map(cell => /^[a-zA-Z0-9]{18}$/.test(cell) ? "--id--" : cell))); records = getRecords(await sfConn.rest("/services/data/v35.0/query/?q=" + encodeURIComponent("select Name from Inspector_Test__c order by Name"))); assertEquals([ - { Name: "test10" }, { Name: "test11" }, { Name: "test12" } + {Name: "test10"}, {Name: "test11"}, {Name: "test12"} ], records); // Errors (local validations) @@ -310,38 +310,38 @@ export async function dataImportTest(test) { vm.setData(""); assertEquals(true, vm.invalidInput()); assertEquals("Error: no data", vm.dataError); - assertEquals({ Queued: 0, Processing: 0, Succeeded: 0, Failed: 0 }, vm.importCounts()); + assertEquals({Queued: 0, Processing: 0, Succeeded: 0, Failed: 0}, vm.importCounts()); vm.setData('"foo","bar"\r\n"baz","unclosed quote'); assertEquals(true, vm.invalidInput()); assertEquals("Error: Quote not closed", vm.dataError); - assertEquals({ Queued: 0, Processing: 0, Succeeded: 0, Failed: 0 }, vm.importCounts()); + assertEquals({Queued: 0, Processing: 0, Succeeded: 0, Failed: 0}, vm.importCounts()); vm.setData('"foo","bar"\r\n"foo","bar"text after quote'); assertEquals(true, vm.invalidInput()); assertEquals("Error: unexpected token 't'", vm.dataError); - assertEquals({ Queued: 0, Processing: 0, Succeeded: 0, Failed: 0 }, vm.importCounts()); + assertEquals({Queued: 0, Processing: 0, Succeeded: 0, Failed: 0}, vm.importCounts()); vm.setData("a,b\r\nc,d\r\ne"); assertEquals(true, vm.invalidInput()); assertEquals("Error: row 3 has 1 cells, expected 2", vm.dataError); - assertEquals({ Queued: 0, Processing: 0, Succeeded: 0, Failed: 0 }, vm.importCounts()); + assertEquals({Queued: 0, Processing: 0, Succeeded: 0, Failed: 0}, vm.importCounts()); vm.setData("Name"); assertEquals(true, vm.invalidInput()); assertEquals("Error: No records to import", vm.dataError); - assertEquals({ Queued: 0, Processing: 0, Succeeded: 0, Failed: 0 }, vm.importCounts()); + assertEquals({Queued: 0, Processing: 0, Succeeded: 0, Failed: 0}, vm.importCounts()); vm.setData("Na*me\r\ntest0"); assertEquals(true, vm.invalidInput()); assertEquals("", vm.dataError); assertEquals("Error: Invalid field name", vm.columns()[0].columnError()); - assertEquals({ Queued: 1, Processing: 0, Succeeded: 0, Failed: 0 }, vm.importCounts()); + assertEquals({Queued: 1, Processing: 0, Succeeded: 0, Failed: 0}, vm.importCounts()); vm.setData("a,b\r\nc,d"); assertEquals(false, vm.invalidInput()); assertEquals("", vm.dataError); - assertEquals({ Queued: 1, Processing: 0, Succeeded: 0, Failed: 0 }, vm.importCounts()); + assertEquals({Queued: 1, Processing: 0, Succeeded: 0, Failed: 0}, vm.importCounts()); // Errors (whole batch) await anonApex("delete [select Id from Inspector_Test__c];"); @@ -351,12 +351,12 @@ export async function dataImportTest(test) { vm.didUpdate(); vm.setData("Name,unknownfield\r\ntest2,222\r\ntest6,666\r\n"); vm.doImport(); - assertEquals({ text: "2 records will be imported.", action: undefined }, vm.confirmPopup); + assertEquals({text: "2 records will be created.", action: undefined}, vm.confirmPopup); vm.confirmPopupYes(); - assertEquals({ Queued: 0, Processing: 2, Succeeded: 0, Failed: 0 }, vm.importCounts()); + assertEquals({Queued: 0, Processing: 2, Succeeded: 0, Failed: 0}, vm.importCounts()); assertEquals([["Name", "unknownfield", "__Status", "__Id", "__Action", "__Errors"], ["test2", "222", "Processing", "", "", ""], ["test6", "666", "Processing", "", "", ""]], vm.importTableResult.table); await waitForSpinner(); - assertEquals({ Queued: 0, Processing: 0, Succeeded: 0, Failed: 2 }, vm.importCounts()); + assertEquals({Queued: 0, Processing: 0, Succeeded: 0, Failed: 2}, vm.importCounts()); assertEquals([["Name", "unknownfield", "__Status", "__Id", "__Action", "__Errors"], ["test2", "222", "Failed", "", "", "INVALID_FIELD: No such column 'unknownfield' on entity 'Inspector_Test__c'. If you are attempting to use a custom field, be sure to append the '__c' after the custom field name. Please reference your WSDL or the describe call for the appropriate names."], ["test6", "666", "Failed", "", "", "INVALID_FIELD: No such column 'unknownfield' on entity 'Inspector_Test__c'. If you are attempting to use a custom field, be sure to append the '__c' after the custom field name. Please reference your WSDL or the describe call for the appropriate names."]], vm.importTableResult.table); records = getRecords(await sfConn.rest("/services/data/v35.0/query/?q=" + encodeURIComponent("select Name, Checkbox__c, Number__c, Lookup__r.Name from Inspector_Test__c order by Name"))); assertEquals([], records); diff --git a/addon/data-import.js b/addon/data-import.js index 92b28c08..05e018d3 100644 --- a/addon/data-import.js +++ b/addon/data-import.js @@ -1,8 +1,8 @@ /* global React ReactDOM */ -import { sfConn, apiVersion } from "./inspector.js"; +import {sfConn, apiVersion} from "./inspector.js"; /* global initButton */ -import { csvParse } from "./csv-parse.js"; -import { DescribeInfo, copyToClipboard, initScrollTable } from "./data-load.js"; +import {csvParse} from "./csv-parse.js"; +import {DescribeInfo, copyToClipboard, initScrollTable} from "./data-load.js"; class Model { @@ -141,7 +141,7 @@ class Model { this.dataError = ""; let header = data.shift().map((c, index) => this.makeColumn(c, index)); this.updateResult(null); // Two updates, the first clears state from the scrolltable - this.updateResult({ header, data }); + this.updateResult({header, data}); //automatically select the SObject if possible let sobj = this.getSObject(data); @@ -168,14 +168,12 @@ class Model { let sobject = json[0]["attributes"]["type"]; if (sobject) { - csv = json.map(function (row) { - return fields.map(function (fieldName) { - let value = fieldName == "_" ? sobject : row[fieldName]; - if (typeof value == "boolean" || (value && typeof value !== "object")) { - return fieldName == "_" ? '"[' + sobject + ']"' : JSON.stringify(value) - } - }).join(separator) - }) + csv = json.map((row) => fields.map((fieldName) => { + let value = fieldName == "_" ? sobject : row[fieldName]; + if (typeof value == "boolean" || (value && typeof value !== "object")) { + return fieldName == "_" ? '"[' + sobject + ']"' : JSON.stringify(value); + } + }).join(separator)); fields = fields.map(str => '"' + str + '"'); csv.unshift(fields.join(separator)); csv = csv.join("\r\n"); @@ -220,7 +218,7 @@ class Model { } sobjectList() { - let { globalDescribe } = this.describeInfo.describeGlobal(this.useToolingApi); + let {globalDescribe} = this.describeInfo.describeGlobal(this.useToolingApi); if (!globalDescribe) { return []; } @@ -244,7 +242,7 @@ class Model { return Array.from(function* () { let importAction = self.importAction; - if (importAction == "delete") { + if (importAction == "delete" || importAction == "undelete") { yield "Id"; } else { let sobjectName = self.importType; @@ -387,7 +385,7 @@ class Model { confirmPopupYes() { this.confirmPopup = null; - let { header, data } = this.importData.importTable; + let {header, data} = this.importData.importTable; let statusColumnIndex = header.findIndex(c => c.columnValue.toLowerCase() == "__status"); if (statusColumnIndex == -1) { @@ -460,13 +458,31 @@ class Model { doImport() { let importedRecords = this.importData.counts.Queued + this.importData.counts.Processing; - let skippedRecords = this.importData.counts.Succeeded + this.importData.counts.Failed; + let skippedRecords = this.importAction != "undelete" ? this.importData.counts.Succeeded + this.importData.counts.Failed : 0; + let actionVerb = this.getActionVerb(this.importAction); this.confirmPopup = { - text: importedRecords + " records will be imported." + text: importedRecords + " records will be " + actionVerb + "." + (skippedRecords > 0 ? " " + skippedRecords + " records will be skipped because they have __Status Succeeded or Failed." : "") }; } + getActionVerb(importAction){ + switch (importAction) { + case "create": + return "created"; + case "update": + return "updated"; + case "upsert": + return "upserted"; + case "delete": + return "deleted"; + case "undelete": + return "undeleted"; + default: + return "imported"; + } + } + retryFailed() { if (!this.importData.importTable) { return; @@ -485,7 +501,7 @@ class Model { } updateResult(importTable) { - let counts = { Queued: 0, Processing: 0, Succeeded: 0, Failed: 0 }; + let counts = {Queued: 0, Processing: 0, Succeeded: 0, Failed: 0}; if (!importTable) { this.importData = { importTable: null, @@ -500,16 +516,16 @@ class Model { for (let cells of importTable.data) { let status = statusColumnIndex < 0 ? "Queued" : cells[statusColumnIndex].toLowerCase() == "queued" ? "Queued" - : cells[statusColumnIndex].toLowerCase() == "" ? "Queued" - : cells[statusColumnIndex].toLowerCase() == "processing" && !this.isWorking() ? "Queued" - : cells[statusColumnIndex].toLowerCase() == "processing" ? "Processing" - : cells[statusColumnIndex].toLowerCase() == "succeeded" ? "Succeeded" - : "Failed"; + : cells[statusColumnIndex].toLowerCase() == "" ? "Queued" + : cells[statusColumnIndex].toLowerCase() == "processing" && !this.isWorking() ? "Queued" + : cells[statusColumnIndex].toLowerCase() == "processing" ? "Processing" + : cells[statusColumnIndex].toLowerCase() == "succeeded" ? "Succeeded" + : "Failed"; counts[status]++; - taggedRows.push({ status, cells }); + taggedRows.push({status, cells}); } // Note: caller will call this.executeBatch() if needed - this.importData = { importTable, counts, taggedRows }; + this.importData = {importTable, counts, taggedRows}; this.updateImportTableResult(); } @@ -594,7 +610,7 @@ class Model { return; } - let { statusColumnIndex, resultIdColumnIndex, actionColumnIndex, errorColumnIndex, importAction, useToolingApi, sobjectType, idFieldName, inputIdColumnIndex } = this.importState; + let {statusColumnIndex, resultIdColumnIndex, actionColumnIndex, errorColumnIndex, importAction, useToolingApi, sobjectType, idFieldName, inputIdColumnIndex} = this.importState; let data = this.importData.importTable.data; let header = this.importData.importTable.header.map(c => c.columnValue); let batchRows = []; @@ -602,7 +618,7 @@ class Model { if (importAction == "upsert") { importArgs.externalIDFieldName = idFieldName; } - if (importAction == "delete") { + if (importAction == "delete" || importAction == "undelete") { importArgs.ID = []; } else { importArgs.sObjects = []; @@ -617,7 +633,7 @@ class Model { } batchRows.push(row); row[statusColumnIndex] = "Processing"; - if (importAction == "delete") { + if (importAction == "delete" || importAction == "undelete") { importArgs.ID.push(row[inputIdColumnIndex]); } else { let sobject = {}; @@ -681,10 +697,11 @@ class Model { row[statusColumnIndex] = "Succeeded"; row[actionColumnIndex] = importAction == "create" ? "Inserted" - : importAction == "update" ? "Updated" - : importAction == "upsert" ? (result.created == "true" ? "Inserted" : "Updated") - : importAction == "delete" ? "Deleted" - : "Unknown"; + : importAction == "update" ? "Updated" + : importAction == "upsert" ? (result.created == "true" ? "Inserted" : "Updated") + : importAction == "delete" ? "Deleted" + : importAction == "undelete" ? "Undeleted" + : "Unknown"; } else { row[statusColumnIndex] = "Failed"; row[actionColumnIndex] = ""; @@ -760,54 +777,57 @@ class App extends React.Component { this.unloadListener = null; } onUseToolingApiChange(e) { - let { model } = this.props; + let {model} = this.props; model.useToolingApi = e.target.checked; model.updateImportTableResult(); model.didUpdate(); } onImportActionChange(e) { - let { model } = this.props; + let {model} = this.props; model.importAction = e.target.value; model.importActionName = e.target.options[e.target.selectedIndex].text; model.importActionSelected = true; + if (model.importAction === "undelete"){ + this.onImportUndelete(model); + } model.didUpdate(); } onImportTypeChange(e) { - let { model } = this.props; + let {model} = this.props; model.importType = e.target.value; model.didUpdate(); } onDataFormatChange(e) { - let { model } = this.props; + let {model} = this.props; model.dataFormat = e.target.value; model.didUpdate(); } onDataPaste(e) { - let { model } = this.props; + let {model} = this.props; let text = e.clipboardData.getData("text/plain"); model.setData(text); model.didUpdate(); } onExternalIdChange(e) { - let { model } = this.props; + let {model} = this.props; model.externalId = e.target.value; model.didUpdate(); } onBatchSizeChange(e) { - let { model } = this.props; + let {model} = this.props; model.batchSize = e.target.value; model.executeBatch(); model.didUpdate(); } onBatchConcurrencyChange(e) { - let { model } = this.props; + let {model} = this.props; model.batchConcurrency = e.target.value; model.executeBatch(); model.didUpdate(); } onToggleHelpClick(e) { e.preventDefault(); - let { model } = this.props; + let {model} = this.props; model.showHelp = !model.showHelp; model.didUpdate(() => { this.scrollTable.viewportChange(); @@ -815,31 +835,31 @@ class App extends React.Component { } onDoImportClick(e) { e.preventDefault(); - let { model } = this.props; + let {model} = this.props; model.doImport(); model.didUpdate(); } onToggleProcessingClick(e) { e.preventDefault(); - let { model } = this.props; + let {model} = this.props; model.isProcessingQueue = !model.isProcessingQueue; model.executeBatch(); model.didUpdate(); } onRetryFailedClick(e) { e.preventDefault(); - let { model } = this.props; + let {model} = this.props; model.retryFailed(); model.didUpdate(); } onCopyAsExcelClick(e) { e.preventDefault(); - let { model } = this.props; + let {model} = this.props; model.copyResult("\t"); } onCopyAsCsvClick(e) { e.preventDefault(); - let { model } = this.props; + let {model} = this.props; let separator = ","; if (localStorage.getItem("csvSeparator")) { separator = localStorage.getItem("csvSeparator"); @@ -848,28 +868,41 @@ class App extends React.Component { } onCopyOptionsClick(e) { e.preventDefault(); - let { model } = this.props; + let {model} = this.props; model.copyOptions(); } onSkipAllUnknownFieldsClick(e) { e.preventDefault(); - let { model } = this.props; + let {model} = this.props; model.skipAllUnknownFields(); } onConfirmPopupYesClick(e) { e.preventDefault(); - let { model } = this.props; + let {model} = this.props; model.confirmPopupYes(); model.didUpdate(); } onConfirmPopupNoClick(e) { e.preventDefault(); - let { model } = this.props; + let {model} = this.props; model.confirmPopupNo(); model.didUpdate(); } + onImportUndelete(model){ + //reinit import table to remove __Status column to be able to undelete rows after deleting it + if (model.importData.importTable.header.find(c => c.columnValue == "__Status")) { + //get indexes to remove + const indices = model.importData.importTable.header.map((element, index) => element.columnValue.startsWith("__") ? index : undefined).filter(index => index !== undefined); + //remove indexes from header and data + model.importData.importTable.header = model.importData.importTable.header.filter((element, index) => !indices.includes(index)); + model.importData.importTable.data = model.importData.importTable.data.map(innerArray => innerArray.filter((element, index) => !indices.includes(index))); + + model.importCounts().Queued = model.importData.importTable.data.length; + model.updateImportTableResult(); + } + } componentDidMount() { - let { model } = this.props; + let {model} = this.props; addEventListener("resize", () => { this.scrollTable.viewportChange(); }); @@ -878,7 +911,7 @@ class App extends React.Component { model.updateImportTableResult(); } componentDidUpdate() { - let { model } = this.props; + let {model} = this.props; // We completely remove the listener when not needed (as opposed to just not setting returnValue in the listener), // because having the listener disables BFCache in Firefox (even if the listener does nothing). @@ -898,156 +931,157 @@ class App extends React.Component { } } render() { - let { model } = this.props; + let {model} = this.props; //console.log(model); return h("div", {}, - h("div", { id: "user-info" }, - h("a", { href: model.sfLink, className: "sf-link" }, - h("svg", { viewBox: "0 0 24 24" }, - h("path", { d: "M18.9 12.3h-1.5v6.6c0 .2-.1.3-.3.3h-3c-.2 0-.3-.1-.3-.3v-5.1h-3.6v5.1c0 .2-.1.3-.3.3h-3c-.2 0-.3-.1-.3-.3v-6.6H5.1c-.1 0-.3-.1-.3-.2s0-.2.1-.3l6.9-7c.1-.1.3-.1.4 0l7 7v.3c0 .1-.2.2-.3.2z" }) + h("div", {id: "user-info"}, + h("a", {href: model.sfLink, className: "sf-link"}, + h("svg", {viewBox: "0 0 24 24"}, + h("path", {d: "M18.9 12.3h-1.5v6.6c0 .2-.1.3-.3.3h-3c-.2 0-.3-.1-.3-.3v-5.1h-3.6v5.1c0 .2-.1.3-.3.3h-3c-.2 0-.3-.1-.3-.3v-6.6H5.1c-.1 0-.3-.1-.3-.2s0-.2.1-.3l6.9-7c.1-.1.3-.1.4 0l7 7v.3c0 .1-.2.2-.3.2z"}) ), " Salesforce Home" ), h("h1", {}, "Data Import"), h("span", {}, " / " + model.userInfo), - h("div", { className: "flex-right" }, - h("div", { id: "spinner", role: "status", className: "slds-spinner slds-spinner_small slds-spinner_inline", hidden: model.spinnerCount == 0 }, - h("span", { className: "slds-assistive-text" }), - h("div", { className: "slds-spinner__dot-a" }), - h("div", { className: "slds-spinner__dot-b" }), + h("div", {className: "flex-right"}, + h("div", {id: "spinner", role: "status", className: "slds-spinner slds-spinner_small slds-spinner_inline", hidden: model.spinnerCount == 0}, + h("span", {className: "slds-assistive-text"}), + h("div", {className: "slds-spinner__dot-a"}), + h("div", {className: "slds-spinner__dot-b"}), ), - h("a", { href: "#", id: "help-btn", title: "Import Help", onClick: this.onToggleHelpClick }, - h("div", { className: "icon" }) + h("a", {href: "#", id: "help-btn", title: "Import Help", onClick: this.onToggleHelpClick}, + h("div", {className: "icon"}) ), ), ), - h("div", { className: "conf-section" }, - h("div", { className: "conf-subsection" }, - h("div", { className: "area configure-import" }, - h("div", { className: "area-header" }, + h("div", {className: "conf-section"}, + h("div", {className: "conf-subsection"}, + h("div", {className: "area configure-import"}, + h("div", {className: "area-header"}, h("h1", {}, "Configure Import") ), - h("div", { className: "conf-line" }, - h("label", { className: "conf-input", title: "With the tooling API you can query more metadata, but you cannot query regular data" }, - h("span", { className: "conf-label" }, "Use Tooling API?"), - h("span", { className: "conf-value" }, - h("input", { type: "checkbox", checked: model.useToolingApi, onChange: this.onUseToolingApiChange, disabled: model.isWorking() }), + h("div", {className: "conf-line"}, + h("label", {className: "conf-input", title: "With the tooling API you can query more metadata, but you cannot query regular data"}, + h("span", {className: "conf-label"}, "Use Tooling API?"), + h("span", {className: "conf-value"}, + h("input", {type: "checkbox", checked: model.useToolingApi, onChange: this.onUseToolingApiChange, disabled: model.isWorking()}), ) ) ), - h("div", { className: "conf-line" }, - h("label", { className: "conf-input" }, - h("span", { className: "conf-label" }, "Action"), - h("span", { className: "conf-value" }, - h("select", { value: model.importAction, onChange: this.onImportActionChange, disabled: model.isWorking() }, - h("option", { value: "create" }, "Insert"), - h("option", { value: "update" }, "Update"), - h("option", { value: "upsert" }, "Upsert"), - h("option", { value: "delete" }, "Delete") + h("div", {className: "conf-line"}, + h("label", {className: "conf-input"}, + h("span", {className: "conf-label"}, "Action"), + h("span", {className: "conf-value"}, + h("select", {value: model.importAction, onChange: this.onImportActionChange, disabled: model.isWorking()}, + h("option", {value: "create"}, "Insert"), + h("option", {value: "update"}, "Update"), + h("option", {value: "upsert"}, "Upsert"), + h("option", {value: "delete"}, "Delete"), + h("option", {value: "undelete"}, "Undelete") ) ) ) ), - h("div", { className: "conf-line" }, - h("label", { className: "conf-input" }, - h("span", { className: "conf-label" }, "Object"), - h("span", { className: "conf-value" }, - h("input", { type: "search", value: model.importType, onChange: this.onImportTypeChange, className: model.importTypeError() ? "object-list confError" : "object-list", disabled: model.isWorking(), list: "sobjectlist" }), - h("div", { className: "conf-error", hidden: !model.importTypeError() }, model.importTypeError()) + h("div", {className: "conf-line"}, + h("label", {className: "conf-input"}, + h("span", {className: "conf-label"}, "Object"), + h("span", {className: "conf-value"}, + h("input", {type: "search", value: model.importType, onChange: this.onImportTypeChange, className: model.importTypeError() ? "object-list confError" : "object-list", disabled: model.isWorking(), list: "sobjectlist"}), + h("div", {className: "conf-error", hidden: !model.importTypeError()}, model.importTypeError()) ) ), - h("a", { className: "button field-info", href: model.showDescribeUrl(), target: "_blank", title: "Show field info for the selected object" }, - h("div", { className: "button-icon" }), + h("a", {className: "button field-info", href: model.showDescribeUrl(), target: "_blank", title: "Show field info for the selected object"}, + h("div", {className: "button-icon"}), ) ), - h("div", { className: "conf-line radio-buttons" }, - h("span", { className: "conf-label" }, "Format"), - h("label", {}, h("input", { type: "radio", name: "data-input-format", value: "excel", checked: model.dataFormat == "excel", onChange: this.onDataFormatChange, disabled: model.isWorking() }), " ", h("span", {}, "Excel")), + h("div", {className: "conf-line radio-buttons"}, + h("span", {className: "conf-label"}, "Format"), + h("label", {}, h("input", {type: "radio", name: "data-input-format", value: "excel", checked: model.dataFormat == "excel", onChange: this.onDataFormatChange, disabled: model.isWorking()}), " ", h("span", {}, "Excel")), " ", - h("label", {}, h("input", { type: "radio", name: "data-input-format", value: "csv", checked: model.dataFormat == "csv", onChange: this.onDataFormatChange, disabled: model.isWorking() }), " ", h("span", {}, "CSV")), + h("label", {}, h("input", {type: "radio", name: "data-input-format", value: "csv", checked: model.dataFormat == "csv", onChange: this.onDataFormatChange, disabled: model.isWorking()}), " ", h("span", {}, "CSV")), " ", - h("label", {}, h("input", { type: "radio", name: "data-input-format", value: "json", checked: model.dataFormat == "json", onChange: this.onDataFormatChange, disabled: model.isWorking() }), " ", h("span", {}, "JSON")) + h("label", {}, h("input", {type: "radio", name: "data-input-format", value: "json", checked: model.dataFormat == "json", onChange: this.onDataFormatChange, disabled: model.isWorking()}), " ", h("span", {}, "JSON")) ), - h("div", { className: "conf-line" }, - h("label", { className: "conf-input" }, - h("span", { className: "conf-label" }, "Data"), - h("span", { className: "conf-value" }, - h("textarea", { id: "data", value: model.message(), onPaste: this.onDataPaste, className: model.dataError ? "confError" : "", disabled: model.isWorking(), readOnly: true, rows: 1 }), - h("div", { className: "conf-error", hidden: !model.dataError }, model.dataError) + h("div", {className: "conf-line"}, + h("label", {className: "conf-input"}, + h("span", {className: "conf-label"}, "Data"), + h("span", {className: "conf-value"}, + h("textarea", {id: "data", value: model.message(), onPaste: this.onDataPaste, className: model.dataError ? "confError" : "", disabled: model.isWorking(), readOnly: true, rows: 1}), + h("div", {className: "conf-error", hidden: !model.dataError}, model.dataError) ) ) ), - h("div", { className: "conf-line", hidden: model.importAction != "upsert" }, - h("label", { className: "conf-input", title: "Used in upserts to determine if an existing record should be updated or a new record should be created" }, - h("span", { className: "conf-label" }, "External ID:"), - h("span", { className: "conf-value" }, - h("input", { type: "text", value: model.externalId, onChange: this.onExternalIdChange, className: model.externalIdError() ? "confError" : "", disabled: model.isWorking(), list: "idlookuplist" }), - h("div", { className: "conf-error", hidden: !model.externalIdError() }, model.externalIdError()) + h("div", {className: "conf-line", hidden: model.importAction != "upsert"}, + h("label", {className: "conf-input", title: "Used in upserts to determine if an existing record should be updated or a new record should be created"}, + h("span", {className: "conf-label"}, "External ID:"), + h("span", {className: "conf-value"}, + h("input", {type: "text", value: model.externalId, onChange: this.onExternalIdChange, className: model.externalIdError() ? "confError" : "", disabled: model.isWorking(), list: "idlookuplist"}), + h("div", {className: "conf-error", hidden: !model.externalIdError()}, model.externalIdError()) ) ) ), - h("div", { className: "conf-line" }, - h("label", { className: "conf-input", title: "The number of records per batch. A higher value is faster but increases the risk of errors due to governor limits." }, - h("span", { className: "conf-label" }, "Batch size"), - h("span", { className: "conf-value" }, - h("input", { type: "number", value: model.batchSize, onChange: this.onBatchSizeChange, className: (model.batchSizeError() ? "confError" : "") + " batch-size" }), - h("div", { className: "conf-error", hidden: !model.batchSizeError() }, model.batchSizeError()) + h("div", {className: "conf-line"}, + h("label", {className: "conf-input", title: "The number of records per batch. A higher value is faster but increases the risk of errors due to governor limits."}, + h("span", {className: "conf-label"}, "Batch size"), + h("span", {className: "conf-value"}, + h("input", {type: "number", value: model.batchSize, onChange: this.onBatchSizeChange, className: (model.batchSizeError() ? "confError" : "") + " batch-size"}), + h("div", {className: "conf-error", hidden: !model.batchSizeError()}, model.batchSizeError()) ) ) ), - h("div", { className: "conf-line" }, - h("label", { className: "conf-input", title: "The number of batches to execute concurrently. A higher number is faster but increases the risk of errors due to lock congestion." }, - h("span", { className: "conf-label" }, "Threads"), - h("span", { className: "conf-value" }, - h("input", { type: "number", value: model.batchConcurrency, onChange: this.onBatchConcurrencyChange, className: (model.batchConcurrencyError() ? "confError" : "") + " batch-size" }), - h("span", { hidden: !model.isWorking() }, model.activeBatches), - h("div", { className: "conf-error", hidden: !model.batchConcurrencyError() }, model.batchConcurrencyError()) + h("div", {className: "conf-line"}, + h("label", {className: "conf-input", title: "The number of batches to execute concurrently. A higher number is faster but increases the risk of errors due to lock congestion."}, + h("span", {className: "conf-label"}, "Threads"), + h("span", {className: "conf-value"}, + h("input", {type: "number", value: model.batchConcurrency, onChange: this.onBatchConcurrencyChange, className: (model.batchConcurrencyError() ? "confError" : "") + " batch-size"}), + h("span", {hidden: !model.isWorking()}, model.activeBatches), + h("div", {className: "conf-error", hidden: !model.batchConcurrencyError()}, model.batchConcurrencyError()) ) ) ), - h("datalist", { id: "sobjectlist" }, model.sobjectList().map(data => h("option", { key: data, value: data }))), - h("datalist", { id: "idlookuplist" }, model.idLookupList().map(data => h("option", { key: data, value: data }))), - h("datalist", { id: "columnlist" }, model.columnList().map(data => h("option", { key: data, value: data }))) + h("datalist", {id: "sobjectlist"}, model.sobjectList().map(data => h("option", {key: data, value: data}))), + h("datalist", {id: "idlookuplist"}, model.idLookupList().map(data => h("option", {key: data, value: data}))), + h("datalist", {id: "columnlist"}, model.columnList().map(data => h("option", {key: data, value: data}))) ), ), - h("div", { className: "conf-subsection columns-mapping" }, - h("div", { className: "area" }, - h("div", { className: "area-header" }, + h("div", {className: "conf-subsection columns-mapping"}, + h("div", {className: "area"}, + h("div", {className: "area-header"}, h("h1", {}, "Field Mapping") ), /* h("div", {className: "columns-label"}, "Field mapping"), */ - h("div", { className: "conf-error confError", hidden: !model.importIdColumnError() }, model.importIdColumnError()), - h("div", { className: "conf-value" }, model.columns().map((column, index) => h(ColumnMapper, { key: index, model, column }))) + h("div", {className: "conf-error confError", hidden: !model.importIdColumnError()}, model.importIdColumnError()), + h("div", {className: "conf-value"}, model.columns().map((column, index) => h(ColumnMapper, {key: index, model, column}))) ) ) ), - h("div", { className: "area import-actions" }, - h("div", { className: "conf-line" }, - h("div", { className: "flex-wrapper" }, - h("button", { onClick: this.onDoImportClick, disabled: model.invalidInput() || model.isWorking() || model.importCounts().Queued == 0, className: "highlighted" }, "Run " + model.importActionName), - h("button", { disabled: !model.isWorking(), onClick: this.onToggleProcessingClick, className: model.isWorking() && !model.isProcessingQueue ? "" : "cancel-btn" }, model.isWorking() && !model.isProcessingQueue ? "Resume Queued" : "Cancel Queued"), - h("button", { disabled: !model.importCounts().Failed > 0, onClick: this.onRetryFailedClick }, "Retry Failed"), - h("div", { className: "button-group" }, - h("button", { disabled: !model.canCopy(), onClick: this.onCopyAsExcelClick, title: "Copy import result to clipboard for pasting into Excel or similar" }, "Copy (Excel format)"), - h("button", { disabled: !model.canCopy(), onClick: this.onCopyAsCsvClick, title: "Copy import result to clipboard for saving as a CSV file" }, "Copy (CSV)"), + h("div", {className: "area import-actions"}, + h("div", {className: "conf-line"}, + h("div", {className: "flex-wrapper"}, + h("button", {onClick: this.onDoImportClick, disabled: model.invalidInput() || model.isWorking() || model.importCounts().Queued == 0, className: "highlighted"}, "Run " + model.importActionName), + h("button", {disabled: !model.isWorking(), onClick: this.onToggleProcessingClick, className: model.isWorking() && !model.isProcessingQueue ? "" : "cancel-btn"}, model.isWorking() && !model.isProcessingQueue ? "Resume Queued" : "Cancel Queued"), + h("button", {disabled: !model.importCounts().Failed > 0, onClick: this.onRetryFailedClick}, "Retry Failed"), + h("div", {className: "button-group"}, + h("button", {disabled: !model.canCopy(), onClick: this.onCopyAsExcelClick, title: "Copy import result to clipboard for pasting into Excel or similar"}, "Copy (Excel format)"), + h("button", {disabled: !model.canCopy(), onClick: this.onCopyAsCsvClick, title: "Copy import result to clipboard for saving as a CSV file"}, "Copy (CSV)"), ), ), - h("div", { className: "status-group" }, + h("div", {className: "status-group"}, h("div", {}, - h(StatusBox, { model, name: "Queued" }), - h(StatusBox, { model, name: "Processing" }) + h(StatusBox, {model, name: "Queued"}), + h(StatusBox, {model, name: "Processing"}) ), h("div", {}, - h(StatusBox, { model, name: "Succeeded" }), - h(StatusBox, { model, name: "Failed" }) + h(StatusBox, {model, name: "Succeeded"}), + h(StatusBox, {model, name: "Failed"}) ), ), - h("div", { className: "flex-right" }, - h("button", { onClick: this.onCopyOptionsClick, title: "Save these import options by pasting them into Excel in the top left cell, just above the header row" }, "Copy Options"), - h("button", { onClick: this.onSkipAllUnknownFieldsClick, disabled: !model.canSkipAllUnknownFields() || model.isWorking() || model.importCounts().Queued == 0 }, "Skip all unknown fields") + h("div", {className: "flex-right"}, + h("button", {onClick: this.onCopyOptionsClick, title: "Save these import options by pasting them into Excel in the top left cell, just above the header row"}, "Copy Options"), + h("button", {onClick: this.onSkipAllUnknownFieldsClick, disabled: !model.canSkipAllUnknownFields() || model.isWorking() || model.importCounts().Queued == 0}, "Skip all unknown fields") ), ), - h("div", { hidden: !model.showHelp, className: "help-text" }, + h("div", {hidden: !model.showHelp, className: "help-text"}, h("h3", {}, "Import Help"), h("p", {}, "Use for quick one-off data imports."), h("ul", {}, @@ -1056,7 +1090,7 @@ class App extends React.Component { h("li", {}, "The input must contain a header row with field API names."), h("li", {}, "To use an external ID for a lookup field, the header row should contain the lookup relation name, the target sobject name and the external ID name separated by colons, e.g. \"MyLookupField__r:MyObject__c:MyExternalIdField__c\"."), h("li", {}, "Empty cells insert null values."), - h("li", {}, "Number, date, time and checkbox values must conform to the relevant ", h("a", { href: "http://www.w3.org/TR/xmlschema-2/#built-in-primitive-datatypes", target: "_blank" }, "XSD datatypes"), "."), + h("li", {}, "Number, date, time and checkbox values must conform to the relevant ", h("a", {href: "http://www.w3.org/TR/xmlschema-2/#built-in-primitive-datatypes", target: "_blank"}, "XSD datatypes"), "."), h("li", {}, "Columns starting with an underscore are ignored."), h("li", {}, "You can resume a previous import by including the \"__Status\" column in your input."), h("li", {}, "You can supply the other import options by clicking \"Copy options\" and pasting the options into Excel in the top left cell, just above the header row.") @@ -1070,17 +1104,17 @@ class App extends React.Component { h("p", {}, "Bulk API is not supported. Large data volumes may freeze or crash your browser.") ), ), - h("div", { className: "area result-area" }, - h("div", { id: "result-table", ref: "scroller" }), + h("div", {className: "area result-area"}, + h("div", {id: "result-table", ref: "scroller"}), model.confirmPopup ? h("div", {}, - h("div", { id: "confirm-background" }, - h("div", { id: "confirm-dialog" }, + h("div", {id: "confirm-background"}, + h("div", {id: "confirm-dialog"}, h("h1", {}, "Import"), h("p", {}, "You are about to modify your data in Salesforce. This action cannot be undone."), h("p", {}, model.confirmPopup.text), - h("div", { className: "dialog-buttons" }, - h("button", { onClick: this.onConfirmPopupYesClick }, model.importActionName), - h("button", { onClick: this.onConfirmPopupNoClick, className: "cancel-btn" }, "Cancel") + h("div", {className: "dialog-buttons"}, + h("button", {onClick: this.onConfirmPopupYesClick}, model.importActionName), + h("button", {onClick: this.onConfirmPopupNoClick, className: "cancel-btn"}, "Cancel") ) ) ) @@ -1097,23 +1131,23 @@ class ColumnMapper extends React.Component { this.onColumnSkipClick = this.onColumnSkipClick.bind(this); } onColumnValueChange(e) { - let { model, column } = this.props; + let {model, column} = this.props; column.columnValue = e.target.value; model.didUpdate(); } onColumnSkipClick(e) { - let { model, column } = this.props; + let {model, column} = this.props; e.preventDefault(); column.columnSkip(); model.didUpdate(); } render() { - let { model, column } = this.props; - return h("div", { className: "conf-line" }, - h("label", { htmlFor: "col-" + column.columnIndex }, column.columnOriginalValue), - h("div", { className: "flex-wrapper" }, - h("input", { type: "search", list: "columnlist", value: column.columnValue, onChange: this.onColumnValueChange, className: column.columnError() ? "confError" : "", disabled: model.isWorking(), id: "col-" + column.columnIndex }), - h("div", { className: "conf-error", hidden: !column.columnError() }, h("span", {}, column.columnError()), " ", h("button", { onClick: this.onColumnSkipClick, hidden: model.isWorking(), title: "Don't import this column" }, "Skip")) + let {model, column} = this.props; + return h("div", {className: "conf-line"}, + h("label", {htmlFor: "col-" + column.columnIndex}, column.columnOriginalValue), + h("div", {className: "flex-wrapper"}, + h("input", {type: "search", list: "columnlist", value: column.columnValue, onChange: this.onColumnValueChange, className: column.columnError() ? "confError" : "", disabled: model.isWorking(), id: "col-" + column.columnIndex}), + h("div", {className: "conf-error", hidden: !column.columnError()}, h("span", {}, column.columnError()), " ", h("button", {onClick: this.onColumnSkipClick, hidden: model.isWorking(), title: "Don't import this column"}, "Skip")) ) ); } @@ -1125,14 +1159,14 @@ class StatusBox extends React.Component { this.onShowStatusChange = this.onShowStatusChange.bind(this); } onShowStatusChange(e) { - let { model, name } = this.props; + let {model, name} = this.props; model.showStatus[name] = e.target.checked; model.updateImportTableResult(); model.didUpdate(); } render() { - let { model, name } = this.props; - return h("label", { className: model.importCounts()[name] == 0 ? "statusGroupEmpty" : "" }, h("input", { type: "checkbox", checked: model.showStatus[name], onChange: this.onShowStatusChange }), " " + model.importCounts()[name] + " " + name); + let {model, name} = this.props; + return h("label", {className: model.importCounts()[name] == 0 ? "statusGroupEmpty" : ""}, h("input", {type: "checkbox", checked: model.showStatus[name], onChange: this.onShowStatusChange}), " " + model.importCounts()[name] + " " + name); } } @@ -1146,12 +1180,12 @@ class StatusBox extends React.Component { let root = document.getElementById("root"); let model = new Model(sfHost, args); model.reactCallback = cb => { - ReactDOM.render(h(App, { model }), root, cb); + ReactDOM.render(h(App, {model}), root, cb); }; - ReactDOM.render(h(App, { model }), root); + ReactDOM.render(h(App, {model}), root); if (parent && parent.isUnitTest) { // for unit tests - parent.insextTestLoaded({ model }); + parent.insextTestLoaded({model}); } });