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