Graphite GitHub button

Add a button to go from app.graphite.com to github.com

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

// ==UserScript==
// @author      [email protected]
// @name        Graphite GitHub button
// @description Add a button to go from app.graphite.com to github.com
// @match       https://app.graphite.dev/*
// @match       https://app.graphite.com/*
// @version      0.5.1
// @run-at      document-start
// @icon         data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @grant        none
// @license      MIT
// @namespace https://app.graphite.dev
// ==/UserScript==

const PATH_REGEX = /^\/github\/pr\/([\w-]+)\/([\w-]+)\/(\d+).*$/;
const SELECTOR =
  '[class^="PullRequestTitleBar_container_"] > div:nth-child(1) > div:nth-child(2)';

/**
 * Finds the "Review changes" button to use as a style template,
 * then creates and appends a new "Open in GitHub" button
 * that matches its appearance.
 *
 * Assumes PATH_REGEX is defined in the script's outer scope.
 *
 * @param {HTMLElement} toolbar - The toolbar element to append the button to.
 */
const addButton = (toolbar) => {
  // --- 1. Get PR info ---
  const [_, org, repo, pr] = window.location.pathname.match(PATH_REGEX);
  const gitHubLink = `https://github.com/${org}/${repo}/pull/${pr}`;

  // --- 2. Check if button already exists ---
  if (document.getElementById("gitHubLink") != null) {
    return;
  }

  // --- 3. Find the "Review changes" button to use as a template ---
  const reviewSpan = document.evaluate(
    "//span[normalize-space()='Review changes']", // Use XPath to find the text node
    document,
    null,
    XPathResult.FIRST_ORDERED_NODE_TYPE,
    null
  ).singleNodeValue;

  if (!reviewSpan) {
    console.error(
      'Tampermonkey Script: Could not find "Review changes" span to clone styles.'
    );
    return;
  }

  const templateButton = reviewSpan.closest("button");

  if (!templateButton) {
    console.error(
      'Tampermonkey Script: Could not find parent "Review changes" button to clone styles.'
    );
    return;
  }

  // --- 4. Get the class names from the template button's inner spans ---
  // We need to replicate the <span class="..."><span class="..."><span...
  const templateInnerSpan = templateButton.querySelector("span");
  const templateTextSpan = templateInnerSpan
    ? templateInnerSpan.querySelector("span")
    : null;

  if (!templateInnerSpan || !templateTextSpan) {
    console.error(
      "Tampermonkey Script: Could not find inner span structure of template button."
    );
    return;
  }

  // --- 5. Create the new "Open in GitHub" link ---
  const anchorEl = document.createElement("a");
  anchorEl.setAttribute("id", "gitHubLink");
  anchorEl.setAttribute("href", gitHubLink);

  // --- 6. Copy all classes and data attributes from the template button ---
  anchorEl.className = templateButton.className;
  for (const attr of templateButton.attributes) {
    if (attr.name.startsWith("data-")) {
      anchorEl.setAttribute(attr.name, attr.value);
    }
  }
  // Ensure it's treated as a button role for accessibility, though it's a link
  anchorEl.setAttribute("role", "button");

  // --- 7. Re-create the inner span structure for correct styling ---
  const span1 = document.createElement("span");
  span1.className = templateInnerSpan.className; // e.g., "Button_gdsButtonContents__5B2fy"

  const span2 = document.createElement("span");
  span2.className = templateTextSpan.className; // e.g., "Button_gdsButtonText__5kyh_"

  const span3 = document.createElement("span");
  span3.appendChild(document.createTextNode("Open in GitHub"));

  span2.appendChild(span3);
  span1.appendChild(span2);
  anchorEl.appendChild(span1);

  // --- 8. Append the new button to the toolbar ---
  toolbar.appendChild(anchorEl);
};

const toolbarObserver = new MutationObserver((_, observer) => {
  const toolbar = document.querySelector(SELECTOR);
  if (toolbar) {
    observer.disconnect();
    addButton(toolbar);
  }
});

let lastPathname;
const routeChangeObserver = new MutationObserver(() => {
  const { pathname } = window.location;

  if (pathname !== lastPathname) {
    lastPathname = pathname;

    if (pathname.match(PATH_REGEX)) {
      toolbarObserver.observe(document.body, {
        childList: true,
        subtree: true,
      });
    }
  }
});

routeChangeObserver.observe(document.body, { childList: true, subtree: true });