kintoneの「アプリ一覧」が使えない?ならこれを使え

こんにちは!
かな打ち派の武井です。
コーディング時も基本かな打ちです!

タイピングについては、中々私の右に出る者はいない武井です。

いわゆる
「俺ちゃんタイピング速いんすよwwネットゲームしまくってたんでwww」
的なやつではなく、趣味としてタイピング速度を日々競い合っている連中の中で揉まれたガチのやつで、
e-typing寿司打といった有名どころのタイピング速度測定サイトでも、
ランキング1位を複数回獲得したことなどの語り切れぬ実績がある武井です。

ここまでの実績があるので、一時期タイピング界ではそれなりに名を馳せていた武井です。

しかし、私の本業はローマ字打ちなのです。
何故かな打ちに浮気しているかというと、ひとえにかな打ちの方が打鍵完了時間が短いからです。

例えば、「武井琢治」とローマ字打ちで打鍵する場合、
「takeitakuji」と11打鍵が必要です。
しかし、かな打ちの場合には、「q:eqhd@」と僅か7打鍵で済みます。

「武井琢治」の場合は36%の省打鍵率でしたが、
「田里友彦」の場合は「tasatotomohiko」→「qxssmvb」と、
驚異の省打鍵率50%となります!

これからも「かな打ちプログラマーの会」を私の中で発展させて参る所存です。

「かな打ち プログラマー」でググると
できるプログラマーは「かな入力」を使っているという話
などのようなものばかりが引っかかり、内容に同意しつつも悦に入っている武井です。

ここまではまだ時候の挨拶

さて、かな打ち並の業務効率化を図れるkintone。

そのkintone利用に際して、よくある系の問題の1つに、
「アプリ一覧が全然使えないやんけ!」
というものがあります。

ポータル画面右下のアプリ一覧には、
アクセス可能なアプリの中から作成日時が最新のもの10件しか表示されていません。

それ以上は、「さらに表示」などというしゃらくさいボタンを押さなければ見ることができません。
100個前に作ったアプリにアクセスするにはこのしゃらくさボタンを5、6回押す羽目になります。

代替手段としてなのか、「お気に入り」機能と「ブックマーク」機能がそれぞれありますが、
誰も使っていないと思います。
少なくとも私は使っていませんし、使っているという話を聞いたこともありません。

一方、代替手段としてめちゃくちゃ使われているのが「お知らせ」や「スレッド」機能です。
どちらも単純にリンクをテキストエリアに書き込むだけの利用法で、
取っつきやすく、表示位置も見やすい部分にあることがウケているのでしょう。

前述の機能たちが使われない理由は「使い方がよくわからん」に尽きるでしょう。

それから「お知らせ」や「スレッド」機能は
「自分流にカスタマイズしたアプリ一覧を他者も使える(使わせられる)」
ここもポイントとなります。

いいものできました

折しもサイボウズさんからjsTreeライブラリを使用した何かを作って欲しい旨の依頼を受けていたため、
ボウズマンのあの必殺技で焼殺されない形での最大限の魔改造を画策しておりました。

結果が↓これです。

appIndex3

これは相当頑張りました!

元々、ドラッグ&ドロップでの動作対応までやるつもりはなかったのですが、
己が能力の極限に挑みました。

詳しい機能は↓のデベロッパーサイトに掲載された記事を見てくださいね――ッッ!

■kintone アプリ一覧プラグイン
https://cybozudev.zendesk.com/hc/ja/articles/213724883

さらに!別のベクトルから攻めるやつもできました

上記も素晴らしく、使い道は多種多様かと思うのですが、
他方で「個人個人で設定させたくないんや。飽くまでアプリ一覧は会社/システム管理者→ユーザーに提供したいんや」というニーズがあります。

そこで、今回私がサイボウズ完全非公認アウトローなアプリ一覧を作成いたしました!!

20160829

それ見たことか!

ちょっと俺を放っておくとこんな素晴らしい逸品を作ってしまう!!

どの辺で非公認かというと、
本来の標準アプリ一覧を無理矢理DOM操作でjsTreeに書き換える形にしているため、
ボウズマンも憤怒してしまう感じのやつになっております。

良薬は口に苦し。

【ポータル版アプリ一覧の使い方】

  1. こちらからアプリテンプレート(「ポータル版アプリ一覧.zip」)をダウンロードします。
  2. アプリテンプレートをkintone環境に読み込み、アプリを作成します。(kintoneヘルプ
  3. 全体JSに後述のJavaScriptファイルを後述のように書き換えて追加します。(kintoneヘルプ
  4. 作成された「ポータル版アプリ一覧」アプリのアクセス権限を変更します。(詳細後述)
  5. 作成された「ポータル版アプリ一覧」アプリにレコードを追加します。(詳細後述)
  6. ポータル画面を開くと、右下部にレコード追加された通りにアプリ一覧ができています。
■全体JSについて

全体JSのソースは以下のようになっております。

// 「ポータル版アプリ一覧」アプリID【!環境により書き換え必須】
var JSTREE_MST_APPID = 41;
var onceFlg = true;
// URLが変更される場合のjsTree書き直し処理
window.onhashchange = writeAppIndex;
// ポータルを最初に読み込んだ時のjsTree表示処理
window.onload = writeAppIndex;
// アプリ一覧書き換え関数
function writeAppIndex() {
	if (onceFlg) {
		if (location.href.indexOf("/k/#/portal") > 0 || location.href.indexOf("/k/#/space/") > 0) {
			onceFlg = false;
			// 標準のアプリ一覧が表示されるまで待つ
			var appIndexInterval = setInterval(function () {
				if ($(".gaia-argoui-appscrollinglist-readmore:last").length > 0 &&
						$(".gaia-argoui-appscrollinglist-list:last").children().length > 0) {
					clearInterval(appIndexInterval);
					// ポータルの「アプリ」からドロップダウンメニューを消す
					$(".gaia-argoui-select.ocean-portal-dropdown-menu[title='表示するアプリを切り替える']").css("display", "none");
					// 「さらに表示」も消す
					$(".gaia-argoui-appscrollinglist-readmore:last").css("display", "none");
					// 各標準のリンクも消す
					$(".gaia-argoui-appscrollinglist-list:last").children().remove();
					// jsTree要素作成
					$(".gaia-argoui-appscrollinglist-list:last").append("<div id='tree'></div>");
					createTree();
					// お知らせを更新した場合等、URLが変わらずに画面が更新されるパターンでアプリ一覧を表示し直す処理
					var reAppIndexInterval = setInterval(function () {
						if ($("#tree").length === 0) {
							clearInterval(reAppIndexInterval);
							writeAppIndex();
						}
					}, 500);
				} else if ($(".gaia-argoui-widget.gaia-argoui-space-applistwidget .gaia-argoui-space-applistwidget-empty").text() === "アプリはありません。アプリを作成") {
					clearInterval(appIndexInterval);
					$(".gaia-argoui-widget.gaia-argoui-space-applistwidget .gaia-argoui-space-applistwidget-empty").css("display", "none");
					// jsTree要素作成
					if($(".gaia-argoui-appscrollinglist-list:last").length > 0){
						$(".gaia-argoui-appscrollinglist-list:last").append("<div id='tree'></div>");
					}else{
						$(".gaia-argoui-widget.gaia-argoui-space-applistwidget .gaia-argoui-widget-body").append("<div id='tree'></div>");
					}
					createTree();
					// お知らせを更新した場合等、URLが変わらずに画面が更新されるパターンでアプリ一覧を表示し直す処理
					var reAppIndexInterval = setInterval(function () {
						if ($("#tree").length === 0) {
							clearInterval(reAppIndexInterval);
							writeAppIndex();
						}
					}, 500);
				}
			}, 100);
		}
	}
}
;
// jsTree描画関数
function createTree() {
	// 最終的に表示すべきノードを入れる配列
	var data = [];
	// 「ポータル版アプリ一覧」をフォルダ名が空でないものをフォルダ順昇順でGET
	fetchRecords(JSTREE_MST_APPID, 'folderName != "" order by folderSort asc').then(function (folderRec) {
		// 第一階層でないフォルダノードを入れる配列
		var notRootFolders = [];
		// 第一階層のフォルダノードをdata配列に格納
		for (var i = 0; i < folderRec.length; i++) {
			if (folderRec[i].fParentFolder.value === "") {
				data.push({
					text: folderRec[i].folderName.value,
					type: 'folder',
					children: [],
					defaultOpen: folderRec[i].defaultOpen.value.length > 0 ? true : false,
					folderSort: folderRec[i].folderSort.value
				});
			} else {
				notRootFolders.push(folderRec[i]);
			}
		}
		// 第一階層でないフォルダノードをdata配列のchildren配列に格納
		for (var j = 0; j < notRootFolders.length; j++) {
			for (var j2 = 0; j2 < data.length; j2++) {
				if (notRootFolders[j].fParentFolder.value === data[j2].text) {
					data[j2].children.push({
						text: notRootFolders[j].folderName.value,
						type: 'folder',
						children: [],
						defaultOpen: notRootFolders[j].defaultOpen.value.length > 0 ? true : false,
						folderSort: notRootFolders[j].folderSort.value
					});
				}
			}
		}
		// 「ポータル版アプリ一覧」をアプリ名が空でないものをアプリ順昇順でGET
		fetchRecords(JSTREE_MST_APPID, 'appName != "" order by appSort asc').then(function (appRec) {
			// 第一階層でないアプリを入れる配列
			var notRootFolderApps = [];
			// 第一階層のフォルダに紐づくアプリをdata配列のchildren配列に格納
			for (var i = 0; i < appRec.length; i++) {
				for (var j = 0; j < data.length; j++) {
					if (appRec[i].aParentFolder.value === data[j].text) {
						data[j].children.push({
							text: appRec[i].appName.value,
							type: 'file',
							appSort: appRec[i].appSort.value,
							attr: appRec[i].url.value
						});
						break;
					} else if (j === (data.length - 1)) {
						notRootFolderApps.push(appRec[i]);
					}
				}
			}
			// 第二階層のフォルダに紐づくアプリをdata配列のchildren配列のchildren配列に格納
			for (var j = 0; j < notRootFolderApps.length; j++) {
				for (var j2 = 0; j2 < data.length; j2++) {
					for (var j3 = 0; j3 < data[j2].children.length; j3++) {
						if (notRootFolderApps[j].aParentFolder.value === data[j2].children[j3].text) {
							data[j2].children[j3].children.push({
								text: notRootFolderApps[j].appName.value,
								type: 'file',
								appSort: notRootFolderApps[j].appSort.value,
								attr: notRootFolderApps[j].url.value
							});
						}
					}
				}
			}
			// data配列の中身をフォルダ、アプリとも順序通りになるようにソート
			data.sort(function (a, b) {
				if (parseInt(a.appSort || a.folderSort, 10) > parseInt(b.appSort || b.folderSort, 10))
					return 1;
				if (parseInt(a.appSort || a.folderSort, 10) < parseInt(b.appSort || b.folderSort, 10))
					return -1;
				return 0;
			});
			for (var i = 0; i < data.length; i++) {
				data[i].children.sort(function (a, b) {
					if (parseInt(a.appSort || a.folderSort, 10) > parseInt(b.appSort || b.folderSort, 10))
						return 1;
					if (parseInt(a.appSort || a.folderSort, 10) < parseInt(b.appSort || b.folderSort, 10))
						return -1;
					return 0;
				});
			}
			// 予め用意したdiv要素へjsTreeの描画
			$('#tree').jstree({
				core: {
					'themes': {
						"variant": "large"
					},
					data: data
				},
				types: {
					'#': {
						valid_children: []
					},
					'default': {
						valid_children: []
					},
					folder: {
						icon: 'https://static.cybozu.com/contents/k/image/argo/header/home_20151016.png'
					},
					file: {
						icon: 'https://static.cybozu.com/contents/k/image/argo/header/star_20151016.png'
					}
				},
				plugins: [
					'types'
				]
			})// ロード終了時、指定のフォルダを開いた状態にする
					.bind("ready.jstree", function (ev, nd) {
						var nodeData = nd.instance._model.data;
						var keys = Object.keys(nodeData);
						keys.shift();
						keys.sort(function (a, b) {
							if (parseInt(a.split("_")[1], 10) > parseInt(b.split("_")[1], 10))
								return 1;
							if (parseInt(a.split("_")[1], 10) < parseInt(b.split("_")[1], 10))
								return -1;
							return 0;
						});
						for (var i = 0; i < keys.length; i++) {
							if (nodeData[keys[i]].original.defaultOpen) {
								var node = nodeData[keys[i]].id;
								$("#tree").jstree("open_node", $("#" + node), false);
							}
						}
						;
						onceFlg = true;
					})
					// ノードを選択時、設定したURLに飛ばす
					.bind("select_node.jstree", function (e, s_data) {
						// ただし右クリック時は飛ばさない
						var evt = window.event || event;
						var button = evt.which || evt.button;
						if (button !== 1 && (typeof button !== "undefined")) {
							return false;
						}
						if (typeof (s_data.node.original.attr) !== 'undefined') {
							var href = s_data.node.original.attr;
							window.open(href);
						}
					});
		});
	});
}
// 全レコード取得関数
function fetchRecords(appId, query, opt_offset, opt_limit, opt_records) {
	var offset = opt_offset || 0;
	var limit = opt_limit || 100;
	var allRecords = opt_records || [];
	var params = {
		app: appId,
		query: query + ' limit ' + limit + ' offset ' + offset
	};
	return kintone.api('/k/v1/records', 'GET', params).then(function (resp) {
		allRecords = allRecords.concat(resp.records);
		if (resp.records.length === limit) {
			return fetchRecords(appId, query, offset + limit, limit, allRecords);
		}
		return allRecords;
	});
}

2行目のJSTREE_MST_APPID変数は、作成した「ポータル版アプリ一覧」のアプリIDに書き換えてください。

全体的に、結構細かいところまで対応しています。
例えば、「お知らせ」を更新した後でも、ちゃんとアプリ一覧が描画されるようになっていたりだとか。

ちなみに、以下は「ポータル版アプリ一覧」のアプリ内に設定しているJavaScriptです。
(アプリテンプレートに含まれるため、設定は不要です。)

(function () {
	"use strict";

	kintone.events.on(['app.record.index.edit.submit', 'app.record.create.submit', 'app.record.edit.submit'], function (event) {
		var record = event.record;
		var folderName = record.folderName.value;
		var folderSort = record.folderSort.value;
		var appName = record.appName.value;
		var appSort = record.appSort.value;
		var aParentFolder = record.aParentFolder.value;
		var url = record.url.value;
		var fParentFolder = record.fParentFolder.value;

		// 入力チェック
		if (folderName && !folderSort) {
			event.error = "フォルダ順を入力してください";
			return event;
		} else if (folderName && appName) {
			event.error = "フォルダ名とアプリ名が両方入力されています";
			return event;
		} else if ((appName && !appSort) || (appName && !aParentFolder) || (appName && !url)) {
			event.error = "未入力項目があります。アプリリンク作成の場合、アプリ項目はすべて必須です。";
			return event;
		}

		return new kintone.Promise(function (resolve, reject) {
			// フォルダ作成時の親フォルダチェック
			if (fParentFolder) {
				fetchRecords(JSTREE_MST_APPID, 'folderName = "' + fParentFolder + '"').then(function (pFolderRec) {
					if (pFolderRec.length > 0) {
						if (pFolderRec[0].fParentFolder.value !== "") {
							resolve("out");
						} else {
							resolve();
						}
					} else {
						resolve("nothing");
					}
				});
				// アプリリンク作成時の親フォルダチェック
			} else if (aParentFolder) {
				fetchRecords(JSTREE_MST_APPID, 'folderName = "' + aParentFolder + '"').then(function (aFolderRec) {
					if (aFolderRec.length > 0) {
						resolve();
					} else {
						resolve("nothing");
					}
				});
			}
			// フォルダ名が変更された場合の処理
			if (folderName) {
				var afterRecord = event.record;
				var recId = event.recordId;

				// まず、変更前のフォルダ名を取得する
				kintone.api('/k/v1/record', 'GET', {app: JSTREE_MST_APPID, id: recId}, function (resp) {
					var beforeRecord = resp.record;

					// 次に、更新対象レコードを取得する
					fetchRecords(JSTREE_MST_APPID, 'aParentFolder = "' + beforeRecord.folderName.value + '" or fParentFolder = "' + beforeRecord.folderName.value + '"').then(function (records) {
						if (records.length > 0) {
							var recCount = records.length;
							var putCount = Math.ceil(recCount / 100);
							for (var i = 0; i < putCount; i++) {
								var offset = i * 100;
								var limit = 100;
								if (offset + limit > recCount) {
									limit = recCount - offset;
								}
								var putLimit = limit + offset;

								var editRecords = [];

								// 更新対象レコードに更新後のデータを上書き
								for (offset; offset < putLimit; offset++) {
									var record = $.extend(true, {}, records[offset]);
									var recNo = record['$id'].value;
									delete record['$id'];
									delete record['$revision'];
									delete record['レコード番号'];
									delete record['作成日時'];
									delete record['作成者'];
									delete record['更新日時'];
									delete record['更新者'];
									if (!record.folderName.value) {
										record['aParentFolder'] = afterRecord.folderName;
									} else if (!record.appName.value) {
										record['fParentFolder'] = afterRecord.folderName;
									}
									editRecords.push({'id': recNo, 'record': record});
								}

								// 最後に更新処理
								kintone.api('/k/v1/records', 'PUT', {app: JSTREE_MST_APPID, 'records': editRecords},
										function (resp) {
											resolve();
										}
								);
							}
						} else {
							resolve();
						}
					});
				});
			}else if(!folderName && !appName){
				resolve("allNothing");
			}
		}).then(function (resp) {
			if (resp === "out") {
				event.error = "フォルダは2階層までです!";
			} else if (resp === "nothing") {
				event.error = "親フォルダ名が見つかりません!親フォルダ名が間違っているか、親フォルダレコードが作られていません。";
			} else if (resp === "allNothing") {
				event.error = "フォルダ名とアプリ名のどちらかは入力必須です。";
			}
			return event;
		});
	});

})();

細かい仕様は後述しますが、
それに対応して色々やっております。

フォルダ名が変更された場合に、いちいちすべてのレコードを変更しなくても済むなど、
なぜか今回の武井は凄く細部にこだわっています!

※全体JSを設定した後でないとレコードの登録は正常に動作しません。

■アクセス権限の変更

こちらは上記のサイボウズ公認プラグインと違って、会社やシステム管理者がレコード操作することを想定しているため、
裏側データは直接レコードを書き換える形になっております。(すなわち情強専用)
従って、一般ユーザーは編集不可能なようにアクセス権限を変更する必要があります。

アプリの権限、レコード権限ともに「アプリ一覧の編集権を与えたい人」には各権限を、
「アプリ一覧の編集権はないが、アプリ一覧を利用させたい人」には「閲覧権限」を付与してください。

基本的には
「システム管理者」=全権限・「EveryOne」=閲覧権限のみ
でよろしいかと思います。

■フォルダレコードの作り方

201608291608

■アプリレコードの作り方

201608291609

ちなみに、フォルダ順、アプリ順は、値が大きいか小さいかしか見ていないので、
通しでの順番をつけてもらっても、フォルダ毎の順番をつけてもらっても、
とにかく表示したい順にさえ数字を入れてくれれば想定通りの動きになると思います。

■プラグイン版アプリ一覧とポータル版アプリ一覧の比較

comp

遅れ馳せながらFullCalendarプラグインのお話

去る7月、拙作kintone イベントカレンダープラグインが出来し、
cybozu developer networkに掲載されました。

コンセプトとしては、「Cybozu CDNのFullCalendarを利用した、開発者向けのサンプルプラグイン」という建前であるため、
定番プラグインである「カレンダーPlus」には機能的に遠く及ばないにせよ、
プロセス管理と連携して、イベントの進捗が一目で分かるなどの工夫が凝らしてありますので、
興味のある方はご一読いただければと思います。

【免責事項】
各プログラムを使用したことによるいかなる損失・損害も責を負いません。
スマホ版では使用できません。
各プログラムに対するサポートは行っておりません。


⭐️トヨクモ kintone連携サービスのおすすめポイント⭐️
・kintone標準機能ではできない業務改善を実現
・カンタン・低コストで運用可能
・kintone1ドメインにつき1契約でOK
・10,000契約以上の実績・信頼
・30日間の無料お試しは何度でも可能