Microsoft Power Platform/Dynamics 365 CE - Generate TypeScript Overload Signatures

Automatically creates TypeScript type definitions compatible with @types/xrm by extracting form attributes and controls from Dynamics 365/Power Platform model-driven applications.

当前为 2025-03-17 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Microsoft Power Platform/Dynamics 365 CE - Generate TypeScript Overload Signatures
  3. // @namespace https://github.com/gncnpk/xrm-generate-ts-overloads
  4. // @author Gavin Canon-Phratsachack (https://github.com/gncnpk)
  5. // @version 1.9
  6. // @license GPL-3.0
  7. // @description Automatically creates TypeScript type definitions compatible with @types/xrm by extracting form attributes and controls from Dynamics 365/Power Platform model-driven applications.
  8. // @match https://*.dynamics.com/main.aspx?appid=*&pagetype=entityrecord&etn=*&id=*
  9. // @grant none
  10. // ==/UserScript==
  11. (function() {
  12. 'use strict';
  13.  
  14. const generateComments = function(fieldName, fieldValue) {
  15. return `
  16. /**
  17. * ${fieldName}: ${fieldValue}
  18. */`
  19. }
  20. // Create a button element and style it to be fixed in the bottom-right corner.
  21. const btn = document.createElement('button');
  22. btn.textContent = 'Generate TypeScript Signatures';
  23. btn.style.position = 'fixed';
  24. btn.style.bottom = '20px';
  25. btn.style.right = '20px';
  26. btn.style.padding = '10px';
  27. btn.style.backgroundColor = '#007ACC';
  28. btn.style.color = '#fff';
  29. btn.style.border = 'none';
  30. btn.style.borderRadius = '5px';
  31. btn.style.cursor = 'pointer';
  32. btn.style.zIndex = 10000;
  33. document.body.appendChild(btn);
  34. btn.addEventListener('click', () => {
  35. // Mapping objects for Xrm attribute and control types.
  36. var attributeTypeMapping = {
  37. "boolean": "Xrm.Attributes.BooleanAttribute",
  38. "datetime": "Xrm.Attributes.DateAttribute",
  39. "decimal": "Xrm.Attributes.NumberAttribute",
  40. "double": "Xrm.Attributes.NumberAttribute",
  41. "integer": "Xrm.Attributes.NumberAttribute",
  42. "lookup": "Xrm.Attributes.LookupAttribute",
  43. "memo": "Xrm.Attributes.StringAttribute",
  44. "money": "Xrm.Attributes.NumberAttribute",
  45. "multiselectoptionset": "Xrm.Attributes.MultiselectOptionSetAttribute",
  46. "optionset": "Xrm.Attributes.OptionSetAttribute",
  47. "string": "Xrm.Attributes.StringAttribute"
  48. };
  49. var controlTypeMapping = {
  50. "standard": "Xrm.Controls.StandardControl",
  51. "iframe": "Xrm.Controls.IframeControl",
  52. "lookup": "Xrm.Controls.LookupControl",
  53. "optionset": "Xrm.Controls.OptionSetControl",
  54. "customsubgrid:MscrmControls.Grid.GridControl": "Xrm.Controls.GridControl",
  55. "subgrid": "Xrm.Controls.GridControl",
  56. "timelinewall": "Xrm.Controls.TimelineWall",
  57. "quickform": "Xrm.Controls.QuickFormControl"
  58. };
  59.  
  60. var specificControlTypeMapping = {
  61. "boolean": "Xrm.Controls.BooleanControl",
  62. "datetime": "Xrm.Controls.DateControl",
  63. "decimal": "Xrm.Controls.NumberControl",
  64. "double": "Xrm.Controls.NumberControl",
  65. "integer": "Xrm.Controls.NumberControl",
  66. "lookup": "Xrm.Controls.LookupControl",
  67. "memo": "Xrm.Controls.StringControl",
  68. "money": "Xrm.Controls.NumberControl",
  69. "multiselectoptionset": "Xrm.Controls.MultiselectOptionSetControl",
  70. "optionset": "Xrm.Controls.OptionSetControl",
  71. "string": "Xrm.Controls.StringControl"
  72. }
  73. // Object to hold the type information.
  74. const typeInfoTemplate = { attributes: {}, controls: {}, possibleTypes: {}, enums: {} }
  75. const entityTypeInfos = {};
  76. // Loop through all controls on the form.
  77. if (typeof Xrm !== 'undefined' && Xrm.Page && typeof Xrm.Page.getControl === 'function') {
  78. let entity = Xrm.Page.data.entity.getEntityName();
  79. let typeInfo = entityTypeInfos[entity] = entityTypeInfos[entity] ?? typeInfoTemplate;
  80. Xrm.Page.getControl().forEach((ctrl) => {
  81. const ctrlType = ctrl.getControlType();
  82. const mappedType = controlTypeMapping[ctrlType];
  83. if (mappedType) {
  84. typeInfo.controls[ctrl.getName()] = mappedType;
  85. typeInfo.possibleTypes[ctrl.getName()] = [];
  86. typeInfo.possibleTypes[ctrl.getName()].push(mappedType);
  87. }
  88. });
  89. } else {
  90. alert("Xrm.Page is not available on this page.");
  91. return;
  92. }
  93.  
  94. // Loop through all Quick View controls on the form.
  95. if (typeof Xrm.Page.ui.quickForms.get === 'function') {
  96. let entity = Xrm.Page.data.entity.getEntityName();
  97. let typeInfo = entityTypeInfos[entity] = entityTypeInfos[entity] ?? typeInfoTemplate;
  98. Xrm.Page.ui.quickForms.get().forEach((ctrl) => {
  99. const ctrlType = ctrl.getControlType();
  100. const mappedType = controlTypeMapping[ctrlType];
  101. if (mappedType) {
  102. typeInfo.possibleTypes[ctrl.getName()] = typeInfo.possibleTypes[ctrl.getName()] ?? [];
  103. typeInfo.possibleTypes[ctrl.getName()].push(mappedType);
  104. }
  105. });
  106. }
  107.  
  108. // Loop through all tabs and sections on the form.
  109. if (typeof Xrm.Page.ui.tabs.get === 'function') {
  110. let entity = Xrm.Page.data.entity.getEntityName();
  111. let typeInfo = entityTypeInfos[entity] = entityTypeInfos[entity] ?? typeInfoTemplate;
  112. Xrm.Page.ui.tabs.get().forEach((tab) => {
  113. typeInfo.possibleTypes[tab.getName()] = typeInfo.possibleTypes[tab.getName()] ?? [];
  114. typeInfo.possibleTypes[tab.getName()].push("Xrm.Controls.Tab");
  115. tab.sections.forEach((section) => {
  116. typeInfo.possibleTypes[section.getName()] = typeInfo.possibleTypes[section.getName()] ?? [];
  117. typeInfo.possibleTypes[section.getName()].push("Xrm.Controls.Section");
  118. });
  119. });
  120. }
  121.  
  122. // Loop through all attributes on the form.
  123. if (typeof Xrm.Page.getAttribute === 'function') {
  124. let entity = Xrm.Page.data.entity.getEntityName();
  125. let typeInfo = entityTypeInfos[entity] = entityTypeInfos[entity] ?? typeInfoTemplate;
  126. Xrm.Page.getAttribute().forEach((attr) => {
  127. const attrType = attr.getAttributeType();
  128. const mappedType = attributeTypeMapping[attrType];
  129. const mappedControlType = specificControlTypeMapping[attrType];
  130. if (mappedType) {
  131. typeInfo.attributes[attr.getName()] = mappedType;
  132. typeInfo.controls[attr.getName()] = mappedControlType;
  133. typeInfo.possibleTypes[attr.getName()] = [];
  134. if (attrType !== "optionset") {
  135. typeInfo.possibleTypes[attr.getName()].push(mappedType);
  136. }
  137. typeInfo.possibleTypes[attr.getName()].push(mappedControlType);
  138. }
  139. });
  140. }
  141.  
  142. // Loop through all enums on the form.
  143. if (typeof Xrm.Page.getAttribute === 'function') {
  144. let entity = Xrm.Page.data.entity.getEntityName();
  145. let typeInfo = entityTypeInfos[entity] = entityTypeInfos[entity] ?? typeInfoTemplate;
  146. Xrm.Page.getAttribute().forEach((attr) => {
  147. if (attr.getAttributeType() === "optionset" && attr.controls.get().length > 0) {
  148. const enumValues = attr.getOptions();
  149. const attrName = attr.getName();
  150. const controlName = attr.controls.get(0).getLabel().replace(/\s+/g, '');
  151. if (enumValues) {
  152. typeInfo.enums[controlName] = {attribute: "", values: []};
  153. typeInfo.enums[controlName].values = enumValues;
  154. typeInfo.enums[controlName].attribute = attrName;
  155. typeInfo.attributes[attrName] = attrName;
  156. typeInfo.possibleTypes[attrName].push(attrName);
  157. }
  158. }
  159. });
  160. }
  161. // Build the TypeScript overload string.
  162. let outputTS = `// This file is generated automatically.
  163. // It extends the Xrm.FormContext interface with overloads for getAttribute and getControl.
  164. // Do not modify this file manually.
  165. `
  166. for(const [entityName, typeInfo] of Object.entries(entityTypeInfos)) {
  167. for(const [enumName, enumValues] of Object.entries(typeInfo.enums)) {
  168. let enumTemplate = [];
  169. for(const enumValue of enumValues.values) {
  170. enumTemplate.push(` ${enumValue.text.replace(/\W/g, '')} = ${enumValue.value}`);
  171. }
  172. outputTS += generateComments("Entity", entityName);
  173. outputTS += `
  174. const enum ${enumName} {
  175. ${enumTemplate.join(",\n")}
  176. }
  177. `
  178. outputTS += generateComments("Entity", entityName);
  179. outputTS += `
  180. interface ${enumValues.attribute} extends Xrm.Attributes.OptionSetAttribute {
  181. getValue(): ${enumName} | null;
  182. setValue(value: ${enumName} | null): void;
  183. }
  184. `
  185. }
  186. }
  187. outputTS += `
  188. declare namespace Xrm {
  189. namespace Collection {
  190. interface ItemCollection<T> {
  191. `
  192. for(const [entityName, typeInfo] of Object.entries(entityTypeInfos)) {
  193. for (const [possibleTypeName, possibleTypesArray] of Object.entries(typeInfo.possibleTypes)) {
  194. let possibleTypeTemplate = "";
  195. for (const possibleType of possibleTypesArray) {
  196. possibleTypeTemplate += ` TSubType extends ${possibleType} ? ${possibleType} :`;
  197. }
  198. outputTS += generateComments("Entity", entityName);
  199. outputTS += ` get<TSubType extends T>(itemName: "${possibleTypeName}"):${possibleTypeTemplate} never;\n`;
  200. }
  201. }
  202. outputTS += `
  203. }
  204. }`
  205. outputTS += `
  206. interface FormContext {
  207. `;
  208. for(const [entityName, typeInfo] of Object.entries(entityTypeInfos)) {
  209. for (const [attributeName, attributeType] of Object.entries(typeInfo.attributes)) {
  210. outputTS += generateComments("Entity", entityName);
  211. outputTS += ` getAttribute(attributeName: "${attributeName}"): ${attributeType};\n`;
  212. }
  213. for (const [controlName, controlType] of Object.entries(typeInfo.controls)) {
  214. outputTS += generateComments("Entity", entityName);
  215. outputTS += ` getControl(controlName: "${controlName}"): ${controlType};\n`;
  216. }
  217. }
  218. outputTS += ` }
  219. }
  220. `;
  221. // Create a new window with a textarea showing the output.
  222. // The textarea is set to readonly to prevent editing.
  223. const w = window.open('', '_blank', 'width=600,height=400,menubar=no,toolbar=no,location=no,resizable=yes');
  224. if (w) {
  225. w.document.write('<html><head><title>TypeScript Overload Signatures</title></head><body>');
  226. w.document.write('<textarea readonly style="width:100%; height:90%;">' + outputTS + '</textarea>');
  227. w.document.write('</body></html>');
  228. w.document.close();
  229. } else {
  230. // Fallback to prompt if popups are blocked.
  231. prompt("Copy the TypeScript definition:", outputTS);
  232. }
  233. });
  234. })();