krewSheetで「保存失敗」を無理やりハンドリングする

# はじめに
kintone連携サービスのkrewSheet便利ですよね！
何よりJavaScript APIがあるのがとても良い！
krewSheetの一覧画面で編集した結果をうまく使ってJSの独自処理を追加できるので、重宝してます。

kintone JavaScript APIの仕様にうまいこと似せて、
こんな感じのイベントハンドリングができます。楽しいね！
https://docs.krew.grapecity.com/krewsheet/#event_recordlist.html

# こんなプログラム作ってました
Webpackでビルドする前提です。

```js
import swal from 'sweetalert'

const CANCEL_MESSAGE = 'キャンセルしました。'

kintone.events.on('app.record.index.show', event => {
  krewsheet.events.on('app.record.index.edit.submit', async event => {
    const confirmed = await swal({
      text: '他のアプリにAPI発行とか色々します。よろしいですか？',
      buttons: {
        // OKするとアラートを消さずにspinner状態になって待機
        confirm: { closeModal: false },
        cancel: true,
      },
    })
    if (!confirmed) {
      event.error = CANCEL_MESSAGE
    }
    return event
  })

  krewsheet.events.on('app.record.index.edit.submit.success', async event => {
    try {
      // 更新したレコードを使って他のアプリにAPI発行とか色々

      // 成功しても失敗しても、spinner状態になってたアラートはここで消える
      await swal('成功しました')
    } catch (e) {
      await swal('失敗しました')
    }
  })

  return event
})
```

4行でまとめるとこんな感じ。

1. krewSheetの一覧画面で何かしらレコード更新
2. [SweetAlertのこんな感じのダイアログ](https://sweetalert.js.org/guides/#ajax-requests)を出して、OK押したらSpinner状態になって待機
3. krewSheetがレコード更新完了したら、更新したレコードを使って他のアプリにAPI発行とか色々
4. 成功しても失敗しても新しいアラートを出して、2のSpinnerはそこで消える

# submit.failureハンドラが欲しい！
ここで一つ問題があるんです。
**kintoneの「レコードアクセス権」を設定していて「編集権限がない」場合、krewSheetがレコード保存に失敗しやがるんですよ！**
その場合、`submit.success`ハンドラが発火しないので、`submit`ハンドラで表示したアラートのSpinnerがずーっと消えずに残っちゃうんです。

画面左下にこっそりこんなエラーが出たり、console.errorにもエラー表示が出るけど、詳しくない人だったら、気づかずにずーっと待ってしまうはず。
<img width="199" alt="スクリーンショット 2019-04-25 2.19.27.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24333/338c8283-c206-8f9e-7fbf-6bea2e381227.png">
<img width="664" alt="スクリーンショット 2019-04-25 2.50.03.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24333/59818f95-4cf1-de04-0b6a-707fc639505e.png">


なので、`app.record.index.edit.submit.failure`みたいなイベントハンドラが本当は欲しいんですよね。そうすれば「krewSheetがレコード保存に失敗した時はこうリカバリ」ってロジックが自前で書ける。

# 無いから自作しました！

unhandledなErrorがthrowでもされていれば、こんな方法で無理やりハンドリングしてやることもできます。
[ユーザのブラウザで起きた JavaScript のエラーを収集する](https://qiita.com/grapswiz/items/71f14e73820e352c1fa3)

でも今回はただ「エラー出力がされているだけ」なので、その方法は無理。
ぢゃあどうするか？コンソールだけじゃなくて画面上にエラーが出てるのなら、**DOM監視でエラーハンドリングしてやる**ことにしました。

この記事がとても参考になりました。感謝！
[JavaScriptのMutationObserverでDOMの変化を監視する方法](https://qiita.com/munieru_jp/items/a6f1433652124a2165e4)

そして、どうせなら`krewsheet.events.on()`みたいなイベントハンドリングをしてやりたいってことで、[EventEmitter2](https://github.com/EventEmitter2/EventEmitter2)ライブラリを使いました。

完成版がこちらになります！

```js:main.js
import swal from 'sweetalert'
import EventEmitter2 from 'eventemitter2'
import emitHandler from './krewErrorHandler'

const CANCEL_MESSAGE = 'キャンセルしました。'

kintone.events.on('app.record.index.show', event => {
  krewsheet.events.on('app.record.index.edit.submit', async event => {
    const confirmed = await swal({
      text: '他のアプリにAPI発行とか色々します。よろしいですか？',
      buttons: {
        cancel: true,
        // OKするとアラートを消さずにspinner状態になって待機
        confirm: { closeModal: false },
      },
    })
    if (!confirmed) {
      event.error = CANCEL_MESSAGE
    }
    return event
  })

  krewsheet.events.on('app.record.index.edit.submit.success', async event => {
    try {
      // 更新したレコードを使って他のアプリにAPI発行とか色々

      // 成功しても失敗しても、spinner状態になってたアラートはここで消える
      await swal('成功しました')
    } catch (e) {
      await swal('失敗しました')
    }
  })

  // EventEmitter2を新しいグローバルオブジェクトに割り当てる
  window.krewsheetNeo = { events: new EventEmitter2() }

  // 保存失敗時のハンドリング処理（Neoがつきます）
  krewsheetNeo.events.on('app.record.index.edit.submit.failure', async event => {
    await swal('エラー：' + event.error)
  })

  // 通常のkrewsheet一覧表示イベント
  krewsheet.events.on('app.record.index.show', event => {
    // failureイベントハンドラの発火タイミングをこの中で設定
    emitHandler(CANCEL_MESSAGE)
  })

  return event
})
```

```js:krewErrorHandler.js
export default (...excludeErrors) => {
  // 監視ターゲットの取得（krewSheetのステータスバー）
  const target = document.querySelector('.GCSK_statusbar')

  // オブザーバーの作成
  const observer = new MutationObserver(records => {
    // エラーメッセージ表示時だけが対象
    if (records.every(r => r.target.textContent !== 'エラー')) {
      return
    }

    // ステータスバー内のエラーメッセージを取得
    const error = document.querySelector('#GCSK-statusMessage').textContent
    if (excludeErrors.includes(error)) {
      // 特定のエラーメッセージはハンドリングから除外
      return
    }

    // イベント発火
    krewsheetNeo.events.emit('app.record.index.edit.submit.failure', { error })
  })

  // 監視オプションの作成
  const options = {
    childList: true, // 子要素リストの変化を監視
    subtree: true, // 子孫ノードを監視対象に含める
  }

  // 監視の開始
  observer.observe(target, options)
}
```

どうですか！めっちゃそれっぽいのが出来ましたよ！
これでレコードアクセス権の問題で保存に失敗した時も、綺麗にアラートを消してあげることができます。

ポイントは、confirmダイアログをキャンセルした時も便宜上エラー扱いになるので、その場合だけはメッセージを判定してハンドリングの対象外にしてあげることですね。
<img width="189" alt="スクリーンショット 2019-04-25 2.19.35.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24333/06f596b0-cfcb-bafd-b816-3e451f297a78.png">

# 終わりに

ってわけで、頑張って作りはしましたが、グレープシティさん、どうか公式で`app.record.index.edit.submit.failure`ハンドラをお願いします:confounded:

あとkintoneのように、[submitハンドラ内で`return false`するとキャンセルできる](https://qiita.com/the_red/items/01c7ad1177be659639f8)ようにもして欲しいなぁ〜。

# 追記）kintone REST APIの `evaluate.json` 使った方が良さそう…
去年の夏に、こんな`/k/v1/records/acl/evaluate.json`と言うAPIコマンドが追加されたんでした！
https://developer.cybozu.io/hc/ja/articles/360000869566

今回`failure`ハンドラを作った唯一の目的が「編集権限がないレコードを更新試みて失敗した場合」だったので、それを回避するだけなら`submit`ハンドラ内で`evaluate.json`を使って事前チェックしてやれば十分じゃないか・・・

```js
import swal from 'sweetalert'

const CANCEL_MESSAGE = 'キャンセルしました。'

kintone.events.on('app.record.index.show', event => {
  krewsheet.events.on('app.record.index.edit.submit', async event => {
    // 追記ここから
    // REST APIで更新対象レコードのアクセス権を調べる
    const ids = event.records.map(record => record.$id.value)
    const { rights } = await kintone.api('/k/v1/records/acl/evaluate', 'GET', {
      app: kintone.app.getId(),
      ids,
    })
    // アクセス権がないレコードが1つでもあったら以降の処理はキャンセル
    if (rights.some(acl => !acl.record.editable)) {
      return event
    }
    // 追記ここまで

    const confirmed = await swal({
      text: '他のアプリにAPI発行とか色々します。よろしいですか？',
      buttons: {
        cancel: true,
        // OKするとアラートを消さずにspinner状態になって待機
        confirm: { closeModal: false },
      },
    })
    if (!confirmed) {
      event.error = CANCEL_MESSAGE
    }
    return event
  })

  krewsheet.events.on('app.record.index.edit.submit.success', async event => {
    try {
      // 更新したレコードを使って他のアプリにAPI発行とか色々

      // 成功しても失敗しても、spinner状態になってたアラートはここで消える
      await swal('成功しました')
    } catch (e) {
      await swal('失敗しました')
    }
  })

  return event
})
```

krewSheet側に改善してもらいたいのは、ぜひ一覧表示時に`evaluate.json`を考慮して「アクセス権のないレコードを編集状態にできない」って仕様にしてもらいたいですね！そうすれば`submit`ハンドラが発火すらしないので、より楽にカスタマイズできる。
（結局`failure`ハンドラは要らなかったのか・・・・orz）