Jira Search Icons Rewriter

Replaces the Jira icons with the relevant issue-type icon in the quick-search-results popup.

// ==UserScript==
// @namespace   BF_Chaos
// @name        Jira Search Icons Rewriter
// @description Replaces the Jira icons with the relevant issue-type icon in the quick-search-results popup.
// @license     GPL
// @version  1
// @grant    GM.xmlHttpRequest
// @include  https://*.atlassian.net/jira/*
// @match    https://*.atlassian.net/jira/*
// ==/UserScript==


let global_IssueIdToIconUrl = {};

let headerNode = document.body;
let domObserverConfig = {
  subtree: true,
  childList: true,
};

const getIssueId = (node) => {
  return node.getAttribute("data-id").replace(/^.*:issue\/(\d+)$/, "$1");
};

const replaceIconInNode = (node, issueType, iconUrl) => {
  console.log(`========= replaceIconInNode(..., ${issueType}, ${iconUrl})`, node);
  let iconNode = node.querySelector("a[data-testid='search-dialog-result-link'] span[role=img]");
  iconNode.innerHTML = `<img src="${iconUrl}">`;
};

const replaceIcon = (issueId, issueType, iconUrl) => {
  global_IssueIdToIconUrl[issueId] = [issueType, iconUrl];
  console.log(`========= replaceIcon(${issueId}, ${issueType}, ${iconUrl})`);
  let node = document.querySelector(`[data-result-type='searchResults'][data-product='jira'][data-id$=':issue/${issueId}'].search-dialog-result`);
  if (node) {
    replaceIconInNode(node, issueType, iconUrl);
  }
};

const rewriteSearchResultsIcons = (container, observer) => {
  let results = container.querySelectorAll("[data-result-type='searchResults'][data-product='jira'][data-id*=':issue/'].search-dialog-result");
  if (results.length === 0) {
    return;
  }
  
  let jiraReqBody = {
    "expand": ["names"],
    "fields": ["issuetype"],
    "issueIdsOrKeys": [],
    "properties": []
  };
  
  let issueIdToIconUrl = {};
  let issueIdToIconUrlCount = 0;
  
  results.forEach((node) => {
    const issueId = getIssueId(node);
    if (global_IssueIdToIconUrl[issueId]) {
      issueIdToIconUrl[issueId] = global_IssueIdToIconUrl[issueId];
      issueIdToIconUrlCount += 1;
    } else {
    	jiraReqBody.issueIdsOrKeys.push(getIssueId(node));
    }
  });
  
  
  console.log(`========= rewriteSearchResultsIcons, found ${issueIdToIconUrlCount} known issues, ${jiraReqBody.issueIdsOrKeys.length} unknown issues.`);
  
  if (issueIdToIconUrl.length !== 0) {
  	console.log(`========= rewriteSearchResultsIcons, replace icons in ${issueIdToIconUrlCount} known issues`);
    observer.disconnect();
    results.forEach((node) => {
      const issueId = getIssueId(node);
      if (issueIdToIconUrl[issueId]) {
        replaceIconInNode(node, issueIdToIconUrl[issueId][0], issueIdToIconUrl[issueId][1]);
      }
    });
    observer.observe(headerNode, domObserverConfig);
  }
  
  if (jiraReqBody.issueIdsOrKeys.length !== 0) {
  	console.log(`========= rewriteSearchResultsIcons, query icons for ${jiraReqBody.issueIdsOrKeys.length} unknown issues`);
    
    let url = `https://${window.location.host}/rest/api/3/issue/bulkfetch`;
    let headers = {
      "Accept": "application/json",
      "Content-Type": "application/json",
      "Origin": `https://${window.location.host}`,
      "Referer": window.location.toString(),
    };
    
    console.log(url);
    console.log(headers);
    console.log(jiraReqBody);
    
    /*
    fetch(url, {
        method: "POST",
        headers: headers,
        body: JSON.stringify(jiraReqBody),
      })
      .then((r) => {
      	console.log(r);
        return r.json();
      })
      .then((r) => {
      	console.log(r);
      	if (r["errorMessages"]) {
          console.log(`========= Failed to query icons: ${r.errorMessages}`);
        } else {
  				console.log(`========= rewriteSearchResultsIcons, replace icons for ${r.issues.length} issues`);
          observer.disconnect();
          for (issue of r.issues) {
            replaceIcon(issue.id, issue.fields.issuetype.name, issue.fields.issuetype.iconUrl);
          }
          observer.observe(headerNode, domObserverConfig);
        }
      });
    */
    GM.xmlHttpRequest({
      method: "POST",
      url: url,
      data: JSON.stringify(jiraReqBody),
      headers: headers,
      onload: function(response) {
        console.log(response);
        let r = JSON.parse(response.responseText);
        console.log(r);
      	if (r["errorMessages"]) {
          console.log(`========= Failed to query icons: ${r.errorMessages}`);
        } else {
  				console.log(`========= rewriteSearchResultsIcons, replace icons for ${r.issues.length} issues`);
          observer.disconnect();
          for (issue of r.issues) {
            replaceIcon(issue.id, issue.fields.issuetype.name, issue.fields.issuetype.iconUrl);
          }
          observer.observe(headerNode, domObserverConfig);
        }
      }
    });
  }
};

if (headerNode) {
  console.log("========= register header observer");
  
  const domObserverCallback = (mutationList, observer) => {
    const searchResultsContainer = headerNode.querySelector("[data-testid='search-dialog']")
    if (searchResultsContainer) {
      console.log("========= search results found, start rewriting");
      rewriteSearchResultsIcons(searchResultsContainer, observer);
      console.log("========= search results found, finished rewriting");
    } else {
      console.log("========= search results not found");
    }
  };

  const observer = new MutationObserver(domObserverCallback);
  observer.observe(headerNode, domObserverConfig);
} else {
  console.log("========= Header node not found, cannot register header observer!");
}