Sign-Off Trello

Create a Sign-Off file from a selected list

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name           Sign-Off Trello
// @namespace      https://openuserjs.org/users/clemente
// @match          https://trello.com/*
// @version        1.0
// @grant          GM_xmlhttpRequest
// @grant          GM_download
// @connect        trello.com
// @author         clemente
// @license        MIT
// @description    Create a Sign-Off file from a selected list
// @icon           https://images.emojiterra.com/mozilla/128px/1f4c4.png
// @inject-into    content
// @run-at         document-idle
// @homepageURL    https://openuserjs.org/scripts/clemente/Sign-Off_Trello
// @supportURL     https://openuserjs.org/scripts/clemente/Sign-Off_Trello/issues
// @noframes
// ==/UserScript==

/* Logic to get the validations from a list */

function gm_fetch(url) {
  return new Promise((resolve, reject) => {
    GM_xmlhttpRequest({
      method: "GET",
      url: url,
      onload: function({ status, responseText }) {
        if (status < 200 && status >= 300) return reject();
        resolve(JSON.parse(responseText));
      },
      onerror: function() { reject(); },
    });
  });
}

async function getBoardId() {
  const url = `${document.URL}.json?fields=id`;
  const boardInformation = await gm_fetch(url);
  return boardInformation.id;
}

async function getBoardsLists(boardId) {
  const url = `https://trello.com/1/boards/${boardId}/lists`;
  const lists = await gm_fetch(url);
  return lists;
}

async function getListCards(listId) {
  const url = `https://trello.com/1/lists/${listId}/cards?fields=id`;
  const cards = await gm_fetch(url);
  return cards.map(card => card.id);
}

async function getCardLastAction(cardId) {
  const url = `https://trello.com/1/cards/${cardId}?actions=updateCard%3AidList&actions_display=true&action_memberCreator_fields=fullNam%2Cusername&actions_limit=1&fields=email`;
  const card = await gm_fetch(url);
  return card.actions[0];
}

async function getInformation(cardId) {
  const action = await getCardLastAction(cardId);
  const date = action.date;
  const validator = action.display.entities.memberCreator.text;
  const shortLink = action.display.entities.card.shortLink;
  const name = action.display.entities.card.text;
  const id = action.data.card.idShort;
  return { date, validator, shortLink, name, id };
}

async function getListValidations(listId) {
  const cards = await getListCards(listId);
  const validations = await Promise.all(cards.map(getInformation));
  return validations;
}

function formatValidations(validations) {
  const headers = "id,nom du ticket,lien,date de validation,validateur\n";
  const content = validations
    .map(({ date, validator, shortLink, name, id }) => `${id},"${name}",https://trello.com/c/${shortLink},${date},"${validator}"`)
    .join('\n');
  return headers + content;
}

async function downloadSignOff(formattedValidations) {
  const content = 'data:application/csv;charset=utf-8,' + encodeURIComponent(formattedValidations);
  GM_download({ url: content, name: 'sign-off.csv' });
}

async function setValidationsInClipboard(listName) {
  try { 
    const boardId = await getBoardId();
    const lists = await getBoardsLists(boardId);
    const listId = lists.find(list => list.name === listName).id;
    const validations = await getListValidations(listId);
    const formattedValidations = formatValidations(validations);
    downloadSignOff(formattedValidations);
  } catch (e) {
    console.log(e);
    alert('Erreur lors de la création du rapport. Veuillez regarder les logs.');
  }
}

/* Logic to add a button to the list options */


function addReportValidationButton(popoverNode, listName) {
  const reportButton = document.createElement('li');
  const reportButtonLink = document.createElement('a');
  reportButtonLink.textContent = "Créer le rapport de Sign-Off";
  reportButtonLink.href = '#';
  reportButton.append(reportButtonLink);
  reportButton.onclick = () => {
    setValidationsInClipboard(listName);
    popoverNode.querySelector('.icon-close').click();
  };
  popoverNode.querySelector('.pop-over-list').append(reportButton);
}

function onPopoverShown(mutations, observer, listName) {
  mutations.forEach(mutation => {
    if (mutation.addedNodes.length > 0) {
      mutation.addedNodes.forEach(node => {
        addReportValidationButton(node, listName);
        observer.disconnect();
      });
    }
  });
}

function onListOptionsClick(event) {
  const listName = event.target.parentNode.parentNode.querySelector('.js-list-name-input').textContent;
  // Create a mutation observer instead of adding directly the button because the popover is mounted a bit after the click
  const popoverObserver = new MutationObserver((mutations, observer) => onPopoverShown(mutations, observer, listName));
  popoverObserver.observe(document.querySelector('.pop-over'), { childList: true });
}

function initListOptionsWatch() {
  document.querySelectorAll('.js-open-list-menu').forEach(node => {
    node.removeEventListener('click', onListOptionsClick); // Remove previous event listener if already set
    node.addEventListener('click', onListOptionsClick);
  });
}

// Wait 30 secondes for the board to load
setTimeout(initListOptionsWatch, 30000);