BacklogやRedmineが要らないとか煽り記事書いてしまった筆者です。どうもです。
さて、Exmentでガントチャートを作ってしまったら、どうせならカンバンボードもやってみたくなるのが人情ではないでしょうか?
というわけで、やってみました。
Expressでフロントを作る
簡易原価計算の時や、ガントチャートの時と同じく、Express + Pugでフロント画面を作ります。
今回、フロントではSortable.jsというライブラリを用いました。HTMLの要素を自由にドラッグアンドドロップできるようになる魔法のようなライブラリです。
まずは、タスク表示のGET部分です。
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です。
<!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
にアクセスしてみましょう。
こんな感じになっていれば成功です!
なお、Trello風のUIを実現するCSSは、以下の記事を参考にしました。
カンバン間を移動したらステータスが変更されるようにPUTしよう
ガントチャートの時もそうでしたが、フロントで弄った時に、きちんとバックエンドに通信が飛んでデータが上書きされてほしいですよね。
Sortable.jsにもコールバックの仕組みがきちんと用意されているので、以下の技術を追記します。
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.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にアクセスし、カンバン上で動かす予定のタスクを確認しておきます。未着手になってますね。
カンバン上でドラッグ&ドロップしてみます。
ドロップ後、リロードしてみましょう。
また、Exment側もリロードして確認してみましょう。
きちんとステータスが変わっているのを確認できました!
課題もあります
まあ、ガントチャートの時ほどではないのですが、課題はあります。
縦方向の並びを変えるのが難しい
無理じゃないとは思うんですが、今回はExment側に並び順のフィールドを作ってないので、並べ替えられません。並べ替えも、並べ替えごとに番号を全部のタスクで書き換えないといけないので、API的にも負荷は大きそうです。
まとめ
というわけで、いかがでしたでしょうか。
この実装ではTrelloほど高機能な実装はしませんでしたが、簡易とはいえ、Exmentでガントチャートも、そしてカンバンも出来るとなると、これでタスク管理したくなってきませんか? もちろん、もっと作り込めば、Backlog/Redmine/Trelloに負けないタスク管理サービスを作ることも可能です。
興味持った方は、ぜひトライしてみてください!