NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @namespace https://openuserjs.org/users/TedCart // @name Mackey JIRA Kanban formatter // @copyright 2020-2021, TedCart (https://openuserjs.org/users/TedCart) // @version 1.2 // @license MIT // @description Reformat Jira for standup meetings // @author You // @match https://jira.mackeyllc.com/secure/RapidBoard.jspa* // @grant none // @run-at document-end // ==/UserScript== // ==OpenUserJS== // @author TedCart // ==/OpenUserJS== // link to source: // https://openuserjs.org/scripts/TedCart/Mackey_JIRA_Kanban_formatter/source (function () { 'use strict'; const addCustomStyleTag = () => { const newStyle = document.createElement("style"); newStyle.setAttribute('type', 'text/css'); newStyle.innerHTML = ` /* DO NOT EDIT BELOW THIS POINT */ /* BEGIN CUSTOM CSS */ body { margin-top: 0; } /* Increase main font-size */ .ghx-issue-fields .ghx-summary .ghx-inner { font-size: 1.8em; max-height: 10em; } /* Begin flattening header rows */ .ghx-controls-plan, .ghx-header-compact .ghx-controls-report, .ghx-controls-work { min-height: 0px; height: 2em; } .ghx-rapid-views #gh #ghx-work #ghx-pool-column #ghx-column-headers .ghx-column { padding: 0px 8px; } .ghx-swimlane-header .ghx-heading { margin: 2px 0; } .ghx-rapid-views #gh #ghx-work #ghx-pool-column .ghx-swimlane .ghx-swimlane-header { top: 25px; } /* End flattening header rows */ .ghx-issue { padding: 2px 10px; } .ghx-issue .ghx-highlighted-fields, .ghx-issue .ghx-card-footer { margin-top: 2px; } .ghx-summary { margin-top: -5px; } .ghx-header-compact #ghx-operations { padding-top: 0px; } .ghx-issue .ghx-extra-fields { margin-top: 2px; } .ghx-issue .ghx-key-link { font-size: 16px; margin: -5px 0px -6px; } /* Increase font-size in tooltip */ .aui-tooltip.tipsy { font-size: 1.5em; line-height: 1.6em; } /* Move user icons further up to the left */ .ghx-issue .ghx-avatar { position: absolute; right: 2px; /* 10px; */ top: 4px; /* 10px; */ } .ghx-issue.ghx-has-avatar .ghx-issue-fields, .ghx-issue.ghx-has-corner .ghx-issue-fields { padding-right: 30px; } /* END CUSTOM CSS */ `; document.head.appendChild(newStyle); }; // import { addCustomModalStyleTag } from "./_styles.js" const modalBlockId = "draggable-modal-block"; const collapsingArrowSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 14" width="17px" height="17px"> <g fill="none" fill-rule="evenodd"> <path d="M3.29175 4.793c-.389.392-.389 1.027 0 1.419l2.939 2.965c.218.215.5.322.779.322s.556-.107.769-.322l2.93-2.955c.388-.392.388-1.027 0-1.419-.389-.392-1.018-.392-1.406 0l-2.298 2.317-2.307-2.327c-.194-.195-.449-.293-.703-.293-.255 0-.51.098-.703.293z" fill="#FFFFFF"> </path> </g> </svg>`; function getMainModalElement () { return document.querySelector(`#${modalBlockId}`) } const maxListHeight = Math.max( Math.floor(window.innerHeight / 2) , 200 ); function createModalBlock () { addCustomModalStyleTag(); const modalBlock = document.createElement('div'); modalBlock.setAttribute('id', modalBlockId); // modalBlock.setAttribute('title', "Click and hold to drag...") document.querySelector('body').prepend(modalBlock); modalBlock.onmousedown = moveModalContainer; modalBlock.ondragstart = () => false; // This function came from https://javascript.info/mouse-drag-and-drop function moveModalContainer (event) { if (event.target.tagName === "INPUT") return if (event.target.tagName === "BUTTON") return if (event.target.tagName === "SPAN") return if (event.target.tagName === "SECTION") return if (event.target.tagName === "LABEL") return if (event.target.tagName === "SVG") return if (event.target.tagName === "G") return if (event.target.tagName === "PATH") return // if (event.target.tagName === "LI") return // const modalBlock = document.querySelector(`#${modalBlockId}`) // if (!modalBlock) return const top = document.querySelector('html').scrollTop; let shiftX = event.clientX - modalBlock.getBoundingClientRect().left; let shiftY = event.clientY - modalBlock.getBoundingClientRect().top; modalBlock.style.position = 'fixed'; modalBlock.style.zIndex = 1000; document.body.append(modalBlock); moveAt(event.pageX, event.pageY); // moves the modalBlock at (pageX, pageY) coordinates // taking initial shifts into account function moveAt(pageX, pageY) { modalBlock.style.left = pageX - shiftX + 'px'; modalBlock.style.top = (pageY - top) - shiftY + 'px'; } function onMouseMove(event) { moveAt(event.pageX, event.pageY); } // move the modalBlock on mousemove document.addEventListener('mousemove', onMouseMove); // drop the modalBlock, remove unneeded handlers modalBlock.onmouseup = function() { document.removeEventListener('mousemove', onMouseMove); modalBlock.onmouseup = null; }; }; // end moveModalContainer // return modalBlock } function createCollapsibleSectionContainer (sectionTitle, idPrefix) { const el = getMainModalElement(); if (!el) return const firstAttempt = el.querySelector(`#${idPrefix}-filter-container`); if (firstAttempt) return firstAttempt function toggleSectionVisibility (ev) { const clickableDiv = ev.target.closest(`#${idPrefix}-filter-container > .section-header`); if (!clickableDiv) return; ev.preventDefault(); const containerDiv = document.querySelector(`#${idPrefix}-filter-container`); const isOpen = /open/.test(containerDiv.className); if (isOpen) { containerDiv.classList.remove('open'); localStorage.setItem(`${idPrefix}IsHidden`, 'true'); } else { containerDiv.classList.add('open'); localStorage.setItem(`${idPrefix}IsHidden`, 'false'); } } // end toggleSectionVisibility // Putting this click listener on the document is (ironically) better for performance // https://gomakethings.com/detecting-click-events-on-svgs-with-vanilla-js-event-delegation/ document.addEventListener('click', toggleSectionVisibility); const sectionContainerDiv = document.createElement('section'); sectionContainerDiv.setAttribute('id',`${idPrefix}-filter-container`); if (!JSON.parse(localStorage.getItem(`${idPrefix}IsHidden`) || 'false')) { sectionContainerDiv.setAttribute('class','open'); } const sectionHeaderDiv = document.createElement('section'); sectionHeaderDiv.setAttribute('class','section-header'); const toggleArrowDiv = document.createElement('section'); toggleArrowDiv.setAttribute('class','toggle-arrow'); toggleArrowDiv.innerHTML = collapsingArrowSvg; sectionHeaderDiv.append(toggleArrowDiv); const sectionHeaderText = document.createElement('span'); sectionHeaderText.setAttribute('class','modal-section-header'); sectionHeaderText.innerText = sectionTitle; sectionHeaderDiv.append(sectionHeaderText); sectionContainerDiv.append(sectionHeaderDiv); el.append(sectionContainerDiv); return sectionContainerDiv } // end createCollapsibleSectionContainer function addCustomModalStyleTag () { const newStyle = document.createElement("style"); newStyle.setAttribute('type', 'text/css'); // classToColor background, color, padding, margin newStyle.innerHTML = ` svg { pointer-events: none; } .modal-button { text-align: center; margin:auto; display: block; min-width: 7em; border: 1px solid #777; padding: 3px 5px; border-radius: 5px; } .modal-section-header { display: inline-block; cursor: pointer; font-size: 1.2em; user-select: none; } .hide-button { margin-bottom:5px; /* background: #DDD; */ /* color:#222; */ border: 1px solid #777; border-radius: 2px; padding: 3px; } .live-button { margin-bottom:5px; font-weight: 800; font-size: 107%; border: 1px solid #777; border-radius: 3px; padding: 3px; color: #EEE; background: #2762a6; } .modal-input-list-button { background: #000; color: #DDD; } #draggable-modal-block { position: fixed; top: 40px; left: 10px; padding: 15px 5px; min-height: 20px; min-width: 20px; background: #333333BB; color: #DDD; z-index: 1000; border: solid transparent 1px; border-radius: 8px; transition: 500ms; opacity: .2; } #draggable-modal-block:hover { background: #333333BB; transition: 0ms; opacity: 1; } .deactivated-filters#draggable-modal-block button, .deactivated-filters#draggable-modal-block section { display: none; } .deactivated-filters#draggable-modal-block button:nth-child(1) { display: block; } .input-modal-checkbox { display:inline-block; margin-left: 25px; } .modal-input-list label { display: inline-block; margin: 0 0 0 8px; font-size: 1em; transition: 250ms; user-select: none; } .modal-input-list label:hover, .modal-input-list li:hover label { color: #FFF; transition: 50ms; } .modal-input-list li { margin: 0; } section.section-header { display: flex; } .toggle-arrow { display: flex; justify-content: space-around; align-items: center; height: 25px; width: 25px; border-radius: 2px; margin: 0; transition: 300 ms; cursor: pointer; /* transform: rotate(-90deg); */ opacity: .8; } .toggle-arrow:hover { opacity: 1; background: black; } .toggle-arrow svg { transform: rotate(-90deg); transition: 300ms; } .open .toggle-arrow svg { transform: rotate(0deg); } .modal-input-list { opacity: 0; display: none; visibility: hidden; transition: visibility 0s lineaer 0.1s, opacity 0.3s ease; padding: 0; margin: 0; max-height: ${maxListHeight}px; overflow-y: auto; } .open .modal-input-list { display: block; visibility: visible; opacity: 1; transition-delay: 0s; } .modal-input-list li { display: block; } `; document.head.appendChild(newStyle); } function createCheckboxWithLabel (options) { /* options example: { id: "modal-checkbox-example-id" , label: "Example Checkbox" , checked: true } */ options = options || {}; const modalBlock = getMainModalElement(); if (!modalBlock) return [] let newCheckbox; if (options && options.id) newCheckbox = modalBlock.querySelector(`#${options.id}`); if (newCheckbox) { console.log("not creating duplicate input", options.id); return [] // don't create duplicates } newCheckbox = document.createElement('input'); newCheckbox.setAttribute('type',`checkbox`); newCheckbox.setAttribute('class',`input-modal-checkbox`); newCheckbox.oninput = function(e) { this.setAttribute('value', newCheckbox.checked); this.blur(); }; for (const key in options) { if (key === 'label') continue if (key === 'checked') { newCheckbox.checked = !!options[key]; } else { newCheckbox.setAttribute(key, options[key]); } } // end for loop const newLabel = document.createElement('label'); newLabel.innerText = options.label; // newLabel.onclick = function () { newCheckbox.checked = !newCheckbox.checked } newLabel.onclick = function () { newCheckbox.click(); }; return [newCheckbox, newLabel] } function createNewCheckboxListItem (el, options) { const newListItem = document.createElement('li'); const newCheckboxElements = createCheckboxWithLabel(options); if (newCheckboxElements.length > 0) { newCheckboxElements.forEach(el => newListItem.append(el)); el.append(newListItem); } } function createSpan (options) { /* options example: { id: "modal-checkbox-example-id" , label: "Example Checkbox" , checked: true } */ options = options || {}; const modalBlock = getMainModalElement(); if (!modalBlock) return let newSpan; if (options && options.id) newSpan = modalBlock.querySelector(`#${options.id}`); if (newSpan) { console.log("not creating duplicate span", options.id); return // don't create duplicates } newSpan = document.createElement('span'); for (const key in options) { newSpan.setAttribute(key, options[key]); } // end for loop return newSpan } function createNewCheckboxListItemWithCount (el, countOptions, checkboxOptions) { const newListItem = document.createElement('li'); const newCheckboxElements = createCheckboxWithLabel(checkboxOptions); if (newCheckboxElements.length > 0) { const countElement = createSpan(countOptions); if (countElement) newCheckboxElements.unshift(countElement); newCheckboxElements.forEach(el => newListItem.append(el)); el.append(newListItem); } } const selectors = { singlePost: '.ghx-avatar > img' // singlePost: '.ghx-columns > .ghx-column:nth-child(2) > div' , postContentSelector: 'table tr:nth-of-type(2) td:nth-of-type(2)' , parentNodeCount: 3 }; const noDisplayString = "display:none;"; const elementSelector = { expandCollapseButtonName: '.ghx-swimlane .aui-button[role="button"]' , expandCollapseButtonParentNodeCount: 3 , buttonContainerParentNodeCount: 1 // I'm not sure which containers are completely re-rendered, // which would remove any MutationObserver I create // So I've opted to use the very highlevel selection of 'section#content' // which contains basically the whole page // , mainContainerForAllTickets: '.ghx-work#ghx-work' // , mainContainerForAllTickets: 'section#content' , mainContainerForAllTickets: 'div#content' }; const hideButtonAttributes = { specialId: "custom-hide-button" , hideLabel: "Disable Filters" , showLabel: "Enable Filters" , hideClass: "hide-button" // This option can invert the active/inactive , showClass: "live-button" // behavior compared to other buttons , localStorageBooleanLabel: "isHidingPosts" , singlePost: "" }; const extraFieldsButtonAttributes = { specialId: "custom-hide-button-extra-fields" , hideLabel: "Show Extra Fields" , showLabel: "Hide Extra Fields" , localStorageBooleanLabel: "hideExtraFields" , singlePost: ".ghx-issue-content .ghx-extra-fields" }; const epicButtonAttributes = { specialId: "custom-hide-button-epic" , hideLabel: "Show Epics" , showLabel: "Hide Epics" , localStorageBooleanLabel: "hideEpics" , singlePost: ".ghx-highlighted-fields" }; const footerButtonAttributes = { specialId: "custom-hide-button-footer" , hideLabel: "Show Card Footers" , showLabel: "Hide Card Footers" , localStorageBooleanLabel: "hideFooters" , singlePost: ".ghx-card-footer > :not(.ghx-avatar)" }; addCustomStyleTag(); createModalBlock(); hidePieces(); const bod = document.querySelector('body'); // bod.addEventListener("keydown", elementScrollJump) setTimeout( () => { // console.log('Running the setTimeout function!') putMutationObserverOnMainElement(bod, hidePieces); hidePieces(); createButtonsToRemoveKanbanSections(); createActiveDeveloperCheckboxes(); } , 1500); // =========================================================================== // Begin support functions // Nothing invoked beneath this point // =========================================================================== function getHideStatus (string) { const rawJSON = localStorage.getItem(string || 'isHidingPosts') || 'false'; return JSON.parse(rawJSON) } function getNameFromDevIcon (el) { const res = { visible: "NO_NAME" , id: "NO_ID" }; if (el.dataset && el.dataset.tooltip) { res.visible = el.dataset.tooltip.split(':')[1].trim(); res.id = res.visible.replace(/[^a-zA-Z]+/, '').toLowerCase(); } return res } function getActiveDevelopers () { const rawJSON = localStorage.getItem('activeDevelopers') || '{}'; const activeDevelopers = JSON.parse(rawJSON); const imgElements = document.querySelectorAll(selectors.singlePost); if (!imgElements) return activeDevelopers for (const key in activeDevelopers) activeDevelopers[key]['present'] = false; imgElements.forEach(el => { const nameObj = getNameFromDevIcon(el); activeDevelopers[nameObj.id] = activeDevelopers[nameObj.id] || {}; const curDev = activeDevelopers[nameObj.id]; curDev.label = nameObj.visible; curDev.active = curDev.active === undefined ? true : curDev.active; curDev.present = true; }); // end forEach imgElement return activeDevelopers } function hidePieces (buttonAttributes) { // each through all selectors collections const mainModalElement = getMainModalElement(); const hideBadPosts = getHideStatus(); hideBadPosts ? mainModalElement.classList.remove('deactivated-filters') : mainModalElement.classList.add('deactivated-filters'); // console.log(`${hideBadPosts ? 'Hiding' : 'Revealing'} those posts...`) const collectionList = buttonAttributes ? [ buttonAttributes ] : [ hideButtonAttributes , extraFieldsButtonAttributes , epicButtonAttributes , footerButtonAttributes ]; collectionList.forEach(buttonAttributesX => { createHideButton(buttonAttributesX); if (!buttonAttributesX.singlePost) return const postElements = document.querySelectorAll(buttonAttributesX.singlePost); if (!postElements) return postElements.forEach((el) => { let targetEl = el; const isHidden = getHideStatus(buttonAttributesX.localStorageBooleanLabel); if (isHidden && hideBadPosts) { targetEl.style = noDisplayString; } else { targetEl.style = ""; // (targetEl.style || '').replace("display:none;", "") } }); // end postElements forEach }); // end forEach collectionList updateActiveDevelopers(); const activeDevelopers = getActiveDevelopers(); // console.log(JSON.stringify(activeDevelopers, null, 2)); const cardElements = document.querySelectorAll(selectors.singlePost); if (!cardElements) return cardElements.forEach(el => { let targetEl = el; for (let i = 0; i < selectors.parentNodeCount; i++) {targetEl = targetEl.parentNode;} const nameObj = getNameFromDevIcon(el); const isHidden = !(nameObj && nameObj.id && activeDevelopers && activeDevelopers[nameObj.id] && activeDevelopers[nameObj.id]['active']); if (isHidden && hideBadPosts) { targetEl.style = noDisplayString; } else { targetEl.style = ""; // (targetEl.style || '').replace("display:none;", "") } }); // end cardElements forEach } // end hidePieces function function createHideFunction(string) { return () => { localStorage.setItem((string || 'isHidingPosts'), JSON.stringify(!getHideStatus(string || 'isHidingPosts'))); hidePieces(); } // end callback function } // end createHideFunction function createHideButton (buttonAttributes) { buttonAttributes = buttonAttributes || hideButtonAttributes; const buttonContainerElement = getMainModalElement(); if (!buttonContainerElement) { console.log('well shit'); return } if (!buttonAttributes.specialId) return let hideButtonElement = buttonContainerElement.querySelector(`#${buttonAttributes.specialId}`); const buttonJustCreated = !hideButtonElement; if (!hideButtonElement) { console.log("creating hide button..."); hideButtonElement = document.createElement('button'); hideButtonElement.id = buttonAttributes.specialId; hideButtonElement.addEventListener('click', createHideFunction(buttonAttributes.localStorageBooleanLabel)); } hideButtonElement.innerText = getHideStatus(buttonAttributes.localStorageBooleanLabel) ? buttonAttributes.hideLabel : buttonAttributes.showLabel; hideButtonElement.className = getHideStatus(buttonAttributes.localStorageBooleanLabel) ? `modal-button ${buttonAttributes.hideClass || 'hide-button'}` : `modal-button ${buttonAttributes.showClass || 'live-button'}`; if (buttonJustCreated) buttonContainerElement.append(hideButtonElement); } // end createHideButton function createButtonsToRemoveKanbanSections () { const buttonList = document.querySelectorAll(elementSelector.expandCollapseButtonName); if (buttonList.length === 0) { console.log("Unable to add buttons for Kanban section removal"); return } buttonList.forEach(button => { let buttonContainerDiv = button; for (let i = 0; i < elementSelector.buttonContainerParentNodeCount; i++) { buttonContainerDiv = buttonContainerDiv.parentNode; } let newButton = buttonContainerDiv.querySelector(`#${hideButtonAttributes.specialId}`); if (!newButton) { const newButton = document.createElement('button'); newButton.id = hideButtonAttributes.specialId; newButton.innerHTML = "Hide Section"; // hideButtonAttributes.hideLabel let swimlaneContainerDiv = button; for (let i = 0; i < elementSelector.expandCollapseButtonParentNodeCount; i++) { swimlaneContainerDiv = swimlaneContainerDiv.parentNode; } newButton.addEventListener('click', createSectionHiderFunction(swimlaneContainerDiv)); buttonContainerDiv.appendChild(newButton); } // end if statement }); } function createSectionHiderFunction (sectionDiv) { return () => { sectionDiv.style = "display: none;"; } } function putMutationObserverOnMainElement (el, callback) { let mainElementTarget = document.querySelector(elementSelector.mainContainerForAllTickets); // configuration of the observer: const mainElementConfig = { attributes: true , childList: true , subtree: true }; // create an observer instance const mainElementObserver = new MutationObserver((mutations) => { // ignore style attribute changes if (mutations.every(m => m.type === 'attributes' && m.attributeName === 'style')) return callback(); }); // end MutationObserver // pass in the target node, as well as the observer options mainElementObserver.observe(mainElementTarget, mainElementConfig); } // end putMutationObserverOnMainElement function createActiveDeveloperCheckboxes () { const modalBlock = getMainModalElement(); if (!modalBlock) return const activeDevContainerDiv = createCollapsibleSectionContainer("Active Developers", "active-dev"); let inputListContainer = modalBlock.querySelector('.modal-input-list.developers'); const inputListIsNew = !inputListContainer; if (!inputListContainer) { inputListContainer = document.createElement('ul'); } inputListContainer.innerHTML = ''; inputListContainer.setAttribute('class','modal-input-list developers'); inputListContainer.onclick="event.stopPropagation()"; inputListContainer.onmousedown="event.stopPropagation()"; const activeDevelopers = getActiveDevelopers(); const sortedKeys = Object.keys(activeDevelopers); sortedKeys.sort(); sortedKeys.forEach(key => { // if (!activeDevelopers[key]['present']) return // to hide devs that are in localStorage but not on the current kanban board const checkboxOptions = { id: `modal-checkbox-${key}` , label: activeDevelopers[key]['label'] , checked: !!activeDevelopers[key]['active'] }; createNewCheckboxListItem(inputListContainer, checkboxOptions); }); // end for each if (inputListIsNew) { putMutationObserverOnInputList(inputListContainer, hidePieces); activeDevContainerDiv.append(inputListContainer); } } // end createDifficultyCheckboxes function putMutationObserverOnInputList (el, callback) { let inputEl = el; if (!inputEl) return // configuration of the observer: const inputElConfig = { attributes: true , characterData: true , subtree: true }; // create an observer instance const mainElementObserver = new MutationObserver(mutations => {callback();}); // end MutationObserver // pass in the target node, as well as the observer options mainElementObserver.observe(inputEl, inputElConfig); } // end createActiveDeveloperCheckboxes function updateActiveDevelopers () { const listInputElements = document.querySelectorAll('.modal-input-list.developers input[type="checkbox"]'); if (!listInputElements) return const activeDevelopers = getActiveDevelopers(); listInputElements.forEach(el => { const key = el.id.replace("modal-checkbox-", ''); if (activeDevelopers[key]) { activeDevelopers[key]['active'] = !!el.checked; } }); localStorage.setItem('activeDevelopers', JSON.stringify(activeDevelopers)); } }());