Gitlab Issues Track

Savior of bug track in Gitlab issue!

As of 2019-04-18. See the latest version.

// ==UserScript==
// @name         Gitlab Issues Track
// @namespace    http://tampermonkey.net/
// @homepage     https://github.com/Priestch/savior
// @version      0.2.0
// @description  Savior of bug track in Gitlab issue!
// @author       Priestch
// @match        https://gitpd.paodingai.com/*/issues/*
// @grant        GM_addStyle
// ==/UserScript==

(function () {
  'use strict';

  function exportToCsv(filename, rows) {
    const processRow = function (row) {
      let finalVal = '';
      for (let j = 0; j < row.length; j++) {
        let innerValue = row[j] === null ? '' : row[j].toString();
        if (row[j] instanceof Date) {
          innerValue = row[j].toLocaleString();
        }
        let result = innerValue.replace(/"/g, '""');
        if (result.search(/("|,|\n)/g) >= 0)
          result = '"' + result + '"';
        if (j > 0)
          finalVal += ',';
        finalVal += result;
      }
      return finalVal + '\n';
    };

    let csvFile = '';
    for (let i = 0; i < rows.length; i++) {
      csvFile += processRow(rows[i]);
    }

    const blob = new Blob([csvFile], { type: 'text/csv;charset=utf-8;' });
    if (navigator.msSaveBlob) { // IE 10+
      navigator.msSaveBlob(blob, filename);
    } else {
      const link = document.createElement('a');
      if (link.download !== undefined) { // feature detection
        // Browsers that support HTML5 download attribute
        const url = URL.createObjectURL(blob);
        link.setAttribute('href', url);
        link.setAttribute('download', filename);
        // link.style.visibility = 'hidden';
        // document.body.appendChild(link);
        link.click();
        // document.body.removeChild(link);
      }
    }
  }


  function formatTask(task) {
    return [
      'author=> ' + task.author,
      'checked=> ' + task.checked,
      'priority=> ' + task.priority,
      'link=> ' + task.link,
    ].join('; ');
  }

  function filterTasksByPriority(tasks, priority) {
    return tasks.filter(function (task) {
      return task.priority === priority;
    });
  }

  function generateReport(tasks) {
    const done = tasks.filter(function (task) {
      return task.checked;
    });
    const left = tasks.filter(function (task) {
      return !task.checked;
    });
    const totalReport = [
      'total=> ' + tasks.length,
      'done=> ' + done.length,
      'left=> ' + left.length,
    ].join('; ');
    console.log('Summary:', totalReport);
    console.log();

    const ALevel = filterTasksByPriority(left, 'A');
    const BLevel = filterTasksByPriority(left, 'B');
    const CLevel = filterTasksByPriority(left, 'C');

    const leftReport = [
      'A=> ' + ALevel.length,
      'B=> ' + BLevel.length,
      'C=> ' + CLevel.length,
    ].join('; ');
    console.log('Left:', leftReport);

    for (let i = 0; i < left.length; i++) {
      console.log(formatTask(left[i]));
    }
  }

  function collectTasks() {
    const noteList = document.querySelectorAll('#notes-list .note');
    const filtered = Array.from(noteList).filter((item) => item.querySelector('.timeline-entry-inner .timeline-content'));
    const tasks = [];
    filtered.forEach((item) => {
      const timelineContent = item.querySelector('.timeline-entry-inner .timeline-content');
      const taskDomList = timelineContent.querySelectorAll('.note-body .task-list');
      const task = {
        author: '',
        link: '',
        checked: false,
        description: '',
        priority: 'C',
        domWrapper: item,
        id: ''
      };
      if (taskDomList.length > 0) {
        task.author = timelineContent.querySelector('.note-header .note-header-author-name').textContent;
        const actions = timelineContent.querySelector('.note-header .note-actions .more-actions-dropdown');

        const actionList = actions.querySelectorAll('li .js-btn-copy-note-link');
        task.link = actionList[0].dataset.clipboardText;
        if (taskDomList.length > 1) {
          console.error(formatTask(task));
        }
        const taskItem = taskDomList[0].querySelector('.task-list-item');
        const taskInput = taskItem.querySelector('input');
        task.checked = taskInput.checked;
        task.description = taskItem.textContent.trim();
        const idMatchResult = task.description.match(/^(\d+)\.?/)
        if (idMatchResult) {
          task.id = idMatchResult[1];
          task.description = task.description.replace(/^(\d+)\./, '').trim();
        }
        const priorityPattern = /([ABC]).*bug/;
        const matchResult = timelineContent.querySelector('.note-body').textContent.match(priorityPattern);
        if (matchResult) {
          task.priority = matchResult[1];
        }
        tasks.push(task);
      }
    });
    tasks.sort(function (a, b) {
      if (a.priority < b.priority) {
        return -1;
      }
      if (a.priority > b.priority) {
        return 1;
      }
      return 0;
    });
    return tasks;
  }

  function collapseGitlabNotes() {
    const tasks = collectTasks();
    for (let i = 0; i < tasks.length; i++) {
      const task = tasks[i];
      if (task.checked) {
        task.domWrapper.classList.add('collapse-item')
      } else {
        if (task.priority === 'A') {
          task.domWrapper.classList.add('highest-level-bug')
        }
      }
    }
  }

  function createMenuItem(content, title, handler) {
    let button = document.createElement('button');
    button.textContent = content;
    button.setAttribute('title', title);
    button.addEventListener('click', handler);
    return button
  }

  function padStart(string, length, pad) {
    const s = String(string);
    if (!s || s.length >= length) return string;
    return `${Array((length + 1) - s.length).join(pad)}${string}`;
  }

  function parseProject() {
    let prefix = window.location.protocol + '//' + window.location.hostname + '/cheftin/';
    return window.location.href.replace(prefix, '').split('/')[0];
  }

  function generateFilename() {
    const now = new Date();
    const year = now.getFullYear();
    const month = padStart(now.getMonth() + 1, 2, '0');
    const day = padStart(`${now.getDate()}`, 2, '0');
    return `${year}_${month}_${day}_${parseProject()}.csv`
  }

  function exportAsCSV() {
    const tasks = collectTasks(false);
    console.log(tasks);
    const rows = [];
    const keys = ['id', 'description', 'checked', 'priority', 'author', 'link'];  // from task key
    rows.push(keys);
    for (let i = 0; i < tasks.length; i++) {
      const task = tasks[i];
      const row = [];
      for (let j = 0; j < keys.length; j++) {
        const key = keys[j];
        row.push(task[key]);
      }
      rows.push(row);
    }
    let filename = generateFilename();
    exportToCsv(filename, rows)
  }

  function createMenu() {
    const descContainer = document.querySelector('.detail-page-description');
    descContainer.classList.add('savior');

    const menuDom = document.createElement('div');
    menuDom.classList.add('savior-menu');
    const menuItems = [
      createMenuItem('导出', '导出CSV', exportAsCSV),
      createMenuItem('折叠', '折叠评论', collapseGitlabNotes),
    ];
    for (let i = 0; i < menuItems.length; i++) {
      const menuItem = menuItems[i];
      menuDom.appendChild(menuItem);
    }
    descContainer.appendChild(menuDom);
  }

  GM_addStyle(`
  .notes .note.collapse-item:not(.highest-level-bug) {
    height: 150px;
    background-color: #67c23a;
    overflow: hidden;
  }
  
  .notes-list .note.highest-level-bug {
    background: #f56c6c;
  }
  
  .detail-page-description.savior {
    position: relative;
    border-top-right-radius: 5px;
    border-bottom-right-radius: 5px;
    border-right: 1px solid #eaeaea;
  }

  .savior-menu {
    top: 0;
    right: -56px;
    position: absolute;
    width: 46px;
    display: inline-flex;
    flex-direction: column;
    padding: 0;
    font-size: 12px;
  }

  .savior-menu button {
    outline: none;
    background-color: #e0e1e2;
    color: #0009;
    padding: 5px 10px;
    border: none;
    box-shadow: 0 0 0 1px transparent inset, 0 0 0 0 rgba(34,36,38,.15) inset;
  }

  .savior-menu button:hover {
    background-color: #cacbcd;
    color: #000c;
  }
  `);

  createMenu();
})();