NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name ReTool Resource Colorizer // @namespace http://fortunabmc.com/ // @version 0.4.0 // @description Colorize the header backgrounds of ReTool Workflow Blocks // @author khill-fbmc // @license MIT // @match https://*.retool.com/workflows/* // @icon https://www.google.com/s2/favicons?sz=64&domain=retool.com // ==/UserScript== const GITHUB_URL = "https://github.com/khill-fbmc/retool-resource-colorizer"; const USERSCRIPT_URL = "https://openuserjs.org/scripts/khill-fbmc/ReTool_Resource_Colorizer"; const ERROR_CLASS = 'rrc-error-block'; const SUCCESS_CLASS = 'rrc-success-block'; const MENU_ID = 'rrc-menu'; const MENU_BUTTON_ID = 'rrc-menu-btn'; const MENU_ITEM_CLASS = 'rrc-menu-item'; const CUSTOM_CSS = ` .${ERROR_CLASS} { background-color: rgba(255, 0, 0, 0.2) !important; } .${SUCCESS_CLASS} { background-color: rgba(0, 255, 0, 0.2) !important; } #${MENU_ID} { position: fixed; bottom: 30px; right: 20px; background-color: rgba(255,255,255,0.9); padding: 10px; border-radius: 5px; display: none; z-index: 1000; box-shadow: 0 4px 6px rgba(0,0,0,0.1); } #${MENU_BUTTON_ID}:hover { cursor: pointer; } .${MENU_ITEM_CLASS} { padding: 5px; margin-bottom: 5px; /* Space between items */ } .${MENU_ITEM_CLASS}:hover, .${MENU_BUTTON_ID}:hover { background-color: #efefef; } .${MENU_ITEM_CLASS}:last-child { margin-bottom: 0; /* Remove margin for the last item */ } `; const log = (...args) => console.log("[RRC]", ...args); function isValidCSSColor(color) { const s = new Option().style; s.color = color; return s.color !== ''; } function applyStylesToElement(element, styles = {}) { Object.assign(element.style, styles); return element; } function createElementWithText(type, text, styles = {}) { const element = document.createElement(type); element.textContent = text; return applyStylesToElement(element, styles); } function createElementWithHTML(type, html, styles = {}) { const element = document.createElement(type); element.innerHTML = html; return applyStylesToElement(element, styles); } function addStyleToHead(styles) { const styleElement = document.createElement('style'); styleElement.type = 'text/css'; styleElement.appendChild(document.createTextNode(styles)); document.head.appendChild(styleElement); } function watchDOMForElements(selector) { return new Promise((resolve, reject) => { const observer = new MutationObserver((mutationsList, observer) => { const elements = document.querySelectorAll(selector); if (elements.length > 0) { observer.disconnect(); resolve(elements); } }); observer.observe(document.body, { subtree: true, childList: true }); }); } function processDivs() { const containsError = text => /fail|error/i.test(text); const containsSuccess = text => /pass|success/i.test(text); const containsRrcDirective = text => containsError(text) || containsSuccess(text); const nodes = document.querySelectorAll('div[data-testid^="Workflows::BlockContainer::"]'); log("Found", nodes.length, "Resource Blocks"); nodes.forEach(div => { const extractedText = div.getAttribute('data-testid').split("::")[2]; //if (containsRrcDirective(extractedText)) { //log(extractedText, "contains a directive"); const header = div.querySelector(".blockHeader"); if (header) { let cssColor = ''; if (extractedText.includes("_$")) { const colorName = extractedText.split("_$")[1]; if (isValidCSSColor(colorName)) { cssColor = colorName; } else { log("-> Invalid CSS color name:", colorName); } } else { const cssClass = containsError(extractedText) ? ERROR_CLASS : containsSuccess(extractedText) ? SUCCESS_CLASS : ""; if (cssClass) { header.classList.add(cssClass); log("Applied css class", cssClass, "to", extractedText); } } if (cssColor) { header.style.backgroundColor = cssColor; log("Applied", cssColor, "to", extractedText); } } //} }); } function createMenu() { const menu = document.createElement('div'); menu.id = MENU_ID; document.body.appendChild(menu); // Add a menu heading const menuHeading = createElementWithText('h5', 'ReTool Resource Colorizer', { padding: '10px', margin: '0', borderRadius: "5px", backgroundColor: 'aliceblue', textAlign: 'center' }); menu.appendChild(menuHeading); const ul = document.createElement('ul'); menu.appendChild(ul); // Define menu items with text and click handlers const menuItems = [{ text: 'Refresh Colors', onClick: () => processDivs() }, { text: 'GitHub Source', url: GITHUB_URL }, { text: 'Userscript Page', url: USERSCRIPT_URL }, ]; log("Creating", menuItems.length, "Menu Items"); menuItems.forEach(itemConfig => { const li = document.createElement('li'); const item = createElementWithHTML('a', itemConfig.text, { display: 'block', padding: '5px 10px' }); item.classList.add(MENU_ITEM_CLASS); if (itemConfig.url) { item.href = itemConfig.url; item.target = "_blank"; // Open in new tab } else if (itemConfig.onClick) { item.addEventListener('click', itemConfig.onClick); } li.appendChild(item); ul.appendChild(li); }); return menu; } function addMenuToFooter() { const menu = document.getElementById(MENU_ID); const views = document.querySelectorAll('div[data-testid="split-view-view"]'); const footer = views[views.length - 1]; if (footer) { const menuButton = createElementWithText('span', 'RRC', { marginRight: '10px', marginLeft: 'auto', color: 'var(--text-secondary)' }); menuButton.id = MENU_BUTTON_ID; menuButton.onclick = () => { menu.style.display = menu.style.display !== 'block' ? 'block' : 'none'; }; footer.firstElementChild.style.display = 'flex'; footer.firstElementChild.style.justifyContent = 'flex-end'; footer.firstElementChild.appendChild(menuButton); } } //------------------------------------- MAIN ---------------------------------- (function () { 'use strict'; console.log("Starting ReTool Response Colorizer [RRC]"); addStyleToHead(CUSTOM_CSS); log("Added custom CSS to head"); watchDOMForElements(`div[data-testid^="Workflows::BlockContainer::"]`).then(() => { log("Adding Menu"); createMenu(); addMenuToFooter(); log("Coloring Headers"); processDivs(); }); })();