Mint.com tags display

Show tags in the transactions listing on Mint.com.

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name        Mint.com tags display
// @match       https://mint.intuit.com/transactions
// @connect     mint.intuit.com
// @description Show tags in the transactions listing on Mint.com.
// @namespace   com.warkmilson.mint.js
// @author      Mark Wilson (update by Shaun Williams)
// @version     2.0.1
// @homepage    https://github.com/mddub/mint-tags-display
// @grant       none
// @noframes
// ==/UserScript==
//

(function() {

  //------------------------------------------------------------------------------
  // Logging
  //------------------------------------------------------------------------------

  const logging = false
  function log(...args) {
    if (logging) console.info('MINT_TAGS', ...args)
  }

  //------------------------------------------------------------------------------
  // Track state by watching XHR
  //------------------------------------------------------------------------------

  // State

  const state = {
    txnTags: {}, // txn id -> [tag name]
    tagOrder: [], // [tag name]
    tagName: {}, // tag id -> tag name
  }
  window._MINT_TAGS = state

  // Update state

  const apiUrl = path => `https://mint.intuit.com/pfm/v1${path}`

  const apiHooks = {
    // when transactions are fetched, save tags belonging to each transaction
    [apiUrl('/transactions/search')]: data => {
      for (const {id,tagData} of data.Transaction) {
        state.txnTags[id] = tagData?.tags.map(tag => tag.name)
      }
    },
    // when the master tag list is fetched, save it
    [apiUrl('/tags')]: data => {
      state.tagOrder = data.Tag.map(tag => tag.name)
      state.tagName = Object.fromEntries(data.Tag.map(tag => [tag.id, tag.name]))
    },
  }

  // when transactions are edited, update our tag records
  function handleTxnEdits(edits) {
    const idsToUpdate = []
    for (const {id,tagData} of edits) {
      if (tagData) {
        state.txnTags[id] = tagData.tags.map(tag => state.tagName[tag.id])
        idsToUpdate.push(id)
      }
    }
    setTimeout(() => idsToUpdate.forEach(updateRowTags), 500)
  }

  // hook XHR to intercept api calls
  function watchXHR() {
    const origOpen = XMLHttpRequest.prototype.open

    XMLHttpRequest.prototype.open = function(method, url) {
      const self = this

      // save XHR responses when needed
      self.addEventListener("readystatechange", function() {
        const hook = apiHooks[url]
        if (self.readyState === 4 && hook) {
          const data = JSON.parse(self.responseText)
          log('HOOKING', url, data)
          hook(data)
        }
      }, false)

      // intercept edits to transactions
      const txnsUrl = apiUrl('/transactions')
      if (method == 'PUT' && url.startsWith(txnsUrl)) {
        const origSend = self.send
        self.send = function(body) {
          const data = JSON.parse(body)
          const edits = url==txnsUrl ? data.Transaction : [{...data, id:url.slice(txnsUrl.length+1)}]
          log('HOOKING EDITS', edits)
          handleTxnEdits(edits)
          origSend.apply(self, arguments)
        }
      }

      origOpen.apply(self, arguments)
    }
  }

  //------------------------------------------------------------------------------
  // Render DOM
  //------------------------------------------------------------------------------

  var TAG_COLORS = [
    // source: http://colorbrewer2.org/#type=qualitative&scheme=Paired&n=12
    // background, foreground
    ['#a6cee3', '#000'],
    ['#b2df8a', '#000'],
    ['#fb9a99', '#000'],
    ['#fdbf6f', '#000'],
    ['#cab2d6', '#000'],
    ['#ffff99', '#000'],
    ['#1f78b4', '#fff'],
    ['#33a02c', '#fff'],
    ['#e31a1c', '#fff'],
    ['#ff7f00', '#fff'],
    ['#6a3d9a', '#fff'],
    ['#b15928', '#fff']
  ];

  function getTagStyle(tag) {
    const i = state.tagOrder.indexOf(tag)
    const [bg,fg] = TAG_COLORS[i]
    return `background:${bg}; color:${fg}`
  }

  // re-render our custom tag annotations in this row
  function updateRowTags(id) {
    log('UPDATING ROW', id)
    const td = document.querySelector(`tr[data-automation-id$=_${id}] td:nth-child(4)`)
    if (!td) return

    const tags = state.txnTags[id]
    const tagsDiv = () => td.querySelector('.gm-tags')
    if (tags?.length) {
      if (!tagsDiv()) td.innerHTML += '<div class="gm-tags" style="font-size:10px; display:inline-block"></div>'
      tagsDiv().innerHTML = tags.map(tag => `<span class="gm-tag" style="${getTagStyle(tag)}; margin-left:4px; padding:0 2px">${tag}</span>`).join('')
    } else {
      tagsDiv()?.remove()
    }
  }

  const rowId = row => row?.dataset?.automationId?.match(/TRANSACTION_TABLE_ROW_(READ|EDIT)_(.*)/)?.[2]

  function initRender() {
    if (!document.querySelector('tr[data-automation-id]')) {
      return setTimeout(initRender, 500)
    }
    log('FOUND TABLE')
    for (const row of document.querySelectorAll('tr[data-automation-id]')) {
      updateRowTags(rowId(row))
    }
    renderWhenDomChanges()
  }

  function renderWhenDomChanges() {
    log('WATCHING FOR CHANGES')
    const observer = new MutationObserver(() => {
      observer.disconnect()
      document.querySelectorAll('tr[data-automation-id]').forEach(row => updateRowTags(rowId(row)))
      log('RELAUNCHING WATCH')
      renderWhenDomChanges()
    })
    observer.observe(document.body, {subtree: true, childList: true, characterData: true})
  }

  //------------------------------------------------------------------------------
  // Main
  //------------------------------------------------------------------------------

  watchXHR()
  initRender()

})();