3
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Trelloも要らない? Exmentでカンバンボードを運用してみる

Last updated at Posted at 2020-09-16

BacklogやRedmineが要らないとか煽り記事書いてしまった筆者です。どうもです。

さて、Exmentでガントチャートを作ってしまったら、どうせならカンバンボードもやってみたくなるのが人情ではないでしょうか?

というわけで、やってみました。

Expressでフロントを作る

簡易原価計算の時や、ガントチャートの時と同じく、Express + Pugでフロント画面を作ります。

今回、フロントではSortable.jsというライブラリを用いました。HTMLの要素を自由にドラッグアンドドロップできるようになる魔法のようなライブラリです。

まずは、タスク表示のGET部分です。

app.js
app.get('/kanban', async(req,res) => {
  ;(async () => {
    const exmentToken = JSON.parse(await fs.readFileSync('./exment_tokens.txt')).access_token
    let tasksData = await axios.get('https://example.com/api/data/tasks/?orderby=start_at', {
      headers: {
        'Authorization': 'Bearer ' + exmentToken
      }
    })
    .then(res => { return res.data.data })
    .catch(err => { console.error(err) })
  
    let tasksArray = []
    tasksData.forEach(item => {
      const id = item.id
      const name = item.value.title
      const status = item.value.status
      tasksArray.push({
        id: String(id),
        name: name,
        status: status,
        endAt: item.value.end_at
      })
    })

    let statuses = await axios.get('https://example.com/api/column/86', {
      headers: {
        'Authorization': 'Bearer ' + exmentToken
      }
    })
    .then(res => { return res.data.options.select_item_valtext })
    .catch(err => { console.error(err) })

    statuses = statuses.split('\r\n')
    let statusData = []
    statuses.forEach(status => {
      status = status.split(',')
      statusData.push({
        id: status[0],
        title: status[1]
      })
    })

    // console.log(statusData)
    
    let payload = []
    let i = 1
    statusData.forEach(item => {
      const tasks = _.filter(tasksArray, { status: item.id })
      let j = 1
      const taskItems = () => {
        let taskItems = []
        tasks.forEach(item => {
          taskItems.push({
            id: `item-id-${j}`,
            exmentId: item.id,
            title: item.name,
            endAt: item.endAt
          })
          j++
        })
        return taskItems
      }
      payload.push({
        id: `board-id-${i}`,
        title: item.title,
        item: taskItems()
      })
      i++
    })

    // console.log(payload)

    res.render('kanban', { payload })
  })()

})

今回も、Exment側からデータを取得して、Sortableのデータ形式に整形しています。なんというか、JSONコネコネ屋さんって感じですね…

続いて、viewです。

kanban.pug
<!DOCTYPE html>
html(lang="ja")
  head
    meta(charset="UTF-8")
    meta(name="viewport", content="width=device-width, initial-scale=1.0")
    title Document
    .
      <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css" integrity="sha512-NhSC1YmyruXifcj/KFRWoC561YpHpc5Jtzgvbuzx5VozKpWvQ+4nXhPdFgmx8xqexRcpAglTj9sIBWINXa8x5w==" crossorigin="anonymous" />
    style.
      * {
        box-sizing: border-box;
      }
      [class^="wrapper-"] {
        background: #ddd;
        padding: 0;
        width: 300px; 
        margin-right: 30px;
        flex: 0 0 auto;
        height: 100%;
      }
      [id^="board-id-"] {
        padding-left: 0;
        padding: 15px;
      }
      #boards {
        width: 100vw;
        overflow-x: scroll;
        display: flex;
        height: 100%;
      }
      .title {
        margin-top: 15px;
        text-align: center;
        margin-bottom: 0;
      }
      .item {
        background: #f2f2f2;
        list-style-type: none;
        padding: 15px;
        margin-bottom: 15px;
        margin-left: 0;
      }
      .item:last-child {
        margin-bottom: 0;
      }
      .due {
        font-size: 11px;
        color: #777;
        font-weight: bold;
      }

        
  body
    #boards
      each board in payload
        div(class="wrapper-" + board.id)
          h2.title=board.title
          ul(id=board.id).sortable
            if board.item
              each item in board.item
                li.item(data-exmentid=item.exmentId)
                  =item.title
                  br
                  span.due=`期日:${item.endAt}`

    .
      <script src="https://cdnjs.cloudflare.com/ajax/libs/Sortable/1.10.2/Sortable.min.js" integrity="sha512-ELgdXEUQM5x+vB2mycmnSCsiDZWQYXKwlzh9+p+Hff4f5LA+uf0w2pOp3j7UAuSAajxfEzmYZNOOLQuiotrt9Q==" crossorigin="anonymous"></script>
      <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.20.0/axios.min.js" integrity="sha512-quHCp3WbBNkwLfYUMd+KwBAgpVukJu5MncuQaWXgCrfgcxCJAq/fo+oqrRKOj+UKEmyMCG3tb8RB63W+EmrOBg==" crossorigin="anonymous"></script>
    script.
      var boards = document.querySelectorAll('.sortable')
      var i = 1
      boards.forEach(function(board) {
        new Sortable(board, {
          group: 'status',
          onEnd: function(event) {
            var newStatus = event.to.id.replace(/[^0-9]/g, '')
            var itemId = event.item.dataset.exmentid
            axios.put('/kanban', {
              id: itemId,
              status: newStatus
            })
          }
        })
      })
      

これでhttp://localhost:3000/kanbanにアクセスしてみましょう。

2020-09-16_10h24_21.png

こんな感じになっていれば成功です!

なお、Trello風のUIを実現するCSSは、以下の記事を参考にしました。

カンバン間を移動したらステータスが変更されるようにPUTしよう

ガントチャートの時もそうでしたが、フロントで弄った時に、きちんとバックエンドに通信が飛んでデータが上書きされてほしいですよね。

Sortable.jsにもコールバックの仕組みがきちんと用意されているので、以下の技術を追記します。

kanban.pug
script.
  var boards = document.querySelectorAll('.sortable')
  var i = 1
  boards.forEach(function(board) {
   new Sortable(board, {
      group: 'status',
      onEnd: function(event) {
        var newStatus = event.to.id.replace(/[^0-9]/g, '')
        var itemId = event.item.dataset.exmentid
        axios.put('/kanban', {
          id: itemId,
          status: newStatus
        })
      }
    })
  })

オプションのプロパティonEndが、ドラッグアンドドロップを完了した時のコールバックです。
ここにExment側のAPIから持ってきたExmentのタスクのIDと、ボードのIDを取り込みます。

SortableのボードのIDは、board-id-1みたいな感じなので、数字以外を取り払ってあげます。Exmentで設定した列設定では、enum型で値は数字を列挙しているので、ID番号=Exment側の値となります。

最後にaxiosで、Express側にデータを投げます。

Express側はこんな感じです。

app.js
app.put('/kanban', async (req, res) => {
  ;(async () => {
    const exmentToken = JSON.parse(await fs.readFileSync('./exment_tokens.txt')).access_token

    // console.log(req.body)

    await axios.put('https://example.com/api/data/tasks/' + req.body.id, {
      value: {
        status: req.body.status
      }
    }, {
      headers: {
        'Authorization': 'Bearer ' + exmentToken
      }
    })
    .then(res => { return res })
    .catch(err => { console.error(err.response.data) })
  })()
})

シンプルですね。タスクのIDをエンドポイントに付加して、PUTします。statusも、先程のボードのID番号を指定してあげれば、ステータスの状態と連動します。

動作確認してみよう

Exmentにアクセスし、カンバン上で動かす予定のタスクを確認しておきます。未着手になってますね。

2020-09-15_15h04_01.png

カンバン上でドラッグ&ドロップしてみます。

2020-09-16_10h19_17.png

ドロップ後、リロードしてみましょう。

2020-09-16_10h22_02.png

また、Exment側もリロードして確認してみましょう。

2020-09-15_15h07_39.png

きちんとステータスが変わっているのを確認できました!

課題もあります

まあ、ガントチャートの時ほどではないのですが、課題はあります。

縦方向の並びを変えるのが難しい

無理じゃないとは思うんですが、今回はExment側に並び順のフィールドを作ってないので、並べ替えられません。並べ替えも、並べ替えごとに番号を全部のタスクで書き換えないといけないので、API的にも負荷は大きそうです。

まとめ

というわけで、いかがでしたでしょうか。

この実装ではTrelloほど高機能な実装はしませんでしたが、簡易とはいえ、Exmentでガントチャートも、そしてカンバンも出来るとなると、これでタスク管理したくなってきませんか? もちろん、もっと作り込めば、Backlog/Redmine/Trelloに負けないタスク管理サービスを作ることも可能です。

興味持った方は、ぜひトライしてみてください!

3
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?