Jira child issues without swimlanes

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

// ==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);
            }
        }
    });
})();