Kintoneでタイムラインを表示してみる
訳合って、社内に存在する「kintone」を弄る事になりまして、このクラウド環境を元に社内の業務改善の取り組みを開始する事に・・・元々G Suiteを長くやってきたので、正直Kintoneの環境はあまり好きじゃないのですが、仕方ないです。
とはいえやる以上はこれでなんとかしなければならないので、今回まずタスクをタイムライン表示からサクっと実装してみようかと思います。ただ、G Suiteとは異なりJavaScriptやCSSでの開発が結構クセが強くて・・・。
今回使用するライブラリ等
今回このライブラリはjQueryが必要です。また、TimelineライブラリがGoogleのVisualization APIによるデータテーブル形式で表示する事もあって、Visualization APIも導入が必要です。
これらのライブラリを利用してkintoneのテーブルデータを元に、カスタマイズビューにてkintone上でタイムラインを描画するようにしています。
図:こんな感じのタイムラインビューを作ります。
事前準備
アプリを新規作成した後、アプリの設定に入ります。各タブ毎に設定が必要です。
フォームの設定
今回のアプリのテーブルの土台にもなる部分です。テーブル構成としては以下の通り。ハイフンより後ろはフィールドコード名となります。
- レコード番号(文字列) - recid
- タスク名(ドロップダウン) - taskname
- サブタスク(文字列) - subtask
- ステータス(ドロップダウン) - statusval
- 開始日付(日付) - startday
- 終了日付(日付) - endday
今回適当に作ってるので、特に細かいフィールドの制限などの設定はしていません。
図:フォームは簡素に作っています。
一覧の設定
一覧では1枚ビューを追加します。今回は「タイムライン表示」というビューの名前を付けました。実際に利用時に画面切り替えのドロップダウンでこれを選べばタイムラインが生成される仕組みです。
- 右側のプラス記号のボタンを押してビューを追加
- レコード一覧の表示形式は「カスタマイズ」を選択
- 一覧を表示する範囲は今回はPC版のみで表示するにチェックを入れる
- HTMLでは、描画する部分だけを記述していますので、非常に簡素です。
/* タイムライン表示用のelement */ <div id="mytimeline" class="mytimeline">タスクをタイムライン表示するよ</div>
図:ものすごくシンプルなビュー
設定
今回は外部アプリからデータを取得して表示するのではなく、内側にあるカスタマイズビューなので、特にAPIトークン関係は弄りません。よって、設定で作業を行う箇所は、「JavaScript / CSSでカスタマイズ」の部分だけ。
- 適用範囲はすべてのユーザに適用をチェック
- PCのJavaScriptファイルおよびPC用のCSSファイルにファイルをアップしたり、参照させたりを追加する。
- CDN配布のjQuery, jQUery UIのJSファイル, jQuery UIのCSS, Visualization APIのライブラリ, CSS Package addonのCSSをそれぞれURLを指定で追加します。
- アップロードして追加にてアップするものとして、timeline.js, timeline-locales.js, timeline.cssを追加しておく
- 今回のメインプログラムファイルとしてtomato2.jsというファイルを最後にアップします。ソースコードを参照して作成してください。
- すべてのファイルがアップロードできたら、保存ボタンをクリックします。
ソースコード
前項の中でメインプログラムとしてtomato2.jsというものを用意する必要がありました、テキストエディタにてjsファイルを作成し、Atomなどのエディタで開発すると良いでしょう。
開発しにくいなぁと思った点は、
- kintoneのメインコードの書き方が、1つが大きなfunctionの中に記述する
- これまでのようにJSの中に適当に関数を用意してUI側から呼び出す感じだとうまくいかない点。
- Google Apps Scriptのスクリプトエディタのような統合開発環境があって、UIはUI、JSはJSといった形で直接編集できれば良いのにね。ウェブアプリケーションが作りたいのに。
- Node.jsのような感じで記述をしていく(IPC通信やappの各イベントを部品で用意していく感じ)
- 今回のライブラリは、マウスのホイールで時間軸の拡大縮小ができ、掴んで左右に移動することで、時間軸の移動が可能な非常に柔軟なライブラリです。左サイドはタスク名、時間軸上の帯が各タスクの開始日付と終了日付で作ったタイムラインになります。会議室アプリや納期管理などで使えそうです。
(function () { //お約束 "use strict"; //Google Visualization APIライブラリの初期化 google.load("visualization", "1"); //レコード一覧画面の表示イベント kintone.events.on('app.record.index.show', function(event) { //ボタンElement var myIndexButton = document.createElement('button'); myIndexButton.id = 'my_index_button'; myIndexButton.classList.add('action'); myIndexButton.innerHTML = '印刷実行'; //ダイアログ用Element var myDialogWindow = document.createElement("div"); myDialogWindow.id = "dialog"; myDialogWindow.title = "タイムライン表示用ダイアログ"; //メニューの右側の空白部分にボタンを設置 kintone.app.getHeaderMenuSpaceElement().appendChild(myIndexButton); kintone.app.getHeaderMenuSpaceElement().appendChild(myDialogWindow); //buttonを初期化 $( "#my_index_button" ).button(); //ボタンにイベントを登録する $('#my_index_button').on('click', function() { printTimeline(); }); //マップ表示 viewDialog(); }); //kintoneと通信を行うクラス var KintoneRecordManager = (function() { KintoneRecordManager.prototype.records = []; // 取得したレコード KintoneRecordManager.prototype.appId = null; // アプリID KintoneRecordManager.prototype.query = ''; // 検索クエリ KintoneRecordManager.prototype.limit = 100; // 一回あたりの最大取得件数(MAX:500) KintoneRecordManager.prototype.offset = 0; // オフセット function KintoneRecordManager() { this.appId = kintone.app.getId(); this.records = []; } // すべてのレコード取得する KintoneRecordManager.prototype.getRecords = function(callback) { //REST APIをぶっ叩く kintone.api( kintone.api.url('/k/v1/records', true), 'GET', { app: this.appId, query: this.query + (' limit ' + 100) }, (function(_this) { return function(res) { //取得データを配列にpushする var len; Array.prototype.push.apply(_this.records, res.records); len = res.records.length; _this.offset += len; if (len < _this.limit) { // まだレコードがあるか?チェック _this.ready = true; if (callback !== null) { callback(_this.records); // レコード取得後のcallback } }else{ _this.getRecords(callback); // 自分自身をコール } } })(this) ); } return KintoneRecordManager; })(); //ダイアログを表示する為の関数 function viewDialog() { //配列を準備しデータをpushする var array = []; //レコードデータ取得準備 var manager = new KintoneRecordManager; manager.getRecords(function(records) { //レコードの件数を取得 var reclength = records.length; var recs = records.length; // レコード取得後の処理 for(var i =0;i<reclength;i++){ //Null値チェック(Null値はタイムラインに反映しない) if(records[i].startday.value == null){ recs = recs - 1; continue; } if(records[i].endday.value == null){ recs = recs - 1; continue; } //一時配列にデータを追加する var tempArray = []; tempArray.push(records[i].recid.value); tempArray.push(records[i].taskname.value); tempArray.push(records[i].statusval.value); tempArray.push(new Date(records[i].startday.value)); tempArray.push(new Date(records[i].endday.value)); tempArray.push(records[i].subtask.value); //親配列にpushする array.push(tempArray); } //タイムライン反映 appendTimeline(array,recs); }); } //タイムラインを印刷する関数 function printTimeline(){ //プリントしたいエリアの取得 var printPage = $(this).closest('#mytimeline').html(); //プリント用の要素「#print」を作成 $('body').append('<div id="print"></div>'); $('#print').append(printPage); //「#print」以外の要素に非表示用のclass「print-off」を指定 $('body > :not(#print)').addClass('print-off'); window.print(); //window.print()の実行後、作成した「#print」と、非表示用のclass「print-off」を削除 $('#print').remove(); $('.print-off').removeClass('print-off'); } //タイムラインを生成する関数 function appendTimeline(array,reclength){ //データテーブルを作成する var data = new google.visualization.DataTable(); data.addColumn('datetime', 'start'); data.addColumn('datetime', 'end'); data.addColumn('string', 'content'); data.addColumn('string', 'group'); data.addColumn('string', 'className'); //データテーブルへデータを追加する for(var i = 0;i<reclength;i++){ //class名を設定 //予約状況に応じてclassNameを変更する var hantei; switch(array[i][2]){ case "開始前": case "保留中": case "作業待ち": hantei = "unavailable"; break; case "作業中": case "見積中": case "確認中": case "開始中": hantei = "available"; break; default: hantei = "maybe" break; } //データテーブルへデータ追加 data.addRows([ [array[i][3],array[i][4],array[i][5],String(array[i][1]),hantei], ]); } // specify options var options = { width: "99%", height: "auto", minHeight: 500, layout: "box", groupsOnRight: false, groupsChangeable : false, eventMargin: 10, // minimal margin between events eventMarginAxis: 0, // minimal margin beteen events and the axis showNavigation: false, axisOnTop: true, locale:"ja", }; // Instantiate our timeline object. var timeline = new links.Timeline(document.getElementById('mytimeline')); // Draw our timeline with the created data and options timeline.draw(data, options); timeline.redraw(); } })();
- 一番冒頭でVisualization APIのライブラリ初期化を行います。Timelineのライブラリがデータテーブルで作成された塊でないとタイムライン表示できない為。
- kintone.events.onの「app.record.index.show」にて、kintone側にボタンを用意したり、ボタンやelementを動的生成。各パーツにイベントを割り当てる
- viewdialog関数にてTimelineを最後に描画するようにしています
- KintoneRecordManager関数にてkintoneへの接続を確立します。ここではレコード取得などのテーブルのデータを取得するメインルーチンになっています。
- viewdialog関数が初期化時に呼び出され、timelineを表示するメインのルーチンです。レコードデータを取得したら配列にpushして、appendTimeline関数にてレンダリングをさせています。
- printtimeline関数は印刷実行ボタンを押した時にプリントアウトを実現する為のルーチンです。
- appendTimeline関数は取得した配列からデータテーブルを生成し、オプション指定を施して、最終的には一覧で作ったビューにある「mytimeline」のelementに対してtimelineを生成しています。
- 実際にはタイムラインの各項目に対して、クリック時イベントでダイアログを表示し詳細な内容を表示するように作り込んで行く予定。
図:Atom等だと開発がとてもしやすくなります
ダイアログを表示させて其の中にガントを表示
おまけとして今回のアプリではないのですが、kintoneにボタンを用意し、ビューではなくjQuery Dialogにて其の中にガントチャートを表示するようなものも作ってみました。
(function () { //お約束 "use strict"; //レコード一覧画面の表示イベント kintone.events.on('app.record.index.show', function(event) { //レコードデータ取得準備 var manager = new KintoneRecordManager; manager.getRecords(function(records) { // レコード取得後の処理 //alert(records[6].レコード番号.value); }); //ボタンElement var myIndexButton = document.createElement('button'); myIndexButton.id = 'my_index_button'; myIndexButton.classList.add('action'); myIndexButton.innerHTML = 'ガント表示'; //ダイアログ用Element var myDialogWindow = document.createElement("div"); myDialogWindow.id = "dialog"; myDialogWindow.title = "ガント表示用ダイアログ"; //dialogのdivにelementをさらに追加 var ganttelem = document.createElement("div"); ganttelem.classList.add('gantt'); ganttelem.innerHTML = "ようこそ!!ガントチャートアプリへ"; //メニューの右側の空白部分にボタンを設置 kintone.app.getHeaderMenuSpaceElement().appendChild(myIndexButton); kintone.app.getHeaderMenuSpaceElement().appendChild(myDialogWindow); //buttonを初期化 $( "#my_index_button" ).button(); //dialogのelementにganttを設置 document.getElementById("dialog").appendChild(ganttelem); //ボタンにイベントを登録する $('#my_index_button').on('click', function() { viewDialog(); }); //いちおうダイアログを初期化しておく $("#dialog").dialog(); }); //kintoneと通信を行うクラス var KintoneRecordManager = (function() { KintoneRecordManager.prototype.records = []; // 取得したレコード KintoneRecordManager.prototype.appId = null; // アプリID KintoneRecordManager.prototype.query = ''; // 検索クエリ KintoneRecordManager.prototype.limit = 100; // 一回あたりの最大取得件数(MAX:500) KintoneRecordManager.prototype.offset = 0; // オフセット function KintoneRecordManager() { this.appId = kintone.app.getId(); this.records = []; } // すべてのレコード取得する KintoneRecordManager.prototype.getRecords = function(callback) { //REST APIをぶっ叩く kintone.api( kintone.api.url('/k/v1/records', true), 'GET', { app: this.appId, query: this.query + (' limit ' + this.limit + ' offset ' + this.offset) }, (function(_this) { return function(res) { //取得データを配列にpushする var len; Array.prototype.push.apply(_this.records, res.records); len = res.records.length; _this.offset += len; if (len < _this.limit) { // まだレコードがあるか?チェック _this.ready = true; if (callback !== null) { callback(_this.records); // レコード取得後のcallback } }else{ _this.getRecords(callback); // 自分自身をコール } } })(this) ); } return KintoneRecordManager; })(); //ダイアログを表示する為の関数 function viewDialog() { //jQueryダイアログ初期化 $("#dialog").dialog({ modal:true, //モーダル表示 title:"テストダイアログ1", //タイトル buttons: { //ボタン "閉じる": function() { $(this).dialog("close"); } }, show : "fade", hide : "fade", height: 600, width: 850, }); //ダイアログを表示する $('#dialog').dialog('open'); //ガントデータを反映 modelGantt(); } //ガントチャートサンプルを表示 function modelGantt(){ $(function() { "use strict"; var today = moment(); var andTwoHours = moment().add(2, "hours"); var today_friendly = "/Date(" + today.valueOf() + ")/"; var next_friendly = "/Date(" + andTwoHours.valueOf() + ")/"; $(".gantt").gantt({ source: [{ name: "設計", desc: "外部", values: [{ from: "/Date(1320192000000)/", to: "/Date(1320392000000)/", label: "外部", customClass: "ganttRed" }] },{ name: " ", desc: "内部", values: [{ from: "/Date(1320392000000)/", to: "/Date(1320592000000)/", label: "内部", customClass: "ganttRed" }] },{ name: "サクラエビ", desc: "内部", values: [{ from: "/Date(1321392000000)/", to: "/Date(1321592000000)/", label: "内部", customClass: "ganttBlue" }] }], scale: "days", minScale: "hours", maxScale: "months", navigate: "scroll", onItemClick: function(data) { alert("チャート上をクリック"); }, onAddClick: function(dt, rowId) { alert("チャート部分以外をクリック"); } }); }); } })();
- サンプルなので、レコードのデータは全然使っていません・・・
- こんな感じのをDialogオープン時のイベントとして生成するか?起動時に生成して反映させてしまうか?
- ただ、動的にelementを生成してそこに対して、ライブラリで生成するというのがどうしても好きになれない。少し別の開発方法を考えなくてはなぁと(現在考えているのは、kintoneはデータ置き場にして、kintone APIを利用してExcelやElectronにてデータの読み書きをするアプリを構築すること。
- Google Apps Scriptからkintoneデータの読み書きをするライブラリを作られている方がいます。
図:ビューじゃなくダイアログで表示してみた