kintoneアプリにゴミ箱機能って欲しくない?

🎄 kintone Advent Calendar 2020 📆 の14日目の記事です。

みなさんこんにちは。プロジェクト・アスノートの松田です。

2020年は、kintoneのアップデートがとても多い一年でしたね。さまざまな機能追加が行われた中で、kintoneを運用していくうえで非常に大きかったものがあります。

誤って削除してしまったアプリやスペースを復旧させることができるようになりました。

検証用やテスト用にアプリを作ることがよくありますが、アプリ名が区別しにくい名前になっていたりするときに、間違えて必要なアプリを削除してしまうことがよくありましたが、これまではアプリを削除するときは、万全の注意を払いましょう!と注意するしかありませんでした。

ところが、ミスが起こる場合の多くは、思い込みによるもの。
やってしまった後に、冷静になって気がつく!!ということが多いと思います。

そんなご操作によるデータ消失のリスクを、回避することができるようになったことは、非常に安心できる機能追加でした。

アプリは復旧できるけど、レコードは??

さて、誤って削除したアプリは復旧できるようになりました。ところが実際にkintoneアプリの運用をしていると、「レコードの削除権限」について、問題になることが多いように思います。

2020年12月現在の kintoneでは、削除してしまったレコードは復旧することができません。

そこでレコードの削除権限を利用者には与えないようにして、別途申請を受けて管理者が削除する等、運用でカバーするための工夫をされている方も多いのではないかと思います。

データベースという性質においては、データを削除するということはあまりないのかもしれませんが、実際間違って作ってしまったレコードをどうするか?という問題は考えておく必要があります。

そこでふと思ったんです。

アプリにゴミ箱機能があったらいいな~

無いものは作りましょう!ということで、まずは標準機能で次のような工夫をしたアプリを作ってみました。
(いつか新機能として追加されたらいいなぁ)

標準機能でのトライアル

【スクショ:削除フラグ設置+レコードのアクセス権】

  • アプリフォームに「削除フラグ」(削除マークでもOKですね)というチェックボックスを設置します。
  • レコードを削除したいときは、この削除フラグをオンにして保存する。
  • レコードのアクセス権を設定しておき、削除フラグがチェックされたレコードは閲覧権限を外しておく(レコードのアクセス権)
  • 通常のレコード削除はできないように、すべてのレコードの削除権限は外しておく(レコードのアクセス権)

シンプルですね!さて、これでうまくいくかどうか試してみましょう。

レコード詳細画面のメニューから「レコードを削除」を選択して、確認ダイアログで「削除する」を選んだところ、次のようなエラーが表示されました。

レコードを編集して保存したときの、kintoneの標準的な動きは、次のようになっています。

  1. レコード編集画面(削除フラグをチェック)
  2. 保存ボタンをクリック
  3. レコードのデータを保存
  4. レコード詳細画面を表示

4のレコード詳細画面の表示をしようとしたタイミングで、先ほど削除フラグを設定したレコード、すなわちレコードの閲覧権限の無いレコードの詳細画面を表示しようとしているという理由で「権限がありません」というエラーになります。

エラーは編集画面の状態で発生しているように見えますが、実際にはレコードの保存は行われています。これはエラー画面が表示されたタイミングで、ブラウザの別のタブ等でアプリの一覧画面を開くとわかります。

一方、一覧画面のインライン編集で削除フラグをチェックして保存した場合は、一覧から該当するレコードが見えなくなるだけなので、上のような問題は起こりません。

ゴミ箱機能を作ってみた

標準機能だけでの運用だと、詳細画面からの保存時のエラーを回避することはできません。
それに、編集→削除フラグをチェック→保存 という操作は、レコードを削除するというよりは、レコードを編集している意識になるため、通常の編集操作時の誤動作等も考慮しておくべきかもしれません。

そこで、次のような考え方で、kintoneアプリにゴミ箱(論理削除)機能を実現するJavaScriptカスタマイズを考えてみました。

サンプルアプリとして下図のようなアプリを作りました。

サンプルアプリ

ゴミ箱機能で利用するフィールド

  • 削除フラグ(チェックボックス)
    論理削除状態(ゴミ箱)を表すチェックボックス
  • 論理削除日時(日時)
    ゴミ箱に入れた日時を記録します
  • 論理削除更新者(ユーザー選択)
    ゴミ箱に入れる作業を行ったユーザーを記録します
  1. 通常のレコード削除操作を行ったタイミングで、
  2. 削除フラグをチェックして
  3. レコード削除はキャンセルさせる
  4. キャンセル後は詳細画面が開くので、強制的に一覧画面に遷移させる

こんな操作感になりました。

アクセス権、一覧の絞り込みは以下のような設定になっています。

レコードのアクセス権の設定
  • 削除フラグがチェックされたレコードは、
    • Everyoneの全ての権限を外す(閲覧・編集・削除)
    • 管理者ユーザーにはすべての権限を付与(閲覧・編集・削除)
一覧の設定

次の2つの一覧を作成しました。

  • 通常の一覧
    • 削除フラグがチェックされていないレコード
  • ゴミ箱(削除レコード)
    • 削除フラグがチェックされたレコード

管理者のユーザーはゴミ箱一覧から、ゴミ箱内のレコードを削除することができるようにもしたいですね。
このときは、本当にレコードを削除してしまうため、間違えてしまわないよう、確認画面を追加しました。

ゴミ箱一覧からレコードを削除(通常の削除)

JavaScriptコード

以下のJavaScriptファイルに加えて、次のライブラリーを使っています。

  • luxon: 日付操作(今回は現在日時の取得に利用しています) 
    https://js.cybozu.com/luxon/1.25.0/luxon.min.js
  • SweetAlert:  アラート生成
    https://js.cybozu.com/sweetalert/v2.1.2/sweetalert.min.js

最新版はCybozu CDNで確認してください。

(function () {
    'use strict';

    // 更新フィールドのフィールドコード定義
    const updateFields = {
        'deleteFlag': '削除フラグ',
        'deleteDateTime': '論理削除日時',
        'deleteUpdater': '論理削除更新者'
    };

    // 削除実行前イベント 
    const delEvents = [
        'app.record.detail.delete.submit',
        'app.record.index.delete.submit',
        'mobile.app.record.detail.delete.submit'
    ];

    // メイン処理(削除実行前イベントで論理削除フラグを書き込み、レコード削除はキャンセル)
    kintone.events.on(delEvents, function (event) {
        const record = event.record;

        // 削除フラグがOFFの場合、論理削除処理を行う
        if (record[updateFields.deleteFlag]['value'].length === 0) {

            // ロンリ削除時のレコード更新データを生成
            const body = {};
            body.app = event.appId;
            body.id = event.recordId;
            body.record = {};
            body.record[updateFields.deleteFlag] = {
                "value": ["削除"]
            };
            body.record[updateFields.deleteDateTime] = {
                "value": luxon.DateTime.local().toString()
            };
            body.record[updateFields.deleteUpdater] = {
                "value": [
                    {
                        "code": kintone.getLoginUser().code
                    }
                ]
            };

            // ロンリ削除処理(削除フラグをONにし、一覧画面に遷移)        
            kintone.api(kintone.api.url('/k/v1/record', true), 'PUT', body).then(function (resp) {
                // 削除フラグにより参照権限が無くなると、詳細画面表示時に権限エラーとなるため、一覧に遷移させる
                return swal('Done!', 'レコードをゴミ箱に入れました!', 'success');
            }).then(function (resp) {
                location.href = window.location.origin + "/k/" + event.appId;
            }).catch(function (resp) {
                swal('Error!!', '論理削除処理がエラーになりました。\n再度実行してください。', 'error');
            });
            return false;

            // 削除フラグがONになっているレコードの削除確認処理    
        } else {
            return swal({
                icon: 'warning',
                title: 'よろしいですか?',
                text: 'ゴミ箱のレコードを削除します。\n(もとに戻すことはできません)',
                buttons: {
                    cancel: true,
                    confirm: true
                },
            }).then(function (result) {
                console.log(result);
                if (result) {
                    return event;   // レコード削除を実行
                }
                else {
                    return false;   // レコード削除をキャンセル
                }
            });
        }
    });

    // 削除管理用フィールドの編集不可処理
    const editEvents = [
        'app.record.create.show',
        'app.record.edit.show',
        'app.record.index.edit.show',
        'mobile.app.record.create.show',
        'mobile.app.record.edit.show'
    ];

    kintone.events.on(editEvents, function (event) {
        const record = event.record;

        // record[updateFields.deleteFlag]['disabled'] = true;
        record[updateFields.deleteDateTime]['disabled'] = true;
        record[updateFields.deleteUpdater]['disabled'] = true;

        return event;
    });
})();

コード解説

// 削除実行前イベント 
const delEvents = [
    'app.record.detail.delete.submit',
    'app.record.index.delete.submit',
    'mobile.app.record.detail.delete.submit'
];
// メイン処理(削除実行前イベントで論理削除フラグを書き込み、レコード削除はキャンセル)
kintone.events.on(delEvents, function (event) {
    // ここに処理を書く
});

今回やりたいことは、レコード削除操作が行われたタイミング(レコード削除前)で、削除フラグを更新したいわけです。カスタマイズを動かすタイミングは、「削除実行前イベント」です。

削除操作は、レコード詳細画面、一覧画面、モバイルの詳細画面から行うことができますので、これら3つのイベントを指定します。

次に処理内容の基本骨格です。

// 削除フラグがOFFの場合、論理削除処理を行う
if (record[updateFields.deleteFlag]['value'].length === 0) {

    // ①ロンリ削除処理(削除フラグをONにし、一覧画面に遷移)        
    // ②一覧画面に遷移

} else {
    // ③削除フラグがONになっているレコードの削除処理
}

論理削除の対象となるのは、まだ削除フラグがチェックされていないレコードです。削除フラグがチェックされているのは、すでにゴミ箱に入っているレコードですから、その判定を行います。

JavaScriptの if-else 構文で削除フラグの値で処理を分岐させます。
削除フラグはチェックボックスなので、値は配列になっていることに注意です。チェックボックスには選択肢が1つしかありませんから、配列内のデータ数によってチェックされているかどうかの判定を行っています。

削除フラグがOFF(なにもチェックされていない)の場合には、このレコードは通常レコードですから、論理削除処理を行います。
そうでない場合(削除フラグがON すなわちゴミ箱内レコード)は、ゴミ箱内のレコードを削除する処理を記載します。

①論理削除処理〜②一覧画面に遷移

レコード削除前イベントでレコード内容を書き換えるためには、eventオブジェクトのreturnではなくて、REST APIによるレコード更新で行う必要があります。そして削除フラグをチェックするというレコード更新に引き続き、一覧画面への遷移を行います。

ここは kintone Promiseを使った処理の基本構造は次のようになります。

kintone.api()  // レコード更新処理
.then()        // レコード更新成功時のアラート表示
.then()        // 一覧画面への遷移処理
.catch();      // エラー処理
return false;  // 削除処理のキャンセル

実際の処理は以下のようになります。
このように、まず基本構造を考えてそれを書いてみてから、その中の細かい処理を書いていくようにすると、考え方が整理しやすくなります。

// ロンリ削除処理(削除フラグをONにし、一覧画面に遷移)        
kintone.api(kintone.api.url('/k/v1/record', true), 'PUT', body).then(function (resp) {
    // 削除フラグにより参照権限が無くなると、詳細画面表示時に権限エラーとなるため、一覧に遷移させる
    return swal('Done!', 'レコードをゴミ箱に入れました!', 'success');
}).then(function (resp) {
    location.href = window.location.origin + "/k/" + event.appId;
}).catch(function (resp) {
    swal('Error!!', '論理削除処理がエラーになりました。\n再度実行してください。', 'error');
});
return false;

レコード更新の内容については、下のようにbodyという変数に定義しておきます。

const body = {};
body.app = event.appId;
body.id = event.recordId;
body.record = {};
body.record[updateFields.deleteFlag] = {
    "value": ["削除"]
};
body.record[updateFields.deleteDateTime] = {
    "value": luxon.DateTime.local().toString()
};
body.record[updateFields.deleteUpdater] = {
    "value": [
        {
            "code": kintone.getLoginUser().code
        }
    ]
};

bodyの中身は、レコード更新処理のドキュメントに従って作成します。今回の場合は下記のような内容となるようにしました。うまくいかない場合は、デベロッパーツールで構文の形式を確認しながらデバッグするといいと思います。

// レコード更新データ
{
  "app": 765,  // アプリID
  "id": 30,    // レコードID
  "record": {
    "削除フラグ": {
      "value": [
        "削除"
      ]
    },
    "論理削除日時": {
      "value": "2020-12-20T15:26:18.249+09:00"
    },
    "論理削除更新者": {
      "value": [
        {
          "code": "更新を行ったユーザーID"
        }
      ]
    }
  }
}

③ゴミ箱内レコードの削除処理

SweetAlertを使わない場合は以下のように書けます。
確認ダイアログを表示させ、OK/キャンセルの結果resultの結果によって処理を分岐させています。
confirmの結果(変数 result)は、OKの場合はtrue、キャンセルの場合はfalseとなります。

let result = confirm('ゴミ箱のレコードを削除します。よろしいですか?');

if (result) { 
    return event;   // 通常の削除処理実行
}
else {
    return false;   // 削除処理キャンセル
}

SweetAlertを使う場合、は、標準のalert/confirmと違って、アラートが表示されても処理は待ってくれずに進行してしまいます。これはSweetAlertが非同期処理だからなのですが、kintoneのREST APIによる更新処理と同様の考え方となります。これをSweetAlertの返答結果を待ってからその内容により次の処理を分岐させるためには、SweetAlertはPromiseオブジェクトを返すという性質を利用して次のように記述します。

swal()というのがSweetAlertの関数です。kintone Promiseのときと同様、これをreturnしてやり、thenチェーンの中でその結果resultを if-else構文で分岐させます。

return swal({
    icon: 'warning',
    title: 'よろしいですか?',
    text: 'ゴミ箱のレコードを削除します。\n(もとに戻すことはできません)',
    buttons: {
        cancel: true,
        confirm: true
    },
}).then(function (result) {
    if (result) {
        return event;
    }
    else {
        return false;
    }
});

最後に、ゴミ箱を入れた際に記録されるフィールド(論理削除日時、論理削除更新者)については、レコード編集時に書き換えられてしまわないように、編集不可にする処理を付け加えておきました。

// 削除管理用フィールドの編集不可処理
const editEvents = [
    'app.record.create.show', 
    'app.record.edit.show', 
    'app.record.index.edit.show',
    'mobile.app.record.create.show', 
    'mobile.app.record.edit.show'
];

kintone.events.on(editEvents, function (event) {
    const record = event.record;

    record[updateFields.deleteFlag]['disabled'] = true;
    record[updateFields.deleteDateTime]['disabled'] = true;
    record[updateFields.deleteUpdater]['disabled'] = true;

    return event;
});

ドシキンカスな人へ(初心者の方へ)

kintoneカスタマイズの初心者、プログラミング初心者の人に伝えたいことがあります。

個々の処理の書き方を学ぶのは非常に大切なことです。しかし、実際のkintoneアプリである目的を達成するために作成するべき処理は、単発の処理だけではなく、いくつかの処理を組み合わせて作ることが多いと思います。そんなときに、最初からすべての機能を持った処理を書いてしまわないことが大切だと思います。

スモールスタート という言葉が、まさにピッタリな言葉だと思います。

まずは最小限の動きだけでプログラムを書いて、動かしてみて、想定通りの動きをすることを確認します。そして、そこに一つずつ付け加えていくように、少しずつ最終目的に近づけていくようにします。

どんなプログラムでもそうだと思いますが、一発目で100%想定通りに動作してくれることは、ほぼありません。最初から複雑ないくつもの処理を書いてしまうと、どこに不具合があって動かなかったのか、探し出すことが非常に難しくなってしまいます。

面倒で遠回りに見えますが、私はいつも、少しずつ作っては動かしてみてデバッグ&修正 を繰り返しながら、カスタマイズを作っています。

まったく初めてkintoneカスタマイズをやる人向けの記事や動画のコンテンツを用意しています。ぜひそちらを参考にして、kintoneカスタマイズを楽しみましょう!

実際にウチの kintoneで使ってみたいという人へ

kintoneカスタマイズの知識がある方は、ぜひ上のサンプルコードを利用して、アプリを作って試してみてください。実際に業務で利用しているアプリにカスタマイズを行う場合は注意が必要です。

今回は、レコードの削除処理をキャンセルさせて、代わりにレコード更新を行うという処理を行っています。プログラムの記載を間違うと、レコードの削除が実行されてしまったりすることも考えられます。テスト用のアプリを作って検証することをオススメします。

またすべての環境での動作を保証するものではありませんので、運用中のアプリへの実装は、テストをしっかりと行い、自己責任で行ってください。

プロジェクト・アスノートでは、今回のようなカスタマイズを含めて、業務改善やkintoneカスタマイズの相談、プラグイン・連携サービスの選定や活用のご支援を行っています。問い合わせフォームからお気軽にご相談ください。

シンプルバージョンもあるよ

今回のカスタマイズから、コア部分だけを取り出して、シンプルにしたものをQiitaで公開しています。
利用者にはほとんど意識させないまま、削除作業を行ってもらい、裏ではしっかり削除されずに保持できている、というものです。

コードも公開していますのでこちらもぜひ参考にしてみてください。
あ、運用については自己責任で!

https://qiita.com/Shokun1108/items/4c594881e9e650db2a41

カスタマイズの過程を動画でも公開しています。
カスタマイズ勉強中の方はこちらも参考になると思います。

kintone導入・活用のご相談はこちら!

こんな悩みを抱えていませんか?

◇ kintone導入がなかなか進まない
◇ アプリが思ったような動きをしてくれない・・・
◇ カスタマイズやプラグインをどう選んだらいいの??
◇ 業務改善の進め方がよくわからない

サイボウズ公認 kintone エバンジェリストの 松田正太郎があなたの相談相手になります。
保有しているkintone認定資格:
・kintone認定アソシエイト(2017)
・kintone認定アプリデザインスペシャリスト(2017)
・kintone認定カスタマイズスペシャリスト(2020)

★業務改善アドバイス、kintone構築支援
★連携サービス・プラグイン選定支援、カスタマイズ
★詳細ヒアリングの上、御社に最適なプランを提案します
★初回打合せ(2時間程度)は無料。まずはお問い合わせください!(WebミーティングOK)