├── .gitignore ├── jsconfig.json ├── LICENSE ├── src ├── html │ ├── PropertyEditorGrid.html │ └── TranslationGrid.html └── js │ ├── PropertyEditor │ ├── EntityPropertyHandler.js │ ├── AttributePropertyHandler.js │ └── XrmPropertyEditor.js │ └── Translator │ ├── EntityHandler.js │ ├── ChartHandler.js │ ├── ViewHandler.js │ ├── AttributeHandler.js │ ├── FormMetaHandler.js │ ├── ContentSnippetHandler.js │ ├── OptionSetHandler.js │ ├── WebResourceHandler.js │ ├── FormHandler.js │ ├── TranslationHandler.js │ └── XrmTranslator.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | Publish/* -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6" 4 | } 5 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Florian Krönert 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/html/PropertyEditorGrid.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Xrm-Property-Editor 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/html/TranslationGrid.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Xrm-Easy-Translation 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/js/PropertyEditor/EntityPropertyHandler.js: -------------------------------------------------------------------------------- 1 | /* @preserve 2 | * MIT License 3 | * 4 | * Copyright (c) 2017 Florian Krönert 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | * 24 | */ 25 | (function (EntityPropertyHandler, undefined) { 26 | "use strict"; 27 | 28 | function ApplyChanges(changes, labels) { 29 | for (var change in changes) { 30 | if (!changes.hasOwnProperty(change)) { 31 | continue; 32 | } 33 | 34 | for (var i = 0; i < labels.length; i++) { 35 | var label = labels[i]; 36 | 37 | if (label.LanguageCode == change) { 38 | label.Label = changes[change]; 39 | label.HasChanged = true; 40 | 41 | break; 42 | } 43 | 44 | // Did not find label for this language 45 | if (i === labels.length - 1) { 46 | labels.push({ LanguageCode: change, Label: changes[change] }) 47 | } 48 | } 49 | } 50 | } 51 | 52 | function GetUpdates() { 53 | var records = XrmPropertyEditor.GetGrid().records; 54 | 55 | var update = XrmPropertyEditor.metadata; 56 | 57 | for (var i = 0; i < records.length; i++) { 58 | var record = records[i]; 59 | 60 | if (record.w2ui && record.w2ui.changes) { 61 | var labels = null; 62 | 63 | if (record.schemaName === "Display Name") { 64 | labels = update.DisplayName.LocalizedLabels; 65 | } else if (record.schemaName === "Collection Name") { 66 | labels = update.DisplayCollectionName.LocalizedLabels; 67 | } 68 | 69 | var changes = record.w2ui.changes; 70 | 71 | ApplyChanges(changes, labels); 72 | } 73 | } 74 | 75 | return update; 76 | } 77 | 78 | function FillTable () { 79 | var grid = XrmPropertyEditor.GetGrid(); 80 | grid.clear(); 81 | 82 | var records = []; 83 | 84 | var entity = XrmPropertyEditor.metadata; 85 | 86 | var displayNames = entity.DisplayName.LocalizedLabels; 87 | var collectionNames = entity.DisplayCollectionName.LocalizedLabels; 88 | 89 | if (!displayNames && !collectionNames) { 90 | return; 91 | } 92 | 93 | var singular = { 94 | recid: XrmPropertyEditor.metadata.MetadataId + "|1", 95 | schemaName: "Display Name" 96 | }; 97 | 98 | var plural = { 99 | recid: XrmPropertyEditor.metadata.MetadataId + "|2", 100 | schemaName: "Collection Name" 101 | }; 102 | 103 | for (var i = 0; i < displayNames.length; i++) { 104 | var displayName = displayNames[i]; 105 | 106 | singular[displayName.LanguageCode.toString()] = displayName.Label; 107 | } 108 | 109 | for (var j = 0; j < collectionNames.length; j++) { 110 | var collectionName = collectionNames[j]; 111 | 112 | plural[collectionName.LanguageCode.toString()] = collectionName.Label; 113 | } 114 | 115 | records.push(singular); 116 | records.push(plural); 117 | 118 | grid.add(records); 119 | grid.unlock(); 120 | } 121 | 122 | EntityPropertyHandler.Load = function() { 123 | var entityName = XrmPropertyEditor.GetEntity(); 124 | var entityMetadataId = XrmPropertyEditor.entityMetadata[entityName]; 125 | 126 | var request = { 127 | entityName: "EntityDefinition", 128 | entityId: entityMetadataId 129 | }; 130 | 131 | WebApiClient.Retrieve(request) 132 | .then(function(response) { 133 | XrmPropertyEditor.metadata = response; 134 | 135 | FillTable(); 136 | }) 137 | .catch(XrmPropertyEditor.errorHandler); 138 | } 139 | 140 | EntityPropertyHandler.Save = function() { 141 | XrmPropertyEditor.LockGrid("Saving"); 142 | 143 | var updates = GetUpdates(); 144 | var entityUrl = WebApiClient.GetApiUrl() + "EntityDefinitions(" + XrmPropertyEditor.GetEntityId() + ")"; 145 | 146 | WebApiClient.SendRequest("PUT", entityUrl, updates, [{key: "MSCRM.MergeLabels", value: "true"}]) 147 | .then(function (response){ 148 | XrmPropertyEditor.LockGrid("Publishing"); 149 | 150 | return XrmPropertyEditor.Publish(); 151 | }) 152 | .then(function (response) { 153 | XrmPropertyEditor.LockGrid("Reloading"); 154 | 155 | return EntityPropertyHandler.Load(); 156 | }) 157 | .catch(XrmPropertyEditor.errorHandler); 158 | } 159 | } (window.EntityPropertyHandler = window.EntityPropertyHandler || {})); -------------------------------------------------------------------------------- /src/js/Translator/EntityHandler.js: -------------------------------------------------------------------------------- 1 | /* @preserve 2 | * MIT License 3 | * 4 | * Copyright (c) 2017 Florian Krönert 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | * 24 | */ 25 | (function (EntityHandler, undefined) { 26 | "use strict"; 27 | 28 | function ApplyChanges(changes, labels) { 29 | for (var change in changes) { 30 | if (!changes.hasOwnProperty(change)) { 31 | continue; 32 | } 33 | 34 | // Skip empty labels 35 | if (!changes[change]) { 36 | continue; 37 | } 38 | 39 | for (var i = 0; i < labels.length; i++) { 40 | var label = labels[i]; 41 | 42 | if (label.LanguageCode == change) { 43 | label.Label = changes[change]; 44 | label.HasChanged = true; 45 | 46 | break; 47 | } 48 | 49 | // Did not find label for this language 50 | if (i === labels.length - 1) { 51 | labels.push({ LanguageCode: change, Label: changes[change] }) 52 | } 53 | } 54 | } 55 | } 56 | 57 | function GetUpdates() { 58 | var records = XrmTranslator.GetGrid().records; 59 | 60 | var update = XrmTranslator.metadata; 61 | 62 | for (var i = 0; i < records.length; i++) { 63 | var record = records[i]; 64 | 65 | if (record.w2ui && record.w2ui.changes) { 66 | var labels = null; 67 | 68 | if (record.schemaName === "Display Name") { 69 | labels = update[XrmTranslator.GetComponent()].LocalizedLabels; 70 | } else if (record.schemaName === "Collection Name") { 71 | labels = update.DisplayCollectionName.LocalizedLabels; 72 | } 73 | 74 | var changes = record.w2ui.changes; 75 | 76 | ApplyChanges(changes, labels); 77 | } 78 | } 79 | 80 | return update; 81 | } 82 | 83 | function FillTable () { 84 | var grid = XrmTranslator.GetGrid(); 85 | grid.clear(); 86 | 87 | var records = []; 88 | 89 | var entity = XrmTranslator.metadata; 90 | 91 | var displayNames = entity[XrmTranslator.GetComponent()].LocalizedLabels; 92 | var collectionNames = entity.DisplayCollectionName.LocalizedLabels; 93 | 94 | if (!displayNames && !collectionNames) { 95 | return; 96 | } 97 | 98 | var singular = { 99 | recid: XrmTranslator.metadata.MetadataId + "|1", 100 | schemaName: "Display Name" 101 | }; 102 | 103 | var plural = { 104 | recid: XrmTranslator.metadata.MetadataId + "|2", 105 | schemaName: "Collection Name" 106 | }; 107 | 108 | for (var i = 0; i < displayNames.length; i++) { 109 | var displayName = displayNames[i]; 110 | 111 | singular[displayName.LanguageCode.toString()] = displayName.Label; 112 | } 113 | 114 | for (var j = 0; j < collectionNames.length; j++) { 115 | var collectionName = collectionNames[j]; 116 | 117 | plural[collectionName.LanguageCode.toString()] = collectionName.Label; 118 | } 119 | 120 | records.push(singular); 121 | records.push(plural); 122 | 123 | XrmTranslator.AddSummary(records); 124 | grid.add(records); 125 | grid.unlock(); 126 | } 127 | 128 | EntityHandler.Load = function() { 129 | var entityName = XrmTranslator.GetEntity(); 130 | var entityMetadataId = XrmTranslator.entityMetadata[entityName]; 131 | 132 | var request = { 133 | entityName: "EntityDefinition", 134 | entityId: entityMetadataId 135 | }; 136 | 137 | return WebApiClient.Retrieve(request) 138 | .then(function(response) { 139 | XrmTranslator.metadata = response; 140 | 141 | FillTable(); 142 | }) 143 | .catch(XrmTranslator.errorHandler); 144 | } 145 | 146 | EntityHandler.Save = function() { 147 | XrmTranslator.LockGrid("Saving"); 148 | 149 | var updates = GetUpdates(); 150 | var entityUrl = WebApiClient.GetApiUrl() + "EntityDefinitions(" + XrmTranslator.GetEntityId() + ")"; 151 | 152 | return WebApiClient.SendRequest("PUT", entityUrl, updates, [{key: "MSCRM.MergeLabels", value: "true"}]) 153 | .then(function (response){ 154 | XrmTranslator.LockGrid("Publishing"); 155 | 156 | return XrmTranslator.Publish(); 157 | }) 158 | .then(function(response) { 159 | return XrmTranslator.AddToSolution([XrmTranslator.GetEntityId()], XrmTranslator.ComponentType.Entity); 160 | }) 161 | .then(function(response) { 162 | return XrmTranslator.ReleaseLockAndPrompt(); 163 | }) 164 | .then(function (response) { 165 | XrmTranslator.LockGrid("Reloading"); 166 | 167 | return EntityHandler.Load(); 168 | }) 169 | .catch(XrmTranslator.errorHandler); 170 | } 171 | } (window.EntityHandler = window.EntityHandler || {})); 172 | -------------------------------------------------------------------------------- /src/js/Translator/ChartHandler.js: -------------------------------------------------------------------------------- 1 | (function (ChartHandler, undefined) { 2 | "use strict"; 3 | 4 | function ApplyChanges(changes, labels) { 5 | for (var change in changes) { 6 | if (!changes.hasOwnProperty(change)) { 7 | continue; 8 | } 9 | 10 | // Skip empty labels 11 | if (!changes[change]) { 12 | continue; 13 | } 14 | 15 | for (var i = 0; i < labels.length; i++) { 16 | var label = labels[i]; 17 | 18 | if (label.LanguageCode == change) { 19 | label.Label = changes[change]; 20 | label.HasChanged = true; 21 | 22 | break; 23 | } 24 | 25 | // Did not find label for this language 26 | if (i === labels.length - 1) { 27 | labels.push({ LanguageCode: change, Label: changes[change] }) 28 | } 29 | } 30 | } 31 | } 32 | 33 | function GetUpdates() { 34 | var records = XrmTranslator.GetGrid().records; 35 | 36 | var updates = []; 37 | 38 | for (var i = 0; i < records.length; i++) { 39 | var record = records[i]; 40 | 41 | if (record.w2ui && record.w2ui.changes) { 42 | var chart = XrmTranslator.GetAttributeByProperty("recid", record.recid); 43 | var labels = chart.labels.Label.LocalizedLabels; 44 | 45 | var changes = record.w2ui.changes; 46 | 47 | ApplyChanges(changes, labels); 48 | updates.push(chart); 49 | } 50 | } 51 | 52 | return updates; 53 | } 54 | function FillTable() { 55 | var grid = XrmTranslator.GetGrid(); 56 | grid.clear(); 57 | 58 | var records = []; 59 | 60 | for (var i = 0; i < XrmTranslator.metadata.length; i++) { 61 | var chart = XrmTranslator.metadata[i]; 62 | 63 | var displayNames = chart.labels.Label.LocalizedLabels; 64 | 65 | if (!displayNames || displayNames.length === 0) { 66 | continue; 67 | } 68 | 69 | var record = { 70 | recid: chart.recid, 71 | schemaName: "Chart" 72 | }; 73 | 74 | for (var j = 0; j < displayNames.length; j++) { 75 | var displayName = displayNames[j]; 76 | 77 | record[displayName.LanguageCode.toString()] = displayName.Label; 78 | } 79 | 80 | records.push(record); 81 | } 82 | 83 | XrmTranslator.AddSummary(records); 84 | grid.add(records); 85 | grid.unlock(); 86 | } 87 | 88 | ChartHandler.Load = function () { 89 | var entityName = XrmTranslator.GetEntity(); 90 | 91 | var entityMetadataId = XrmTranslator.entityMetadata[entityName]; 92 | 93 | var queryRequest = { 94 | entityName: "savedqueryvisualization", 95 | queryParams: "?$filter=primaryentitytypecode eq '" + entityName.toLowerCase() + "' and iscustomizable/Value eq true&$orderby=savedqueryvisualizationid asc" 96 | }; 97 | 98 | var languages = XrmTranslator.installedLanguages.LocaleIds; 99 | var initialLanguage = XrmTranslator.userSettings.uilanguageid; 100 | 101 | return WebApiClient.Retrieve(queryRequest) 102 | .then(function (response) { 103 | 104 | var charts = response.value; 105 | var requests = []; 106 | 107 | for (var i = 0; i < charts.length; i++) { 108 | var chart = charts[i]; 109 | 110 | var retrieveLabelsRequest = WebApiClient.Requests.RetrieveLocLabelsRequest 111 | .with({ 112 | urlParams: { 113 | EntityMoniker: "{'@odata.id':'savedqueryvisualizations(" + chart.savedqueryvisualizationid + ")'}", 114 | AttributeName: "'name'", 115 | IncludeUnpublished: true 116 | } 117 | }) 118 | 119 | var prop = WebApiClient.Promise.props({ 120 | recid: chart.savedqueryvisualizationid, 121 | labels: WebApiClient.Execute(retrieveLabelsRequest) 122 | }); 123 | 124 | requests.push(prop); 125 | } 126 | 127 | return WebApiClient.Promise.all(requests); 128 | }) 129 | .then(function (responses) { 130 | var charts = responses; 131 | XrmTranslator.metadata = charts; 132 | 133 | FillTable(); 134 | }) 135 | .catch(XrmTranslator.errorHandler); 136 | } 137 | 138 | ChartHandler.Save = function () { 139 | XrmTranslator.LockGrid("Saving"); 140 | 141 | var updates = GetUpdates(); 142 | var requests = []; 143 | 144 | for (var i = 0; i < updates.length; i++) { 145 | var update = updates[i]; 146 | 147 | var request = WebApiClient.Requests.SetLocLabelsRequest 148 | .with({ 149 | payload: { 150 | Labels: update.labels.Label.LocalizedLabels, 151 | EntityMoniker: { 152 | "@odata.type": "Microsoft.Dynamics.CRM.savedqueryvisualization", 153 | savedqueryvisualizationid: update.recid 154 | }, 155 | AttributeName: "name" 156 | } 157 | }); 158 | 159 | requests.push(request); 160 | } 161 | 162 | return WebApiClient.Promise.resolve(requests) 163 | .each(function (request) { 164 | return WebApiClient.Execute(request); 165 | }) 166 | .then(function (response) { 167 | XrmTranslator.LockGrid("Publishing"); 168 | 169 | return XrmTranslator.Publish(); 170 | }) 171 | .then(function(response) { 172 | return XrmTranslator.AddToSolution(updates.map(function(u) { return u.recid; }), XrmTranslator.ComponentType.SavedQueryVisualization); 173 | }) 174 | .then(function(response) { 175 | return XrmTranslator.ReleaseLockAndPrompt(); 176 | }) 177 | .then(function (response) { 178 | XrmTranslator.LockGrid("Reloading"); 179 | 180 | return ChartHandler.Load(); 181 | }) 182 | .catch(XrmTranslator.errorHandler); 183 | } 184 | 185 | }(window.ChartHandler = window.ChartHandler || {})); 186 | -------------------------------------------------------------------------------- /src/js/Translator/ViewHandler.js: -------------------------------------------------------------------------------- 1 | /* @preserve 2 | * MIT License 3 | * 4 | * Copyright (c) 2017 Florian Krönert 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | * 24 | */ 25 | (function (ViewHandler, undefined) { 26 | "use strict"; 27 | 28 | function ApplyChanges(changes, labels) { 29 | for (var change in changes) { 30 | if (!changes.hasOwnProperty(change)) { 31 | continue; 32 | } 33 | 34 | // Skip empty labels 35 | if (!changes[change]) { 36 | continue; 37 | } 38 | 39 | for (var i = 0; i < labels.length; i++) { 40 | var label = labels[i]; 41 | 42 | if (label.LanguageCode == change) { 43 | label.Label = changes[change]; 44 | label.HasChanged = true; 45 | 46 | break; 47 | } 48 | 49 | // Did not find label for this language 50 | if (i === labels.length - 1) { 51 | labels.push({ LanguageCode: change, Label: changes[change] }) 52 | } 53 | } 54 | } 55 | } 56 | 57 | function GetUpdates() { 58 | var records = XrmTranslator.GetGrid().records; 59 | 60 | var updates = []; 61 | 62 | for (var i = 0; i < records.length; i++) { 63 | var record = records[i]; 64 | 65 | if (record.w2ui && record.w2ui.changes) { 66 | var view = XrmTranslator.GetAttributeByProperty("recid", record.recid); 67 | var labels = view.labels.Label.LocalizedLabels; 68 | 69 | var changes = record.w2ui.changes; 70 | 71 | ApplyChanges(changes, labels); 72 | updates.push(view); 73 | } 74 | } 75 | 76 | return updates; 77 | } 78 | 79 | function FillTable () { 80 | var grid = XrmTranslator.GetGrid(); 81 | grid.clear(); 82 | 83 | var records = []; 84 | 85 | for (var i = 0; i < XrmTranslator.metadata.length; i++) { 86 | var view = XrmTranslator.metadata[i]; 87 | 88 | var displayNames = view.labels.Label.LocalizedLabels; 89 | 90 | if (!displayNames || displayNames.length === 0) { 91 | continue; 92 | } 93 | 94 | var record = { 95 | recid: view.recid, 96 | schemaName: "View" 97 | }; 98 | 99 | for (var j = 0; j < displayNames.length; j++) { 100 | var displayName = displayNames[j]; 101 | 102 | record[displayName.LanguageCode.toString()] = displayName.Label; 103 | } 104 | 105 | records.push(record); 106 | } 107 | 108 | XrmTranslator.AddSummary(records); 109 | grid.add(records); 110 | grid.unlock(); 111 | } 112 | 113 | ViewHandler.Load = function() { 114 | var entityName = XrmTranslator.GetEntity(); 115 | 116 | var entityMetadataId = XrmTranslator.entityMetadata[entityName]; 117 | 118 | var queryRequest = { 119 | entityName: "savedquery", 120 | queryParams: "?$filter=returnedtypecode eq '" + entityName.toLowerCase() + "' and iscustomizable/Value eq true&$orderby=savedqueryid asc" 121 | }; 122 | 123 | var languages = XrmTranslator.installedLanguages.LocaleIds; 124 | var initialLanguage = XrmTranslator.userSettings.uilanguageid; 125 | 126 | return WebApiClient.Retrieve(queryRequest) 127 | .then(function(response) { 128 | var views = response.value; 129 | var requests = []; 130 | 131 | for (var i = 0; i < views.length; i++) { 132 | var view = views[i]; 133 | 134 | var retrieveLabelsRequest = WebApiClient.Requests.RetrieveLocLabelsRequest 135 | .with({ 136 | urlParams: { 137 | EntityMoniker: "{'@odata.id':'savedqueries(" + view.savedqueryid + ")'}", 138 | AttributeName: "'name'", 139 | IncludeUnpublished: true 140 | } 141 | }) 142 | 143 | var prop = WebApiClient.Promise.props({ 144 | recid: view.savedqueryid, 145 | labels: WebApiClient.Execute(retrieveLabelsRequest) 146 | }); 147 | 148 | requests.push(prop); 149 | } 150 | 151 | return WebApiClient.Promise.all(requests); 152 | }) 153 | .then(function(responses) { 154 | var views = responses; 155 | XrmTranslator.metadata = views; 156 | 157 | FillTable(); 158 | }) 159 | .catch(XrmTranslator.errorHandler); 160 | } 161 | 162 | ViewHandler.Save = function() { 163 | XrmTranslator.LockGrid("Saving"); 164 | 165 | var updates = GetUpdates(); 166 | var requests = []; 167 | 168 | for (var i = 0; i < updates.length; i++) { 169 | var update = updates[i]; 170 | 171 | var request = WebApiClient.Requests.SetLocLabelsRequest 172 | .with({ 173 | payload: { 174 | Labels: update.labels.Label.LocalizedLabels, 175 | EntityMoniker: { 176 | "@odata.type": "Microsoft.Dynamics.CRM.savedquery", 177 | savedqueryid: update.recid 178 | }, 179 | AttributeName: "name" 180 | } 181 | }); 182 | 183 | requests.push(request); 184 | } 185 | 186 | return WebApiClient.Promise.resolve(requests) 187 | .each(function(request) { 188 | return WebApiClient.Execute(request); 189 | }) 190 | .then(function (response){ 191 | XrmTranslator.LockGrid("Publishing"); 192 | 193 | return XrmTranslator.Publish(); 194 | }) 195 | .then(function(response) { 196 | return XrmTranslator.AddToSolution(updates.map(function(u) { return u.recid; }), XrmTranslator.ComponentType.SavedQuery); 197 | }) 198 | .then(function(response) { 199 | return XrmTranslator.ReleaseLockAndPrompt(); 200 | }) 201 | .then(function (response) { 202 | XrmTranslator.LockGrid("Reloading"); 203 | 204 | return ViewHandler.Load(); 205 | }) 206 | .catch(XrmTranslator.errorHandler); 207 | } 208 | } (window.ViewHandler = window.ViewHandler || {})); 209 | -------------------------------------------------------------------------------- /src/js/Translator/AttributeHandler.js: -------------------------------------------------------------------------------- 1 | /* @preserve 2 | * MIT License 3 | * 4 | * Copyright (c) 2017 Florian Krönert 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | * 24 | */ 25 | (function (AttributeHandler, undefined) { 26 | "use strict"; 27 | 28 | function ApplyChanges(changes, labels) { 29 | for (var change in changes) { 30 | if (!changes.hasOwnProperty(change)) { 31 | continue; 32 | } 33 | 34 | // Skip empty labels 35 | if (!changes[change]) { 36 | continue; 37 | } 38 | 39 | for (var i = 0; i < labels.length; i++) { 40 | var label = labels[i]; 41 | 42 | if (label.LanguageCode == change) { 43 | label.Label = changes[change]; 44 | label.HasChanged = true; 45 | 46 | break; 47 | } 48 | 49 | // Did not find label for this language 50 | if (i === labels.length - 1) { 51 | labels.push({ LanguageCode: change, Label: changes[change] }) 52 | } 53 | } 54 | } 55 | } 56 | 57 | function GetUpdates() { 58 | var records = XrmTranslator.GetGrid().records; 59 | 60 | var updates = []; 61 | 62 | for (var i = 0; i < records.length; i++) { 63 | var record = records[i]; 64 | 65 | if (record.w2ui && record.w2ui.changes) { 66 | var attribute = XrmTranslator.GetAttributeById (record.recid); 67 | var labels = attribute[XrmTranslator.GetComponent()].LocalizedLabels; 68 | 69 | var changes = record.w2ui.changes; 70 | 71 | ApplyChanges(changes, labels); 72 | updates.push(attribute); 73 | } 74 | } 75 | 76 | return updates; 77 | } 78 | 79 | function FillTable () { 80 | var grid = XrmTranslator.GetGrid(); 81 | grid.clear(); 82 | 83 | var records = []; 84 | 85 | var excludedColumns = XrmTranslator.metadata.reduce(function(all, attribute) { 86 | // If attribute has a formula definition, it is a rollup field. 87 | // Their accompanying fields for date, state and base cause CRM exceptions when being translated, so we need to skip these 88 | if (attribute.FormulaDefinition) { 89 | if (attribute.AttributeType === "Money") { 90 | /// Skip _Base, _Date, _State 91 | all.push(attribute.SchemaName + "_Base", attribute.SchemaName + "_Date", attribute.SchemaName + "_State"); 92 | } 93 | else { 94 | // Skip _Date, _State 95 | all.push(attribute.SchemaName + "_Date", attribute.SchemaName + "_State"); 96 | } 97 | } 98 | // Some attributes such as versionnumber can not be renamed / translated 99 | else if (attribute.IsRenameable && !attribute.IsRenameable.Value) { 100 | all.push(attribute.SchemaName); 101 | } 102 | 103 | return all; 104 | }, []); 105 | 106 | for (var i = 0; i < XrmTranslator.metadata.length; i++) { 107 | var attribute = XrmTranslator.metadata[i]; 108 | 109 | if (excludedColumns.indexOf(attribute.SchemaName) !== -1) { 110 | continue; 111 | } 112 | 113 | var displayNames = attribute[XrmTranslator.GetComponent()].LocalizedLabels; 114 | 115 | if (!displayNames || displayNames.length === 0) { 116 | continue; 117 | } 118 | 119 | var record = { 120 | recid: attribute.MetadataId, 121 | schemaName: attribute.SchemaName 122 | }; 123 | 124 | for (var j = 0; j < displayNames.length; j++) { 125 | var displayName = displayNames[j]; 126 | 127 | record[displayName.LanguageCode.toString()] = displayName.Label; 128 | } 129 | 130 | records.push(record); 131 | } 132 | 133 | XrmTranslator.AddSummary(records); 134 | grid.add(records); 135 | grid.unlock(); 136 | } 137 | 138 | AttributeHandler.Load = function() { 139 | var entityName = XrmTranslator.GetEntity(); 140 | 141 | var entityMetadataId = XrmTranslator.entityMetadata[entityName]; 142 | 143 | var request = { 144 | entityName: "EntityDefinition", 145 | entityId: entityMetadataId, 146 | queryParams: "/Attributes?$filter=IsCustomizable/Value eq true" 147 | }; 148 | 149 | return WebApiClient.Retrieve(request) 150 | .then(function(response) { 151 | var attributes = response.value.sort(XrmTranslator.SchemaNameComparer); 152 | XrmTranslator.metadata = attributes; 153 | 154 | FillTable(); 155 | }) 156 | .catch(XrmTranslator.errorHandler); 157 | } 158 | 159 | AttributeHandler.Save = function() { 160 | XrmTranslator.LockGrid("Saving"); 161 | 162 | var updates = GetUpdates(); 163 | 164 | var requests = []; 165 | var entityUrl = WebApiClient.GetApiUrl() + "EntityDefinitions(" + XrmTranslator.GetEntityId() + ")/Attributes("; 166 | 167 | for (var i = 0; i < updates.length; i++) { 168 | var update = updates[i]; 169 | var url = entityUrl + update.MetadataId + ")"; 170 | 171 | var request = { 172 | method: "PUT", 173 | url: url, 174 | attribute: update, 175 | headers: [{key: "MSCRM.MergeLabels", value: "true"}] 176 | }; 177 | requests.push(request); 178 | } 179 | 180 | return WebApiClient.Promise.resolve(requests) 181 | .each(function(request) { 182 | return WebApiClient.SendRequest(request.method, request.url, request.attribute, request.headers); 183 | }) 184 | .then(function (response){ 185 | XrmTranslator.LockGrid("Publishing"); 186 | 187 | return XrmTranslator.Publish(); 188 | }) 189 | .then(function(response) { 190 | return XrmTranslator.AddToSolution(updates.map(function(u) { return u.MetadataId; }), XrmTranslator.ComponentType.Attribute); 191 | }) 192 | .then(function(response) { 193 | return XrmTranslator.ReleaseLockAndPrompt(); 194 | }) 195 | .then(function (response) { 196 | XrmTranslator.LockGrid("Reloading"); 197 | 198 | return AttributeHandler.Load(); 199 | }) 200 | .catch(XrmTranslator.errorHandler); 201 | } 202 | } (window.AttributeHandler = window.AttributeHandler || {})); 203 | -------------------------------------------------------------------------------- /src/js/PropertyEditor/AttributePropertyHandler.js: -------------------------------------------------------------------------------- 1 | /* @preserve 2 | * MIT License 3 | * 4 | * Copyright (c) 2017 Florian Krönert 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | * 24 | */ 25 | (function (AttributePropertyHandler, undefined) { 26 | "use strict"; 27 | 28 | var levels = [{ id: "None", text: "None" }, 29 | { id: "Recommended", text: "Recommended" }, 30 | { id: "ApplicationRequired", text: "Application Required" }, 31 | { id: "SystemRequired", text: "System Required" }]; 32 | 33 | function ApplyChanges(attribute, changes) { 34 | for (var change in changes) { 35 | if (!changes.hasOwnProperty(change)) { 36 | continue; 37 | } 38 | 39 | if (change === "RequiredLevel") { 40 | // System required can not be changed or set 41 | if (attribute.RequiredLevel.Value === "SystemRequired" || changes[change] === "SystemRequired") { 42 | continue; 43 | } 44 | } 45 | 46 | if (attribute[change].ManagedPropertyLogicalName) { 47 | attribute[change].Value = changes[change]; 48 | } else { 49 | attribute[change] = changes[change]; 50 | } 51 | } 52 | } 53 | 54 | function GetUpdates() { 55 | var records = XrmPropertyEditor.GetGrid().records; 56 | 57 | var updates = []; 58 | 59 | for (var i = 0; i < records.length; i++) { 60 | var record = records[i]; 61 | 62 | if (record.w2ui && record.w2ui.changes) { 63 | var attribute = XrmPropertyEditor.GetAttributeById (record.recid); 64 | 65 | var changes = record.w2ui.changes; 66 | 67 | ApplyChanges(attribute, changes); 68 | updates.push(attribute); 69 | } 70 | } 71 | 72 | return updates; 73 | } 74 | 75 | function FillTable () { 76 | var grid = XrmPropertyEditor.GetGrid(); 77 | grid.clear(); 78 | 79 | var records = []; 80 | 81 | for (var i = 0; i < XrmPropertyEditor.metadata.length; i++) { 82 | var attribute = XrmPropertyEditor.metadata[i]; 83 | 84 | var record = { 85 | recid: attribute.MetadataId, 86 | schemaName: attribute.SchemaName, 87 | RequiredLevel: attribute.RequiredLevel.Value, 88 | IsAuditEnabled: attribute.IsAuditEnabled.Value, 89 | IsValidForAdvancedFind: attribute.IsValidForAdvancedFind.Value, 90 | IsSecured: attribute.IsSecured 91 | }; 92 | 93 | records.push(record); 94 | } 95 | 96 | grid.add(records); 97 | grid.unlock(); 98 | } 99 | 100 | function InitializeColumns () { 101 | var grid = XrmPropertyEditor.GetGrid(); 102 | 103 | var columnSize = 100 / 5; 104 | 105 | grid.columns = [ 106 | { field: 'schemaName', caption: 'Schema Name', size: columnSize + '%', sortable: true, resizable: true, frozen: true }, 107 | { field: 'RequiredLevel', caption: 'Required Level', size: columnSize + '%', sortable: true, resizable: true, 108 | editable: { type: 'select', items: levels, showAll: true }, 109 | render: function (record, index, col_index) { 110 | var html = ''; 111 | for (var i = 0; i < levels.length; i++) { 112 | var level = levels[i] 113 | if (level.id == this.getCellValue(index, col_index)) { 114 | html = level.text; 115 | } 116 | } 117 | return html; 118 | } 119 | }, 120 | { field: 'IsAuditEnabled', caption: 'Is Audit Enabled', size: columnSize + '%', sortable: true, resizable: true, style: 'text-align: center', 121 | editable: { type: 'checkbox', style: 'text-align: center' } 122 | }, 123 | { field: 'IsValidForAdvancedFind', caption: 'Is Valid For Advanced Find', size: columnSize + '%', sortable: true, resizable: true, style: 'text-align: center', 124 | editable: { type: 'checkbox', style: 'text-align: center' } 125 | }, 126 | { field: 'IsSecured', caption: 'Is Secured', size: columnSize + '%', sortable: true, resizable: true, style: 'text-align: center', 127 | editable: { type: 'checkbox', style: 'text-align: center' } 128 | } 129 | ]; 130 | 131 | grid.refresh(); 132 | } 133 | 134 | AttributePropertyHandler.Load = function() { 135 | XrmPropertyEditor.RestoreInitialColumns(); 136 | InitializeColumns(); 137 | var entityName = XrmPropertyEditor.GetEntity(); 138 | var entityMetadataId = XrmPropertyEditor.entityMetadata[entityName]; 139 | 140 | var request = { 141 | entityName: "EntityDefinition", 142 | entityId: entityMetadataId, 143 | queryParams: "/Attributes?$filter=IsCustomizable/Value eq true" 144 | }; 145 | 146 | WebApiClient.Retrieve(request) 147 | .then(function(response) { 148 | var attributes = response.value.sort(XrmPropertyEditor.SchemaNameComparer); 149 | XrmPropertyEditor.metadata = attributes; 150 | 151 | FillTable(); 152 | }) 153 | .catch(XrmPropertyEditor.errorHandler); 154 | } 155 | 156 | AttributePropertyHandler.Save = function() { 157 | XrmPropertyEditor.LockGrid("Saving"); 158 | 159 | var updates = GetUpdates(); 160 | 161 | var requests = []; 162 | var entityUrl = WebApiClient.GetApiUrl() + "EntityDefinitions(" + XrmPropertyEditor.GetEntityId() + ")/Attributes("; 163 | 164 | for (var i = 0; i < updates.length; i++) { 165 | var update = updates[i]; 166 | var url = entityUrl + update.MetadataId + ")"; 167 | 168 | var request = { 169 | method: "PUT", 170 | url: url, 171 | attribute: update, 172 | headers: [{key: "MSCRM.MergeLabels", value: "true"}] 173 | }; 174 | requests.push(request); 175 | } 176 | 177 | WebApiClient.Promise.resolve(requests) 178 | .each(function(request) { 179 | return WebApiClient.SendRequest(request.method, request.url, request.attribute, request.headers); 180 | }) 181 | .then(function (response){ 182 | XrmPropertyEditor.LockGrid("Publishing"); 183 | 184 | return XrmPropertyEditor.Publish(); 185 | }) 186 | .then(function (response) { 187 | XrmPropertyEditor.LockGrid("Reloading"); 188 | 189 | return AttributePropertyHandler.Load(); 190 | }) 191 | .catch(XrmPropertyEditor.errorHandler); 192 | } 193 | } (window.AttributePropertyHandler = window.AttributePropertyHandler || {})); 194 | -------------------------------------------------------------------------------- /src/js/Translator/FormMetaHandler.js: -------------------------------------------------------------------------------- 1 | /* @preserve 2 | * MIT License 3 | * 4 | * Copyright (c) 2017 Florian Krönert 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | * 24 | */ 25 | (function (FormMetaHandler, undefined) { 26 | "use strict"; 27 | 28 | function ApplyChanges(changes, labels) { 29 | for (var change in changes) { 30 | if (!changes.hasOwnProperty(change)) { 31 | continue; 32 | } 33 | 34 | // Skip empty labels 35 | if (!changes[change]) { 36 | continue; 37 | } 38 | 39 | for (var i = 0; i < labels.length; i++) { 40 | var label = labels[i]; 41 | 42 | if (label.LanguageCode == change) { 43 | label.Label = changes[change]; 44 | label.HasChanged = true; 45 | 46 | break; 47 | } 48 | 49 | // Did not find label for this language 50 | if (i === labels.length - 1) { 51 | labels.push({ LanguageCode: change, Label: changes[change] }) 52 | } 53 | } 54 | } 55 | } 56 | 57 | function GetUpdates() { 58 | var records = XrmTranslator.GetGrid().records; 59 | 60 | var updates = []; 61 | 62 | for (var i = 0; i < records.length; i++) { 63 | var record = records[i]; 64 | 65 | if (record.w2ui && record.w2ui.changes) { 66 | var view = XrmTranslator.GetAttributeByProperty("recid", record.recid); 67 | var labels = view.labels.Label.LocalizedLabels; 68 | 69 | var changes = record.w2ui.changes; 70 | 71 | ApplyChanges(changes, labels); 72 | updates.push(view); 73 | } 74 | } 75 | 76 | return updates; 77 | } 78 | 79 | function FillTable () { 80 | var grid = XrmTranslator.GetGrid(); 81 | grid.clear(); 82 | 83 | var records = []; 84 | 85 | for (var i = 0; i < XrmTranslator.metadata.length; i++) { 86 | var form = XrmTranslator.metadata[i]; 87 | 88 | var displayNames = form.labels.Label.LocalizedLabels; 89 | 90 | if (!displayNames || displayNames.length === 0) { 91 | continue; 92 | } 93 | 94 | var record = { 95 | recid: form.recid, 96 | schemaName: "Form" 97 | }; 98 | 99 | for (var j = 0; j < displayNames.length; j++) { 100 | var displayName = displayNames[j]; 101 | 102 | record[displayName.LanguageCode.toString()] = displayName.Label; 103 | } 104 | 105 | records.push(record); 106 | } 107 | 108 | XrmTranslator.AddSummary(records); 109 | grid.add(records); 110 | grid.unlock(); 111 | } 112 | 113 | FormMetaHandler.Load = function() { 114 | var entityName = XrmTranslator.GetEntity(); 115 | 116 | var entityMetadataId = XrmTranslator.entityMetadata[entityName]; 117 | 118 | var formRequest = { 119 | entityName: "systemform", 120 | queryParams: "?$filter=objecttypecode eq '" + entityName.toLowerCase() + "' and iscustomizable/Value eq true and formactivationstate eq 1" 121 | }; 122 | 123 | if (entityName.toLowerCase() === "none") { 124 | formRequest.queryParams = "?$filter=formactivationstate eq 1 and iscustomizable/Value eq true and (type eq 0 or type eq 10)" 125 | } 126 | 127 | return WebApiClient.Retrieve(formRequest) 128 | .then(function(response) { 129 | var forms = response.value; 130 | var requests = []; 131 | 132 | for (var i = 0; i < forms.length; i++) { 133 | var form = forms[i]; 134 | 135 | var retrieveLabelsRequest = WebApiClient.Requests.RetrieveLocLabelsRequest 136 | .with({ 137 | urlParams: { 138 | EntityMoniker: "{'@odata.id':'systemforms(" + form.formid + ")'}", 139 | AttributeName: "'name'", 140 | IncludeUnpublished: true 141 | } 142 | }) 143 | 144 | var prop = WebApiClient.Promise.props({ 145 | recid: form.formid, 146 | labels: WebApiClient.Execute(retrieveLabelsRequest) 147 | }); 148 | 149 | requests.push(prop); 150 | } 151 | 152 | return WebApiClient.Promise.all(requests); 153 | }) 154 | .then(function(responses) { 155 | var forms = responses; 156 | XrmTranslator.metadata = forms; 157 | 158 | FillTable(); 159 | }) 160 | .catch(XrmTranslator.errorHandler); 161 | } 162 | 163 | FormMetaHandler.Save = function() { 164 | XrmTranslator.LockGrid("Saving"); 165 | 166 | var updates = GetUpdates(); 167 | var requests = []; 168 | 169 | for (var i = 0; i < updates.length; i++) { 170 | var update = updates[i]; 171 | 172 | var request = WebApiClient.Requests.SetLocLabelsRequest 173 | .with({ 174 | payload: { 175 | Labels: update.labels.Label.LocalizedLabels, 176 | EntityMoniker: { 177 | "@odata.type": "Microsoft.Dynamics.CRM.systemform", 178 | formid: update.recid 179 | }, 180 | AttributeName: "name" 181 | } 182 | }); 183 | 184 | requests.push(request); 185 | } 186 | 187 | return WebApiClient.Promise.resolve(requests) 188 | .each(function(request) { 189 | return WebApiClient.Execute(request); 190 | }) 191 | .then(function (response){ 192 | XrmTranslator.LockGrid("Publishing"); 193 | var entityName = XrmTranslator.GetEntity(); 194 | if (entityName.toLowerCase() === "none") { 195 | return XrmTranslator.PublishDashboard(updates); 196 | } 197 | else { 198 | return XrmTranslator.Publish(); 199 | } 200 | }) 201 | .then(function(response) { 202 | if (XrmTranslator.GetEntity().toLowerCase() === "none") { 203 | return XrmTranslator.AddToSolution(updates.map(function(u) { return u.recid; }), XrmTranslator.ComponentType.SystemForm, true, true); 204 | } 205 | else { 206 | return XrmTranslator.AddToSolution(updates.map(function(u) { return u.recid; }), XrmTranslator.ComponentType.SystemForm); 207 | } 208 | }) 209 | .then(function(response) { 210 | return XrmTranslator.ReleaseLockAndPrompt(); 211 | }) 212 | .then(function (response) { 213 | XrmTranslator.LockGrid("Reloading"); 214 | 215 | return FormMetaHandler.Load(); 216 | }) 217 | .catch(XrmTranslator.errorHandler); 218 | } 219 | } (window.FormMetaHandler = window.FormMetaHandler || {})); 220 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dynamics CRM Quick Edit 2 | 3 | ## Purpose 4 | This is a tool that eases development tasks in CRM. 5 | It supports changing of existing translations and adding of new translations for all kind of CRM parts. 6 | In addition to that, you can change properties such as field security status on fields in bulk. 7 | 8 | There is an automated translation feature for missing labels, that tries to get a translation using the free Glosbe translation API. 9 | 10 | This is a beta, use at your own risk and export a backup solution before testing. 11 | 12 | ## How to use 13 | After installing the solution (download latest version [here](https://github.com/DigitalFlow/Xrm-Quick-Edit/releases)), there will be some dashboards and their requirements added to your organization. 14 | 15 | ## Translating Dynamics 365 Portals 16 | Since v3.7.0, content snippets for Dynamics 365 Portals can be translated as well. 17 | For doing so, simply choose "Content Snippet (Adx_contentsnippet)" as entity to translate and "Content" as type. 18 | You will then be able to translate the values of content snippets. 19 | Be aware that all languages of all websites are added as columns. 20 | If any values are not saved, you most probably don't have that specific language enabled for the contained website. 21 | 22 | ## Configuration 23 | There are multiple settings which you can manipulate inside the "oss_/XrmQuickEdit/config/XrmQuickEditConfig.js" webresource. 24 | 25 | ### entityWhiteList 26 | Type: Array 27 | List of entity logical names which should be available in the translation dashboard. Allows all if empty. 28 | 29 | ### hideAutoTranslate 30 | Type: boolean 31 | Define whether to hide the Auto Translate button. 32 | 33 | ### hideFindAndReplace 34 | Type: boolean 35 | Define whether to hide the Find and Replace button. 36 | 37 | ### hideLanguagesByDefault 38 | Type: boolean 39 | Define whether to hide all language columns but the current user's language by default. More columns can then be included in the grid. 40 | 41 | ### lockedLanguages 42 | Type: Array 43 | Locale IDs of Languages that should not be translatable 44 | 45 | ### solutionUniqueName 46 | Type: string 47 | If set, components that were translated will be automatically added to the solution with the defined unique name. 48 | 49 | ## Dashboards 50 | ### Translation Management Dashboard 51 | There will be a column in the translation grid for every language installed in the organization. 52 | Once the list of entities is loaded, select the one you want to translate, as well as which part. 53 | For entities and attributes you can even select, whether you want to translate the display names, or the descriptions. 54 | This does not have an effect on any of the other types right now. 55 | Just add/change the translations using inline-editing in the grid. 56 | For missing translations, you can click the Auto Translate button, which will try to find fitting translations and enter them for you. You'll first have to select the source LCID, which is the column name of the column that contains the labels that should be translated and the destination LCID, which is the column name of the column that should be translated automatically. 57 | 58 | After you did your changes, the save button will be enabled. By clicking it, the labels will be saved to CRM and the entity will be published. 59 | 60 | #### Attributes 61 | ![attributetranslation](https://cloud.githubusercontent.com/assets/4287938/23101939/9e48451e-f69f-11e6-9572-3480aa0eed8d.PNG) 62 | 63 | #### OptionSet Values 64 | ![optiontranslator](https://cloud.githubusercontent.com/assets/4287938/23101940/9e48b558-f69f-11e6-86a6-f1bcbb34fbfe.PNG) 65 | 66 | #### Views 67 | ![viewtranslator](https://cloud.githubusercontent.com/assets/4287938/23101937/9e46a95c-f69f-11e6-9340-1a810e091140.PNG) 68 | 69 | #### System Forms 70 | ![formhandler](https://cloud.githubusercontent.com/assets/4287938/23101938/9e482f2a-f69f-11e6-909f-619ddfffcdcb.PNG) 71 | 72 | Note regarding form translations: Unfortunately the CRM only returns the current user's language labels when retrieving a system form. Other language labels, even if present, are not returned. Therefore the dashboard changes the user language to each installed language and retrieves the form, for being able to display all labels. After having retrieved all of the forms, your user language is restored to your initial value again. 73 | So please note that you should not abort loading of a form, as you might end up with a different language id (which you can of course just switch back in your options). 74 | In addition to that, sometimes publishing of CRM forms does not finish, if the UI language does not match the base language. Be sure to upgrade to at least v2.6.1 of this project, because since this version, the UI language is set to the base language before saving and publishing the changes. Your initial language is restored afterwards. 75 | If you still experience issues with the latest version, please file an issue on GitHub. When publishing should get stuck, publish changes on another entity and try again afterwards. 76 | 77 | ##### Form Labels are not updated 78 | You might come across issues where you translate attributes and the labels in the form do not update appropriately. 79 | In that case, you probably overwrote the form labels for this attribute, which is why changes in the attribute label take no effect. 80 | Since v3.15.0 there is a cure for this issue: When inside the form translator, there is a button "Remove Overridden Attribute Labels", which removes all overridden attribute labels from your form. After that, your update attribute labels should display in the form as well again. 81 | 82 | > Use "Remove Overridden Attribute Labels" at your own risk. Please backup the forms before using this function by putting them in a solution and exporting it. 83 | 84 | #### System Form Names 85 | ![formmetahandler](https://cloud.githubusercontent.com/assets/4287938/23101941/9e4994f0-f69f-11e6-9c7e-e8d39aa2ce21.PNG) 86 | 87 | #### Entity Display (Collection) Names 88 | ![entityhandler](https://cloud.githubusercontent.com/assets/4287938/23101942/9e4cc1ca-f69f-11e6-8e0a-8f040380623a.PNG) 89 | 90 | #### Functions 91 | ##### Find and Replace 92 | When clicking Find and Replace, you can enter your search text as either regex (JS style) or plain text. 93 | There is an option for ignoring the case when searching for matches. 94 | When using it with regular expressions, JS regular expressions are used. This gives you also the possibility for using capture groups and reordering in your replace expression. For example when using find text: ```(Account) (.*)``` and replace text: ```$2 $1``` you can reorder the text, so that `Account Number` becomes `Number Account`. 95 | 96 | ![findandreplacestart](https://cloud.githubusercontent.com/assets/4287938/22790460/93e81880-eee6-11e6-87ef-a9761ccd821c.PNG) 97 | 98 | After the find and replace has processed all records, you will be presented with a selection dialog. 99 | Select all replacements that you want to apply and they will be changed in the grid. 100 | 101 | ![applyfindandreplace](https://cloud.githubusercontent.com/assets/4287938/22790577/f210c70e-eee6-11e6-8b86-a32fd65ba017.PNG) 102 | 103 | ### Property Editor Dashboard 104 | ![propertyeditor](https://cloud.githubusercontent.com/assets/4287938/22862381/b547167c-f12d-11e6-838c-633358003d59.PNG) 105 | 106 | This is a dashboard for changing properties on CRM parts. Currently there is support for CRM fields only. 107 | You can change the following properties in bulk: 108 | - Required Level 109 | - Is Audit Enabled 110 | - Is Valid for Advanced Find 111 | - Is Secured 112 | 113 | Afterwards hit "Save" and the updates will be sent and published. 114 | When changing the field security state on fields, you might receive the following error: 115 | ``` 116 | The user does not have full permissions to unsecure the attribute... 117 | ``` 118 | 119 | There seems to be a background workflow in CRM that works on change of these properties, if you receive above error, wait a few minutes and try again. 120 | Setting required level of a field to SystemRequired as well as trying to change a field's level from SystemRequired will not send any updates for this, since this is not allowed. 121 | 122 | 123 | ## System requirements 124 | ### CRM Version 125 | This solution is available for CRM 2016 >= 8.0, since it requires the Web API for operating. 126 | 127 | ### User permissions 128 | This tool uses a wide range of metadata operations, your user should best be system administrator. 129 | 130 | ## Tools used 131 | I used [jQuery](https://github.com/jquery/jquery) and [w2ui](https://github.com/vitmalina/w2ui) for working with the grid. 132 | Requests to the CRM are sent using my [Web API Client](https://github.com/DigitalFlow/Xrm-WebApi-Client). 133 | Automated translations are gathered using the awesome [Glosbe translation API](https://de.glosbe.com/a-api). 134 | 135 | ## License 136 | This tool is licensed under the MIT, enjoy! 137 | -------------------------------------------------------------------------------- /src/js/Translator/ContentSnippetHandler.js: -------------------------------------------------------------------------------- 1 | /* @preserve 2 | * MIT License 3 | * 4 | * Copyright (c) 2017 Florian Krönert 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | * 24 | */ 25 | (function (ContentSnippetHandler, undefined) { 26 | "use strict"; 27 | let snippets = []; 28 | let idSeparator = "|"; 29 | let websites = []; 30 | let portalLanguages = []; 31 | 32 | function GetWebsiteId (id) { 33 | var separatorIndex = id.indexOf(idSeparator); 34 | 35 | if (separatorIndex === -1) { 36 | return id; 37 | } 38 | 39 | return id.substring(0, separatorIndex); 40 | } 41 | 42 | function GetPayload (contentSnippet, websiteId, languageId, value, snippetName) { 43 | if (!websiteId || !languageId) { 44 | return undefined; 45 | } 46 | 47 | return { 48 | entityName: "adx_contentsnippet", 49 | entityId: contentSnippet ? contentSnippet.adx_contentsnippetid : undefined, 50 | entity: { 51 | adx_name: snippetName, 52 | adx_value: w2utils.decodeTags(value), 53 | "adx_websiteid@odata.bind": "/adx_websites(" + websiteId + ")", 54 | "adx_contentsnippetlanguageid@odata.bind": "/adx_websitelanguages(" + languageId + ")" 55 | } 56 | }; 57 | } 58 | 59 | function GetUpdates(records) { 60 | var updates = []; 61 | 62 | var languageList = portalLanguages; 63 | 64 | for (var i = 0; i < records.length; i++) { 65 | var record = records[i]; 66 | 67 | if (record.w2ui && record.w2ui.changes) { 68 | var websiteId = GetWebsiteId(record.recid); 69 | var snippetName = record.recid.replace(websiteId + idSeparator, ""); 70 | 71 | var changes = record.w2ui.changes; 72 | 73 | for (var change in changes) { 74 | if (!changes.hasOwnProperty(change)) { 75 | continue; 76 | } 77 | 78 | // Skip empty data 79 | if (!changes[change]) { 80 | continue; 81 | } 82 | 83 | var language = languageList.find(function(l) { return l._adx_websiteid_value === websiteId && l.adx_PortalLanguageId.adx_lcid == change }); 84 | 85 | // This will be the case when the language has not been enabled for the website this content snippet belongs to 86 | if (!language) { 87 | continue; 88 | } 89 | 90 | var snippet = snippets.find(function(s) { return s.adx_name === snippetName && s.adx_contentsnippetlanguageid && s.adx_contentsnippetlanguageid.adx_websitelanguageid === language.adx_websitelanguageid }); 91 | var update = GetPayload(snippet, websiteId, language.adx_websitelanguageid, changes[change], snippetName); 92 | 93 | if (update) { 94 | updates.push(update); 95 | } 96 | } 97 | } 98 | } 99 | 100 | return updates; 101 | } 102 | 103 | function HandleSnippets(website, websiteSnippets, records) { 104 | if (!websiteSnippets || websiteSnippets.length === 0) { 105 | return; 106 | } 107 | 108 | var record = { 109 | recid: website.adx_websiteid, 110 | schemaName: website.adx_name, 111 | w2ui: { 112 | editable: false, 113 | children: [] 114 | } 115 | }; 116 | 117 | var groupedSnippets = websiteSnippets.reduce(function(all, cur) { 118 | var key = website.adx_websiteid + idSeparator + cur.adx_name; 119 | 120 | if (all[key]) { 121 | all[key].push(cur); 122 | } 123 | else { 124 | all[key] = [ cur ]; 125 | } 126 | 127 | return all; 128 | }, {}); 129 | 130 | var keys = Object.keys(groupedSnippets); 131 | 132 | for (var j = 0; j < keys.length; j++) { 133 | var key = keys[j]; 134 | var snippetsByGroup = groupedSnippets[key]; 135 | 136 | var child = { 137 | recid: key, 138 | schemaName: key.substr(key.indexOf(idSeparator) + 1), 139 | w2ui: { 140 | hideCheckBox: true 141 | } 142 | }; 143 | 144 | for (var k = 0; k < snippetsByGroup.length; k++) { 145 | var snippet = snippetsByGroup[k]; 146 | 147 | if (!snippet.adx_contentsnippetlanguageid) { 148 | continue; 149 | } 150 | 151 | var websiteLanguage = portalLanguages.find(function(l) { return l.adx_websitelanguageid === snippet.adx_contentsnippetlanguageid.adx_websitelanguageid; }); 152 | 153 | if (!websiteLanguage) { 154 | continue; 155 | } 156 | 157 | var language = websiteLanguage.adx_PortalLanguageId.adx_lcid.toString(); 158 | child[language] = snippet.adx_value; 159 | } 160 | 161 | record.w2ui.children.push(child); 162 | } 163 | 164 | records.push(record); 165 | } 166 | 167 | function FillTable () { 168 | var grid = XrmTranslator.GetGrid(); 169 | grid.clear(); 170 | 171 | var records = []; 172 | var keys = Object.keys(websites); 173 | 174 | for (var i = 0; i < keys.length; i++) { 175 | var website = websites[keys[i]]; 176 | var websiteSnippets = snippets.filter(function(s) { return s.adx_websiteid && s.adx_websiteid.adx_websiteid === keys[i] }); 177 | 178 | HandleSnippets(website, websiteSnippets, records); 179 | } 180 | 181 | XrmTranslator.AddSummary(records); 182 | grid.add(records); 183 | grid.unlock(); 184 | } 185 | 186 | ContentSnippetHandler.Load = function () { 187 | XrmTranslator.columnRestoreNeeded = true; 188 | 189 | snippets = []; 190 | websites = []; 191 | portalLanguages = []; 192 | 193 | XrmTranslator.ClearColumns(); 194 | 195 | return TranslationHandler.FindPortalLanguages() 196 | .then(function(languages) { 197 | if (languages) { 198 | portalLanguages = languages; 199 | TranslationHandler.FillPortalLanguageCodes(portalLanguages); 200 | } 201 | 202 | return WebApiClient.Retrieve({entityName: "adx_contentsnippet", queryParams: "?$select=adx_name,adx_value&$filter=_adx_contentsnippetlanguageid_value ne null&$expand=adx_websiteid($select=adx_websiteid,adx_name),adx_contentsnippetlanguageid($select=adx_websitelanguageid)&$orderby=adx_name", returnAllPages: true }); 203 | }) 204 | .then(function(response) { 205 | snippets = response.value.map(function (s) { s.adx_value = w2utils.encodeTags(s.adx_value); return s; }); 206 | websites = snippets.reduce(function(all, cur) { 207 | if (!all[cur.adx_websiteid.adx_websiteid]) { 208 | all[cur.adx_websiteid.adx_websiteid] = cur.adx_websiteid; 209 | } 210 | 211 | return all; 212 | }, {}); 213 | 214 | FillTable() 215 | }) 216 | .catch(XrmTranslator.errorHandler); 217 | } 218 | 219 | ContentSnippetHandler.Save = function() { 220 | XrmTranslator.LockGrid("Saving"); 221 | 222 | var records = XrmTranslator.GetAllRecords(); 223 | var updates = GetUpdates(records); 224 | 225 | if (!updates || updates.length === 0) { 226 | XrmTranslator.LockGrid("Reloading"); 227 | 228 | return ContentSnippetHandler.Load(); 229 | } 230 | 231 | return WebApiClient.Promise.resolve(updates) 232 | .each(function(payload) { 233 | if (payload.entityId) { 234 | return WebApiClient.Update(payload); 235 | } 236 | else { 237 | return WebApiClient.Create(payload); 238 | } 239 | }) 240 | .then(function(response) { 241 | return XrmTranslator.ReleaseLockAndPrompt(); 242 | }) 243 | .then(function (response) { 244 | XrmTranslator.LockGrid("Reloading"); 245 | 246 | return ContentSnippetHandler.Load(); 247 | }) 248 | .catch(XrmTranslator.errorHandler); 249 | } 250 | } (window.ContentSnippetHandler = window.ContentSnippetHandler || {})); 251 | -------------------------------------------------------------------------------- /src/js/PropertyEditor/XrmPropertyEditor.js: -------------------------------------------------------------------------------- 1 | /* @preserve 2 | * MIT License 3 | * 4 | * Copyright (c) 2017 Florian Krönert 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | * 24 | */ 25 | (function (XrmPropertyEditor, undefined) { 26 | "use strict"; 27 | 28 | XrmPropertyEditor.entityMetadata = {}; 29 | XrmPropertyEditor.metadata = []; 30 | 31 | XrmPropertyEditor.entity = null; 32 | XrmPropertyEditor.type = null; 33 | 34 | var currentHandler = null; 35 | var initialColumns = [ 36 | { field: 'schemaName', caption: 'Schema Name', size: '20%', sortable: true, resizable: true, frozen: true } 37 | ]; 38 | 39 | XrmPropertyEditor.RestoreInitialColumns = function () { 40 | var grid = XrmPropertyEditor.GetGrid(); 41 | 42 | grid.columns = initialColumns; 43 | 44 | grid.refresh(); 45 | }; 46 | 47 | XrmPropertyEditor.GetEntity = function() { 48 | return w2ui.grid_toolbar.get("entitySelect").selected; 49 | } 50 | 51 | XrmPropertyEditor.GetEntityId = function() { 52 | return XrmPropertyEditor.entityMetadata[XrmPropertyEditor.GetEntity()] 53 | } 54 | 55 | XrmPropertyEditor.GetType = function() { 56 | return w2ui.grid_toolbar.get("type").selected; 57 | } 58 | 59 | function SetHandler() { 60 | if (XrmPropertyEditor.GetType() === "attributes") { 61 | currentHandler = AttributePropertyHandler; 62 | } 63 | else if (XrmPropertyEditor.GetType() === "entities") { 64 | currentHandler = EntityPropertyHandler; 65 | } 66 | } 67 | 68 | XrmPropertyEditor.errorHandler = function(error) { 69 | if(error.statusText) { 70 | w2alert(error.statusText); 71 | } 72 | else { 73 | w2alert(error); 74 | } 75 | 76 | XrmPropertyEditor.UnlockGrid(); 77 | } 78 | 79 | XrmPropertyEditor.SchemaNameComparer = function(e1, e2) { 80 | if (e1.SchemaName < e2.SchemaName) { 81 | return -1; 82 | } 83 | 84 | if (e1.SchemaName > e2.SchemaName) { 85 | return 1; 86 | } 87 | 88 | return 0; 89 | } 90 | 91 | XrmPropertyEditor.GetGrid = function() { 92 | return w2ui.grid; 93 | } 94 | 95 | XrmPropertyEditor.LockGrid = function (message) { 96 | XrmPropertyEditor.GetGrid().lock(message, true); 97 | } 98 | 99 | XrmPropertyEditor.UnlockGrid = function () { 100 | XrmPropertyEditor.GetGrid().unlock(); 101 | } 102 | 103 | XrmPropertyEditor.Publish = function() { 104 | var xml = "" + XrmPropertyEditor.GetEntity().toLowerCase() + ""; 105 | 106 | var request = WebApiClient.Requests.PublishXmlRequest 107 | .with({ 108 | payload: { 109 | ParameterXml: xml 110 | } 111 | }) 112 | return WebApiClient.Execute(request); 113 | } 114 | 115 | XrmPropertyEditor.GetRecord = function(records, selector) { 116 | for (var i = 0; i < records.length; i++) { 117 | var record = records[i]; 118 | 119 | if (selector(record)) { 120 | return record; 121 | } 122 | } 123 | 124 | return null; 125 | } 126 | 127 | XrmPropertyEditor.SetSaveButtonDisabled = function (disabled) { 128 | var saveButton = w2ui.grid_toolbar.get("w2ui-save"); 129 | saveButton.disabled = disabled; 130 | w2ui.grid_toolbar.refresh(); 131 | } 132 | 133 | XrmPropertyEditor.GetAttributeById = function(id) { 134 | return XrmPropertyEditor.GetAttributeByProperty("MetadataId", id); 135 | } 136 | 137 | XrmPropertyEditor.GetByRecId = function (records, recid) { 138 | function selector(rec) { 139 | if (rec.recid === recid) { 140 | return true; 141 | } 142 | return false; 143 | } 144 | 145 | return XrmPropertyEditor.GetRecord(records, selector); 146 | }; 147 | 148 | XrmPropertyEditor.GetAttributeByProperty = function(property, value) { 149 | for (var i = 0; i < XrmPropertyEditor.metadata.length; i++) { 150 | var attribute = XrmPropertyEditor.metadata[i]; 151 | 152 | if (attribute[property] === value) { 153 | return attribute; 154 | } 155 | } 156 | 157 | return null; 158 | } 159 | 160 | function InitializeGrid (entities) { 161 | $('#grid').w2grid({ 162 | name: 'grid', 163 | show: { 164 | toolbar: true, 165 | footer: true, 166 | toolbarSave: true, 167 | toolbarSearch: true 168 | }, 169 | multiSearch: true, 170 | searches: [ 171 | { field: 'schemaName', caption: 'Schema Name', type: 'text' } 172 | ], 173 | columns: initialColumns, 174 | onSave: function (event) { 175 | currentHandler.Save(); 176 | }, 177 | toolbar: { 178 | items: [ 179 | { type: 'menu-radio', id: 'entitySelect', img: 'icon-folder', 180 | text: function (item) { 181 | var text = item.selected; 182 | var el = this.get('entitySelect:' + item.selected); 183 | 184 | if (el) { 185 | return 'Entity: ' + el.text; 186 | } 187 | else { 188 | return "Choose entity"; 189 | } 190 | }, 191 | items: [] 192 | }, 193 | { type: 'menu-radio', id: 'type', img: 'icon-folder', 194 | text: function (item) { 195 | var text = item.selected; 196 | var el = this.get('type:' + item.selected); 197 | return 'Type: ' + el.text; 198 | }, 199 | selected: 'attributes', 200 | items: [ 201 | { id: 'attributes', text: 'Attributes', icon: 'fa-camera' } 202 | //{ id: 'entities', text: 'Entities', icon: 'fa-picture' } 203 | ] 204 | }, 205 | { type: 'button', id: 'load', text: 'Load', img:'w2ui-icon-reload', onClick: function (event) { 206 | var entity = XrmPropertyEditor.GetEntity(); 207 | 208 | if (!entity || !XrmPropertyEditor.GetType()) { 209 | return; 210 | } 211 | 212 | SetHandler(); 213 | 214 | XrmPropertyEditor.LockGrid("Loading " + entity + " attributes"); 215 | 216 | currentHandler.Load(); 217 | } } 218 | ] 219 | } 220 | }); 221 | 222 | XrmPropertyEditor.LockGrid("Loading entities"); 223 | } 224 | 225 | function FillEntitySelector (entities) { 226 | entities = entities.sort(XrmPropertyEditor.SchemaNameComparer); 227 | var entitySelect = w2ui.grid_toolbar.get("entitySelect").items; 228 | 229 | for (var i = 0; i < entities.length; i++) { 230 | var entity = entities[i]; 231 | 232 | entitySelect.push(entity.SchemaName); 233 | XrmPropertyEditor.entityMetadata[entity.SchemaName] = entity.MetadataId; 234 | } 235 | 236 | return entities; 237 | } 238 | 239 | function GetEntities() { 240 | var request = { 241 | entityName: "EntityDefinition", 242 | queryParams: "?$select=SchemaName,MetadataId&$filter=IsCustomizable/Value eq true" 243 | }; 244 | 245 | return WebApiClient.Retrieve(request); 246 | } 247 | 248 | function RegisterReloadPrevention () { 249 | // Dashboards are automatically refreshed on browser window resize, we don't want to loose changes. 250 | window.onbeforeunload = function(e) { 251 | var records = XrmPropertyEditor.GetGrid().records; 252 | var unsavedChanges = false; 253 | 254 | for (var i = 0; i < records.length; i++) { 255 | var record = records[i]; 256 | 257 | if (record.w2ui && record.w2ui.changes) { 258 | unsavedChanges = true; 259 | break; 260 | } 261 | } 262 | 263 | if (unsavedChanges) { 264 | var warning = "There are unsaved changes in the dashboard, are you sure you want to reload and discard changes?"; 265 | e.returnValue = warning; 266 | return warning; 267 | } 268 | }; 269 | } 270 | 271 | XrmPropertyEditor.Initialize = function() { 272 | InitializeGrid(); 273 | RegisterReloadPrevention(); 274 | 275 | GetEntities() 276 | .then(function(response) { 277 | return FillEntitySelector(response.value); 278 | }) 279 | .then(function () { 280 | XrmPropertyEditor.UnlockGrid(); 281 | }) 282 | .catch(XrmPropertyEditor.errorHandler); 283 | } 284 | } (window.XrmPropertyEditor = window.XrmPropertyEditor || {})); 285 | -------------------------------------------------------------------------------- /src/js/Translator/OptionSetHandler.js: -------------------------------------------------------------------------------- 1 | /* @preserve 2 | * MIT License 3 | * 4 | * Copyright (c) 2017 Florian Krönert 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | * 24 | */ 25 | (function (OptionSetHandler, undefined) { 26 | "use strict"; 27 | var idSeparator = "|"; 28 | 29 | function GetRecordId (id) { 30 | var separatorIndex = id.indexOf(idSeparator); 31 | 32 | if (separatorIndex === -1) { 33 | return id; 34 | } 35 | 36 | return id.substring(0, separatorIndex); 37 | } 38 | 39 | function GetComponent () { 40 | var component = XrmTranslator.GetComponent(); 41 | 42 | if (component === "DisplayName") { 43 | return "Label"; 44 | } 45 | 46 | return component; 47 | } 48 | 49 | function GetOptionValueUpdate (attribute, value, labels) { 50 | if (!attribute.GlobalOptionSet && !attribute.OptionSet) { 51 | throw new Error("Either the global option set or the OptionSet have to be passed!"); 52 | } 53 | 54 | var updateComponent = GetComponent(); 55 | 56 | var update = { 57 | Value: value, 58 | [updateComponent]: { 59 | LocalizedLabels: labels 60 | }, 61 | MergeLabels: true 62 | }; 63 | 64 | if (attribute.GlobalOptionSet && attribute.GlobalOptionSet.IsGlobal) { 65 | update.OptionSetName = attribute.GlobalOptionSet.Name; 66 | } 67 | else { 68 | update.EntityLogicalName = XrmTranslator.GetEntity().toLowerCase(); 69 | update.AttributeLogicalName = attribute.LogicalName; 70 | } 71 | 72 | return update; 73 | } 74 | 75 | function GetUpdateIds(records) { 76 | var optionSets = []; 77 | var globalOptionSets = []; 78 | var globalOptionSetNames = []; 79 | 80 | for (var i = 0; i < records.length; i++) { 81 | var record = records[i]; 82 | 83 | if (record.w2ui && record.w2ui.changes) { 84 | var recordId = GetRecordId(record.recid); 85 | var attribute = XrmTranslator.GetAttributeById (recordId); 86 | 87 | if (attribute.GlobalOptionSet && attribute.GlobalOptionSet.IsGlobal) { 88 | if (globalOptionSets.indexOf(attribute.GlobalOptionSet.MetadataId) === -1) { 89 | globalOptionSets.push(attribute.GlobalOptionSet.MetadataId); 90 | globalOptionSetNames.push(attribute.GlobalOptionSet.Name); 91 | } 92 | } 93 | else { 94 | if (optionSets.indexOf(recordId) === -1) { 95 | optionSets.push(recordId); 96 | } 97 | } 98 | } 99 | } 100 | 101 | return [optionSets, globalOptionSets, globalOptionSetNames]; 102 | } 103 | 104 | function GetUpdates(records) { 105 | var updates = []; 106 | 107 | for (var i = 0; i < records.length; i++) { 108 | var record = records[i]; 109 | 110 | if (record.w2ui && record.w2ui.changes) { 111 | var recordId = GetRecordId(record.recid); 112 | var attribute = XrmTranslator.GetAttributeById (recordId); 113 | var optionSetValue = parseInt(record.schemaName); 114 | var changes = record.w2ui.changes; 115 | 116 | if (optionSetValue === null || typeof(optionSetValue) === "undefined") { 117 | continue; 118 | } 119 | 120 | var labels = []; 121 | 122 | for (var change in changes) { 123 | if (!changes.hasOwnProperty(change)) { 124 | continue; 125 | } 126 | 127 | // Skip empty labels 128 | if (!changes[change]) { 129 | continue; 130 | } 131 | 132 | var label = { LanguageCode: change, Label: changes[change] }; 133 | 134 | labels.push(label); 135 | } 136 | 137 | if (labels.length < 1) { 138 | continue; 139 | } 140 | 141 | var update = GetOptionValueUpdate(attribute, optionSetValue, labels); 142 | updates.push(update); 143 | } 144 | } 145 | 146 | return updates; 147 | } 148 | 149 | function HandleOptionSets(attribute, options, records) { 150 | if (!options || options.length === 0) { 151 | return; 152 | } 153 | 154 | var record = { 155 | recid: attribute.MetadataId, 156 | schemaName: attribute.LogicalName, 157 | w2ui: { 158 | editable: false, 159 | children: [] 160 | } 161 | }; 162 | 163 | for (var j = 0; j < options.length; j++) { 164 | var option = options[j]; 165 | var labels = option[GetComponent()].LocalizedLabels; 166 | 167 | var child = { 168 | recid: record.recid + idSeparator + option.Value, 169 | schemaName: option.Value 170 | }; 171 | 172 | for (var k = 0; k < labels.length; k++) { 173 | var label = labels[k]; 174 | child[label.LanguageCode.toString()] = label.Label; 175 | } 176 | 177 | record.w2ui.children.push(child); 178 | } 179 | 180 | records.push(record); 181 | } 182 | 183 | function FillTable () { 184 | var grid = XrmTranslator.GetGrid(); 185 | grid.clear(); 186 | 187 | var records = []; 188 | 189 | for (var i = 0; i < XrmTranslator.metadata.length; i++) { 190 | var attribute = XrmTranslator.metadata[i]; 191 | var optionSet = attribute.OptionSet; 192 | 193 | if (!optionSet) { 194 | optionSet = attribute.GlobalOptionSet; 195 | } 196 | 197 | if (!!optionSet.TrueOption) { 198 | HandleOptionSets(attribute, [optionSet.TrueOption, optionSet.FalseOption], records); 199 | } 200 | else { 201 | var options = optionSet.Options; 202 | 203 | HandleOptionSets(attribute, options, records); 204 | } 205 | } 206 | 207 | XrmTranslator.AddSummary(records); 208 | grid.add(records); 209 | grid.unlock(); 210 | } 211 | 212 | OptionSetHandler.Load = function () { 213 | var entityName = XrmTranslator.GetEntity(); 214 | var entityMetadataId = XrmTranslator.entityMetadata[entityName]; 215 | 216 | var optionSetRequest = { 217 | entityName: "EntityDefinition", 218 | entityId: entityMetadataId, 219 | queryParams: "/Attributes/Microsoft.Dynamics.CRM.PicklistAttributeMetadata?$expand=OptionSet,GlobalOptionSet" 220 | }; 221 | 222 | var booleanRequest = { 223 | entityName: "EntityDefinition", 224 | entityId: entityMetadataId, 225 | queryParams: "/Attributes/Microsoft.Dynamics.CRM.BooleanAttributeMetadata?$expand=OptionSet,GlobalOptionSet" 226 | }; 227 | 228 | var statusRequest = { 229 | entityName: "EntityDefinition", 230 | entityId: entityMetadataId, 231 | queryParams: "/Attributes/Microsoft.Dynamics.CRM.StatusAttributeMetadata?$expand=OptionSet,GlobalOptionSet" 232 | }; 233 | 234 | var multiOptionSetRequest = { 235 | entityName: "EntityDefinition", 236 | entityId: entityMetadataId, 237 | queryParams: "/Attributes/Microsoft.Dynamics.CRM.MultiSelectPicklistAttributeMetadata?$expand=OptionSet,GlobalOptionSet" 238 | }; 239 | 240 | return WebApiClient.Promise.all([WebApiClient.Retrieve(optionSetRequest), WebApiClient.Retrieve(booleanRequest), WebApiClient.Retrieve(statusRequest), WebApiClient.Retrieve(multiOptionSetRequest)]) 241 | .then(function(responses){ 242 | var responseValues = responses[0].value.concat(responses[1].value).concat(responses[2].value).concat(responses[3].value); 243 | var attributes = responseValues.sort(XrmTranslator.SchemaNameComparer); 244 | 245 | XrmTranslator.metadata = attributes; 246 | 247 | FillTable(); 248 | }) 249 | .catch(XrmTranslator.errorHandler); 250 | } 251 | 252 | OptionSetHandler.Save = function() { 253 | XrmTranslator.LockGrid("Saving"); 254 | 255 | var records = XrmTranslator.GetAllRecords(); 256 | var updates = GetUpdates(records); 257 | var updateIds = GetUpdateIds(records); 258 | 259 | if (!updates || updates.length === 0) { 260 | XrmTranslator.LockGrid("Reloading"); 261 | 262 | return OptionSetHandler.Load(); 263 | } 264 | 265 | return WebApiClient.Promise.resolve(updates) 266 | .each(function(payload) { 267 | return WebApiClient.SendRequest("POST", WebApiClient.GetApiUrl() + "UpdateOptionValue", payload); 268 | }) 269 | .then(function (response){ 270 | XrmTranslator.LockGrid("Publishing"); 271 | 272 | return XrmTranslator.Publish(updateIds[2]); 273 | }) 274 | .then(function(response) { 275 | return Promise.all([ 276 | XrmTranslator.AddToSolution(updateIds[0], XrmTranslator.ComponentType.Attribute), 277 | XrmTranslator.AddToSolution(updateIds[1], XrmTranslator.ComponentType.OptionSet, true, true) 278 | ]) 279 | }) 280 | .then(function(response) { 281 | return XrmTranslator.ReleaseLockAndPrompt(); 282 | }) 283 | .then(function (response) { 284 | XrmTranslator.LockGrid("Reloading"); 285 | 286 | return OptionSetHandler.Load(); 287 | }) 288 | .catch(XrmTranslator.errorHandler); 289 | } 290 | } (window.OptionSetHandler = window.OptionSetHandler || {})); 291 | -------------------------------------------------------------------------------- /src/js/Translator/WebResourceHandler.js: -------------------------------------------------------------------------------- 1 | /* @preserve 2 | * MIT License 3 | * 4 | * Copyright (c) 2017 Florian Krönert 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | * 24 | */ 25 | (function (WebResourceHandler, undefined) { 26 | "use strict"; 27 | 28 | var idSeparator = "|"; 29 | var lcidRegex = /([0-9]+)\.js/i; 30 | 31 | function GetGroupKey (id) { 32 | var separatorIndex = id.indexOf(idSeparator); 33 | 34 | if (separatorIndex === -1) { 35 | return id; 36 | } 37 | 38 | return id.substring(0, separatorIndex); 39 | } 40 | 41 | function GetUpdates(records) { 42 | var updates = []; 43 | 44 | for (var i = 0; i < records.length; i++) { 45 | var record = records[i]; 46 | var groupKey = GetGroupKey(record.recid); 47 | 48 | if (record.w2ui && record.w2ui.changes) { 49 | var group = XrmTranslator.metadata[groupKey]; 50 | var property = record.schemaName; 51 | 52 | var changes = record.w2ui.changes; 53 | 54 | for (var change in changes) { 55 | if (!changes.hasOwnProperty(change)) { 56 | continue; 57 | } 58 | 59 | var updateRecord = group.find(function(w) { return w.__lcid === change }) || updates.find(function(w) { return w.__lcid === change }); 60 | 61 | // In this case, we need to create a new web resource 62 | if (!updateRecord) { 63 | var baseLanguageRecord = group.find(function(w) { return w.__lcid == XrmTranslator.baseLanguage }); 64 | 65 | updateRecord = { 66 | __lcid: change, 67 | webresourceid: undefined, 68 | name: baseLanguageRecord ? baseLanguageRecord.name.replace(XrmTranslator.baseLanguage.toString(), change) : groupKey + change + ".js", 69 | content: baseLanguageRecord ? Object.keys(baseLanguageRecord.content).reduce(function(all, cur) { all[cur] = null; return all; }, {}) : { } 70 | } 71 | } 72 | 73 | var value = changes[change]; 74 | updateRecord.content[property] = w2utils.decodeTags(value); 75 | 76 | if (updates.indexOf(updateRecord) === -1) { 77 | updates.push(updateRecord); 78 | } 79 | } 80 | } 81 | } 82 | 83 | return updates; 84 | } 85 | 86 | function FillKey(record, property, group) { 87 | var keyRecord = { 88 | recid: record.recid + idSeparator + property, 89 | schemaName: property 90 | }; 91 | 92 | for (var i = 0; i < group.length; i++) { 93 | var resource = group[i]; 94 | 95 | var value = resource.content[property]; 96 | 97 | if (!resource.__lcid) { 98 | continue; 99 | } 100 | 101 | keyRecord[resource.__lcid] = w2utils.encodeTags(value); 102 | } 103 | 104 | record.w2ui.children.push(keyRecord); 105 | } 106 | 107 | function FillTable () { 108 | var grid = XrmTranslator.GetGrid(); 109 | grid.clear(); 110 | 111 | var records = []; 112 | 113 | var groups = Object.keys(XrmTranslator.metadata); 114 | 115 | for (var i = 0; i < groups.length; i++) { 116 | var key = groups[i]; 117 | var group = XrmTranslator.metadata[key]; 118 | 119 | var record = { 120 | recid: key, 121 | schemaName: key, 122 | w2ui: { 123 | editable: false, 124 | children: [] 125 | } 126 | }; 127 | 128 | var properties = Array.from(new Set(group.map(function(g) { return Object.keys(g.content); }).reduce(function(all, cur) { return all.concat(cur); }, []))); 129 | 130 | for (var j = 0; j < properties.length; j++) { 131 | var property = properties[j]; 132 | 133 | FillKey(record, property, group); 134 | } 135 | 136 | records.push(record); 137 | } 138 | 139 | XrmTranslator.AddSummary(records); 140 | grid.add(records); 141 | grid.unlock(); 142 | } 143 | 144 | // https://stackoverflow.com/a/30106551 145 | function b64EncodeUnicode(str) { 146 | // first we use encodeURIComponent to get percent-encoded UTF-8, 147 | // then we convert the percent encodings into raw bytes which 148 | // can be fed into btoa. 149 | return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, 150 | function toSolidBytes(match, p1) { 151 | return String.fromCharCode('0x' + p1); 152 | })); 153 | } 154 | 155 | // https://stackoverflow.com/a/30106551 156 | function b64DecodeUnicode(str) { 157 | // Going backwards: from bytestream, to percent-encoding, to original string. 158 | return decodeURIComponent(atob(str).split('').map(function(c) { 159 | return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); 160 | }).join('')); 161 | } 162 | 163 | WebResourceHandler.Load = function() { 164 | XrmTranslator.GetBaseLanguage() 165 | .then(function(baseLanguage) { 166 | var request = { 167 | overriddenSetName: "webresourceset", 168 | queryParams: "?$select=webresourceid,name,content&$filter=contains(name, '" + baseLanguage + ".js')" 169 | }; 170 | 171 | return WebApiClient.Promise.all([baseLanguage, WebApiClient.Retrieve(request)]); 172 | }) 173 | .then(function(r) { 174 | var baseLanguage = r[0]; 175 | var records = r[1].value; 176 | 177 | return WebApiClient.Promise.all(records.map(function(rec) { 178 | var groupingKey = rec.name.substr(0, rec.name.indexOf(baseLanguage)); 179 | 180 | return WebApiClient.Retrieve({ overriddenSetName: "webresourceset", queryParams: "?$select=webresourceid,name,content&$filter=contains(name, '" + groupingKey + "')"}) 181 | .then(function (g) { 182 | return { 183 | key: groupingKey, 184 | value: g.value.map(function(w) { 185 | try { 186 | var lcidMatches = w.name.match(lcidRegex); 187 | return Object.assign(w, { content: JSON.parse(b64DecodeUnicode(w.content)), __lcid: lcidMatches.length > 1 ? lcidMatches[1] : undefined }); 188 | } 189 | catch { 190 | return {}; 191 | } 192 | }) 193 | }; 194 | }); 195 | })); 196 | }) 197 | .then(function(responses) { 198 | var groupedResponses = responses.reduce(function(all, cur) { 199 | // Filter out resources that could not be parsed 200 | var resources = cur.value.filter(function(g) { return typeof(g.content) === "object" }); 201 | 202 | if (resources.length > 0) { 203 | all[cur.key] = resources; 204 | } 205 | 206 | return all; 207 | }, {}); 208 | 209 | XrmTranslator.metadata = groupedResponses; 210 | 211 | FillTable(); 212 | }) 213 | .catch(XrmTranslator.errorHandler); 214 | } 215 | 216 | WebResourceHandler.Save = function() { 217 | XrmTranslator.LockGrid("Saving"); 218 | 219 | var records = XrmTranslator.GetAllRecords(); 220 | var updates = GetUpdates(records); 221 | 222 | return WebApiClient.Promise.resolve(updates) 223 | .mapSeries(function(webresource) { 224 | var content = b64EncodeUnicode(JSON.stringify(webresource.content)); 225 | 226 | if (webresource.webresourceid) { 227 | return WebApiClient.Update({ 228 | overriddenSetName: "webresourceset", 229 | entityId: webresource.webresourceid, 230 | entity: { 231 | content: content 232 | } 233 | }) 234 | .then(function() { 235 | return webresource.webresourceid; 236 | }); 237 | } 238 | else { 239 | return WebApiClient.Create({ 240 | overriddenSetName: "webresourceset", 241 | entity: { 242 | name: webresource.name, 243 | displayname: webresource.name, 244 | content: content, 245 | webresourcetype: 3 246 | } 247 | }) 248 | .then(function(response) { 249 | // "Cut out" created Guid, response format is http://orgname/api/data/v8.0/webresourceset(49f117b8-287a-ea11-8106-0050568e4745) 250 | return response.substr(response.length - 37, 36) 251 | }); 252 | } 253 | }) 254 | .then(function (ids){ 255 | XrmTranslator.LockGrid("Publishing"); 256 | 257 | return XrmTranslator.PublishWebResources(ids) 258 | .then(function() { 259 | return ids; 260 | }); 261 | }) 262 | .then(function(ids) { 263 | // WebResources can't be added with defined componenent settings or DoNotIncludeSubcomponents flag set to true 264 | return XrmTranslator.AddToSolution(ids, XrmTranslator.ComponentType.WebResource, true, true); 265 | }) 266 | .then(function(response) { 267 | return XrmTranslator.ReleaseLockAndPrompt(); 268 | }) 269 | .then(function (response) { 270 | XrmTranslator.LockGrid("Reloading"); 271 | 272 | return WebResourceHandler.Load(); 273 | }) 274 | .catch(XrmTranslator.errorHandler); 275 | } 276 | } (window.WebResourceHandler = window.WebResourceHandler || {})); 277 | -------------------------------------------------------------------------------- /src/js/Translator/FormHandler.js: -------------------------------------------------------------------------------- 1 | /* @preserve 2 | * MIT License 3 | * 4 | * Copyright (c) 2017 Florian Krönert 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | * 24 | */ 25 | (function (FormHandler, undefined) { 26 | "use strict"; 27 | 28 | FormHandler.selectedForms = null; 29 | FormHandler.formsByLanguage = null; 30 | FormHandler.lastId = null; 31 | 32 | function GetParsedForm (form) { 33 | var parser = new DOMParser(); 34 | var formXml = parser.parseFromString(form.formxml, "text/xml"); 35 | 36 | return formXml; 37 | } 38 | 39 | function NodesWithIdAndLabels (node) { 40 | if (node.id && node.getElementsByTagName("labels").length > 0 && node.getElementsByTagName("control").length > 0) { 41 | return NodeFilter.FILTER_ACCEPT; 42 | } 43 | return NodeFilter.FILTER_SKIP; 44 | } 45 | 46 | function CreateTreeWalker(elementFilter, form, formXml) { 47 | if (!formXml) { 48 | formXml = GetParsedForm(form); 49 | } 50 | var treeWalker = document.createTreeWalker(formXml, NodeFilter.SHOW_ALL, elementFilter, false); 51 | 52 | return treeWalker; 53 | } 54 | 55 | function TraverseTree (treeWalker, tree) { 56 | // Dive down 57 | var child = CreateGridNode(treeWalker.firstChild()); 58 | 59 | if (!child) { 60 | return; 61 | } 62 | 63 | // Push each first child per level 64 | tree.push(child); 65 | TraverseTree(treeWalker, child.w2ui.children); 66 | 67 | // We'll dive up level to level now and add all siblings 68 | while (treeWalker.nextSibling()) { 69 | var sibling = CreateGridNode(treeWalker.currentNode); 70 | tree.push(sibling); 71 | TraverseTree(treeWalker, sibling.w2ui.children); 72 | } 73 | 74 | treeWalker.parentNode(); 75 | } 76 | 77 | function GetUpdates(records) { 78 | var updates = []; 79 | 80 | for (var i = 0; i < records.length; i++) { 81 | var record = records[i]; 82 | 83 | if (record.w2ui && record.w2ui.changes) { 84 | var changes = record.w2ui.changes; 85 | 86 | var labels = []; 87 | 88 | for (var change in changes) { 89 | if (!changes.hasOwnProperty(change)) { 90 | continue; 91 | } 92 | 93 | // Skip empty labels 94 | if (!changes[change]) { 95 | continue; 96 | } 97 | 98 | var label = { LanguageCode: change, Text: changes[change] }; 99 | labels.push(label); 100 | } 101 | 102 | if (labels.length < 1) { 103 | continue; 104 | } 105 | 106 | updates.push({ 107 | id: record.recid, 108 | labels: labels 109 | }); 110 | } 111 | } 112 | 113 | return updates; 114 | } 115 | 116 | function GetLabels(node) { 117 | var labelsNode = null; 118 | var children = node.children; 119 | 120 | for (var i = 0; i < children.length; i++) { 121 | var child = children[i]; 122 | 123 | if (child && child.tagName === "labels") { 124 | labelsNode = child; 125 | break; 126 | } 127 | } 128 | 129 | return labelsNode; 130 | } 131 | 132 | function AttachLabels(node, gridNode) { 133 | var labels = GetLabels(node); 134 | 135 | if (!labels) { 136 | return; 137 | } 138 | 139 | for (var i = 0; i < labels.children.length; i++) { 140 | var label = labels.children[i]; 141 | 142 | var text = label.attributes["description"].value; 143 | var languageCode = label.attributes["languagecode"].value; 144 | 145 | gridNode[languageCode] = text; 146 | } 147 | } 148 | 149 | function CreateGridNode (node) { 150 | if (!node) { 151 | return null; 152 | } 153 | 154 | var attributes = node.attributes; 155 | var name = ""; 156 | 157 | if (attributes["name"]) { 158 | name = attributes["name"].value; 159 | } 160 | else { 161 | // var nodeid = attributes["id"].value; 162 | name = node.tagName; 163 | } 164 | 165 | var gridNode = { 166 | recid: node.id, 167 | schemaName: name, 168 | w2ui: { 169 | children: [] 170 | } 171 | }; 172 | 173 | if (XrmTranslator.config.lockFormCells && name.toLowerCase() === "cell") { 174 | gridNode.w2ui.editable = false; 175 | } 176 | 177 | AttachLabels(node, gridNode); 178 | 179 | for (var i = 0; i < FormHandler.selectedForms.length; i++) { 180 | var node = GetById(node.id, FormHandler.selectedForms[i]); 181 | AttachLabels(node, gridNode); 182 | } 183 | 184 | return gridNode; 185 | } 186 | 187 | function FillTable () { 188 | var grid = XrmTranslator.GetGrid(); 189 | grid.clear(); 190 | 191 | var records = []; 192 | 193 | var treeWalker = CreateTreeWalker(NodesWithIdAndLabels, XrmTranslator.metadata); 194 | TraverseTree(treeWalker, records); 195 | 196 | XrmTranslator.AddSummary(records, true); 197 | grid.add(records); 198 | grid.unlock(); 199 | } 200 | 201 | function GetUserLanguageForm (forms) { 202 | for (var i = 0; i < forms.length; i++) { 203 | if (forms[i].languageCode === XrmTranslator.userSettings.uilanguageid) { 204 | return forms[i]; 205 | } 206 | } 207 | 208 | return null; 209 | } 210 | 211 | function ProcessSelection(formId) { 212 | var formsByLanguage = FormHandler.formsByLanguage; 213 | var userLanguageForms = GetUserLanguageForm(formsByLanguage).forms.value; 214 | 215 | for (var i = 0; i < userLanguageForms.length; i++) { 216 | var languageForm = userLanguageForms[i]; 217 | 218 | if (languageForm.formid === formId) { 219 | XrmTranslator.metadata = languageForm; 220 | break; 221 | } 222 | } 223 | 224 | FormHandler.selectedForms = []; 225 | for (var i = 0; i < formsByLanguage.length; i++) { 226 | var languageForms = formsByLanguage[i]; 227 | 228 | for (var j = 0; j < languageForms.forms.value.length; j++) { 229 | var languageForm = languageForms.forms.value[j]; 230 | 231 | if (languageForm.formid === formId) { 232 | FormHandler.selectedForms.push(languageForm); 233 | break; 234 | } 235 | } 236 | } 237 | 238 | FillTable(); 239 | } 240 | 241 | function ShowFormSelection () { 242 | var formsByLanguage = FormHandler.formsByLanguage; 243 | 244 | if (!w2ui.formSelectionPrompt) { 245 | $().w2form({ 246 | name: 'formSelectionPrompt', 247 | style: 'border: 0px; background-color: transparent;', 248 | formHTML: 249 | '
'+ 250 | '
'+ 251 | ' '+ 252 | '
'+ 253 | ' '+ 254 | '
'+ 255 | '
'+ 256 | '
'+ 257 | '
'+ 258 | ' '+ 259 | ' '+ 260 | '
', 261 | fields: [ 262 | { field: 'formSelection', type: 'list', required: true, html: {attr: 'style="width: 80%"'} } 263 | ], 264 | actions: { 265 | "ok": function () { 266 | this.validate(); 267 | 268 | ProcessSelection(this.record.formSelection.id); 269 | 270 | w2popup.close(); 271 | }, 272 | "cancel": function () { 273 | XrmTranslator.UnlockGrid(); 274 | w2popup.close(); 275 | } 276 | } 277 | }); 278 | } 279 | 280 | var userLanguageForms = GetUserLanguageForm(formsByLanguage).forms.value; 281 | 282 | var formItems = []; 283 | 284 | for (var i = 0; i < userLanguageForms.length; i++) { 285 | var form = userLanguageForms[i]; 286 | 287 | formItems.push({ 288 | id: form.formid, 289 | text: form.name + " - " + form.description 290 | }); 291 | } 292 | 293 | w2ui.formSelectionPrompt.record.formSelection = null; 294 | w2ui.formSelectionPrompt.fields[0].options = { items: formItems }; 295 | 296 | $().w2popup('open', { 297 | title : 'Choose Form', 298 | name : 'formSelectionPopup', 299 | body : '
', 300 | style : 'padding: 15px 0px 0px 0px', 301 | width : 500, 302 | height : 300, 303 | showMax : true, 304 | onToggle: function (event) { 305 | $(w2ui.formSelection.box).hide(); 306 | event.onComplete = function () { 307 | $(w2ui.formSelection.box).show(); 308 | w2ui.formSelection.resize(); 309 | } 310 | }, 311 | onOpen: function (event) { 312 | event.onComplete = function () { 313 | // specifying an onOpen handler instead is equivalent to specifying an onBeforeOpen handler, which would make this code execute too early and hence not deliver. 314 | $('#w2ui-popup #form').w2render('formSelectionPrompt'); 315 | } 316 | } 317 | }); 318 | } 319 | 320 | function IdFilter (node) { 321 | if (node.id == this.id) { 322 | return NodeFilter.FILTER_ACCEPT; 323 | } 324 | return NodeFilter.FILTER_SKIP; 325 | } 326 | 327 | function GetById (id, form, formXml) { 328 | var treeWalker = CreateTreeWalker(IdFilter.bind({ id: id }), form, formXml); 329 | 330 | return treeWalker.nextNode(); 331 | } 332 | 333 | function ApplyLabelUpdates (labels, updates, formXml) { 334 | for (var i = 0; i < updates.length; i++) { 335 | var update = updates[i]; 336 | 337 | for (var j = 0; j < labels.children.length; j++) { 338 | var label = labels.children[j]; 339 | 340 | if (update.LanguageCode === label.attributes["languagecode"].value) { 341 | label.attributes["description"].value = update.Text; 342 | } 343 | // We did not find it 344 | else if (j === labels.children.length - 1) { 345 | var newLabel = formXml.createElement("label"); 346 | 347 | newLabel.setAttribute("description", update.Text); 348 | newLabel.setAttribute("languagecode", update.LanguageCode); 349 | 350 | labels.appendChild(newLabel); 351 | } 352 | } 353 | } 354 | } 355 | 356 | function SerializeXml(formXml) { 357 | var serializer = new XMLSerializer(); 358 | 359 | return serializer.serializeToString(formXml); 360 | } 361 | 362 | function ApplyUpdates(updates, form, formXml) { 363 | for (var i = 0; i < updates.length; i++) { 364 | var update = updates[i]; 365 | 366 | var node = GetById(update.id, form, formXml); 367 | var labels = GetLabels(node); 368 | 369 | ApplyLabelUpdates(labels, update.labels, formXml); 370 | } 371 | 372 | var serialized = SerializeXml(formXml); 373 | 374 | return { 375 | formxml: serialized 376 | }; 377 | } 378 | 379 | function uuidv4() { 380 | return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c => 381 | (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) 382 | ); 383 | } 384 | 385 | FormHandler.RemoveOverriddenCellLabels = function() { 386 | if (!XrmTranslator.metadata || !XrmTranslator.metadata.formid) { 387 | return; 388 | } 389 | 390 | var formXml = GetParsedForm(XrmTranslator.metadata); 391 | 392 | Array.from(formXml.getElementsByTagName("cell")) 393 | .forEach(function(c) { 394 | const control = c.getElementsByTagName("control"); 395 | 396 | // We only want to fix overridden labels for attributes, all attribute controls have a datafieldname 397 | if (!control || !control.length || !control[0].getAttribute("datafieldname")) { 398 | return; 399 | } 400 | 401 | // Regenerate cell id so that MS can't find the old overridden labels 402 | c.id = uuidv4(); 403 | 404 | const labels = c.getElementsByTagName("labels"); 405 | 406 | if(labels && labels.length) { 407 | const labelsNode = labels[0]; 408 | 409 | // Remove all labels 410 | Array.from(labelsNode.getElementsByTagName("label")).forEach(function(l) { labelsNode.removeChild(l); }); 411 | } 412 | }); 413 | 414 | const serializer = new XMLSerializer(); 415 | const payload = { 416 | formxml: serializer.serializeToString(formXml) 417 | }; 418 | 419 | return FormHandler.Save(payload) 420 | .then(function() { alert("Successfully removed all cell labels!"); }) 421 | .catch(function(e) { alert("Failed to remove cell labels: " + e.message); }); 422 | }; 423 | 424 | FormHandler.Load = function () { 425 | var entityName = XrmTranslator.GetEntity(); 426 | 427 | var formRequest = { 428 | entityName: "systemform", 429 | queryParams: "?$filter=objecttypecode eq '" + entityName.toLowerCase() + "' and iscustomizable/Value eq true and formactivationstate eq 1" 430 | }; 431 | 432 | if (entityName.toLowerCase() === "none") { 433 | formRequest.queryParams = "?$filter=formactivationstate eq 1 and iscustomizable/Value eq true and (type eq 0 or type eq 10)" 434 | } 435 | 436 | var languages = XrmTranslator.installedLanguages.LocaleIds; 437 | var initialLanguage = XrmTranslator.userSettings.uilanguageid; 438 | var forms = []; 439 | var requests = []; 440 | 441 | for (var i = 0; i < languages.length; i++) { 442 | requests.push({ 443 | action: "Update", 444 | language: languages[i] 445 | }); 446 | 447 | requests.push({ 448 | action: "Retrieve", 449 | language: languages[i] 450 | }); 451 | } 452 | 453 | requests.push({ 454 | action: "Update", 455 | language: initialLanguage 456 | }); 457 | 458 | return WebApiClient.Promise.reduce(requests, function(total, request){ 459 | if (request.action === "Update") { 460 | return WebApiClient.Update({ 461 | overriddenSetName: "usersettingscollection", 462 | entityId: XrmTranslator.userId, 463 | entity: { uilanguageid: request.language } 464 | }) 465 | .then(function(response) { 466 | return total; 467 | }); 468 | } 469 | else if (request.action === "Retrieve") { 470 | return WebApiClient.Promise.props({ 471 | forms: WebApiClient.Retrieve(formRequest), 472 | languageCode: request.language 473 | }) 474 | .then(function (response) { 475 | total.push(response); 476 | 477 | return total; 478 | }); 479 | } 480 | }, []) 481 | .then(function(responses) { 482 | FormHandler.formsByLanguage = responses; 483 | 484 | if (FormHandler.lastId) { 485 | ProcessSelection(FormHandler.lastId); 486 | FormHandler.lastId = null; 487 | } 488 | else { 489 | ShowFormSelection(); 490 | } 491 | }) 492 | .catch(XrmTranslator.errorHandler); 493 | } 494 | 495 | FormHandler.Save = function(payload) { 496 | XrmTranslator.LockGrid("Saving"); 497 | 498 | var update = undefined; 499 | 500 | if (payload) { 501 | update = payload; 502 | } 503 | else { 504 | var records = XrmTranslator.GetAllRecords(); 505 | var formXml = GetParsedForm(XrmTranslator.metadata); 506 | var updates = GetUpdates(records); 507 | 508 | update = ApplyUpdates(updates, XrmTranslator.metadata, formXml); 509 | } 510 | 511 | return XrmTranslator.SetBaseLanguage(XrmTranslator.userId) 512 | .then(function() { 513 | return WebApiClient.Update({ 514 | entityName: "systemform", 515 | entityId: XrmTranslator.metadata.formid, 516 | entity: update 517 | }); 518 | }) 519 | .then(function (response){ 520 | XrmTranslator.LockGrid("Publishing"); 521 | var entityName = XrmTranslator.GetEntity(); 522 | if (entityName.toLowerCase() === "none") { 523 | return XrmTranslator.PublishDashboard([{ recid: XrmTranslator.metadata.formid }]); 524 | } 525 | else { 526 | return XrmTranslator.Publish(); 527 | } 528 | }) 529 | .then(function(response) { 530 | if (XrmTranslator.GetEntity().toLowerCase() === "none") { 531 | // Dashboards can't be added with defined componenent settings or DoNotIncludeSubcomponents flag set to true 532 | return XrmTranslator.AddToSolution([XrmTranslator.metadata.formid], XrmTranslator.ComponentType.SystemForm, true, true); 533 | } 534 | else { 535 | return XrmTranslator.AddToSolution([XrmTranslator.metadata.formid], XrmTranslator.ComponentType.SystemForm); 536 | } 537 | }) 538 | .then(function(response) { 539 | return XrmTranslator.ReleaseLockAndPrompt(); 540 | }) 541 | .then(function (response) { 542 | XrmTranslator.LockGrid("Reloading"); 543 | 544 | FormHandler.lastId = XrmTranslator.metadata.formid; 545 | return FormHandler.Load(); 546 | }) 547 | .catch(XrmTranslator.errorHandler); 548 | } 549 | } (window.FormHandler = window.FormHandler || {})); 550 | -------------------------------------------------------------------------------- /src/js/Translator/TranslationHandler.js: -------------------------------------------------------------------------------- 1 | /* @preserve 2 | * MIT License 3 | * 4 | * Copyright (c) 2017 Florian Krönert 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | * 24 | */ 25 | (function (TranslationHandler, undefined) { 26 | "use strict"; 27 | 28 | var locales = null; 29 | 30 | function GetLanguageIsoByLcid (lcid) { 31 | var locByLocales = locales.find(function(loc) { return loc.localeid === lcid; }); 32 | 33 | if (locByLocales) { 34 | return locByLocales.code.substr(0, 2); 35 | } 36 | 37 | var locByColumns = XrmTranslator.GetGrid().columns.find(function(c) { return c.field === lcid}); 38 | 39 | if (locByColumns) { 40 | return locByColumns.caption.substr(0, 2); 41 | } 42 | 43 | return null; 44 | } 45 | 46 | const deeplTranslator = function (authKey) { 47 | var baseUrl = "https://api.deepl.com/v2"; 48 | var translationApiUrl = baseUrl + "/translate?auth_key=[auth_key]&source_lang=[source_lang]&target_lang=[target_lang]&text=[text]&tag_handling=xml"; 49 | 50 | function BuildTranslationUrl (fromLanguage, destLanguage, phrase) { 51 | return translationApiUrl 52 | .replace("[auth_key]", authKey) 53 | .replace("[source_lang]", fromLanguage) 54 | .replace("[target_lang]", destLanguage) 55 | .replace("[text]", encodeURIComponent(phrase)); 56 | } 57 | 58 | this.GetTranslation = function(fromLanguage, destLanguage, phrase) { 59 | $.support.cors = true; 60 | 61 | return WebApiClient.Promise.resolve($.ajax({ 62 | url: BuildTranslationUrl(fromLanguage, destLanguage, phrase), 63 | type: "GET", 64 | crossDomain: true, 65 | dataType: "json" 66 | })); 67 | } 68 | 69 | this.AddTranslations = function(fromLcid, destLcid, updateRecords, responses) { 70 | var translations = []; 71 | 72 | for (var i = 0; i < updateRecords.length; i++) { 73 | var response = responses[i]; 74 | var updateRecord = updateRecords[i]; 75 | 76 | if (response.translations.length > 0) { 77 | var decoded = response.translations[0].text.replace(/))"\/>/gi, "$1"); 78 | var translation = w2utils.encodeTags(decoded); 79 | 80 | var record = XrmTranslator.GetByRecId(updateRecords, updateRecord.recid); 81 | 82 | if (!record) { 83 | continue; 84 | } 85 | 86 | translations.push({ 87 | recid: record.recid, 88 | schemaName: record.schemaName, 89 | column: destLcid, 90 | source: record[fromLcid], 91 | translation: translation 92 | }); 93 | } 94 | } 95 | 96 | return translations; 97 | } 98 | 99 | this.CanTranslate = function(fromLcid, destLcid) { 100 | $.support.cors = true; 101 | 102 | return WebApiClient.Promise.resolve($.ajax({ 103 | url: baseUrl + "/languages?auth_key=" + authKey, 104 | type: "GET", 105 | crossDomain: true, 106 | dataType: "json" 107 | })) 108 | .then(function(result) { 109 | const canTranslateSource = result.some(function (l) { 110 | return l.language.toLowerCase() === fromLcid.toLowerCase() 111 | }); 112 | 113 | const canTranslateTarget = result.some(function (l) { 114 | return l.language.toLowerCase() === destLcid.toLowerCase() 115 | }); 116 | 117 | return { 118 | [fromLcid]: canTranslateSource, 119 | [destLcid]: canTranslateTarget 120 | }; 121 | }); 122 | } 123 | }; 124 | 125 | const azureTranslator = function (authKey, region) { 126 | var baseUrl = "https://api.cognitive.microsofttranslator.com"; 127 | var translationApiUrl = baseUrl + "/translate?api-version=3.0&from=[source_lang]&to=[target_lang]&textType=html"; 128 | var languageUrl = baseUrl + "/languages?api-version=3.0"; 129 | 130 | function BuildTranslationUrl (fromLanguage, destLanguage) { 131 | return translationApiUrl 132 | .replace("[source_lang]", fromLanguage) 133 | .replace("[target_lang]", destLanguage); 134 | } 135 | 136 | this.GetTranslation = function(fromLanguage, destLanguage, phrase) { 137 | $.support.cors = true; 138 | 139 | const headers = { 140 | "Ocp-Apim-Subscription-Key": authKey 141 | }; 142 | 143 | if (region) { 144 | headers["Ocp-Apim-Subscription-Region"] = region; 145 | } 146 | 147 | return WebApiClient.Promise.resolve($.ajax({ 148 | url: BuildTranslationUrl(fromLanguage, destLanguage), 149 | dataType: "json", 150 | contentType: "application/json", 151 | type: "POST", 152 | data: JSON.stringify([{"Text":phrase}]), 153 | crossDomain: true, 154 | dataType: "json", 155 | headers: headers 156 | })); 157 | } 158 | 159 | this.AddTranslations = function(fromLcid, destLcid, updateRecords, responses) { 160 | var translations = []; 161 | 162 | for (var i = 0; i < updateRecords.length; i++) { 163 | var response = responses[i][0]; 164 | var updateRecord = updateRecords[i]; 165 | 166 | if (!response) { 167 | continue; 168 | } 169 | 170 | if (response.translations.length > 0) { 171 | var decoded = response.translations[0].text.replace(/))"\/>/gi, "$1"); 172 | var translation = w2utils.encodeTags(decoded); 173 | 174 | var record = XrmTranslator.GetByRecId(updateRecords, updateRecord.recid); 175 | 176 | if (!record) { 177 | continue; 178 | } 179 | 180 | translations.push({ 181 | recid: record.recid, 182 | schemaName: record.schemaName, 183 | column: destLcid, 184 | source: record[fromLcid], 185 | translation: translation 186 | }); 187 | } 188 | } 189 | 190 | return translations; 191 | } 192 | 193 | this.CanTranslate = function(fromLcid, destLcid) { 194 | $.support.cors = true; 195 | 196 | return WebApiClient.Promise.resolve($.ajax({ 197 | url: languageUrl, 198 | dataType: "json", 199 | type: "GET", 200 | crossDomain: true, 201 | headers: { 202 | "Ocp-Apim-Subscription-Key": authKey 203 | } 204 | })) 205 | .then(function(result) { 206 | const canTranslateSource = !!result.translation[fromLcid.toLowerCase()]; 207 | const canTranslateTarget = !!result.translation[destLcid.toLowerCase()]; 208 | 209 | return { 210 | [fromLcid]: canTranslateSource, 211 | [destLcid]: canTranslateTarget 212 | }; 213 | }); 214 | } 215 | }; 216 | 217 | TranslationHandler.ApplyTranslations = function (selected, results) { 218 | var grid = XrmTranslator.GetGrid(); 219 | var savable = false; 220 | 221 | for (var i = 0; i < selected.length; i++) { 222 | var select = selected[i]; 223 | 224 | var result = XrmTranslator.GetByRecId(results, select); 225 | var record = XrmTranslator.GetByRecId(XrmTranslator.GetAllRecords(), result.recid); 226 | 227 | if (!record) { 228 | continue; 229 | } 230 | 231 | if (!record.w2ui) { 232 | record["w2ui"] = {}; 233 | } 234 | 235 | if (!record.w2ui.changes) { 236 | record.w2ui["changes"] = {}; 237 | } 238 | 239 | record.w2ui.changes[result.column] = (result.w2ui &&result.w2ui.changes) ? result.w2ui.changes.translation : result.translation; 240 | savable = true; 241 | grid.refreshRow(record.recid); 242 | } 243 | 244 | if (savable) { 245 | XrmTranslator.SetSaveButtonDisabled(false); 246 | } 247 | } 248 | 249 | function ShowTranslationResults (results) { 250 | if (!w2ui.translationResultGrid) { 251 | var grid = { 252 | name: 'translationResultGrid', 253 | show: { selectColumn: true }, 254 | multiSelect: true, 255 | columns: [ 256 | { field: 'schemaName', caption: 'Schema Name', size: '25%', sortable: true, searchable: true }, 257 | { field: 'column', caption: 'Column LCID', sortable: true, searchable: true, hidden: true }, 258 | { field: 'source', caption: 'Source Text', size: '25%', sortable: true, searchable: true }, 259 | { field: 'translation', caption: 'Translated Text', size: '25%', sortable: true, searchable: true, editable: { type: 'text' } } 260 | ], 261 | records: [] 262 | }; 263 | 264 | $(function () { 265 | // initialization in memory 266 | $().w2grid(grid); 267 | }); 268 | } 269 | 270 | w2ui.translationResultGrid.clear(); 271 | w2ui.translationResultGrid.add(results); 272 | 273 | w2popup.open({ 274 | title : 'Apply Translation Results', 275 | buttons : ' '+ 276 | '', 277 | width : 900, 278 | height : 600, 279 | showMax : true, 280 | body : '
', 281 | onOpen : function (event) { 282 | event.onComplete = function () { 283 | $('#w2ui-popup #main').w2render('translationResultGrid'); 284 | w2ui.translationResultGrid.selectAll(); 285 | }; 286 | }, 287 | onToggle: function (event) { 288 | $(w2ui.translationResultGrid.box).hide(); 289 | event.onComplete = function () { 290 | $(w2ui.translationResultGrid.box).show(); 291 | w2ui.translationResultGrid.resize(); 292 | } 293 | } 294 | }); 295 | } 296 | 297 | function CreateTranslator (apiProvider, authKey, region) { 298 | switch ((apiProvider ||"").trim().toLowerCase()) { 299 | case "deepl": 300 | return new deeplTranslator(authKey); 301 | case "azure": 302 | return new azureTranslator(authKey, region); 303 | default: 304 | return null; 305 | } 306 | } 307 | 308 | function BuildError(preFallBackError, error) { 309 | return [preFallBackError, error] 310 | .filter(function(e) { return !!e }) 311 | .join("
"); 312 | } 313 | 314 | function FindTranslator(authKey, authProvider, region, fromLcid, destLcid, apiProvider, preFallBackError) { 315 | if(apiProvider !== "auto" && (authProvider ||"").trim().toLowerCase() !== apiProvider) { 316 | return WebApiClient.Promise.resolve([null, BuildError(preFallBackError, "")]); 317 | } 318 | 319 | if (!authKey) { 320 | XrmTranslator.UnlockGrid(); 321 | return WebApiClient.Promise.resolve([null, BuildError(preFallBackError, authProvider + ": Auth Key is missing, please add one in the config web resource")]); 322 | } 323 | 324 | var translator = CreateTranslator(authProvider, authKey, region); 325 | 326 | if (!translator) { 327 | XrmTranslator.UnlockGrid(); 328 | return WebApiClient.Promise.resolve([null, BuildError(preFallBackError, authProvider + ": Found not supported or missing API Provider, please set one in the config web resource (currently only 'deepl' and 'azure' are supported")]); 329 | } 330 | 331 | return translator.CanTranslate(fromLcid, destLcid) 332 | .then(function(canTranslate) { 333 | if (canTranslate[fromLcid] && canTranslate[destLcid]) { 334 | return [translator]; 335 | } 336 | 337 | const errorMsg = BuildError(preFallBackError, authProvider + " translator does not support the current languages: " + fromLcid + "(" + canTranslate[fromLcid] + "), " + destLcid + "(" + canTranslate[destLcid] + ")"); 338 | 339 | return [null, errorMsg]; 340 | }) 341 | } 342 | 343 | TranslationHandler.ProposeTranslations = function(recordsRaw, fromLcid, destLcid, translateMissing, apiProvider) { 344 | XrmTranslator.LockGrid("Translating..."); 345 | 346 | var records = !translateMissing 347 | ? recordsRaw 348 | : recordsRaw.filter(function (record) { 349 | // If original record had translation set and it was not cleared by pending changes, we skip this record 350 | if (record[destLcid] && (!record.w2ui || !record.w2ui.changes || record.w2ui.changes[destLcid]) && (translateMissing !== "missingOrIdentical" || record[fromLcid] !== record[destLcid])) { 351 | return false; 352 | } 353 | 354 | return true; 355 | }); 356 | 357 | var fromIso = GetLanguageIsoByLcid(fromLcid); 358 | var toIso = GetLanguageIsoByLcid(destLcid); 359 | 360 | if (!fromIso || !toIso) { 361 | XrmTranslator.UnlockGrid(); 362 | 363 | w2alert("Could not find source or target language mapping, source iso:" + fromIso + ", target iso: " + toIso); 364 | 365 | return; 366 | } 367 | 368 | FindTranslator(XrmTranslator.config.translationApiKey, XrmTranslator.config.translationApiProvider, XrmTranslator.config.translationApiRegion, fromIso, toIso, apiProvider) 369 | .then(function (result) { 370 | if (!result[0] && XrmTranslator.config.translationApiProviderFallback) { 371 | return FindTranslator(XrmTranslator.config.translationApiKeyFallback, XrmTranslator.config.translationApiProviderFallback, XrmTranslator.config.translationApiRegionFallback, fromIso, toIso, apiProvider, result[1]) 372 | } 373 | return result; 374 | }) 375 | .then(function(result) { 376 | var translator = result[0]; 377 | 378 | if (!translator) { 379 | w2alert(result[1]); 380 | return null; 381 | } 382 | 383 | var updateRecords = []; 384 | var translationRequests = []; 385 | 386 | for (var i = 0; i < records.length; i++) { 387 | var record = records[i]; 388 | 389 | // Skip records that have no source text 390 | if (!record[fromLcid]) { 391 | continue; 392 | } 393 | 394 | const source = XrmTranslator.config.translationExceptions && XrmTranslator.config.translationExceptions.length 395 | ? XrmTranslator.config.translationExceptions.reduce(function(all, cur) { 396 | return (all || "").replace(new RegExp(cur, "gmi"), '') 397 | }, record[fromLcid]) 398 | : record[fromLcid] 399 | 400 | updateRecords.push(record); 401 | translationRequests.push(translator.GetTranslation(fromIso, toIso, w2utils.decodeTags(source))); 402 | } 403 | 404 | return WebApiClient.Promise.all(translationRequests) 405 | .then(function (responses) { 406 | ShowTranslationResults(translator.AddTranslations(fromLcid, destLcid, updateRecords, responses)); 407 | XrmTranslator.UnlockGrid(); 408 | }); 409 | }) 410 | .catch(XrmTranslator.errorHandler); 411 | } 412 | 413 | function InitializeTranslationPrompt () { 414 | var languageItems = []; 415 | var availableLanguages = XrmTranslator.GetGrid().columns; 416 | 417 | for (var i = 0; i < availableLanguages.length; i++) { 418 | if (availableLanguages[i].field === "schemaName") { 419 | continue; 420 | } 421 | 422 | languageItems.push({ id: availableLanguages[i].field, text: availableLanguages[i].caption }); 423 | } 424 | 425 | if (!w2ui.translationPrompt) 426 | { 427 | $().w2form({ 428 | name: 'translationPrompt', 429 | style: 'border: 0px; background-color: transparent;', 430 | formHTML: 431 | '
'+ 432 | '
'+ 433 | ' '+ 434 | '
'+ 435 | ' '+ 436 | '
'+ 437 | '
'+ 438 | '
'+ 439 | ' '+ 440 | '
'+ 441 | ' '+ 442 | '
'+ 443 | '
'+ 444 | '
'+ 445 | ' '+ 446 | '
'+ 447 | ' '+ 448 | '
'+ 449 | '
'+ 450 | '
'+ 451 | ' '+ 452 | '
'+ 453 | ' '+ 454 | '
'+ 455 | '
'+ 456 | '
'+ 457 | '
'+ 458 | ' '+ 459 | ' '+ 460 | '
', 461 | fields: [ 462 | { field: 'targetLcid', type: 'list', required: true, options: { items: languageItems } }, 463 | { field: 'sourceLcid', type: 'list', required: true, options: { items: languageItems } }, 464 | { field: 'translateMissing', type: 'list', required: false, options: { items: [{id: " ", text: " " }, { id: "missing", text: "All Missing" }, { id: "missingOrIdentical", text: "All Missing Or Identical"}] } }, 465 | { field: 'apiProvider', type: 'list', required: false, options: { selected: { id: "auto" }, items: [{id: "auto", text: "Auto" }, { id: "deepl", text: "DeepL" }, { id: "azure", text: "Azure"}] } } 466 | ], 467 | actions: { 468 | "ok": function () { 469 | this.validate(); 470 | w2popup.close(); 471 | 472 | XrmTranslator.ShowRecordSelector("TranslationHandler.ProposeTranslations", [this.record.sourceLcid.id, this.record.targetLcid.id, this.record.translateMissing ? this.record.translateMissing.id.trim() : "", this.record.apiProvider ? this.record.apiProvider.id : ""], (XrmTranslator.GetGrid().getSelection() || [])); 473 | }, 474 | "cancel": function () { 475 | w2popup.close(); 476 | } 477 | } 478 | }); 479 | } 480 | else { 481 | // Columns will be different when user switches to portal content snippet or back from it, we need to make sure columns always match current grid columns 482 | w2ui.translationPrompt.fields[0].options.items = languageItems; 483 | w2ui.translationPrompt.fields[1].options.items = languageItems; 484 | 485 | w2ui.translationPrompt.refresh(); 486 | } 487 | 488 | return Promise.resolve({}); 489 | } 490 | 491 | TranslationHandler.ShowTranslationPrompt = function() { 492 | InitializeTranslationPrompt() 493 | .then(function() { 494 | $().w2popup('open', { 495 | title : 'Choose tranlations source and destination', 496 | name : 'translationPopup', 497 | body : '
', 498 | style : 'padding: 15px 0px 0px 0px', 499 | width : 500, 500 | height : 300, 501 | showMax : true, 502 | onToggle: function (event) { 503 | $(w2ui.translationPrompt.box).hide(); 504 | event.onComplete = function () { 505 | $(w2ui.translationPrompt.box).show(); 506 | w2ui.translationPrompt.resize(); 507 | } 508 | }, 509 | onOpen: function (event) { 510 | event.onComplete = function () { 511 | // specifying an onOpen handler instead is equivalent to specifying an onBeforeOpen handler, which would make this code execute too early and hence not deliver. 512 | $('#w2ui-popup #form').w2render('translationPrompt'); 513 | } 514 | } 515 | }); 516 | }); 517 | } 518 | 519 | function GetLocales () { 520 | if (locales) { 521 | return Promise.resolve(locales); 522 | } 523 | 524 | return WebApiClient.Retrieve({overriddenSetName: "languagelocale", queryParams: "?$select=language,localeid,code"}) 525 | .then(function(result) { 526 | locales = result.value; 527 | 528 | return locales; 529 | }); 530 | } 531 | 532 | TranslationHandler.GetLanguageNamesByLcids = function(lcids) { 533 | return GetLocales() 534 | .then(function (locales) { 535 | return lcids.map(function (lcid) { 536 | var locale = locales.find(function (l) { return l.localeid == lcid }) || {}; 537 | 538 | return { 539 | lcid: lcid, 540 | locale: locale.language || lcid 541 | }; 542 | }); 543 | }); 544 | } 545 | 546 | TranslationHandler.FillLanguageCodes = function(languages, userSettings, config) { 547 | var grid = XrmTranslator.GetGrid(); 548 | var languageCount = languages.length; 549 | 550 | // Reset schema name col 551 | grid.columns[0].size = XrmTranslator.defaultSchemaNameSize; 552 | 553 | return GetLocales() 554 | .then(function(locales) { 555 | // 100% full width, minus length of the schema name grid, divided by number of languages is space left for each language 556 | var columnWidth = (100 - parseInt(XrmTranslator.defaultSchemaNameSize.replace("%"))) / languageCount; 557 | 558 | for (var i = 0; i < languages.length; i++) { 559 | var language = languages[i]; 560 | var locale = locales.find(function (l) { return l.localeid == language }) || {}; 561 | 562 | var editable = config.lockedLanguages && config.lockedLanguages.indexOf(language) !== -1 ? null : { type: 'text' }; 563 | 564 | grid.addColumn({ field: language, caption: `${locale.language || language} (${locale.code})`, size: columnWidth + "%", sortable: true, editable: editable }); 565 | grid.addSearch({ field: language, caption: `${locale.language || language} (${locale.code})`, type: 'text' }); 566 | 567 | if (config.hideLanguagesByDefault && language !== userSettings.uilanguageid) { 568 | grid.hideColumn(language); 569 | } 570 | } 571 | 572 | return languages; 573 | }); 574 | } 575 | 576 | TranslationHandler.FillPortalLanguageCodes = function(portalLanguages) { 577 | var grid = XrmTranslator.GetGrid(); 578 | 579 | // Reset schema name col 580 | grid.columns[0].size = XrmTranslator.defaultSchemaNameSize; 581 | 582 | var languages = portalLanguages 583 | .reduce(function(all, cur) { if (!all[cur.adx_PortalLanguageId.adx_languagecode]) { all[cur.adx_PortalLanguageId.adx_languagecode] = cur.adx_PortalLanguageId.adx_lcid.toString() } return all; }, {}); 584 | 585 | var locales = Object.keys(languages); 586 | var columnWidth = (100 - parseInt(XrmTranslator.defaultSchemaNameSize.replace("%"))) / locales.length; 587 | 588 | for (var i = 0; i < locales.length; i++) { 589 | var locale = locales[i]; 590 | 591 | var editable = { type: 'text' }; 592 | 593 | grid.addColumn({ field: languages[locale], caption: locale, size: columnWidth + "%", sortable: true, editable: editable }); 594 | grid.addSearch({ field: languages[locale], caption: locale, type: 'text' }); 595 | } 596 | 597 | return languages; 598 | } 599 | 600 | /** 601 | * Returns object with adx_websitelanguageid as key and string lcid as value 602 | */ 603 | TranslationHandler.FindPortalLanguages = function () { 604 | return WebApiClient.Retrieve({entityName: "adx_websitelanguage", queryParams: "?$select=_adx_websiteid_value&$expand=adx_PortalLanguageId($select=adx_lcid,adx_languagecode,adx_portallanguageid)"}) 605 | .then(function (r) { 606 | const languages = r.value; 607 | languages.sort(function(a, b) { return ((a.adx_PortalLanguageId || {}).adx_languagecode || "").localeCompare((b.adx_PortalLanguageId || {}).adx_languagecode || "")}); 608 | 609 | return languages; 610 | }); 611 | } 612 | 613 | TranslationHandler.GetAvailableLanguages = function() { 614 | return WebApiClient.Execute(WebApiClient.Requests.RetrieveAvailableLanguagesRequest); 615 | } 616 | } (window.TranslationHandler = window.TranslationHandler || {})); 617 | -------------------------------------------------------------------------------- /src/js/Translator/XrmTranslator.js: -------------------------------------------------------------------------------- 1 | /* @preserve 2 | * MIT License 3 | * 4 | * Copyright (c) 2017 Florian Krönert 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | * 24 | */ 25 | (function (XrmTranslator, undefined) { 26 | "use strict"; 27 | 28 | XrmTranslator.entityMetadata = {}; 29 | XrmTranslator.metadata = []; 30 | 31 | XrmTranslator.entity = null; 32 | XrmTranslator.type = null; 33 | 34 | XrmTranslator.lockAcquired = null; 35 | 36 | // We need those for the FormHandleer, uilanguageid is current user language, formXml only contains labels for this locale by default 37 | XrmTranslator.userId = null; 38 | XrmTranslator.userSettings = null; 39 | XrmTranslator.installedLanguages = null; 40 | XrmTranslator.baseLanguage = null; 41 | 42 | XrmTranslator.config = null; 43 | 44 | XrmTranslator.columnRestoreNeeded = false; 45 | 46 | XrmTranslator.defaultSchemaNameSize = "20%"; 47 | 48 | var currentHandler = null; 49 | 50 | RegExp.escape= function(s) { 51 | return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); 52 | }; 53 | 54 | function ExpandRecord (record) { 55 | XrmTranslator.GetGrid().expand(record.recid); 56 | } 57 | 58 | function CollapseRecord (record) { 59 | XrmTranslator.GetGrid().collapse(record.recid); 60 | } 61 | 62 | function ToggleExpandCollapse (expand) { 63 | for (var i = 0; i < XrmTranslator.GetGrid().records.length; i++) { 64 | var record = XrmTranslator.GetGrid().records[i]; 65 | 66 | if (!record.w2ui || !record.w2ui.children) { 67 | continue; 68 | } 69 | 70 | if (expand) { 71 | ExpandRecord(record); 72 | } else { 73 | CollapseRecord(record); 74 | } 75 | } 76 | } 77 | 78 | XrmTranslator.ComponentType = { 79 | Entity: 1, 80 | Attribute: 2, 81 | Relationship: 3, 82 | AttributePicklistValue: 4, 83 | AttributeLookupValue: 5, 84 | ViewAttribute: 6, 85 | LocalizedLabel: 7, 86 | RelationshipExtraCondition: 8, 87 | OptionSet: 9, 88 | EntityRelationship: 10, 89 | EntityRelationshipRole: 11, 90 | EntityRelationshipRelationships: 12, 91 | ManagedProperty: 13, 92 | EntityKey: 14, 93 | Role: 20, 94 | RolePrivilege: 21, 95 | DisplayString: 22, 96 | DisplayStringMap: 23, 97 | Form: 24, 98 | Organization: 25, 99 | SavedQuery: 26, 100 | Workflow: 29, 101 | Report: 31, 102 | ReportEntity: 32, 103 | ReportCategory: 33, 104 | ReportVisibility: 34, 105 | Attachment: 35, 106 | EmailTemplate: 36, 107 | ContractTemplate: 37, 108 | KBArticleTemplate: 38, 109 | MailMergeTemplate: 39, 110 | DuplicateRule: 44, 111 | DuplicateRuleCondition: 45, 112 | EntityMap: 46, 113 | AttributeMap: 47, 114 | RibbonCommand: 48, 115 | RibbonContextGroup: 49, 116 | RibbonCustomization: 50, 117 | RibbonRule: 52, 118 | RibbonTabToCommandMap: 53, 119 | RibbonDiff: 55, 120 | SavedQueryVisualization: 59, 121 | SystemForm: 60, 122 | WebResource: 61, 123 | SiteMap: 62, 124 | ConnectionRole: 63, 125 | FieldSecurityProfile: 70, 126 | FieldPermission: 71, 127 | PluginType: 90, 128 | PluginAssembly: 91, 129 | SDKMessageProcessingStep: 92, 130 | SDKMessageProcessingStepImage: 93, 131 | ServiceEndpoint: 95, 132 | RoutingRule: 150, 133 | RoutingRuleItem: 151, 134 | SLA: 152, 135 | SLAItem: 153, 136 | ConvertRule: 154, 137 | ConvertRuleItem: 155, 138 | HierarchyRule: 65, 139 | MobileOfflineProfile: 161, 140 | MobileOfflineProfileItem: 162, 141 | SimilarityRule: 165, 142 | CustomControl: 66, 143 | CustomControlDefaultConfig: 68, 144 | }; 145 | 146 | XrmTranslator.GetEntity = function() { 147 | return w2ui.grid_toolbar.get("entitySelect").selected; 148 | } 149 | 150 | XrmTranslator.GetEntityId = function() { 151 | return XrmTranslator.entityMetadata[XrmTranslator.GetEntity()] 152 | } 153 | 154 | XrmTranslator.GetType = function() { 155 | return w2ui.grid_toolbar.get("type").selected; 156 | } 157 | 158 | XrmTranslator.GetComponent = function() { 159 | return w2ui.grid_toolbar.get("component").selected; 160 | } 161 | 162 | function SetHandler() { 163 | // Deactivate selectColumn on each change, only ContentSnippetHandler supports this right now 164 | w2ui.grid.show.selectColumn = false; 165 | 166 | w2ui['grid_toolbar'].hide("removeOverriddenAttributeLabels"); 167 | 168 | if (XrmTranslator.GetType() === "attributes") { 169 | currentHandler = AttributeHandler; 170 | } 171 | else if (XrmTranslator.GetType() === "options") { 172 | currentHandler = OptionSetHandler; 173 | } 174 | else if (["forms", "dashboards"].indexOf(XrmTranslator.GetType()) !== -1) { 175 | w2ui['grid_toolbar'].show("removeOverriddenAttributeLabels"); 176 | currentHandler = FormHandler; 177 | } 178 | else if (XrmTranslator.GetType() === "views") { 179 | currentHandler = ViewHandler; 180 | } 181 | else if (XrmTranslator.GetType() === "formMeta") { 182 | currentHandler = FormMetaHandler; 183 | } 184 | else if (XrmTranslator.GetType() === "entityMeta") { 185 | currentHandler = EntityHandler; 186 | } 187 | else if (XrmTranslator.GetType() === "charts") { 188 | currentHandler = ChartHandler; 189 | } 190 | else if (XrmTranslator.GetType() === "content") { 191 | w2ui.grid.show.selectColumn = true; 192 | currentHandler = ContentSnippetHandler; 193 | } 194 | else if (XrmTranslator.GetType() === "webresources") { 195 | currentHandler = WebResourceHandler; 196 | } 197 | 198 | w2ui.grid.refresh(); 199 | w2ui.grid_toolbar.refresh(); 200 | } 201 | 202 | XrmTranslator.errorHandler = function(error) { 203 | if(error.statusText) { 204 | w2alert(error.statusText); 205 | } 206 | else { 207 | w2alert(error); 208 | } 209 | 210 | XrmTranslator.UnlockGrid(); 211 | }; 212 | 213 | XrmTranslator.SchemaNameComparer = function(e1, e2) { 214 | if (e1.SchemaName < e2.SchemaName) { 215 | return -1; 216 | } 217 | 218 | if (e1.SchemaName > e2.SchemaName) { 219 | return 1; 220 | } 221 | 222 | return 0; 223 | }; 224 | 225 | XrmTranslator.EntityComparer = function(e1, e2) { 226 | var e1localizedLabel = e1.DisplayName.UserLocalizedLabel || {}; 227 | var e2localizedLabel = e2.DisplayName.UserLocalizedLabel || {}; 228 | 229 | var e1compareValue = (e1localizedLabel.Label || e1.SchemaName).toLowerCase(); 230 | var e2compareValue = (e2localizedLabel.Label || e2.SchemaName).toLowerCase(); 231 | 232 | if (e1compareValue < e2compareValue) { 233 | return -1; 234 | } 235 | 236 | if (e1compareValue > e2compareValue) { 237 | return 1; 238 | } 239 | 240 | return 0; 241 | }; 242 | 243 | XrmTranslator.GetGrid = function() { 244 | return w2ui.grid; 245 | }; 246 | 247 | XrmTranslator.LockGrid = function (message) { 248 | XrmTranslator.GetGrid().lock(message, true); 249 | }; 250 | 251 | XrmTranslator.UnlockGrid = function () { 252 | XrmTranslator.GetGrid().unlock(); 253 | }; 254 | 255 | XrmTranslator.SetUserLanguage = function (userId, language) { 256 | return WebApiClient.Update({ 257 | overriddenSetName: "usersettingscollection", 258 | entityId: userId, 259 | entity: { 260 | uilanguageid: language, 261 | helplanguageid: language 262 | } 263 | }); 264 | }; 265 | 266 | XrmTranslator.GetBaseLanguage = function() { 267 | if (XrmTranslator.baseLanguage) { 268 | return Promise.resolve(XrmTranslator.baseLanguage); 269 | } 270 | 271 | return WebApiClient.Retrieve({entityName: "organization"}) 272 | .then(function(orgs) { 273 | // Org exists always 274 | var org = orgs.value[0]; 275 | 276 | XrmTranslator.baseLanguage = org.languagecode; 277 | 278 | return org.languagecode; 279 | }); 280 | }; 281 | 282 | XrmTranslator.SetBaseLanguage = function (userId) { 283 | return XrmTranslator.GetBaseLanguage() 284 | .then(function(baseLanguage) { 285 | return XrmTranslator.SetUserLanguage(userId, baseLanguage); 286 | }); 287 | }; 288 | 289 | XrmTranslator.RestoreUserLanguage = function () { 290 | var initialLanguage = XrmTranslator.userSettings.uilanguageid; 291 | 292 | return XrmTranslator.SetUserLanguage(XrmTranslator.userId, initialLanguage); 293 | }; 294 | 295 | XrmTranslator.Publish = function(globalOptionSetNames) { 296 | return XrmTranslator.SetBaseLanguage(XrmTranslator.userId) 297 | .then(function() { 298 | var options = (globalOptionSetNames || []); 299 | var optionSetString = "" + options.map(function(o) { return "" + o + ""; }).join("") + ""; 300 | 301 | var xml = "" + XrmTranslator.GetEntity().toLowerCase() + "" + (options.length ? optionSetString : "") + ""; 302 | 303 | var request = WebApiClient.Requests.PublishXmlRequest 304 | .with({ 305 | payload: { 306 | ParameterXml: xml 307 | } 308 | }) 309 | return WebApiClient.Execute(request); 310 | }) 311 | .then(function() { 312 | return XrmTranslator.RestoreUserLanguage(); 313 | }) 314 | .catch(XrmTranslator.errorHandler); 315 | } 316 | 317 | XrmTranslator.PublishDashboard = function (dashboardIds) { 318 | return XrmTranslator.SetBaseLanguage(XrmTranslator.userId) 319 | .then(function () { 320 | 321 | var xml = ""; 322 | for (var i = 0; i < dashboardIds.length; i++) { 323 | xml += `{${dashboardIds[i].recid}}`; 324 | } 325 | xml += ""; 326 | 327 | var request = WebApiClient.Requests.PublishXmlRequest 328 | .with({ 329 | payload: { 330 | ParameterXml: xml 331 | } 332 | }) 333 | return WebApiClient.Execute(request); 334 | }) 335 | .then(function () { 336 | return XrmTranslator.RestoreUserLanguage(); 337 | }) 338 | .catch(XrmTranslator.errorHandler); 339 | } 340 | 341 | XrmTranslator.PublishWebResources = function (webresourceIds) { 342 | return XrmTranslator.SetBaseLanguage(XrmTranslator.userId) 343 | .then(function () { 344 | 345 | var xml = ""; 346 | for (var i = 0; i < webresourceIds.length; i++) { 347 | xml += "" + webresourceIds[i] + ""; 348 | } 349 | xml += ""; 350 | 351 | var request = WebApiClient.Requests.PublishXmlRequest 352 | .with({ 353 | payload: { 354 | ParameterXml: xml 355 | } 356 | }) 357 | return WebApiClient.Execute(request); 358 | }) 359 | .then(function () { 360 | return XrmTranslator.RestoreUserLanguage(); 361 | }) 362 | .catch(XrmTranslator.errorHandler); 363 | } 364 | 365 | XrmTranslator.AddToSolution = function(componentIds, componentType, includeComponentSettings, includeSubComponents) { 366 | if (!XrmTranslator.config.solutionUniqueName) { 367 | return Promise.resolve(null); 368 | } 369 | 370 | XrmTranslator.LockGrid("Adding components to solution"); 371 | 372 | return WebApiClient.Promise.resolve(componentIds) 373 | .each(function(c) { 374 | var request = WebApiClient.Requests.AddSolutionComponentRequest.with({ 375 | payload: { 376 | ComponentId: c, 377 | ComponentType: componentType, // Gather this from CRM SDK in SampleCode/CS/HelperCode/OptionSets.cs, named ComponentType 378 | SolutionUniqueName: XrmTranslator.config.solutionUniqueName, 379 | AddRequiredComponents: false, 380 | IncludedComponentSettingsValues: includeComponentSettings ? null : [], 381 | DoNotIncludeSubcomponents: includeSubComponents ? false : true 382 | } 383 | }); 384 | 385 | return WebApiClient.Execute(request); 386 | }) 387 | .catch(XrmTranslator.errorHandler); 388 | } 389 | 390 | XrmTranslator.GetRecord = function(records, selector) { 391 | for (var i = 0; i < records.length; i++) { 392 | var record = records[i]; 393 | 394 | if (selector(record)) { 395 | return record; 396 | } 397 | } 398 | 399 | return null; 400 | } 401 | 402 | XrmTranslator.SetSaveButtonDisabled = function (disabled) { 403 | var saveButton = w2ui.grid_toolbar.get("w2ui-save"); 404 | saveButton.disabled = disabled; 405 | w2ui.grid_toolbar.refresh(); 406 | } 407 | 408 | XrmTranslator.GetAttributeById = function(id) { 409 | return XrmTranslator.GetAttributeByProperty("MetadataId", id); 410 | } 411 | 412 | XrmTranslator.GetByRecId = function (records, recid) { 413 | function selector(rec) { 414 | if (rec.recid === recid) { 415 | return true; 416 | } 417 | return false; 418 | } 419 | 420 | return XrmTranslator.GetRecord(records, selector); 421 | }; 422 | 423 | function FlattenRecords (recs) { 424 | return recs.reduce(function(all, cur) { 425 | 426 | const children = FlattenRecords((cur.w2ui && cur.w2ui.children) ? cur.w2ui.children : []); 427 | 428 | if (children && children.length) { 429 | all = all.concat(children); 430 | } 431 | 432 | return all.concat([cur]); 433 | }, []) 434 | }; 435 | 436 | XrmTranslator.GetManyByRecId = function (records, recids) { 437 | function buildSelector(recid) { 438 | return function selector(rec) { 439 | if (rec.recid === recid) { 440 | return true; 441 | } 442 | return false; 443 | }; 444 | } 445 | 446 | var searchRecords = records || XrmTranslator.GetAllRecords(); 447 | 448 | return recids.reduce(function(all, cur) { 449 | var record = XrmTranslator.GetRecord(searchRecords, buildSelector(cur)); 450 | 451 | if (!record) { 452 | return all; 453 | } 454 | 455 | // Leave out parent nodes that are not editable 456 | if (!record.w2ui || record.w2ui.editable == null || record.w2ui.editable) { 457 | all.push(record); 458 | } 459 | 460 | return all; 461 | }, []); 462 | }; 463 | 464 | XrmTranslator.GetAttributeByProperty = function(property, value) { 465 | for (var i = 0; i < XrmTranslator.metadata.length; i++) { 466 | var attribute = XrmTranslator.metadata[i]; 467 | 468 | if (attribute[property] === value) { 469 | return attribute; 470 | } 471 | } 472 | 473 | return null; 474 | } 475 | 476 | XrmTranslator.ApplyFindAndReplace = function (selected, results) { 477 | var grid = XrmTranslator.GetGrid(); 478 | var savable = false; 479 | 480 | for (var i = 0; i < selected.length; i++) { 481 | var select = selected[i]; 482 | 483 | var result = XrmTranslator.GetByRecId(results, select); 484 | var record = XrmTranslator.GetByRecId(XrmTranslator.GetAllRecords(), result.recid); 485 | 486 | if (!record) { 487 | continue; 488 | } 489 | 490 | if (!record.w2ui) { 491 | record["w2ui"] = {}; 492 | } 493 | 494 | if (!record.w2ui.changes) { 495 | record.w2ui["changes"] = {}; 496 | } 497 | 498 | record.w2ui.changes[result.column] = (result.w2ui &&result.w2ui.changes) ? result.w2ui.changes.replaced : result.replaced; 499 | savable = true; 500 | grid.refreshRow(record.recid); 501 | } 502 | 503 | if (savable) { 504 | XrmTranslator.SetSaveButtonDisabled(false); 505 | } 506 | } 507 | 508 | function ShowFindAndReplaceResults (results) { 509 | if (!w2ui.findAndReplaceGrid) { 510 | var grid = { 511 | name: 'findAndReplaceGrid', 512 | show: { selectColumn: true }, 513 | multiSelect: true, 514 | columns: [ 515 | { field: 'schemaName', caption: 'Schema Name', size: '25%', sortable: true, searchable: true }, 516 | { field: 'column', caption: 'Column LCID', sortable: true, searchable: true, hidden: true }, 517 | { field: 'columnName', caption: 'Column', size: '25%', sortable: true, searchable: true }, 518 | { field: 'current', caption: 'Current Text', size: '25%', sortable: true, searchable: true }, 519 | { field: 'replaced', caption: 'Replaced Text', size: '25%', sortable: true, searchable: true, editable: { type: 'text' } } 520 | ], 521 | records: [] 522 | }; 523 | 524 | $(function () { 525 | // initialization in memory 526 | $().w2grid(grid); 527 | }); 528 | } 529 | 530 | w2ui.findAndReplaceGrid.clear(); 531 | w2ui.findAndReplaceGrid.add(results); 532 | 533 | w2popup.open({ 534 | title : 'Apply Find and Replace', 535 | buttons : ' '+ 536 | '', 537 | width : 900, 538 | height : 600, 539 | showMax : true, 540 | body : '
', 541 | onOpen : function (event) { 542 | event.onComplete = function () { 543 | $('#w2ui-popup #main').w2render('findAndReplaceGrid'); 544 | w2ui.findAndReplaceGrid.selectAll(); 545 | }; 546 | }, 547 | onToggle: function (event) { 548 | $(w2ui.findAndReplaceGrid.box).hide(); 549 | event.onComplete = function () { 550 | $(w2ui.findAndReplaceGrid.box).show(); 551 | w2ui.findAndReplaceGrid.resize(); 552 | } 553 | } 554 | }); 555 | } 556 | 557 | XrmTranslator.FindRecords = function(records, find, replace, useRegex, ignoreCase, column, columnName, selectRecords) { 558 | if (!records && selectRecords) { 559 | XrmTranslator.ShowRecordSelector("XrmTranslator.FindRecords", [find, replace, useRegex, ignoreCase, column, columnName, selectRecords], (XrmTranslator.GetGrid().getSelection() || [])); 560 | return; 561 | } 562 | else if (!records) { 563 | records = XrmTranslator.GetAllRecords(); 564 | } 565 | 566 | var findings = []; 567 | 568 | var regex = null; 569 | 570 | if (useRegex) { 571 | if (ignoreCase) { 572 | regex = new RegExp(find, "i"); 573 | } else { 574 | regex = new RegExp(find); 575 | } 576 | } else { 577 | if (ignoreCase) { 578 | regex = new RegExp(RegExp.escape(find), "i"); 579 | } else { 580 | regex = new RegExp(RegExp.escape(find)); 581 | } 582 | } 583 | 584 | for (var i = 0; i < records.length; i++) { 585 | var record = records[i]; 586 | var value = record[column]; 587 | 588 | if (record.w2ui && record.w2ui.changes && record.w2ui.changes[column]) { 589 | value = record.w2ui.changes[column]; 590 | } 591 | 592 | if (value === null || typeof(value) === "undefined") { 593 | continue; 594 | } 595 | 596 | var replaced = null; 597 | 598 | replaced = value.replace(regex, replace); 599 | 600 | // No hit for search and replace 601 | if (value === replaced) { 602 | continue; 603 | } 604 | 605 | findings.push({ 606 | recid: record.recid, 607 | schemaName: record.schemaName, 608 | column: column, 609 | columnName: columnName, 610 | current: value, 611 | replaced: replaced 612 | }); 613 | } 614 | 615 | ShowFindAndReplaceResults(findings); 616 | } 617 | 618 | function removeHideCheckBoxFlag (r) { 619 | if (r.w2ui && r.w2ui.hideCheckBox) { 620 | r.w2ui.hideCheckBox = false; 621 | } 622 | 623 | if (r.w2ui && r.w2ui.children) { 624 | r.w2ui.children = r.w2ui.children.map(removeHideCheckBoxFlag); 625 | } 626 | 627 | return r; 628 | } 629 | 630 | XrmTranslator.ShowRecordSelector = function (callbackName, callbackParameters, preselectedRecords) { 631 | if (!w2ui.recordSelectorGrid) { 632 | var grid = { 633 | name: 'recordSelectorGrid', 634 | show: { selectColumn: true }, 635 | multiSelect: true, 636 | columns: [ 637 | { field: 'schemaName', caption: 'Schema Name', size: '100%', sortable: true, searchable: true } 638 | ], 639 | records: [], 640 | onSelect: function(event) { 641 | const record = XrmTranslator.GetByRecId(XrmTranslator.GetAllRecords(), event.recid); 642 | 643 | if (record && record.w2ui && record.w2ui.children) { 644 | w2ui.recordSelectorGrid.expand(event.recid); 645 | record.w2ui.children.map(function(c) { return c.recid; }).forEach(function(id) { w2ui.recordSelectorGrid.select(id); }); 646 | } 647 | }, 648 | onUnselect: function(event) { 649 | const record = XrmTranslator.GetByRecId(XrmTranslator.GetAllRecords(), event.recid); 650 | 651 | if (record && record.w2ui && record.w2ui.children) { 652 | w2ui.recordSelectorGrid.expand(event.recid); 653 | record.w2ui.children.map(function(c) { return c.recid; }).forEach(function(id) { w2ui.recordSelectorGrid.unselect(id); }); 654 | } 655 | }, 656 | onExpand: function(event) { 657 | event.onComplete = function() { 658 | const record = XrmTranslator.GetByRecId(XrmTranslator.GetAllRecords(), event.recid); 659 | 660 | if (record && record.w2ui && record.w2ui.children) { 661 | record.w2ui.children.map(function(c) { return c.recid; }).forEach(function(id) { w2ui.recordSelectorGrid.expand(id); }); 662 | } 663 | }; 664 | }, 665 | onCollapse: function(event) { 666 | event.preventDefault(); 667 | } 668 | }; 669 | 670 | $(function () { 671 | // initialization in memory 672 | $().w2grid(grid); 673 | }); 674 | } 675 | 676 | w2ui.recordSelectorGrid.reset(true); 677 | w2ui.recordSelectorGrid.clear(); 678 | w2ui.recordSelectorGrid.add(JSON.parse(JSON.stringify(XrmTranslator.GetGrid().records)).map(removeHideCheckBoxFlag)); 679 | w2ui.recordSelectorGrid.refresh(); 680 | 681 | var callbackString = (callbackParameters || []).map(function(p) { return typeof(p) === "string" ? "'" + p + "'" : p + ""; }).join(","); 682 | 683 | w2popup.open({ 684 | title : 'Select Records', 685 | buttons : ' '+ 686 | '', 687 | width : 900, 688 | height : 600, 689 | showMax : true, 690 | body : '
', 691 | onOpen : function (event) { 692 | event.onComplete = function () { 693 | $('#w2ui-popup #main').w2render('recordSelectorGrid'); 694 | w2ui.recordSelectorGrid.records.slice().forEach(function(r) { w2ui.recordSelectorGrid.expand(r.recid); }); 695 | 696 | if (preselectedRecords) { 697 | for (let i = 0; i < preselectedRecords.length; i++) { 698 | const id = preselectedRecords[i]; 699 | 700 | w2ui.recordSelectorGrid.select(id); 701 | } 702 | } 703 | }; 704 | }, 705 | onToggle: function (event) { 706 | $(w2ui.recordSelectorGrid.box).hide(); 707 | event.onComplete = function () { 708 | $(w2ui.recordSelectorGrid.box).show(); 709 | w2ui.recordSelectorGrid.resize(); 710 | } 711 | } 712 | }); 713 | } 714 | 715 | function InitializeFindAndReplaceDialog() { 716 | var languageItems = []; 717 | var availableLanguages = XrmTranslator.GetGrid().columns; 718 | 719 | for (var i = 0; i < availableLanguages.length; i++) { 720 | if (availableLanguages[i].field === "schemaName") { 721 | continue; 722 | } 723 | 724 | languageItems.push({ id: availableLanguages[i].field, text: availableLanguages[i].caption }); 725 | } 726 | 727 | if (!w2ui.findAndReplace) { 728 | $().w2form({ 729 | name: 'findAndReplace', 730 | style: 'border: 0px; background-color: transparent;', 731 | formHTML: 732 | '
'+ 733 | '
'+ 734 | ' '+ 735 | '
'+ 736 | ' '+ 737 | '
'+ 738 | '
'+ 739 | '
'+ 740 | ' '+ 741 | '
'+ 742 | ' '+ 743 | '
'+ 744 | '
'+ 745 | '
'+ 746 | ' '+ 747 | '
'+ 748 | ' '+ 749 | '
'+ 750 | '
'+ 751 | '
'+ 752 | ' '+ 753 | '
'+ 754 | ' '+ 755 | '
'+ 756 | '
'+ 757 | '
'+ 758 | ' '+ 759 | '
'+ 760 | ' '+ 761 | '
'+ 762 | '
'+ 763 | '
'+ 764 | ' '+ 765 | '
'+ 766 | ' '+ 767 | '
'+ 768 | '
'+ 769 | '
'+ 770 | '
'+ 771 | ' '+ 772 | ' '+ 773 | '
', 774 | fields: [ 775 | { field: 'find', type: 'text', required: true }, 776 | { field: 'replace', type: 'text', required: true }, 777 | { field: 'regex', type: 'checkbox', required: true }, 778 | { field: 'ignoreCase', type: 'checkbox', required: true }, 779 | { field: 'selectRecords', type: 'checkbox', required: false }, 780 | { field: 'column', type: 'list', required: true, options: { items: languageItems } } 781 | ], 782 | actions: { 783 | "ok": function () { 784 | this.validate(); 785 | w2popup.close(); 786 | XrmTranslator.FindRecords(undefined, this.record.find, this.record.replace, this.record.regex, this.record.ignoreCase, this.record.column.id, this.record.column.text, this.record.selectRecords); 787 | }, 788 | "cancel": function () { 789 | w2popup.close(); 790 | } 791 | } 792 | }); 793 | } 794 | else { 795 | // Columns will be different when user switches to portal content snippet or back from it, we need to make sure columns always match current grid columns 796 | w2ui.findAndReplace.fields[4].options.items = languageItems; 797 | 798 | w2ui.findAndReplace.refresh(); 799 | } 800 | 801 | return Promise.resolve({}); 802 | } 803 | 804 | function OpenFindAndReplaceDialog () { 805 | InitializeFindAndReplaceDialog() 806 | .then(function() { 807 | $().w2popup('open', { 808 | title : 'Find and Replace', 809 | name : 'findAndReplacePopup', 810 | body : '
', 811 | style : 'padding: 15px 0px 0px 0px', 812 | width : 500, 813 | height : 300, 814 | showMax : true, 815 | onToggle: function (event) { 816 | $(w2ui.findAndReplace.box).hide(); 817 | event.onComplete = function () { 818 | $(w2ui.findAndReplace.box).show(); 819 | w2ui.findAndReplace.resize(); 820 | } 821 | }, 822 | onOpen: function (event) { 823 | event.onComplete = function () { 824 | // specifying an onOpen handler instead is equivalent to specifying an onBeforeOpen handler, which would make this code execute too early and hence not deliver. 825 | $('#w2ui-popup #form').w2render('findAndReplace'); 826 | } 827 | } 828 | }); 829 | }); 830 | } 831 | 832 | function IsLockedForUser(entity) { 833 | return WebApiClient.Retrieve({ 834 | entityName: "oss_translationlock", 835 | queryParams: "?$select=_ownerid_value&$filter=oss_name eq '" + entity + "'", 836 | headers: [ 837 | { key: "Prefer", value: 'odata.include-annotations="*"' } 838 | ] 839 | }) 840 | .then(function (response){ 841 | if (response.value.length) { 842 | return (response.value[0]._ownerid_value.toLowerCase() === Xrm.Page.context.getUserId().replace("{", "").replace("}", "").toLowerCase()); 843 | } 844 | return false; 845 | }); 846 | } 847 | 848 | function DisableColumns() { 849 | XrmTranslator.GetGrid().toolbar.set("lockOrUnlock", { img: XrmTranslator.lockAcquired ? 'w2ui-icon-pencil' : 'w2ui-icon-cross' }); 850 | 851 | w2ui['grid_toolbar'].disable("autoTranslate"); 852 | w2ui['grid_toolbar'].disable("findReplace"); 853 | 854 | XrmTranslator.GetGrid().columns.forEach(function(c) { 855 | if (c["editable"]) { 856 | c["editableBackup"] = c["editable"]; delete c["editable"]; 857 | } 858 | }); 859 | XrmTranslator.GetGrid().refresh(); 860 | } 861 | 862 | function EnableColumns() { 863 | XrmTranslator.GetGrid().toolbar.set("lockOrUnlock", { img: XrmTranslator.lockAcquired ? 'w2ui-icon-pencil' : 'w2ui-icon-cross' }); 864 | 865 | w2ui['grid_toolbar'].enable("autoTranslate"); 866 | w2ui['grid_toolbar'].enable("findReplace"); 867 | 868 | XrmTranslator.GetGrid().columns.forEach(function(c) { 869 | if (c["editableBackup"]) { 870 | c["editable"] = c["editableBackup"]; delete c["editableBackup"]; 871 | } 872 | }); 873 | XrmTranslator.GetGrid().refresh(); 874 | } 875 | 876 | function AcquireLock() { 877 | XrmTranslator.LockGrid("Acquiring lock for entity " + XrmTranslator.GetEntity().toLowerCase()); 878 | 879 | const entity = XrmTranslator.GetEntity().toLowerCase(); 880 | 881 | if (!entity) { 882 | return Promise.resolve(null); 883 | } 884 | 885 | return WebApiClient.Create({ 886 | entityName: "oss_translationlock", 887 | entity: { 888 | oss_name: entity, 889 | oss_language: "any" 890 | } 891 | }) 892 | .then(function() { 893 | XrmTranslator.lockAcquired = true; 894 | EnableColumns(); 895 | XrmTranslator.UnlockGrid(); 896 | }) 897 | .catch(function(e) { 898 | XrmTranslator.UnlockGrid(); 899 | 900 | return WebApiClient.Retrieve({ 901 | entityName: "oss_translationlock", 902 | queryParams: "?$select=_ownerid_value&$filter=oss_name eq '" + entity + "'", 903 | headers: [ 904 | { key: "Prefer", value: 'odata.include-annotations="*"' } 905 | ] 906 | }) 907 | .then(function (response){ 908 | if (response.value.length) { 909 | if (response.value[0]._ownerid_value.toLowerCase() === Xrm.Page.context.getUserId().replace("{", "").replace("}", "").toLowerCase()) { 910 | XrmTranslator.lockAcquired = true; 911 | EnableColumns(); 912 | return null; 913 | } 914 | else { 915 | alert("Failed to acquire lock, it is currently locked by " + response.value[0]["_ownerid_value@OData.Community.Display.V1.FormattedValue"] + ". Opening in readonly mode."); 916 | } 917 | } 918 | else { 919 | alert("Failed to acquire lock, error: " + (e.message || e)); 920 | } 921 | 922 | DisableColumns(); 923 | return null; 924 | }); 925 | }); 926 | } 927 | 928 | function ReleaseLock(entity) { 929 | if (!entity) { 930 | return Promise.resolve(null); 931 | } 932 | 933 | const userId = Xrm.Page.context.getUserId().replace("{", "").replace("}", ""); 934 | 935 | return WebApiClient.Retrieve({ 936 | entityName: "oss_translationlock", 937 | queryParams: "?$select=oss_translationlockid&$filter=oss_name eq '" + entity + "' and _ownerid_value eq " + userId, 938 | }) 939 | .then(function(response) { 940 | const lock = response.value.length ? response.value[0] : null; 941 | 942 | if (!lock) { 943 | return null; 944 | } 945 | 946 | return WebApiClient.Delete({ 947 | entityName: "oss_translationlock", 948 | entityId: lock.oss_translationlockid 949 | }); 950 | }) 951 | .then(function(){ 952 | XrmTranslator.lockAcquired = false; 953 | XrmTranslator.GetGrid().refresh(); 954 | }) 955 | .then(DisableColumns); 956 | } 957 | 958 | XrmTranslator.ReleaseLockAndPrompt = function(entity) { 959 | if (!XrmTranslator.config.enableLocking || !XrmTranslator.config.autoRelease) { 960 | return Promise.resolve(null); 961 | } 962 | 963 | return ReleaseLock(entity || XrmTranslator.GetEntity()) 964 | .then(function() { 965 | return new Promise(function(resolve, reject) { 966 | w2confirm("Saving is done and your lock was released.\nDo you want to reacquire your lock to continue editing?", function (answer) { 967 | resolve(answer === "Yes"); 968 | }); 969 | }); 970 | }) 971 | .then(function(reacquireLock) { 972 | if (reacquireLock) { 973 | return AcquireLock(); 974 | } 975 | 976 | return null; 977 | }); 978 | }; 979 | 980 | function LoadHandler () { 981 | var entity = XrmTranslator.GetEntity(); 982 | 983 | if (!entity || !XrmTranslator.GetType()) { 984 | return; 985 | } 986 | 987 | if (XrmTranslator.lockAcquired && entity === XrmTranslator.entity) { 988 | LockAndLoad(entity, true); 989 | } 990 | else { 991 | LockAndLoad(entity); 992 | } 993 | } 994 | 995 | function LockAndLoad (entity, lock) { 996 | if (XrmTranslator.config.enableLocking && entity) { 997 | IsLockedForUser(entity) 998 | .then(function(alreadyLockedByUser) { 999 | if(alreadyLockedByUser || lock || confirm("Do you want to lock this entity for translating? If you do not, it will be readonly.")) { 1000 | AcquireLock() 1001 | .then(function() { 1002 | TriggerLoading(entity); 1003 | }); 1004 | } 1005 | else { 1006 | XrmTranslator.lockAcquired = false; 1007 | // Refresh when moving from locked to unlocked entity and not choosing to lock 1008 | w2ui.grid_toolbar.refresh(); 1009 | DisableColumns(); 1010 | TriggerLoading(entity); 1011 | } 1012 | }); 1013 | } 1014 | else { 1015 | TriggerLoading(entity); 1016 | } 1017 | } 1018 | 1019 | function TriggerLoading(entity) { 1020 | let promise = undefined; 1021 | 1022 | if (XrmTranslator.columnRestoreNeeded) { 1023 | XrmTranslator.ClearColumns(); 1024 | promise = TranslationHandler.FillLanguageCodes(XrmTranslator.installedLanguages.LocaleIds, XrmTranslator.userSettings, XrmTranslator.config); 1025 | } 1026 | else { 1027 | promise = Promise.resolve(null); 1028 | } 1029 | 1030 | promise.then(function(){ 1031 | XrmTranslator.columnRestoreNeeded = false; 1032 | XrmTranslator.entity = entity; 1033 | SetHandler(); 1034 | 1035 | XrmTranslator.LockGrid("Loading " + entity + " attributes"); 1036 | 1037 | // Reset column sorting 1038 | XrmTranslator.GetGrid().sort(); 1039 | currentHandler.Load(); 1040 | }); 1041 | } 1042 | 1043 | function InitializeGrid (entities) { 1044 | var items = [ 1045 | { type: 'menu-radio', id: 'entitySelect', img: 'icon-folder', 1046 | text: function (item) { 1047 | var text = item.selected; 1048 | var el = this.get('entitySelect:' + item.selected); 1049 | 1050 | if (el) { 1051 | return 'Entity: ' + el.text; 1052 | } 1053 | else { 1054 | return "Choose entity"; 1055 | } 1056 | }, 1057 | selected: "none", 1058 | items: [ 1059 | { id: 'none', text: 'None' }, 1060 | { text: '--' } 1061 | ] 1062 | }, 1063 | { type: 'menu-radio', id: 'type', img: 'icon-folder', 1064 | text: function (item) { 1065 | var text = item.selected; 1066 | var el = this.get('type:' + item.selected); 1067 | return 'Type: ' + el.text; 1068 | }, 1069 | selected: 'attributes', 1070 | items: [ 1071 | { id: 'attributes', text: 'Attributes', icon: 'fa-camera' }, 1072 | { id: 'options', text: 'Options', icon: 'fa-picture' }, 1073 | { id: 'forms', text: 'Forms', icon: 'fa-picture' }, 1074 | { id: 'views', text: 'Views', icon: 'fa-picture' }, 1075 | { id: 'formMeta', text: 'Form Metadata', icon: 'fa-picture' }, 1076 | { id: 'entityMeta', text: 'Entity Metadata', icon: 'fa-picture' }, 1077 | { id: 'charts', text: 'Charts', icon: 'fa-picture' }, 1078 | { id: 'content', text: 'Content', icon: 'fa-picture' }, 1079 | { id: 'dashboards', text: 'Dashboards', icon: 'fa-picture' }, 1080 | { id: 'webresources', text: 'Web Resources', icon: 'fa-picture' } 1081 | ] 1082 | }, 1083 | { type: 'menu-radio', id: 'component', img: 'icon-folder', 1084 | text: function (item) { 1085 | var text = item.selected; 1086 | var el = this.get('component:' + item.selected); 1087 | return 'Component: ' + el.text; 1088 | }, 1089 | selected: 'DisplayName', 1090 | items: [ 1091 | { id: 'DisplayName', text: 'DisplayName', icon: 'fa-picture' }, 1092 | { id: 'Description', text: 'Description', icon: 'fa-picture' } 1093 | ] 1094 | }, 1095 | { type: 'button', id: 'load', text: 'Load', img:'w2ui-icon-reload', onClick: LoadHandler }, 1096 | { type: 'button', hidden: true, id: 'removeOverriddenAttributeLabels', text: 'Remove Overridden Attribute Labels', img:'w2ui-icon-cross', onClick: function(event) { 1097 | FormHandler.RemoveOverriddenCellLabels(); 1098 | }} 1099 | ]; 1100 | 1101 | if (!XrmTranslator.config.hideAutoTranslate) { 1102 | items.push({ type: 'button', id: 'autoTranslate', text: 'Auto Translate', img:'icon-page', onClick: function (event) { 1103 | TranslationHandler.ShowTranslationPrompt(); 1104 | } }); 1105 | } 1106 | 1107 | if (XrmTranslator.config.enableLocking) { 1108 | items.push({ type: 'menu-radio', id: 'lockOrUnlock', img: 'w2ui-icon-cross', 1109 | text: function (name, item) { 1110 | return XrmTranslator.lockAcquired ? "Locked" : "Not Locked"; 1111 | }, 1112 | items: [ 1113 | { type: 'button', id: 'lock', text: 'Lock Entity', img:'w2ui-icon-pencil' }, 1114 | { type: 'button', id: 'unlock', text: 'Unlock Entity', img:'w2ui-icon-cross' } 1115 | ] 1116 | }); 1117 | } 1118 | 1119 | items.push({ type: 'menu', id: 'toggle', img: 'icon-folder', 1120 | text: "Toggle", 1121 | items: [ 1122 | { type: 'button', text: 'Expand all records', id: 'expandAll' }, 1123 | { type: 'button', text: 'Collapse all records', id: 'collapseAll' } 1124 | ] 1125 | }); 1126 | 1127 | if (!XrmTranslator.config.hideFindAndReplace) { 1128 | items.push({ type: 'button', text: 'Find and Replace', img:'icon-page', id: 'findReplace', onClick: function (event) { 1129 | OpenFindAndReplaceDialog(); 1130 | } }); 1131 | } 1132 | 1133 | $('#grid').w2grid({ 1134 | name: 'grid', 1135 | show: { 1136 | toolbar: true, 1137 | footer: true, 1138 | toolbarSave: true, 1139 | toolbarSearch: true 1140 | }, 1141 | multiSearch: true, 1142 | searches: [ 1143 | { field: 'schemaName', caption: 'Schema Name', type: 'text' } 1144 | ], 1145 | columns: [ 1146 | { field: 'schemaName', caption: 'Schema Name', size: XrmTranslator.defaultSchemaNameSize, sortable: true, resizable: true, frozen: true } 1147 | ], 1148 | onSave: function (event) { 1149 | currentHandler.Save(); 1150 | }, 1151 | toolbar: { 1152 | items: items, 1153 | onClick: function (event) { 1154 | var target = event.target; 1155 | 1156 | if (target.startsWith("entitySelect:")) { 1157 | if (target === "entitySelect:none") { //None click 1158 | w2ui['grid_toolbar'].disable('type:attributes'); 1159 | w2ui['grid_toolbar'].disable('type:options'); 1160 | w2ui['grid_toolbar'].disable('type:views'); 1161 | w2ui['grid_toolbar'].disable('type:entityMeta'); 1162 | w2ui['grid_toolbar'].disable('type:charts'); 1163 | w2ui['grid_toolbar'].disable('type:content'); 1164 | w2ui['grid_toolbar'].disable('type:forms'); 1165 | w2ui['grid_toolbar'].disable('type:formMeta'); 1166 | 1167 | w2ui['grid_toolbar'].enable('type:webresources'); 1168 | w2ui['grid_toolbar'].enable('type:dashboards'); 1169 | } 1170 | else { 1171 | w2ui['grid_toolbar'].enable('type:attributes'); 1172 | w2ui['grid_toolbar'].enable('type:options'); 1173 | w2ui['grid_toolbar'].enable('type:views'); 1174 | w2ui['grid_toolbar'].enable('type:entityMeta'); 1175 | w2ui['grid_toolbar'].enable('type:charts'); 1176 | w2ui['grid_toolbar'].enable('type:forms'); 1177 | w2ui['grid_toolbar'].enable('type:formMeta'); 1178 | 1179 | w2ui['grid_toolbar'].disable('type:webresources'); 1180 | w2ui['grid_toolbar'].disable('type:dashboards'); 1181 | w2ui['grid_toolbar'].disable('type:content'); 1182 | 1183 | if (target === "entitySelect:Adx_contentsnippet") { 1184 | w2ui['grid_toolbar'].enable('type:content'); 1185 | } 1186 | 1187 | // Switch back to attributes if one of the now disabled options was set 1188 | if (["content", "webresources", "dashboards"].indexOf(w2ui.grid_toolbar.get("type").selected) !== -1) { 1189 | w2ui.grid_toolbar.get("type").selected = "attributes"; 1190 | w2ui.grid_toolbar.refresh(); 1191 | } 1192 | } 1193 | } 1194 | 1195 | if (target.indexOf("expandAll") !== -1) { 1196 | ToggleExpandCollapse(true); 1197 | } else if (target.indexOf("collapseAll") !== -1) { 1198 | ToggleExpandCollapse(false); 1199 | } 1200 | 1201 | switch(event.target) { 1202 | case "lockOrUnlock:lock": 1203 | LockAndLoad(XrmTranslator.GetEntity(), true); 1204 | break; 1205 | case "lockOrUnlock:unlock": 1206 | ReleaseLock(XrmTranslator.GetEntity()) 1207 | break; 1208 | } 1209 | } 1210 | } 1211 | }); 1212 | 1213 | XrmTranslator.LockGrid("Loading entities"); 1214 | } 1215 | 1216 | function FillEntitySelector (entities) { 1217 | if (XrmTranslator.config.entityWhitelist && XrmTranslator.config.entityWhitelist.length) { 1218 | entities = entities.filter(function (e) { return XrmTranslator.config.entityWhitelist.indexOf(e.LogicalName) !== -1 }); 1219 | } 1220 | 1221 | entities = entities.sort(XrmTranslator.EntityComparer); 1222 | var entitySelect = w2ui.grid_toolbar.get("entitySelect").items; 1223 | 1224 | for (var i = 0; i < entities.length; i++) { 1225 | var entity = entities[i]; 1226 | 1227 | var localizedLabel = entity.DisplayName.UserLocalizedLabel || {}; 1228 | entitySelect.push({id: entity.SchemaName, text: localizedLabel.Label ? `${localizedLabel.Label} (${entity.LogicalName})` : entity.LogicalName }); 1229 | XrmTranslator.entityMetadata[entity.SchemaName] = entity.MetadataId; 1230 | } 1231 | 1232 | return entities; 1233 | } 1234 | 1235 | function GetEntities() { 1236 | var queryParams = "?$select=SchemaName,LogicalName,MetadataId,DisplayName&$filter=IsCustomizable/Value eq true"; 1237 | 1238 | var request = { 1239 | entityName: "EntityDefinition", 1240 | queryParams: queryParams 1241 | }; 1242 | 1243 | return WebApiClient.Retrieve(request); 1244 | } 1245 | 1246 | function GetUserId() { 1247 | return WebApiClient.Execute(WebApiClient.Requests.WhoAmIRequest); 1248 | } 1249 | 1250 | function GetUserSettings(userId) { 1251 | return WebApiClient.Retrieve({ 1252 | overriddenSetName: "usersettingscollection", 1253 | entityId: userId 1254 | }); 1255 | } 1256 | 1257 | function RegisterReloadPrevention () { 1258 | // Dashboards are automatically refreshed on browser window resize, we don't want to lose changes. 1259 | window.onbeforeunload = function(e) { 1260 | var records = XrmTranslator.GetGrid().records; 1261 | var unsavedChanges = false; 1262 | 1263 | for (var i = 0; i < records.length; i++) { 1264 | var record = records[i]; 1265 | 1266 | if (record.w2ui && record.w2ui.changes) { 1267 | unsavedChanges = true; 1268 | break; 1269 | } 1270 | } 1271 | 1272 | if (unsavedChanges) { 1273 | var warning = "There are unsaved changes in the dashboard, are you sure you want to reload and discard changes?"; 1274 | e.returnValue = warning; 1275 | return warning; 1276 | } 1277 | }; 1278 | } 1279 | 1280 | XrmTranslator.GetAllRecords = function() { 1281 | var records = XrmTranslator.GetGrid().records; 1282 | 1283 | return Array.from(new Set(FlattenRecords(records))); 1284 | }; 1285 | 1286 | XrmTranslator.GetColumns = function (includeSchemaName) { 1287 | var columns = XrmTranslator.GetGrid().columns.map(function(c) { return c.field; }); 1288 | 1289 | if (includeSchemaName) { 1290 | return columns; 1291 | } 1292 | 1293 | return columns.filter(function(c) { return c !== "schemaName" }); 1294 | } 1295 | 1296 | XrmTranslator.ClearColumns = function() { 1297 | // Don't remove schema name column 1298 | var columns = XrmTranslator.GetColumns(false); 1299 | 1300 | columns.forEach(function(l) { 1301 | XrmTranslator.GetGrid().removeColumn(l); 1302 | }); 1303 | } 1304 | 1305 | XrmTranslator.AddSummary = function(records, countChildParents) { 1306 | var parentCount = records.length; 1307 | var childCount = records.map(function(r) { return r.w2ui && r.w2ui.children && r.w2ui.children.length; }).reduce(function(a, b) { return a + (b || 0); }, 0); 1308 | 1309 | var count = 0; 1310 | 1311 | if (childCount > 0) { 1312 | count = childCount; 1313 | 1314 | if (countChildParents) { 1315 | count += parentCount; 1316 | } 1317 | } 1318 | else { 1319 | count = parentCount; 1320 | } 1321 | 1322 | var summary = { 1323 | w2ui: { summary: true }, 1324 | recid: 'Summary-1', 1325 | schemaName: 'Of ' + count + ' labels in total' 1326 | }; 1327 | 1328 | for (var i = 0; i < XrmTranslator.installedLanguages.LocaleIds.length; i++) { 1329 | var language = XrmTranslator.installedLanguages.LocaleIds[i].toString(); 1330 | 1331 | var translatedParents = records.filter(function(r) { return !!r[language]; }).length; 1332 | var translatedChildren = records.map(function(r) { return r.w2ui && r.w2ui.children && r.w2ui.children.filter(function(c) { return !!c[language]; })}).reduce(function(a, b) { return a + (b || []).length; }, 0); 1333 | 1334 | var translatedRecords = 0; 1335 | 1336 | if (translatedChildren > 0) { 1337 | translatedRecords = translatedChildren; 1338 | 1339 | if (countChildParents) { 1340 | translatedRecords += translatedParents; 1341 | } 1342 | } 1343 | else { 1344 | translatedRecords = translatedParents; 1345 | } 1346 | 1347 | summary[language] = translatedRecords + " translated (" + (count - translatedRecords) + " untranslated)"; 1348 | } 1349 | 1350 | records.push(summary); 1351 | }; 1352 | 1353 | function FetchConfig() { 1354 | return WebApiClient.Retrieve({ overriddenSetName: "webresourceset", entityId: "8AF4EAED-7454-E911-80FA-0050568E4745"}) 1355 | .then(function (result) { 1356 | var config = JSON.parse(atob(result.content)); 1357 | 1358 | XrmTranslator.config = config; 1359 | }); 1360 | } 1361 | 1362 | XrmTranslator.Initialize = function() { 1363 | FetchConfig() 1364 | .then(function() { 1365 | return XrmTranslator.GetBaseLanguage(); 1366 | }) 1367 | .then(function() { 1368 | InitializeGrid(); 1369 | RegisterReloadPrevention(); 1370 | 1371 | return GetUserId(); 1372 | }) 1373 | .then(function (response) { 1374 | XrmTranslator.userId = response.UserId; 1375 | 1376 | return GetUserSettings(XrmTranslator.userId); 1377 | }) 1378 | .then(function (response) { 1379 | XrmTranslator.userSettings = response; 1380 | 1381 | return GetEntities(); 1382 | }) 1383 | .then(function(response) { 1384 | return FillEntitySelector(response.value); 1385 | }) 1386 | .then(function () { 1387 | return TranslationHandler.GetAvailableLanguages(); 1388 | }) 1389 | .then(function(languages) { 1390 | XrmTranslator.installedLanguages = languages; 1391 | return TranslationHandler.FillLanguageCodes(languages.LocaleIds, XrmTranslator.userSettings, XrmTranslator.config); 1392 | }) 1393 | .then(function () { 1394 | XrmTranslator.UnlockGrid(); 1395 | }) 1396 | .catch(XrmTranslator.errorHandler); 1397 | } 1398 | } (window.XrmTranslator = window.XrmTranslator || {})); 1399 | --------------------------------------------------------------------------------