myconnectwise.net enhancements

Modifies Manage page to allow for collapsible ticket notes

当前为 2023-12-01 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name myconnectwise.net enhancements
  3. // @namespace Violentmonkey Scripts
  4. // @match https://aus.myconnectwise.net/v2022_2/connectwise.aspx
  5. // @match https://aus.myconnectwise.net/v2022_2/ConnectWise.aspx
  6. // @grant none
  7. // @version 2.12
  8. // @author mike-Inside
  9. // @description Modifies Manage page to allow for collapsible ticket notes
  10. // @require https://cdn.jsdelivr.net/npm/@violentmonkey/dom@2
  11. // @require https://cdn.jsdelivr.net/npm/@violentmonkey/shortcut@1
  12. // ==/UserScript==
  13.  
  14. // watch the page for elements that are created dynamically and may not be ready even on document-end
  15. const disconnect = VM.observe(document.body, () => {
  16. // find the target node
  17. const node = document.querySelector('#SR_Service_RecID-input');
  18. const rowWrap = document.querySelectorAll('.TicketNote-rowWrap');
  19.  
  20. // runs on service board pages to stop autocomplete from blocking visibility
  21. if (node) {
  22. node.setAttribute("autocomplete", "off");
  23. return true;
  24. }
  25.  
  26. // runs on individual ticket pages
  27. if (rowWrap[0]) { // wait until the ticket notes have been generated before starting the script
  28. // start by generating the primary controller buttons
  29. generateControls();
  30.  
  31. // wait an additonal couple of seconds for all ticket notes to finish loading
  32. setTimeout(function() {
  33. modifyNotes();
  34. }, 2000);
  35.  
  36. // disconnect observer
  37. return true;
  38. }
  39. });
  40.  
  41.  
  42. function generateControls(){
  43. let noteTab = document.querySelectorAll(".TicketNote-newNoteButton");
  44. let controlButtons = document.getElementsByClassName("control-button"); // make sure we don't add them more than once
  45.  
  46. if(noteTab[0] && controlButtons.length == 0) {
  47. noteTab[0].nextElementSibling.style.display = "none"; //hide the buttons that filter by ticket type - this conflicts with my changes and sometimes causes the entire notes section to disappear
  48. noteTab[0].after(createButton("Next"));
  49. noteTab[0].after(createButton("Previous"));
  50. noteTab[0].after(createButton("Expand All"));
  51. noteTab[0].after(createButton("Collapse All"));
  52. }
  53. }
  54.  
  55. // used for the controller buttons above the notes, they are all exactly the same except for their content
  56. function createButton(buttName){
  57. let bCss = "padding:3px; margin-left:12px; margin-bottom:0px; margin-top:22px; font-size:0.90em; width:125px";
  58. let butt;
  59.  
  60. butt = document.createElement("button");
  61. butt.classList.add("control-button");
  62. butt.style.cssText = bCss;
  63. butt.textContent = buttName;
  64. butt.addEventListener("click", buttonPress);
  65. return butt;
  66. }
  67.  
  68. // runs when a controller button is pressed, loops through ticket notes to set the display attribute
  69. function buttonPress(evt){
  70. let buttName = evt.currentTarget.textContent;
  71. let newDisplayState = 0;
  72. if (buttName == "Expand All") {
  73. newDisplayState = 1;
  74. }
  75. let coll = document.getElementsByClassName('collapsible');
  76. if (coll.length == 0) { // check if collapsibles have been wiped out (eg. by a ticket note refresh)
  77. modifyNotes();
  78. }
  79. let found = -1; // value to store the location of the first ticket note which is visible / not hidden
  80. for (let i = 0; i < coll.length; ++i) {
  81. let initialState = displayChange(coll[i], newDisplayState);
  82.  
  83. if(newDisplayState == 0 && initialState != "none" && found < 0) {
  84. found = i;
  85. }
  86. }
  87.  
  88. if(buttName == "Previous") {
  89. if (found > 0) { // open the note prior to the one that was open before the 'previous' button was clicked, unless...
  90. displayChange(coll[found-1], 1);
  91. } else { // ...if no note open, or the first note was open, then open the final note
  92. displayChange(coll[coll.length-1], 1);
  93. }
  94. } else if (buttName == "Next") {
  95. if (found < coll.length-1) { // if no note was open, or any note except the last note was open, then open the next note after the opened one
  96. displayChange(coll[found+1], 1);
  97. } else { // otherwise open the first note
  98. displayChange(coll[0], 1);
  99. }
  100. }
  101. }
  102.  
  103. function modifyNotes(){
  104. const rowWrap = document.querySelectorAll('.TicketNote-rowWrap');
  105. rowWrap.forEach((rowItem) => {
  106.  
  107. // create button to be placed above each ticket note
  108. let butt = document.createElement("button");
  109. butt.classList.add("collapsible");
  110. butt.style.cssText = 'padding:3px; margin-bottom:0px; margin-top:0px; width:100%; font-size:0.90em';
  111. butt.innerHTML = "";
  112.  
  113. let basicName = classText(rowItem, "TicketNote-basicName", "<strong>", "</strong>")
  114. let clickableName = classText(rowItem, "TicketNote-clickableName", "<strong>", "</strong>")
  115. butt.innerHTML += basicName + clickableName; // add name to button
  116.  
  117. // let timeDateText = classText(rowItem, "TimeText-date", " [","]"); // copy date to button without any changes - nah
  118. // let's instead change date to Australian locale:
  119. let timeDateChild = rowItem.getElementsByClassName("TimeText-date");
  120. if (timeDateChild[0]) {
  121. let timeDateText = timeDateChild[0].textContent;
  122. let timeDateHTML = timeDateChild[0].innerHTML;
  123. const regexDate = new RegExp(/(\d{1,2})\/(\d{1,2})\/(\d{4})/, "g"); //matches date, connectwise uses (en-US) M/D/YYYY format
  124. //let timeDateFormat = timeDateText.replace(regexDate, "$2/$1/$3"); // simple method to just swap month and day around - nah let's go all in
  125. let timeDateMatch = regexDate.exec(timeDateText);
  126. let dateObject = new Date(timeDateMatch[3],timeDateMatch[1]-1,timeDateMatch[2]); //creates JS Date object, note month parameter is inexplicably 0-11 to represent jan-dec
  127. const dateOptions = {
  128. weekday: "long",
  129. year: "numeric",
  130. month: "long",
  131. day: "numeric",
  132. };
  133. let dateFormat = dateObject.toLocaleString("en-AU", dateOptions);
  134. let timeDateFormat = timeDateText.replace(regexDate, dateFormat); //adds the time info back to formatted date
  135. butt.innerHTML += " [" + timeDateFormat + "]"; //adds date to button
  136. //timeDateChild[0].innerHTML = timeDateChild[0].innerHTML.replace(timeDateText, timeDateFormat); //replaces the existing date inside ticket note with formatted date - nah
  137. timeDateChild[0].innerHTML = timeDateChild[0].innerHTML.replace(timeDateText, ""); // no need to double up, we can just delete the existing date
  138. }
  139.  
  140. // this is the pill shaped icon that shows for 'resolved' or 'internal' notes
  141. let pill = rowItem.getElementsByClassName("TicketNote-pill");
  142. let pillText = "";
  143. if (pill[0]) {
  144. pillText = pill[0].innerText;
  145. }
  146.  
  147. // set custom styles for each ticket button
  148. if (pillText == "Internal") {
  149. butt.style.cssText += ";background-color:#026CCF;color:#DDEEFF;text-align:right";
  150. } else if (pillText == "Resolution") {
  151. butt.style.cssText += ";background-color:#549c05;color:#DDFFCC;text-align:right";
  152. } else if (clickableName.length > 0) { //only falco team have this class
  153. butt.style.cssText += ";background-color:#AABBEE;color:#3366AA;text-align:right";
  154. } else if (basicName.length > 0) { // only end users have this class
  155. butt.style.cssText += ";background-color:#CCAAEE;color:#6644AA;text-align:left";
  156. }
  157.  
  158. //rowItem.style.removeProperty('margin-top');
  159. rowItem.style.cssText = "margin-top:6px";
  160. // rowItem.parentElement.insertBefore(butt, rowItem); //previous method, works but causes issues
  161. // instead we place the button inside the "TicketNote-rowWrap" class so that it will get wiped if the ticket notes are refreshed
  162. let rowChild = rowItem.getElementsByClassName("TicketNote-row");
  163. if (rowChild[0]) {
  164. rowChild[0].before(butt);
  165. }
  166.  
  167. } );
  168.  
  169. //add click listener functions to all collapsible buttons
  170. var coll = document.getElementsByClassName("collapsible");
  171. for (let i = 0; i < coll.length; i++) {
  172. coll[i].addEventListener("click", function() {
  173. this.classList.toggle("active");
  174. displayChange(this, -1);
  175. });
  176. }
  177. }
  178.  
  179. // toggle the display state of the sibling element that is directly after the passed parameter
  180. // (We use this to hide or show the 'TicketNote-row' located directly after the 'collapsible' button in the DOM)
  181. function displayChange(collapsible, state = -1) {
  182. // state -1 toggle, 0 off, 1 on
  183. let content = collapsible.nextElementSibling;
  184. let initialState = content.style.display;
  185. if ((state != 0 && initialState === "none")) {
  186. content.style.display = "flex";
  187. } else if (state < 1) {
  188. content.style.display = "none";
  189. }
  190. return initialState; // returns the state the element was in *prior* to being changed
  191. }
  192.  
  193.  
  194. // find first matching classString that is a child of the startParent, return its innerText with optional pre/postfix text
  195. // ended up not using this much, special handling required too often
  196. function classText(startParent, classString, prefix = "", postfix = "") {
  197. let foundChild = startParent.getElementsByClassName(classString);
  198. if (foundChild[0]) {
  199. return prefix + foundChild[0].innerText + postfix;
  200. } else {
  201. return "";
  202. }
  203. }
  204.  
  205. // Press Control-i in order to regenerate the controls and buttons
  206. VM.shortcut.register('c-i', () => {
  207. generateControls();
  208. modifyNotes();
  209. // just a reminder to self on ways to debug output:
  210. // console.log('You just pressed Ctrl-I');
  211. // alert("I am an alert box!");
  212. });
  213.  
  214.