ポータルのアプリ一覧をカスタマイズ

2019年7月のアップデートで、kintoneのポータル画面のカスタマイズが可能になりました。 今回は、ポータル画面にオリジナルのアプリ一覧を設置するカスタマイズを紹介します。

サンプル

デフォルトのアプリ一覧は、作成日時の降順(アプリIDの降順)に表示されます。

本サンプルでは、レコードの更新日時の降順でアプリを表示します。

また、チェックボックスを押すと、ソート対象をログインユーザーが更新したレコードのみにする機能も付けました。

※アイコンは表示せず、アプリ名のみ表示しております。 アイコンを表示する場合は、アプリの一般設定を取得しアイコン情報を取得します。 ただし、「kintoneに組み込みのアイコン」については、アイコンのキーとURLを紐づける必要があります。

コード

「kintoneシステム管理 > カスタマイズ > JavaScript / CSSでカスタマイズ」にて、ファイルを登録します。

JavaScript

本サンプルでは、Moment.jsを利用しています。 Cybozu CDNからご利用ください。 「Moment.js」を読み込み後、下記「sample.js」を読み込みます。 表示件数上限は、「limit」で設定しています。

・sample.js

(function() {
  "use strict";
  kintone.events.on('portal.show', function() {
    var limit = 30; //表示件数上限
    var getApps = function(apps){
      apps = apps || [];
      return kintone.api(kintone.api.url('/k/v1/apps', true), 'GET', {
        offset: apps.length
      }).then(function(response){
        apps = apps.concat(response.apps);
        return response.apps.length === 100 ? getApps(apps) : apps;
      });
    };
    var getUpdateFieldCode = function(appId){
      return kintone.api(kintone.api.url('/k/v1/app/form/fields', true), 'GET', {
        app: appId
      }).then(function(response){
        return {
          modifiedAtField: Object.keys(response.properties).find(function(fieldCode){
            return response.properties[fieldCode].type === 'UPDATED_TIME';
          }),
          modifierField: Object.keys(response.properties).find(function(fieldCode){
            return response.properties[fieldCode].type === 'MODIFIER';
          })
        };
      }).catch(function(){
        return {};
      });
    };
    var getLatestRecord = function(app, userFilter){
      if(!app.modifiedAtField || !app.modifierField) return false;
      return kintone.api(kintone.api.url('/k/v1/records', true), 'GET', {
        app: app.appId,
        fields: [app.modifiedAtField, app.modifierField],
        query: (userFilter ? (app.modifierField + ' in (LOGINUSER()) ') : '') + 'order by ' + app.modifiedAtField + ' desc limit 1'
      }).then(function(response){
        return response.records[0];
      });
    };
    var container = document.createElement('div');
    var toggleButton = document.createElement('label');
    var appsBox = document.createElement('div');
    var loader = document.createElement('div');
    container.classList.add('my-portal-container');
    appsBox.classList.add('apps-box');
    container.innerHTML = '<h3>アプリ(レコードの更新日時順に表示)</h3>'
    loader.classList.add('loader');
    container.appendChild(toggleButton);
    container.appendChild(appsBox);
    container.appendChild(loader);
    kintone.portal.getContentSpaceElement().appendChild(container);
    getApps().then(function(apps){
      kintone.Promise.all(apps.map(function(app){
        return getUpdateFieldCode(app.appId);
      })).then(function(fieldCodes){
        toggleButton.innerHTML = '<input type="checkbox">ソート対象を自分が更新したレコードのみにする';
        toggleButton.addEventListener('change', function(e){
          appsBox.innerHTML = '';
          loadAppsBox(e.target.checked);
        });
        apps = apps.map(function(app, index){
          return {
            appId: app.appId,
            name: app.name,
            modifiedAtField: fieldCodes[index].modifiedAtField,
            modifierField: fieldCodes[index].modifierField
          };
        });
        var loadAppsBox = function(userFilter){
          loader.style.display = 'block';
          kintone.Promise.all(apps.map(function(app){
            return getLatestRecord(app, userFilter);
          })).then(function(records){
            apps = apps.map(function(app, index){
              app.modifiedAt = records[index] ? records[index][app.modifiedAtField].value : '';
              app.modifier = records[index] ? records[index][app.modifierField].value.name : '';
              return app;
            });
            apps.sort(function(a, b){
              if(a.modifiedAt >= b.modifiedAt) return -1;
              if(a.modifiedAt < b.modifiedAt) return 1;
            });
            apps.some(function(app, index){
              if(index === limit || !app.modifiedAt) return true;
              var appBox = document.createElement('a');
              appBox.classList.add('app-box');
              appBox.setAttribute('href', location.protocol + '//' + location.host + '/k/' + app.appId);
              appBox.innerHTML =
                '<h4>' + app.name + '</h4>' +
                '<div><p>レコード更新日時</p><p>' + moment(app.modifiedAt).format('YYYY/MM/DD HH:mm') + '</p></div>' +
                '<div><p>レコード更新者</p><p>' + app.modifier + '</p></div>';
              appsBox.appendChild(appBox);
            });
            loader.style.display = 'none';
          });
        };
        loadAppsBox(false);
      });
    });
  });
})();

CSS

下記「loader.css」と「sample.css」を読み込みます。
※「loader.css」は、Single Element CSS Spinnersを用いて作成しました。

・loader.css

.loader,
.loader:after {
  border-radius: 50%;
  width: 10em;
  height: 10em;
}
.loader {
  margin: 60px auto;
  font-size: 10px;
  position: relative;
  text-indent: -9999em;
  border-top: 1.1em solid rgba(71,172,221, 0.2);
  border-right: 1.1em solid rgba(71,172,221, 0.2);
  border-bottom: 1.1em solid rgba(71,172,221, 0.2);
  border-left: 1.1em solid #47acdd;
  -webkit-transform: translateZ(0);
  -ms-transform: translateZ(0);
  transform: translateZ(0);
  -webkit-animation: load8 1.1s infinite linear;
  animation: load8 1.1s infinite linear;
}
@-webkit-keyframes load8 {
  0% {
    -webkit-transform: rotate(0deg);
    transform: rotate(0deg);
  }
  100% {
    -webkit-transform: rotate(360deg);
    transform: rotate(360deg);
  }
}
@keyframes load8 {
  0% {
    -webkit-transform: rotate(0deg);
    transform: rotate(0deg);
  }
  100% {
    -webkit-transform: rotate(360deg);
    transform: rotate(360deg);
  }
}

・sample.css

.my-portal-container{
  margin: 16px;
  padding: 16px;
  background: #eee;
}
.my-portal-container h3{
  font-size: 20px;
  margin: 0;
}
.apps-box:after{
  content: '';
  display: block;
  clear: both;
}
.app-box{
  float: left;
  display: block;
  background: #fff;
  padding: 5px;
  box-sizing: border-box;
  width: 150px;
  height: 200px;
  margin: 5px;
  color: #000;
}
.app-box h4{
  font-weight: bold;
}
.app-box div{
  padding: 5px;
}
.app-box p{
  margin: 0;
  padding: 0;
}