Microsoft Power Platform/Dynamics 365 CE - Generate TypeScript Definitions

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

当前为 2025-04-02 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Microsoft Power Platform/Dynamics 365 CE - Generate TypeScript Definitions
  3. // @namespace https://github.com/gncnpk/xrm-generate-ts-overloads
  4. // @author Gavin Canon-Phratsachack (https://github.com/gncnpk)
  5. // @version 1.954
  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.  
  12. (function () {
  13. "use strict";
  14.  
  15. const generateComments = function (fieldName, fieldValue) {
  16. return `\n/** ${fieldName}: ${fieldValue} */`;
  17. };
  18. // Create a button element and style it to be fixed in the bottom-right corner.
  19. const btn = document.createElement("button");
  20. btn.textContent = "Generate TypeScript Definitions";
  21. btn.style.position = "fixed";
  22. btn.style.bottom = "20px";
  23. btn.style.right = "20px";
  24. btn.style.padding = "10px";
  25. btn.style.backgroundColor = "#007ACC";
  26. btn.style.color = "#fff";
  27. btn.style.border = "none";
  28. btn.style.borderRadius = "5px";
  29. btn.style.cursor = "pointer";
  30. btn.style.zIndex = 10000;
  31. document.body.appendChild(btn);
  32.  
  33. btn.addEventListener("click", () => {
  34. // Mapping objects for Xrm attribute and control types.
  35. var attributeTypeMapping = {
  36. boolean: "Xrm.Attributes.BooleanAttribute",
  37. datetime: "Xrm.Attributes.DateAttribute",
  38. decimal: "Xrm.Attributes.NumberAttribute",
  39. double: "Xrm.Attributes.NumberAttribute",
  40. integer: "Xrm.Attributes.NumberAttribute",
  41. lookup: "Xrm.Attributes.LookupAttribute",
  42. memo: "Xrm.Attributes.StringAttribute",
  43. money: "Xrm.Attributes.NumberAttribute",
  44. multiselectoptionset: "Xrm.Attributes.MultiselectOptionSetAttribute",
  45. optionset: "Xrm.Attributes.OptionSetAttribute",
  46. string: "Xrm.Attributes.StringAttribute",
  47. };
  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":
  55. "Xrm.Controls.GridControl",
  56. subgrid: "Xrm.Controls.GridControl",
  57. timelinewall: "Xrm.Controls.TimelineWall",
  58. quickform: "Xrm.Controls.QuickFormControl",
  59. };
  60.  
  61. var specificControlTypeMapping = {
  62. boolean: "Xrm.Controls.BooleanControl",
  63. datetime: "Xrm.Controls.DateControl",
  64. decimal: "Xrm.Controls.NumberControl",
  65. double: "Xrm.Controls.NumberControl",
  66. integer: "Xrm.Controls.NumberControl",
  67. lookup: "Xrm.Controls.LookupControl",
  68. memo: "Xrm.Controls.StringControl",
  69. money: "Xrm.Controls.NumberControl",
  70. multiselectoptionset: "Xrm.Controls.MultiselectOptionSetControl",
  71. optionset: "Xrm.Controls.OptionSetControl",
  72. string: "Xrm.Controls.StringControl",
  73. };
  74.  
  75. // Object to hold the type information.
  76. const typeInfo = {
  77. subGrids: {},
  78. quickViews: {},
  79. formAttributes: {},
  80. formControls: {},
  81. formPossibleTypes: {},
  82. formTabs: {},
  83. formEnums: {},
  84. quickViewControls: {},
  85. quickViewAttributes: {},
  86. };
  87. class Subgrid {
  88. constructor() {
  89. this.attributes = {};
  90. this.enums = {};
  91. }
  92. }
  93. class QuickForm {
  94. constructor() {
  95. this.attributes = {};
  96. this.controls = {};
  97. this.enums = {};
  98. }
  99. }
  100.  
  101. class Tab {
  102. constructor() {
  103. this.sections = {};
  104. }
  105. }
  106.  
  107. const currentFormName = Xrm.Page.ui.formSelector.getCurrentItem().getLabel();
  108.  
  109. // Loop through all controls on the form.
  110. if (
  111. typeof Xrm !== "undefined" &&
  112. Xrm.Page &&
  113. typeof Xrm.Page.getControl === "function"
  114. ) {
  115. Xrm.Page.getControl().forEach((ctrl) => {
  116. const ctrlType = ctrl.getControlType();
  117. const mappedType = controlTypeMapping[ctrlType];
  118. if (mappedType) {
  119. typeInfo.formControls[ctrl.getName()] = mappedType;
  120. }
  121. });
  122. } else {
  123. alert("Xrm.Page is not available on this page.");
  124. return;
  125. }
  126.  
  127. // Loop through all tabs and sections on the form.
  128. if (typeof Xrm.Page.ui.tabs.get === "function") {
  129. Xrm.Page.ui.tabs.get().forEach((tab) => {
  130. let formTab = typeInfo.formTabs[tab.getName()] = new Tab();
  131. tab.sections.forEach((section) => {
  132. formTab.sections[section.getName()] = `${section.getName()}_section`;
  133. });
  134. });
  135. }
  136.  
  137. // Loop through all attributes on the form.
  138. if (typeof Xrm.Page.getAttribute === "function") {
  139. Xrm.Page.getAttribute().forEach((attr) => {
  140. const attrType = attr.getAttributeType();
  141. const attrName = attr.getName();
  142. const mappedType = attributeTypeMapping[attrType];
  143. const mappedControlType = specificControlTypeMapping[attrType];
  144. if (mappedType) {
  145. typeInfo.formAttributes[attrName] = mappedType;
  146. typeInfo.formControls[attrName] = mappedControlType;
  147. }
  148. if (
  149. attr.getAttributeType() === "optionset" &&
  150. attr.controls.get().length > 0
  151. ) {
  152. const enumValues = attr.getOptions();
  153. if (enumValues) {
  154. typeInfo.formEnums[attrName] = { attribute: "", values: [] };
  155. typeInfo.formEnums[attrName].values = enumValues;
  156. typeInfo.formEnums[attrName].attribute = attrName;
  157. typeInfo.formAttributes[attrName] = `${attrName}_attribute`;
  158. }
  159. }
  160. });
  161. }
  162.  
  163. // Loop through all subgrids on the form.
  164. if (typeof Xrm.Page.getControl === "function") {
  165. Xrm.Page.getControl().forEach((ctrl) => {
  166. if (
  167. ctrl.getControlType() === "subgrid" ||
  168. ctrl.getControlType() ===
  169. "customsubgrid:MscrmControls.Grid.GridControl"
  170. ) {
  171. const gridRow = ctrl.getGrid().getRows().get(0);
  172. const gridName = ctrl.getName();
  173. let subgrid = (typeInfo.subGrids[gridName] = new Subgrid());
  174. typeInfo.formControls[gridName] = `${gridName}_gridcontrol`;
  175. if (gridRow !== null) {
  176. gridRow.data.entity.attributes.forEach((attr) => {
  177. const attrType = attr.getAttributeType();
  178. const attrName = attr.getName();
  179. const mappedType = attributeTypeMapping[attrType];
  180. if (mappedType) {
  181. subgrid.attributes[attrName] = mappedType;
  182. }
  183. if (
  184. attr.getAttributeType() === "optionset" &&
  185. attr.controls.get().length > 0
  186. ) {
  187. const enumValues = attr.getOptions();
  188. if (enumValues) {
  189. subgrid.enums[attrName] = { attribute: "", values: [] };
  190. subgrid.enums[attrName].values = enumValues;
  191. subgrid.enums[attrName].attribute = attrName;
  192. subgrid.attributes[attrName] = `${attrName}_attribute`;
  193. }
  194. }
  195. });
  196. }
  197. }
  198. });
  199. }
  200.  
  201. // Loop through all Quick View controls and attributes on the form.
  202. if (typeof Xrm.Page.ui.quickForms.get === "function") {
  203. Xrm.Page.ui.quickForms.get().forEach((ctrl) => {
  204. const quickViewName = ctrl.getName();
  205. let quickView = (typeInfo.quickViews[quickViewName] = new QuickForm());
  206. const ctrlType = ctrl.getControlType();
  207. typeInfo.formControls[quickViewName] = `${quickViewName}_quickformcontrol`;
  208. ctrl.getControl().forEach((subCtrl) => {
  209. if (typeof subCtrl.getAttribute !== "function") {
  210. return;
  211. }
  212. const subCtrlAttrType = subCtrl.getAttribute().getAttributeType();
  213. const mappedControlType =
  214. specificControlTypeMapping[subCtrlAttrType] ??
  215. controlTypeMapping[subCtrl.getControlType()];
  216. if (mappedControlType) {
  217. quickView.controls[subCtrl.getName()] = mappedControlType;
  218. }
  219. });
  220. ctrl.getAttribute().forEach((attr) => {
  221. const attrType = attr.getAttributeType();
  222. const attrName = attr.getName();
  223. const mappedType = attributeTypeMapping[attrType];
  224. if (mappedType) {
  225. quickView.attributes[attrName] = mappedType;
  226. }
  227. if (attrType === "optionset" && attr.controls.get().length > 0) {
  228. const enumValues = attr.getOptions();
  229. if (enumValues) {
  230. quickView.enums[attrName] = { attribute: "", values: [] };
  231. quickView.enums[attrName].values = enumValues;
  232. quickView.enums[attrName].attribute = attrName;
  233. quickView.attributes[attrName] = `${attrName}_attribute`;
  234. }
  235. }
  236. });
  237. });
  238. }
  239.  
  240. // Build the TypeScript overload string.
  241. let outputTS = `// This file is generated automatically.
  242. // It extends the Xrm.FormContext interface with overloads for getAttribute and getControl.
  243. // Do not modify this file manually.
  244.  
  245. `;
  246. let extendedAttributeTypes = [];
  247. for (const [originalEnumName, enumValues] of Object.entries(
  248. typeInfo.formEnums
  249. )) {
  250. let enumName = `${originalEnumName}_enum`;
  251. let enumTemplate = [];
  252. let textLiteralTypes = [];
  253. let valueLiteralTypes = [];
  254. for (const enumValue of enumValues.values) {
  255. enumTemplate.push(
  256. ` ${enumValue.text.replace(/\W/g, "").replace(/[0-9]/g, "")} = ${
  257. enumValue.value
  258. }`
  259. );
  260. textLiteralTypes.push(`"${enumValue.text}"`);
  261. valueLiteralTypes.push(
  262. `${enumName}.${enumValue.text
  263. .replace(/\W/g, "")
  264. .replace(/[0-9]/g, "")}`
  265. );
  266. }
  267. outputTS += `
  268. const enum ${enumName} {
  269. ${enumTemplate.join(",\n")}
  270. }
  271. `;
  272. outputTS += `
  273. interface ${enumValues.attribute}_value extends Xrm.OptionSetValue {
  274. text: ${textLiteralTypes.join(" | ")};
  275. value: ${valueLiteralTypes.join(" | ")};
  276. }
  277. `;
  278. outputTS += `
  279. interface ${enumValues.attribute}_attribute extends Xrm.Attributes.OptionSetAttribute {
  280. getOptions(): ${enumValues.attribute}_value[];
  281. getSelectedOption(): ${enumValues.attribute}_value | null;
  282. getValue(): ${enumName} | null;
  283. setValue(value: ${enumName} | null): void;
  284. }
  285. `;
  286. extendedAttributeTypes.push(`${enumValues.attribute}_attribute`);
  287. }
  288. for (const [subgridName, subgrid] of Object.entries(typeInfo.subGrids)) {
  289. for (const [enumName, enumValues] of Object.entries(subgrid.enums)) {
  290. let enumTemplate = [];
  291. let textLiteralTypes = [];
  292. let valueLiteralTypes = [];
  293. for (const enumValue of enumValues.values) {
  294. enumTemplate.push(
  295. ` ${enumValue.text.replace(/\W/g, "").replace(/[0-9]/g, "")} = ${
  296. enumValue.value
  297. }`
  298. );
  299. textLiteralTypes.push(`"${enumValue.text}"`);
  300. valueLiteralTypes.push(
  301. `${enumName}.${enumValue.text
  302. .replace(/\W/g, "")
  303. .replace(/[0-9]/g, "")}`
  304. );
  305. }
  306. outputTS += `
  307. const enum ${enumName}_enum {
  308. ${enumTemplate.join(",\n")}
  309. }
  310. `;
  311. outputTS += `
  312. interface ${enumValues.attribute}_value extends Xrm.OptionSetValue {
  313. text: ${textLiteralTypes.join(" | ")};
  314. value: ${valueLiteralTypes.join(" | ")};
  315. }
  316. `;
  317. outputTS += `
  318. interface ${enumValues.attribute}_attribute extends Xrm.Attributes.OptionSetAttribute {
  319. getOptions(): ${enumValues.attribute}_value[];
  320. getSelectedOption(): ${enumValues.attribute}_value | null;
  321. getValue(): ${enumName}_enum | null;
  322. setValue(value: ${enumName}_enum | null): void;
  323. }
  324. `;
  325. }
  326.  
  327. outputTS += `
  328. interface ${subgridName}_attributes extends Xrm.Collection.ItemCollection {`;
  329. for (const [itemName, itemType] of Object.entries(subgrid.attributes)) {
  330. outputTS += `get(itemName:"${itemName}"): ${itemType};\n`;
  331. }
  332. outputTS += `}
  333. `;
  334.  
  335. outputTS += `
  336.  
  337. interface ${subgridName}_entity extends Xrm.Entity {
  338. attributes: ${subgridName}_attributes;
  339. }
  340. interface ${subgridName}_data extends Xrm.Data {
  341. entity: ${subgridName}_entity;
  342. }
  343. interface ${subgridName}_gridrow extends Xrm.Controls.Grid.GridRow {
  344. data: ${subgridName}_data;
  345. }
  346. interface ${subgridName}_grid extends Xrm.Controls.Grid {
  347. getRows(): Xrm.Collection.ItemCollection<${subgridName}_gridrow>;
  348. }
  349. interface ${subgridName}_gridcontrol extends Xrm.Controls.GridControl {
  350. getGrid(): ${subgridName}_grid;
  351. }
  352. interface ${subgridName}_context extends Xrm.FormContext {`
  353. for (const [itemName, itemType] of Object.entries(subgrid.attributes)) {
  354. outputTS += `getAttribute(attributeName:"${itemName}"): ${itemType};\n`;
  355. }
  356. outputTS += `getAttribute(delegateFunction?: Collection.MatchingDelegate<Xrm.Attributes.SpecificAttributeTypes>): ${subgridName}_attributes;`;
  357. outputTS += `
  358. }
  359. `;
  360. }
  361. for (const [quickViewName, quickView] of Object.entries(
  362. typeInfo.quickViews
  363. )) {
  364. for (const [enumName, enumValues] of Object.entries(quickView.enums)) {
  365. let enumTemplate = [];
  366. let textLiteralTypes = [];
  367. let valueLiteralTypes = [];
  368. for (const enumValue of enumValues.values) {
  369. enumTemplate.push(
  370. ` ${enumValue.text.replace(/\W/g, "").replace(/[0-9]/g, "")} = ${
  371. enumValue.value
  372. }`
  373. );
  374. textLiteralTypes.push(`"${enumValue.text}"`);
  375. valueLiteralTypes.push(
  376. `${enumName}.${enumValue.text
  377. .replace(/\W/g, "")
  378. .replace(/[0-9]/g, "")}`
  379. );
  380. }
  381. outputTS += `
  382. const enum ${enumName}_enum {
  383. ${enumTemplate.join(",\n")}
  384. }
  385. `;
  386. outputTS += `
  387. interface ${enumValues.attribute}_value extends Xrm.OptionSetValue {
  388. text: ${textLiteralTypes.join(" | ")};
  389. value: ${valueLiteralTypes.join(" | ")};
  390. }
  391. `;
  392. outputTS += `
  393. interface ${enumValues.attribute}_attribute extends Xrm.Attributes.OptionSetAttribute {
  394. getOptions(): ${enumValues.attribute}_value[];
  395. getSelectedOption(): ${enumValues.attribute}_value | null;
  396. getValue(): ${enumName}_enum | null;
  397. setValue(value: ${enumName}_enum | null): void;
  398. }
  399. `;
  400. }
  401. outputTS += `interface ${quickViewName}_controls extends Xrm.Collection.ItemCollection {`
  402. for(const [itemName, itemType] of Object.entries(quickView.controls)) {
  403. outputTS += `get(itemName:"${itemName}"): ${itemType};\n`;
  404. }
  405. outputTS += `}`
  406. outputTS += `
  407. interface ${quickViewName}_attributes extends Xrm.Collection.ItemCollection {`;
  408. for (const [itemName, itemType] of Object.entries(quickView.attributes)) {
  409. outputTS += `get(itemName:"${itemName}"): ${itemType};\n`;
  410. }
  411. outputTS += `}`;
  412. outputTS+= `
  413. interface ${quickViewName}_quickformcontrol extends Xrm.Controls.QuickFormControl {
  414. `
  415. for (const [itemName, itemType] of Object.entries(quickView.controls)) {
  416. outputTS += `getControl(controlName:"${itemName}"): ${itemType};\n`;
  417. }
  418. outputTS += `
  419. getControl(): ${quickViewName}_controls
  420. `
  421. for (const [itemName, itemType] of Object.entries(quickView.attributes)) {
  422. outputTS += `getAttribute(attributeName:"${itemName}"): ${itemType};\n`;
  423. }
  424. outputTS+=`
  425. getAttribute(): ${quickViewName}_attributes;
  426. }`;
  427. }
  428. for (const [tabName, tab] of Object.entries(typeInfo.formTabs)) {
  429. outputTS += `
  430. interface ${tabName}_sections extends Xrm.Collection.ItemCollection {`;
  431. for (const [sectionName, section] of Object.entries(tab.sections)) {
  432. outputTS += `get(itemName:"${sectionName}"): Xrm.Controls.Section;\n`;
  433. }
  434. outputTS += `}`;
  435. outputTS += `
  436. interface ${tabName}_tab extends Xrm.Controls.Tab {
  437. sections: ${tabName}_sections;
  438. }`
  439. }
  440. outputTS += `
  441. interface ${currentFormName}_tabs extends Xrm.Collection.ItemCollection {`;
  442. for (const [itemName, itemType] of Object.entries(typeInfo.formTabs)) {
  443. outputTS += `get(itemName:"${itemName}"): ${itemName}_tab;\n`;
  444. }
  445. outputTS += `}`;
  446. outputTS += `
  447. interface ${currentFormName}_quickforms extends Xrm.Collection.ItemCollection {`;
  448. for (const [itemName, itemType] of Object.entries(typeInfo.quickViews)) {
  449. outputTS += `get(itemName:"${itemName}"): ${itemName}_quickformcontrol;\n`;
  450. }
  451. outputTS += `}`;
  452. outputTS += `
  453. interface ${currentFormName}_attributes extends Xrm.Collection.ItemCollection {`;
  454. for (const [itemName, itemType] of Object.entries(typeInfo.formAttributes)) {
  455. outputTS += `get(itemName:"${itemName}"): ${itemType};\n`;
  456. }
  457. outputTS += `}`;
  458. outputTS += `
  459. interface ${currentFormName}_controls extends Xrm.Collection.ItemCollection {`;
  460. for (const [itemName, itemType] of Object.entries(typeInfo.formControls)) {
  461. outputTS += `get(itemName:"${itemName}"): ${itemType};\n`;
  462. }
  463. outputTS += `}`;
  464. outputTS += `
  465. interface ${currentFormName}_ui extends Xrm.Ui {
  466. quickForms: ${currentFormName}_quickforms | null;
  467. tabs: ${currentFormName}_tabs;
  468.  
  469. }
  470. `
  471. outputTS+= `
  472. interface ${currentFormName}_context extends Xrm.FormContext {
  473. ui: ${currentFormName}_ui;
  474. `
  475. for (const [itemName, itemType] of Object.entries(typeInfo.formControls)) {
  476. outputTS += `getControl(controlName:"${itemName}"): ${itemType};\n`;
  477. }
  478. for (const [itemName, itemType] of Object.entries(typeInfo.formAttributes)) {
  479. outputTS += `getAttribute(attributeName:"${itemName}"): ${itemType};\n`;
  480. }
  481. outputTS += `
  482. getAttribute(attributeName: string): Xrm.Attributes.Attribute | null;
  483. getControl(controlName: string): Xrm.Controls.Control | null;
  484. getAttribute(delegateFunction?: Collection.MatchingDelegate<Xrm.Attributes.SpecificAttributeTypes>): ${currentFormName}_attributes;
  485. getControl(delegateFunction?: Collection.MatchingDelegate<Xrm.Controls.SpecificControls>): ${currentFormName}_controls;
  486. }`;
  487.  
  488. // outputTS += `
  489. //
  490. //declare namespace Xrm {
  491. //
  492. // interface Ui {
  493. // quickForms: Collection.ItemCollection<${
  494. // Object.keys(typeInfo.quickViews)
  495. // .map((quickViewName) => `${quickViewName}_quickformcontrol`)
  496. // .join(" | ")
  497. // }>
  498. // }
  499. //
  500. //`;
  501. // outputTS += ` namespace Collection {
  502. // interface ItemCollection<T> {
  503. //`;
  504. // for (const [possibleTypeName, possibleTypesArray] of Object.entries(
  505. // typeInfo.formPossibleTypes
  506. // )) {
  507. // let possibleTypeTemplate = "";
  508. // for (const possibleType of possibleTypesArray) {
  509. // possibleTypeTemplate += ` TSubType extends ${possibleType} ? ${possibleType} :`;
  510. // }
  511. // outputTS += `\nget<TSubType extends T>(itemName: "${possibleTypeName}"):${possibleTypeTemplate} never;\n`;
  512. // }
  513. // outputTS += `
  514. // }
  515. //}`;
  516. // outputTS += `
  517. // interface FormContext {
  518. //`;
  519. // for (const [attributeName, attributeType] of Object.entries(
  520. // typeInfo.formAttributes
  521. // )) {
  522. // outputTS += `\ngetAttribute(attributeName: "${attributeName}"): ${attributeType};\n`;
  523. // }
  524. // for (const [controlName, controlType] of Object.entries(
  525. // typeInfo.formControls
  526. // )) {
  527. // outputTS += `\ngetControl(controlName: "${controlName}"): ${controlType};\n`;
  528. // }
  529. // outputTS += ` }
  530. // }
  531. //`;
  532.  
  533. // Create a new window with a textarea showing the output.
  534. // The textarea is set to readonly to prevent editing.
  535. const w = window.open(
  536. "",
  537. "_blank",
  538. "width=600,height=400,menubar=no,toolbar=no,location=no,resizable=yes"
  539. );
  540. if (w) {
  541. w.document.write(
  542. "<html><head><title>TypeScript Definitions</title></head><body>"
  543. );
  544. w.document.write(
  545. '<textarea readonly style="width:100%; height:90%;">' +
  546. outputTS +
  547. "</textarea>"
  548. );
  549. w.document.write("</body></html>");
  550. w.document.close();
  551. } else {
  552. // Fallback to prompt if popups are blocked.
  553. prompt("Copy the TypeScript definition:", outputTS);
  554. }
  555. });
  556. })();