// vim:ts=4:sts=4:sw=4:et // // Author: Hari Sekhon // Date: 2024-11-19 16:40:30 +0400 (Tue, 19 Nov 2024) // // https///github.com/HariSekhon/TamperMonkey // // License: see accompanying Hari Sekhon LICENSE file // // If you're using my code you're welcome to connect with me on LinkedIn and optionally send me feedback to help steer this or other code I publish // // https://www.linkedin.com/in/HariSekhon // // ==UserScript== // @name Jira Description Autofill // @namespace http://tampermonkey.net/ // @version 1.8 // @description Insert editable text in Jira's description field // @author Hari Sekhon // @match https://*.atlassian.net/* // @grant none // @require https://cdnjs.cloudflare.com/ajax/libs/dompurify/2.4.0/purify.min.js // ==/UserScript== // Tested on Chrome (function () { 'use strict'; const script_name = 'TamperMonkey: Jira Description Autofill'; console.log(`${script_name}: Initializing...`); // XXX: Edit to suit your template preference - the reason to make this HTML is this is how the Jira UI does it // so you can edit it normally, otherwise you'll have weird behaviour in the editor const descriptionHTML = ` <h3>Summary</h3> <p><strong>As a</strong> platform engineer<br /> <strong>I want</strong> ...<br /> <strong>So that</strong> ...</p> <h3>Acceptance Criteria</h3> <ul class="ak-ul" data-indent-level="1"> <li><p>Criteria one...</p></li> <li><p>Criteria two...</p></li> </ul> <h3>Engineering Notes & References</h3> <ul class="ak-ul" data-indent-level="1"> <li><p>Put Notes here</p></li> <li><p>Record design decisions</p></li> <li><p>URLs to references and relevant docs</p></li> </ul> `; // prevent excessive executions let isProcessing = false; //let textInserted = false; function fillJiraDescription() { // If closing and re-opening or opening more than Jira ticket in the same tab, // this fails to insert the description into the new Jira ticket //if (textInserted) { // console.log('Text already inserted, skipping...'); // return //} if (isProcessing) { console.log(`${script_name}: processing semaphore is set, skipping execution...`); return; } console.log(`${script_name}: setting semaphore to prevent re-running function concurrently`); isProcessing = true; console.log(`${script_name}: searching for description area...`); // matches comment boxes //const editorArea = document.querySelector('div#ak-editor-textarea'); // this is more targeted const editorArea = document.querySelector('div[aria-label*="Description"]'); if (editorArea) { console.log(`${script_name}: found the description editor:`, editorArea); const placeholder = editorArea.querySelector('span[data-testid="placeholder-test-id"]'); if (placeholder) { placeholder.remove(); //const editableParagraph = document.createElement('p'); //editableParagraph.contentEditable = true; //editableParagraph.innerHTML = description.trim() //.replace(/\n/g, '<br>'); // Convert newlines to <br>; //editorArea.innerHTML = ''; // Clear existing content //editorArea.appendChild(editableParagraph); // Add the editable paragraph const template = document.createElement('template'); // Trim to avoid extra text nodes leading to whitespace at top of description const descriptionHTMLtrimmed = descriptionHTML.trim(); // imported implicity by TamperMonkey header // eslint-disable-next-line no-undef const sanitizedHTML = DOMPurify.sanitize(descriptionHTMLtrimmed); template.innerHTML = sanitizedHTML; // clear all existing children to remove trailingBreak and other placeholders // which cause prepended space to the top of the description box while (editorArea.firstChild) { editorArea.removeChild(editorArea.firstChild); } // append the native elements directly to the editor while (template.content.firstChild) { editorArea.appendChild(template.content.firstChild); } //textInserted = true; console.log(`${script_name}: added Jira Description`); } } else { console.log(`${script_name}: Jira Description editor not found`); } console.log(`${script_name}: setting timeout of 2000ms before resetting the processing flag`); setTimeout(() => { isProcessing = false; }, 2000); // 2 second delay before the function can run again } console.log(`${script_name}: starting MutationObserver to monitor dynamic DOM changes`); const observer = new MutationObserver(() => { // Only trigger the function if the description area is not yet populated const editorArea = document.querySelector('div#ak-editor-textarea'); if (editorArea && !editorArea.querySelector('p[contenteditable="true"]')) { fillJiraDescription(); } }); // Observe the entire body for changes observer.observe(document.body, { childList: true, subtree: true } ); console.log(`${script_name}: initial call to autofill description if already present`); fillJiraDescription(); //function sleep(ms) { // return new Promise(resolve => setTimeout(resolve, ms)); //} })();