Skip to content

Instantly share code, notes, and snippets.

@inad9300
Last active September 8, 2024 21:13
Show Gist options
  • Save inad9300/90be33471763812da3769e06d0a07b1c to your computer and use it in GitHub Desktop.
Save inad9300/90be33471763812da3769e06d0a07b1c to your computer and use it in GitHub Desktop.
GitLab board enhancements via "User JavaScript and CSS" Chrome extension (https://gitlab.com/*/boards)
// Icons (source: https://css.gg)
.gg-notes {
box-sizing: border-box;
position: relative;
display: block;
width: 20px;
height: 22px;
border: 2px solid;
border-radius: 3px;
}
.gg-notes::after,
.gg-notes::before {
content: '';
display: block;
box-sizing: border-box;
position: absolute;
border-radius: 3px;
height: 2px;
background: currentColor;
left: 2px;
}
.gg-notes::before {
box-shadow: 0 4px 0, 0 8px 0;
width: 12px;
top: 2px;
}
.gg-notes::after {
width: 6px;
top: 14px;
}
.gg-list-tree {
box-sizing: border-box;
position: relative;
display: block;
width: 22px;
height: 22px;
background:
linear-gradient(to left, currentcolor 8px, transparent 0) no-repeat left top/8px 8px,
linear-gradient(to left, currentcolor 8px, transparent 0) no-repeat center 3px/8px 2px,
linear-gradient(to left, currentcolor 8px, transparent 0) no-repeat 10px 17px/6px 2px,
linear-gradient(to left, currentcolor 8px, transparent 0) no-repeat 10px 3px/2px 16px;
}
.gg-list-tree::after,
.gg-list-tree::before {
content: '';
display: block;
box-sizing: border-box;
position: absolute;
width: 8px;
height: 8px;
border: 2px solid;
right: 0;
}
.gg-list-tree::after {
bottom: 0;
}
.gg-comment {
box-sizing: border-box;
position: relative;
display: block;
width: 20px;
height: 16px;
border: 2px solid;
border-bottom: 0;
box-shadow:
-6px 8px 0 -6px,
6px 8px 0 -6px;
}
.gg-comment::after,
.gg-comment::before {
content: "";
display: block;
box-sizing: border-box;
position: absolute;
width: 8px;
}
.gg-comment::before {
border: 2px solid;
border-top-color: transparent;
border-bottom-left-radius: 20px;
right: 4px;
bottom: -6px;
height: 6px;
}
.gg-comment::after {
height: 2px;
background: currentColor;
box-shadow: 0 4px 0 0;
left: 4px;
top: 4px;
}
// Custom styles
.board-card-number-container {
align-items: center !important;
}
.issue-card-badge {
display: flex;
align-items: center;
margin-right: 2px;
font-size: smaller;
color: var(--gl-text-secondary, #737278);
}
.issue-card-icon {
transform: scale(0.6364);
color: var(--gl-text-secondary, #737278);
}
.issue-side-block {
white-space: pre-wrap;
font-size: smaller;
font-style: italic;
color: gray;
}
{
const h = tag => document.createElement(tag)
const $ = selector => document.querySelector(selector)
const $$ = selector => Array.from(document.querySelectorAll(selector))
const waitFor = selector => new Promise(resolve => {
const timerId = setInterval(() => {
const node = $(selector)
if (node) {
clearInterval(timerId)
resolve(node)
}
}, 100)
})
const graphql = query => {
return fetch('/api/graphql', {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'X-CSRF-Token': $('meta[name=csrf-token]').content
},
body: JSON.stringify({ query })
})
.then(res => res.json())
}
const descriptionBadge = desc => {
const root = h('span')
root.className = 'issue-card-badge issue-card-icon gg-notes'
root.style.order = '1'
root.title = desc
return root
}
const descriptionBlock = desc => {
const root = h('div')
root.className = 'issue-side-block'
root.textContent = desc
return root
}
const commentsBadge = count => {
const icon = h('span')
icon.className = 'issue-card-icon gg-comment'
const root = h('span')
root.className = 'issue-card-badge'
root.style.order = '2'
root.style.marginTop = '-3px'
root.append(icon, count)
return root
}
const issuesToMarkdown = issues =>
issues
.map(iss => `- [${iss.state === 'OPEN' ? ' ' : 'x'}] ${iss.name}`)
.join('\n')
const childIssuesBadge = issues => {
const doneIssues = issues.filter(iss => iss.state !== 'OPEN')
const count = `${doneIssues.length}/${issues.length}`
const icon = h('span')
icon.className = 'issue-card-icon gg-list-tree'
const root = h('span')
root.className = 'issue-card-badge'
root.style.order = '3'
root.append(icon, count)
root.title = issuesToMarkdown(issues)
return root
}
const childIssuesBlock = issues => {
const root = h('div')
root.className = 'issue-side-block'
root.textContent = issuesToMarkdown(issues)
return root
}
let interval = 100
let times = 0
const enhanceCards = () => {
for (const card of $$('.board-card:not([data-visited])')) {
card.dataset.visited = ''
const metadataContainer = card.querySelector('.board-card-number-container')
const issue = { id: card.dataset.itemId }
graphql(`query { issue(id: "${issue.id}") { description userNotesCount } }`)
.then(({ data }) => {
Object.assign(issue, data.issue)
if (issue.description)
metadataContainer.append(descriptionBadge(issue.description))
if (issue.userNotesCount)
metadataContainer.append(commentsBadge(issue.userNotesCount))
})
graphql(`query {
workItem(id: "${issue.id.replace('/Issue/', '/WorkItem/')}") {
widgets {
... on WorkItemWidgetHierarchy {
type
children {
nodes { name state }
}
}
}
}
}`)
.then(({ data }) => {
issue.children = data
.workItem
.widgets
.find(w => w.type === 'HIERARCHY')
.children
.nodes
if (issue.children.length > 0)
metadataContainer.append(childIssuesBadge(issue.children))
})
card.addEventListener('click', () => {
waitFor('.boards-sidebar').then(sidebar => {
sidebar.querySelector('.description-block')?.remove()
sidebar.querySelector('.child-issues-block')?.remove()
const sidebarTitle = sidebar.querySelector('[data-testid=sidebar-title]')
if (issue.children.length > 0)
sidebarTitle.after(childIssuesBlock(issue.children))
if (issue.description)
sidebarTitle.after(descriptionBlock(issue.description))
})
})
}
if (times++ > 10) interval = 1000
setTimeout(enhanceCards, interval)
}
enhanceCards()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment