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-07 提交的版本,查看 最新版本

  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.974
  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
  108. .getCurrentItem()
  109. .getLabel();
  110.  
  111. // Loop through all controls on the form.
  112. if (
  113. typeof Xrm !== "undefined" &&
  114. Xrm.Page &&
  115. typeof Xrm.Page.getControl === "function"
  116. ) {
  117. Xrm.Page.getControl().forEach((ctrl) => {
  118. const ctrlType = ctrl.getControlType();
  119. const mappedType = controlTypeMapping[ctrlType];
  120. if (mappedType) {
  121. typeInfo.formControls[ctrl.getName()] = mappedType;
  122. }
  123. });
  124. } else {
  125. alert("Xrm.Page is not available on this page.");
  126. return;
  127. }
  128.  
  129. // Loop through all tabs and sections on the form.
  130. if (typeof Xrm.Page.ui.tabs.get === "function") {
  131. Xrm.Page.ui.tabs.get().forEach((tab) => {
  132. let formTab = (typeInfo.formTabs[tab.getName()] = new Tab());
  133. tab.sections.forEach((section) => {
  134. formTab.sections[section.getName()] = `${section.getName()}_section`;
  135. });
  136. });
  137. }
  138.  
  139. // Loop through all attributes on the form.
  140. if (typeof Xrm.Page.getAttribute === "function") {
  141. Xrm.Page.getAttribute().forEach((attr) => {
  142. const attrType = attr.getAttributeType();
  143. const attrName = attr.getName();
  144. const mappedType = attributeTypeMapping[attrType];
  145. const mappedControlType = specificControlTypeMapping[attrType];
  146. if (mappedType) {
  147. typeInfo.formAttributes[attrName] = mappedType;
  148. typeInfo.formControls[attrName] = mappedControlType;
  149. }
  150. if (
  151. attr.getAttributeType() === "optionset" &&
  152. attr.controls.get().length > 0
  153. ) {
  154. const enumValues = attr.getOptions();
  155. if (enumValues) {
  156. typeInfo.formEnums[attrName] = { attribute: "", values: [] };
  157. typeInfo.formEnums[attrName].values = enumValues;
  158. typeInfo.formEnums[attrName].attribute = attrName;
  159. typeInfo.formAttributes[attrName] = `${attrName}_attribute`;
  160. }
  161. }
  162. });
  163. }
  164.  
  165. // Loop through all subgrids on the form.
  166. if (typeof Xrm.Page.getControl === "function") {
  167. Xrm.Page.getControl().forEach((ctrl) => {
  168. if (
  169. ctrl.getControlType() === "subgrid" ||
  170. ctrl.getControlType() ===
  171. "customsubgrid:MscrmControls.Grid.GridControl"
  172. ) {
  173. const gridRow = ctrl.getGrid().getRows().get(0);
  174. const gridName = ctrl.getName();
  175. let subgrid = (typeInfo.subGrids[gridName] = new Subgrid());
  176. typeInfo.formControls[gridName] = `${gridName}_gridcontrol`;
  177. if (gridRow !== null) {
  178. gridRow.data.entity.attributes.forEach((attr) => {
  179. const attrType = attr.getAttributeType();
  180. const attrName = attr.getName();
  181. const mappedType = attributeTypeMapping[attrType];
  182. if (mappedType) {
  183. subgrid.attributes[attrName] = mappedType;
  184. }
  185. if (
  186. attr.getAttributeType() === "optionset" &&
  187. attr.controls.get().length > 0
  188. ) {
  189. const enumValues = attr.getOptions();
  190. if (enumValues) {
  191. subgrid.enums[attrName] = { attribute: "", values: [] };
  192. subgrid.enums[attrName].values = enumValues;
  193. subgrid.enums[attrName].attribute = attrName;
  194. subgrid.attributes[attrName] = `${attrName}_attribute`;
  195. }
  196. }
  197. });
  198. }
  199. }
  200. });
  201. }
  202.  
  203. // Loop through all Quick View controls and attributes on the form.
  204. if (typeof Xrm.Page.ui.quickForms.get === "function") {
  205. Xrm.Page.ui.quickForms.get().forEach((ctrl) => {
  206. const quickViewName = ctrl.getName();
  207. let quickView = (typeInfo.quickViews[quickViewName] = new QuickForm());
  208. const ctrlType = ctrl.getControlType();
  209. typeInfo.formControls[
  210. quickViewName
  211. ] = `${quickViewName}_quickformcontrol`;
  212. ctrl.getControl().forEach((subCtrl) => {
  213. if (typeof subCtrl.getAttribute !== "function") {
  214. return;
  215. }
  216. const subCtrlAttrType = subCtrl.getAttribute().getAttributeType();
  217. const mappedControlType =
  218. specificControlTypeMapping[subCtrlAttrType] ??
  219. controlTypeMapping[subCtrl.getControlType()];
  220. if (mappedControlType) {
  221. quickView.controls[subCtrl.getName()] = mappedControlType;
  222. }
  223. });
  224. ctrl.getAttribute().forEach((attr) => {
  225. const attrType = attr.getAttributeType();
  226. const attrName = attr.getName();
  227. const mappedType = attributeTypeMapping[attrType];
  228. if (mappedType) {
  229. quickView.attributes[attrName] = mappedType;
  230. }
  231. if (attrType === "optionset" && attr.controls.get().length > 0) {
  232. const enumValues = attr.getOptions();
  233. if (enumValues) {
  234. quickView.enums[attrName] = { attribute: "", values: [] };
  235. quickView.enums[attrName].values = enumValues;
  236. quickView.enums[attrName].attribute = attrName;
  237. quickView.attributes[attrName] = `${attrName}_attribute`;
  238. }
  239. }
  240. });
  241. });
  242. }
  243.  
  244. // Build the TypeScript overload string.
  245. let outputTS = `// These TypeScript definitions were generated automatically on: ${new Date().toDateString()}\n`;
  246. for (const [originalEnumName, enumValues] of Object.entries(
  247. typeInfo.formEnums
  248. )) {
  249. let enumName = `${originalEnumName}_enum`;
  250. let enumTemplate = [];
  251. let textLiteralTypes = [];
  252. let valueLiteralTypes = [];
  253. for (const enumValue of enumValues.values) {
  254. enumTemplate.push(
  255. ` ${enumValue.text.replace(/\W/g, "").replace(/[0-9]/g, "")} = ${
  256. enumValue.value
  257. }`
  258. );
  259. textLiteralTypes.push(`"${enumValue.text}"`);
  260. valueLiteralTypes.push(
  261. `${enumName}.${enumValue.text
  262. .replace(/\W/g, "")
  263. .replace(/[0-9]/g, "")}`
  264. );
  265. }
  266. outputTS += `
  267. const enum ${enumName} {
  268. ${enumTemplate.join(",\n")}
  269. }
  270. `;
  271. outputTS += `
  272. interface ${enumValues.attribute}_value extends Xrm.OptionSetValue {
  273. text: ${textLiteralTypes.join(" | ")};
  274. value: ${valueLiteralTypes.join(" | ")};
  275. }
  276. `;
  277. outputTS += `
  278. interface ${enumValues.attribute}_attribute extends Xrm.Attributes.OptionSetAttribute {
  279. `
  280. valueLiteralTypes.forEach((value, index) => {
  281. outputTS+= `getOption(value: ${value}): {text: ${textLiteralTypes[index]}, value: ${value}};\n`;
  282. })
  283. outputTS+=`getOption(value: ${enumValues.attribute}_value['value']): ${enumValues.attribute}_value | null;
  284. getOptions(): ${enumValues.attribute}_value[];
  285. getSelectedOption(): ${enumValues.attribute}_value | null;
  286. getValue(): ${enumName} | null;
  287. setValue(value: ${enumName} | null): void;
  288. getText(): ${enumValues.attribute}_value['text'] | null;
  289. }
  290. `;
  291. }
  292. for (const [subgridName, subgrid] of Object.entries(typeInfo.subGrids)) {
  293. for (const [enumName, enumValues] of Object.entries(subgrid.enums)) {
  294. let enumTemplate = [];
  295. let textLiteralTypes = [];
  296. let valueLiteralTypes = [];
  297. for (const enumValue of enumValues.values) {
  298. enumTemplate.push(
  299. ` ${enumValue.text.replace(/\W/g, "").replace(/[0-9]/g, "")} = ${
  300. enumValue.value
  301. }`
  302. );
  303. textLiteralTypes.push(`"${enumValue.text}"`);
  304. valueLiteralTypes.push(
  305. `${enumName}_enum.${enumValue.text
  306. .replace(/\W/g, "")
  307. .replace(/[0-9]/g, "")}`
  308. );
  309. }
  310. outputTS += `
  311. const enum ${enumName}_enum {
  312. ${enumTemplate.join(",\n")}
  313. }
  314. `;
  315. outputTS += `
  316. interface ${enumValues.attribute}_value extends Xrm.OptionSetValue {
  317. text: ${textLiteralTypes.join(" | ")};
  318. value: ${valueLiteralTypes.join(" | ")};
  319. }
  320. `;
  321. outputTS += `
  322. interface ${enumValues.attribute}_attribute extends Xrm.Attributes.OptionSetAttribute {
  323. `
  324. valueLiteralTypes.forEach((value, index) => {
  325. outputTS+= `getOption(value: ${value}): {text: ${textLiteralTypes[index]}, value: ${value}};\n`;
  326. })
  327. outputTS+=`
  328. getOption(value: ${enumValues.attribute}_value['value']): ${enumValues.attribute}_value | null;
  329. getOptions(): ${enumValues.attribute}_value[];
  330. getSelectedOption(): ${enumValues.attribute}_value | null;
  331. getValue(): ${enumName}_enum | null;
  332. setValue(value: ${enumName}_enum | null): void;
  333. getText(): ${enumValues.attribute}_value['text'] | null;
  334. }
  335. `;
  336. }
  337.  
  338. outputTS += `
  339. interface ${subgridName}_attributes extends Xrm.Collection.ItemCollection<${new Set(
  340. Object.values(subgrid.attributes)
  341. )
  342. .map((attrType) => `${attrType}`)
  343. .join(" | ")}> {`;
  344. for (const [itemName, itemType] of Object.entries(subgrid.attributes)) {
  345. outputTS += `get(itemName:"${itemName}"): ${itemType};\n`;
  346. }
  347. outputTS += `}
  348. `;
  349.  
  350. outputTS += `
  351.  
  352. interface ${subgridName}_entity extends Xrm.Entity {
  353. attributes: ${subgridName}_attributes;
  354. }
  355. interface ${subgridName}_data extends Xrm.Data {
  356. entity: ${subgridName}_entity;
  357. }
  358. interface ${subgridName}_gridrow extends Xrm.Controls.Grid.GridRow {
  359. data: ${subgridName}_data;
  360. }
  361. interface ${subgridName}_grid extends Xrm.Controls.Grid {
  362. getRows(): Xrm.Collection.ItemCollection<${subgridName}_gridrow>;
  363. }
  364. interface ${subgridName}_gridcontrol extends Xrm.Controls.GridControl {
  365. getGrid(): ${subgridName}_grid;
  366. }
  367. interface ${subgridName}_context extends Xrm.FormContext {`;
  368. for (const [itemName, itemType] of Object.entries(subgrid.attributes)) {
  369. outputTS += `getAttribute(attributeName:"${itemName}"): ${itemType};\n`;
  370. }
  371. outputTS += `getAttribute(delegateFunction?): ${subgridName}_attributes;`;
  372. outputTS += `
  373. }
  374. `;
  375. outputTS += `
  376. interface ${subgridName}_eventcontext extends Xrm.Events.EventContext {
  377. getFormContext(): ${subgridName}_context;
  378. }`;
  379. }
  380. for (const [quickViewName, quickView] of Object.entries(
  381. typeInfo.quickViews
  382. )) {
  383. for (const [enumName, enumValues] of Object.entries(quickView.enums)) {
  384. let enumTemplate = [];
  385. let textLiteralTypes = [];
  386. let valueLiteralTypes = [];
  387. for (const enumValue of enumValues.values) {
  388. enumTemplate.push(
  389. ` ${enumValue.text.replace(/\W/g, "").replace(/[0-9]/g, "")} = ${
  390. enumValue.value
  391. }`
  392. );
  393. textLiteralTypes.push(`"${enumValue.text}"`);
  394. valueLiteralTypes.push(
  395. `${enumName}_enum.${enumValue.text
  396. .replace(/\W/g, "")
  397. .replace(/[0-9]/g, "")}`
  398. );
  399. }
  400. outputTS += `
  401. const enum ${enumName}_enum {
  402. ${enumTemplate.join(",\n")}
  403. }
  404. `;
  405. outputTS += `
  406. interface ${enumValues.attribute}_value extends Xrm.OptionSetValue {
  407. text: ${textLiteralTypes.join(" | ")};
  408. value: ${valueLiteralTypes.join(" | ")};
  409. }
  410. `;
  411. outputTS += `
  412. interface ${enumValues.attribute}_attribute extends Xrm.Attributes.OptionSetAttribute {
  413. `
  414. valueLiteralTypes.forEach((value, index) => {
  415. outputTS+= `getOption(value: ${value}): {text: ${textLiteralTypes[index]}, value: ${value}};\n`;
  416. })
  417. outputTS+=`
  418. getOption(value: ${enumValues.attribute}_value['value']): ${enumValues.attribute}_value | null;
  419. getOptions(): ${enumValues.attribute}_value[];
  420. getSelectedOption(): ${enumValues.attribute}_value | null;
  421. getValue(): ${enumName}_enum | null;
  422. setValue(value: ${enumName}_enum | null): void;
  423. getText(): ${enumValues.attribute}_value['text'] | null;
  424. }
  425. `;
  426. }
  427. outputTS += `type ${quickViewName}_controls = ${new Set(
  428. Object.values(quickView.controls)
  429. )
  430. .map((controlType) => `${controlType}`)
  431. .join(" | ")}`;
  432. outputTS += `
  433. type ${quickViewName}_attributes = ${new Set(
  434. Object.values(quickView.attributes)
  435. )
  436. .map((attrType) => `${attrType}`)
  437. .join(" | ")}`;
  438. outputTS += `
  439. interface ${quickViewName}_quickformcontrol extends Xrm.Controls.QuickFormControl {
  440. `;
  441. for (const [itemName, itemType] of Object.entries(quickView.controls)) {
  442. outputTS += `getControl(controlName:"${itemName}"): ${itemType};\n`;
  443. }
  444. outputTS += `
  445. getControl(): ${quickViewName}_controls[]
  446. getControl(controlName: string): ${quickViewName}_controls | null;
  447. `;
  448. for (const [itemName, itemType] of Object.entries(quickView.attributes)) {
  449. outputTS += `getAttribute(attributeName:"${itemName}"): ${itemType};\n`;
  450. }
  451. outputTS += `
  452. getAttribute(attributeName: string): ${quickViewName}_attributes | null;
  453. getAttribute(): ${quickViewName}_attributes[];
  454. }`;
  455. }
  456. for (const [tabName, tab] of Object.entries(typeInfo.formTabs)) {
  457. outputTS += `
  458. interface ${tabName}_sections extends Xrm.Collection.ItemCollection<Xrm.Controls.Section> {`;
  459. for (const [sectionName, section] of Object.entries(tab.sections)) {
  460. outputTS += `get(itemName:"${sectionName}"): Xrm.Controls.Section;\n`;
  461. }
  462. outputTS += `}`;
  463.  
  464. outputTS += `
  465. interface ${tabName}_tab extends Xrm.Controls.Tab {
  466. sections: ${tabName}_sections;
  467. }`;
  468. }
  469. outputTS += `
  470. interface ${currentFormName}_tabs extends Xrm.Collection.ItemCollection<${new Set(
  471. Object.keys(typeInfo.formTabs)
  472. )
  473. .map((tabName) => `${tabName}_tab`)
  474. .join(" | ")}> {`;
  475. for (const [itemName, itemType] of Object.entries(typeInfo.formTabs)) {
  476. outputTS += `get(itemName:"${itemName}"): ${itemName}_tab;\n`;
  477. }
  478. outputTS += `}`;
  479. outputTS += `
  480. interface ${currentFormName}_quickforms extends Xrm.Collection.ItemCollection<${new Set(
  481. Object.keys(typeInfo.quickViews)
  482. )
  483. .map((quickViewName) => `${quickViewName}_quickformcontrol`)
  484. .join(" | ")}> {`;
  485. for (const [itemName, itemType] of Object.entries(typeInfo.quickViews)) {
  486. outputTS += `get(itemName:"${itemName}"): ${itemName}_quickformcontrol;\n`;
  487. }
  488. outputTS += `}`;
  489. outputTS += `
  490. type ${currentFormName}_attributes = ${new Set(
  491. Object.values(typeInfo.formAttributes)
  492. )
  493. .map((attrType) => `${attrType}`)
  494. .join(" | ")}`;
  495. outputTS += `
  496. type ${currentFormName}_controls = ${new Set(
  497. Object.values(typeInfo.formControls)
  498. )
  499. .map((ctrlType) => `${ctrlType}`)
  500. .join(" | ")}`
  501. outputTS += `
  502. interface ${currentFormName}_ui extends Xrm.Ui {
  503. quickForms: ${currentFormName}_quickforms | null;
  504. tabs: ${currentFormName}_tabs;
  505.  
  506. }
  507. `;
  508. outputTS += `
  509. interface ${currentFormName}_context extends Xrm.FormContext {
  510. ui: ${currentFormName}_ui;
  511. `;
  512. for (const [itemName, itemType] of Object.entries(typeInfo.formControls)) {
  513. outputTS += `getControl(controlName:"${itemName}"): ${itemType};\n`;
  514. }
  515. for (const [itemName, itemType] of Object.entries(
  516. typeInfo.formAttributes
  517. )) {
  518. outputTS += `getAttribute(attributeName:"${itemName}"): ${itemType};\n`;
  519. }
  520. outputTS += `
  521. getAttribute(attributeName: string): ${currentFormName}_attributes | null;
  522. getControl(controlName: string): ${currentFormName}_controls | null;
  523. getAttribute(delegateFunction?): ${currentFormName}_attributes[];
  524. getControl(delegateFunction?): ${currentFormName}_controls[];
  525. }`;
  526. outputTS += `
  527. interface ${currentFormName}_eventcontext extends Xrm.Events.EventContext {
  528. getFormContext(): ${currentFormName}_context;
  529. }`;
  530.  
  531. // Create a new window with a textarea showing the output.
  532. // The textarea is set to readonly to prevent editing.
  533. const w = window.open(
  534. "",
  535. "_blank",
  536. "width=600,height=400,menubar=no,toolbar=no,location=no,resizable=yes"
  537. );
  538. if (w) {
  539. w.document.write(
  540. "<html><head><title>TypeScript Definitions</title></head><body>"
  541. );
  542. w.document.write(
  543. '<textarea readonly style="width:100%; height:90%;">' +
  544. outputTS +
  545. "</textarea>"
  546. );
  547. w.document.write("</body></html>");
  548. w.document.close();
  549. } else {
  550. // Fallback to prompt if popups are blocked.
  551. prompt("Copy the TypeScript definition:", outputTS);
  552. }
  553. });
  554. })();