GitLab Extension

Allows to fold any board in GitLab boards, shows estimate and last modified in issue card

  1. // ==UserScript==
  2. // @name GitLab Extension
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.2
  5. // @description Allows to fold any board in GitLab boards, shows estimate and last modified in issue card
  6. // @author Himalay
  7. // @include https://gitlab.*
  8. // ==/UserScript==
  9.  
  10. // estimate and modified time in card
  11.  
  12. // Board fold
  13. let foldableGitLabBoardsIntervalCount = 0
  14. const foldableGitLabBoardsInterval = setInterval(() => {
  15. const boards = [...document.querySelectorAll('.board.is-draggable')]
  16.  
  17. if (foldableGitLabBoardsIntervalCount > 100)
  18. clearInterval(foldableGitLabBoardsInterval)
  19. if (boards.length) {
  20. clearInterval(foldableGitLabBoardsInterval)
  21.  
  22. document.body.appendChild(
  23. Object.assign(document.createElement('style'), {
  24. textContent: `.board.is-collapsed .board-title>span {
  25. width: auto;
  26. margin-top: 24px;
  27. }`,
  28. }),
  29. )
  30.  
  31. boards.forEach((board) => {
  32. const boardTitle = board.querySelector('.board-title')
  33. const toggleIcon = Object.assign(document.createElement('i'), {
  34. classList: 'fa fa-fw board-title-expandable-toggle fa-caret-down',
  35. style: 'cursor: pointer',
  36. })
  37.  
  38. toggleIcon.addEventListener('click', (e) => {
  39. board.classList.toggle('is-collapsed')
  40. e.target.classList.toggle('fa-caret-down')
  41. e.target.classList.toggle('fa-caret-right')
  42. })
  43.  
  44. boardTitle.prepend(toggleIcon)
  45. })
  46. }
  47.  
  48. foldableGitLabBoardsIntervalCount++
  49. }, 100)
  50.  
  51. var TimeAgo = (function() {
  52. var self = {}
  53. // Public Methods
  54. self.locales = {
  55. prefix: `It's been`,
  56. sufix: '',
  57.  
  58. seconds: 'less than a minute.',
  59. minute: 'about a minute.',
  60. minutes: '%d minutes.',
  61. hour: 'about an hour.',
  62. hours: 'about %d hours.',
  63. day: 'a day.',
  64. days: '%d days.',
  65. month: 'about a month.',
  66. months: '%d months.',
  67. year: 'about a year.',
  68. years: '%d years.',
  69. }
  70.  
  71. self.inWords = function(timeAgo) {
  72. var seconds = Math.floor((new Date() - parseInt(timeAgo)) / 1000),
  73. separator = this.locales.separator || ' ',
  74. words = this.locales.prefix + separator,
  75. interval = 0,
  76. intervals = {
  77. year: seconds / 31536000,
  78. month: seconds / 2592000,
  79. day: seconds / 86400,
  80. hour: seconds / 3600,
  81. minute: seconds / 60,
  82. }
  83.  
  84. var distance = this.locales.seconds
  85.  
  86. for (var key in intervals) {
  87. interval = Math.floor(intervals[key])
  88.  
  89. if (interval > 1) {
  90. distance = this.locales[key + 's']
  91. break
  92. } else if (interval === 1) {
  93. distance = this.locales[key]
  94. break
  95. }
  96. }
  97.  
  98. distance = distance.replace(/%d/i, interval)
  99. words += distance + separator + this.locales.sufix
  100.  
  101. return words.trim()
  102. }
  103.  
  104. return self
  105. })()
  106.  
  107. const shouldFetch = document.querySelector('.board-card,.issue')
  108. const fetchThemAll = async (url) => {
  109. let nextPage = 1
  110. let data = []
  111. while (true) {
  112. const res = await fetch(url.replace('{{page}}', nextPage), {
  113. method: 'GET',
  114. credentials: 'include',
  115. headers: {
  116. accept: 'application/json, text/plain, */*',
  117. 'x-requested-with': 'XMLHttpRequest',
  118. },
  119. mode: 'cors',
  120. })
  121. data.push(...(await res.json()))
  122. const previousPage = nextPage
  123. nextPage = res.headers.get('x-next-page')
  124. console.log({ previousPage, nextPage })
  125. if (!nextPage || nextPage === previousPage) break
  126. }
  127. return data
  128. }
  129.  
  130. const isLessThanAgo = (hour = 1, date) => date > Date.now() - hour * 3600000
  131. const setLabels = () =>
  132. [...document.querySelectorAll('.board-card,.issue')].forEach((card) => {
  133. const { issueId, id } = card.dataset
  134. const onlyCard = id
  135. const issue = issues[issueId || id]
  136. if (issue) {
  137. const {
  138. assignee,
  139. state,
  140. updated_at,
  141. time_stats: { time_estimate, total_time_spent },
  142. } = issue
  143. const isOpen = state === 'opened'
  144. const updatedDate = new Date(updated_at)
  145. const lastUpdate = TimeAgo.inWords(updatedDate.getTime())
  146. let emoji = isLessThanAgo(4, updatedDate)
  147. ? '👍'
  148. : isLessThanAgo(24, updatedDate)
  149. ? '👎'
  150. : '🙏'
  151. emoji = assignee && isOpen ? emoji : ''
  152. const cardStyle = `
  153. height: 1.5em;
  154. width: 1em;
  155. padding: 1px;
  156. border-radius: 3px;
  157. text-align: center;
  158. font-size: small;
  159. margin-left: 0.5em;
  160. background: #5cb85b;
  161. color: white;
  162. position: absolute;
  163. top: 0.5em;
  164. right: 0.5em;
  165. ${total_time_spent ? 'text-decoration: line-through;' : ''}
  166. `
  167. const sp = time_estimate
  168. ? `<span style="${cardStyle}">${time_estimate / 60 / 60}</span>`
  169. : ''
  170. const assignie = card.querySelector('.board-card-assignee,.controls')
  171. const pointAndTime = card.querySelector('.point-and-time')
  172. const content = onlyCard ? sp : emoji + lastUpdate + sp
  173. if (pointAndTime) {
  174. pointAndTime.innerHTML = content
  175. } else {
  176. let assignieHtml = assignie.innerHTML
  177. assignieHtml += `<span class="point-and-time" style="margin-left: 0.5em">${content}</span>`
  178. assignie.innerHTML = assignieHtml
  179. }
  180. }
  181. })
  182.  
  183. const cachedIssues = localStorage.getItem('issues')
  184. let issues = JSON.parse(cachedIssues || '{}')
  185. setLabels()
  186.  
  187. if (shouldFetch || !cachedIssues) {
  188. ;(async function iife() {
  189. issues = (await fetchThemAll(
  190. 'https://gitlab.innovatetech.io/api/v4/groups/ap/issues?page={{page}}&per_page=100',
  191. )).reduce((acc, { id, ...issue }) => {
  192. acc[id] = issue
  193. return acc
  194. }, {})
  195. localStorage.setItem('issues', JSON.stringify(issues))
  196. setLabels()
  197. })()
  198. }