Jira child issues without swimlanes

Show child issues inline without swimlanes in next-gen JIRA projects on grouping None

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Jira child issues without swimlanes
// @namespace    ydushko/jira-child-issues-without-swimlanes
// @version      0.6
// @description  Show child issues inline without swimlanes in next-gen JIRA projects on grouping None
// @author       Yaroslav Dushko
// @match        https://*.atlassian.net/jira/software/projects/*/boards/*
// @grant        none
// ==/UserScript==

/* jshint esversion: 8 */

(async function() {
    'use strict';

    function log(msg) {
        console.log('[JiraChildIssues]', msg);
    }

    function appendStyles() {
        const styleEl = document.createElement('style');
        styleEl.innerText = `
a.parent-link, a.child-link {
    color: inherit;
}
a.parent-link:hover, a.child-link:hover {
    color: inherit;
    text-decoration: none;
}
.wrapper-card {
    display: inline-block;
    border-radius: 3px;
    padding: 3px;
    background: #e3e4e6;
    box-shadow: rgba(23, 43, 77, 0.2) 0px 1px 1px, rgba(23, 43, 77, 0.2) 0px 0px 1px;
    margin: 2px 0;
    cursor: pointer;
    width: calc(100% - 6px);
}
.wrapper-card .head, .wrapper-card .head div {
    display: flex;
    flex-direction: row;
    padding: 3px;
    align-items: center;
}
.child-card {
    background: white;
    border-radius: 3px;
    box-shadow: rgba(23, 43, 77, 0.2) 0px 1px 1px, rgba(23, 43, 77, 0.2) 0px 0px 1px;
    padding: 12px 9px;
    display: flex;
    flex-direction: column;
}
.child-card:hover {
    background: rgb(244, 245, 247);
}
.child-card .footer, .child-card .footer div {
    margin-top: 6px;
    display: flex;
    flex-direction: row;
    justify-content: space-between;
    align-items: center;
}
.issue-type {
    margin-right: 6px;
}
.issue-key {
    font-size: 12px;
    font-weight: 600;
    color: rgb(122, 134, 154);
}
.assignee {
    border-radius: 50%;
    width: 24px;
    height: 24px;
    margin: 0 0 0 6px !important;
}
.estimate {
    background-color: rgb(223, 225, 230);
    color: rgb(23, 43, 77);
    border-radius: 2em;
    display: inline-block;
    font-size: 12px;
    font-weight: normal;
    line-height: 1;
    min-width: 1px;
    padding: 0.166667em 0.5em;
    text-align: center;
}
`;
        document.head.appendChild(styleEl);
    }
    appendStyles();

    async function fetchBoardData() {
        const boardId = window.location.pathname.split('/').slice(-1)[0];
        const gqlQuery = {
            operationName: 'SoftwareBoardScopeData',
            query: `
query SoftwareBoardScopeData($boardId: ID!) {
	boardScope(boardId: $boardId) {
		userSwimlaneStrategy
		board {
			assignees {
				accountId
				displayName
				avatarUrl
			}
			columns {
				id
				name
				maxIssueCount
				status {
					id
					name
				}
				columnStatus {
					status {
						id
						name
						category
					}
					transitions {
						id
						name
						status {
							id
						}
						originStatus {
							id
						}
						cardType {
							id
						}
						isGlobal
						isInitial
						hasConditions
					}
				}
				isDone
				isInitial
				transitionId
				cards {
					id
					flagged
					done
					parentId
					estimate {
						storyPoints
					}
					issue {
						id
						key
						summary
						labels
						assignee {
							accountId
							displayName
							avatarUrl
						}
						type {
							id
							name
							iconUrl
						}
						status {
							id
						}
					}
					coverMedia {
						attachmentId
						endpointUrl
						clientId
						token
						attachmentMediaApiId
						hiddenByUser
					}
					priority {
						name
						iconUrl
					}
					dueDate
					childIssuesMetadata {
						complete
						total
					}
				}
			}
			issueTypes {
				id
				name
				iconUrl
				hierarchyLevelType
			}
			inlineIssueCreate {
				enabled
			}
			cardMedia {
				enabled
			}
			issueChildren {
				id
				flagged
				done
				parentId
				estimate {
					storyPoints
				}
				issue {
					id
					key
					summary
					labels
					assignee {
						accountId
						displayName
						avatarUrl
					}
					type {
						id
						name
						iconUrl
					}
					status {
						id
					}
				}
				coverMedia {
					attachmentId
					endpointUrl
					clientId
					token
					attachmentMediaApiId
					hiddenByUser
				}
				priority {
					name
					iconUrl
				}
				dueDate
			}
			cards {
				id
			}
		}
		features {
			key
			status
			toggle
			category
		}
		projectLocation {
			id
			key
			name
			isSimplifiedProject
			issueTypes {
				id
				name
				iconUrl
				hierarchyLevelType
			}
		}
		issueParents {
			id
			key
			summary
			issue {
				status {
					id
				}
			}
			issueType {
				id
				name
				iconUrl
			}
			color
		}
	}
}`,
            variables: {
                boardId: boardId
            }
        };

        return fetch(`https://${window.location.host}/jsw/graphql?operation=SoftwareBoardScopeData`, {
            "headers": {
                "accept": "application/json,text/javascript,*/*",
                "content-type": "application/json",
            },
            "referrer": window.location.href,
            "referrerPolicy": "same-origin",
            "body": JSON.stringify(gqlQuery),
            "method": "POST",
            "mode": "cors",
            "credentials": "include"
        }).then(
            (res) => res.json()
        );
    }

    async function onGroupByChange(cb){
        const groupByButton = document.querySelector('[data-test-id="software-board.header.controls-bar.swimlane-switch"] button')
        let groupBy = groupByButton.innerText;
        cb(groupBy);
        groupByButton.addEventListener('click', () => {
            setTimeout(() => {
                const dropdownItems = document.querySelectorAll('[data-test-id="software-board.header.controls-bar.swimlane-switch.dropdown-item"]');
                let dropdownClickListener = () => {
                    for (const item of dropdownItems) {
                        item.removeEventListener('click', dropdownClickListener);
                    }
                    setTimeout(() => {
                        groupBy = groupByButton.innerText;
                        cb(groupBy);
                    }, 1);
                };
                for (const item of dropdownItems) {
                    item.addEventListener('click', dropdownClickListener);
                }
            }, 1);
        });
    }

    onGroupByChange(async (groupBy) => {
        if (groupBy === 'None') {
            const data = await fetchBoardData();
            let statusColumns = {};
            let issueCards = {};
            for (const column of data.data.boardScope.board.columns) {
                const columnEl = document.querySelector('[data-rbd-droppable-id="COLUMN::' + column.id + '"]');
                statusColumns[column.status[0].id] = columnEl;
                for (const card of column.cards) {
                    issueCards[card.issue.id] = card;
                }
            }
            for (const child of data.data.boardScope.board.issueChildren) {
                const parent = issueCards[child.parentId];
                const parentEl = document.querySelector('[data-rbd-draggable-id="ISSUE::' + parent.issue.id + '"]');
                const columnEl = statusColumns[child.issue.status.id];
                const cardEl = document.createElement('div');
                let assigneeHtml = '';
                if (child.issue.assignee) {
                    assigneeHtml = `<img class="assignee" src="${child.issue.assignee.avatarUrl}" title="${child.issue.assignee.displayName}"/>`;
                }
                let estimateHtml = '';
                if (child.estimate.storyPoints) {
                    estimateHtml = `<span class="estimate">${child.estimate.storyPoints}</span>`;
                }
                cardEl.style.padding = '0px 4px';
                cardEl.innerHTML = `
<a class="parent-link" href="${window.location.pathname + '?selectedIssue=' + parent.issue.key}"><div class="wrapper-card">
      <div class="head">
          <div>
              <img class="issue-type" src="${parent.issue.type.iconUrl}" />
              <span class="issue-key">${parent.issue.key}</span>
          </div>
      </div>
      <a class="child-link" href="${window.location.pathname + '?selectedIssue=' + child.issue.key}"><div class="child-card">
          <div>${child.issue.summary}</div>
          <div class="footer">
              <div>
                  <img class="issue-type" src="${child.issue.type.iconUrl}" />
                  <span class="issue-key">${child.issue.key}</span>
              </div>
              <div>
                  ${estimateHtml}
                  ${assigneeHtml}
              </div>
          </div>
     </div></a>
</div></a>`;
                columnEl.prepend(cardEl);
            }
        }
    });
})();