Kintone Portal Designerでポータルから添付ファイルをアップしよう

著者名: Fuji Business International / Mamoru Fujinoki (External link)

目次

はじめに

kintoneのポータル画面では、社員へのアナウンスメントや頻繁に使用するアプリの表示、アプリへアクセスするリンク等を集約できます。
たとえば、経費精算アプリを作成すると経費の情報にレシートを添付して、社員が容易に会計に提出することが可能となります。
本来のやり方ですと、次のように複数のステップが必要でした。

  1. ポータルに経費精算アプリへのリンクを設置する。
  2. 経費精算アプリ画面に遷移してから、レコードを追加する。
  3. レシートの画像を添付ファイルとしてアップロードしてレコードを保存する。

ただ、本来のやり方では、頻繁に経費精算の必要な社員にとっては操作が多く、思った以上に手間をかけてしまいます。

そこで、今回はポータルをカスタマイズして、ポータル上で経費精算ができるようにします。
また、レシートのデータをドラッグ&ドロップでアップロードすることによって操作の手間を減らし、業務改善を実現したいと思います。

経費精算アプリの作成

下記画像および、フィールドの設定を参考に経費精算アプリを作成します。

フィールドの種類 フィールド名 フィールドコード
文字列(複数行) 概要 description
添付ファイル レシート receipt
数値 費用(税抜) cost
日付 日付 date

Kintone Portal Designerの設定

Kintone Portal Designerを使ってポータルをデザインしようの記事を参考にKintone Portal Designerをインストールします。
インストール後、kintoneのポータルを表示して、ツールバーの「</>」ボタンをクリックして、起動します。

以下のような画面が表示されますので、それぞれのタブの項目を以下を参考にHTML, CSS, JavaScriptを編集して、保存します。

HTMLの編集

以下を参考にHTMLを編集し、保存します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<!-- 
 * Kintone Portal Designerでポータルから添付ファイルをアップするサンプルプログラム
 * Copyright (c) 2021 Cybozu
 *
 * Licensed under the MIT License
 * https://opensource.org/license/mit/
-->
<form>
  <div class="parent">
    <div class="header">
        <h2>経費精算</h2>
    </div>  
    <div class="label1">
      <label for="description">概要&nbsp;</label>
    </div>
    <div class="input1">
      <input type="text" id="description" name="description">
    </div>
    <div class="label2">
      <label for="amount">費用(税抜)&nbsp;</label>
    </div>
    <div class="input2">
      <input type="text" id="amount" name="amount">
    </div>
    <div class="label3">  
      <label for="date">日付(YYYY-MM-DD)&nbsp;</label>
    </div>
    <div class="input3">
      <input type="text" id="date" name="date">
    </div>
    <div class="drop_zone" ondrop="dropHandler(event);" ondragover="dragOverHandler(event);">
          <div id="file_name" class="tool_tip">ここにファイルをドロップします</div>
    </div>
    <div class="button">
      <input type="submit" value="登録" class="bSubmit" onclick="registerExpense(event);">
    </div>
  </div>
</form>

CSSの編集

以下を参考にCSSを編集し、保存します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
/*
 * Kintone Portal Designerでポータルから添付ファイルをアップするサンプルプログラム
 * Copyright (c) 2021 Cybozu
 *
 * Licensed under the MIT License
 * https://opensource.org/license/mit/
*/
.parent {
  display: grid;
  grid-template-columns: repeat(3,300px);
  grid-template-rows: 50px repeat(3,30px) 50px;
  border: 3px solid green;
  border-radius: 15px;
  margin: 10px 10px 10px 10px;
  column-gap: 10px;
  row-gap: 1em;
  background-color: #ffffff;
}
label {
  font-weight: bold;
}
input {
  border: 2px solid green;
  border-radius: 5px;
}
.button {
   grid-area: 5 / 1 / 6 / 4;
   justify-self: center;
   align-self: center;
}
.bSubmit {
   padding: 5px 30px;
   font-weight: bold;
}
.drop_zone {
  border: 2px dotted green;
  border-radius: 10px;
  grid-column: 3 / 4;
  grid-row: 2 / 5;
  margin: 10px 10px;
}
.tool_tip{
  text-align: center;
  padding: 30px 0;
  opacity: 0.5;
}
.header {
  grid-area: 1 / 1 / 2 / 4;
  font-weight: bold;
  font-size: 1.5em;
  padding: 0px 50px;
}
.label1{
  grid-column: 1 / 2;
  grid-row: 2 / 3;
  text-align: right;
}
.label2{
  grid-column: 1 / 2;
  grid-row: 3 / 4;
  text-align: right;
}
.label3{
  grid-column: 1 / 2;
  grid-row: 4 / 5;
  text-align: right;
}
.input1{
  grid-column: 2 / 3;
  grid-row: 2 / 3;
}
.input2{
  grid-column: 2 / 3;
  grid-row: 3 / 4;
}
.input3{
  grid-column: 2 / 3;
  grid-row: 4 / 5;
}

JavaScriptの編集

以下を参考にJavaScriptを編集し保存します。
今回は、MDN Web Docsの「 File drag and drop (External link) 」を参考にしています。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
/*
 * Kintone Portal Designerでポータルから添付ファイルをアップするサンプルプログラム
 * Copyright (c) 2021 Cybozu
 *
 * Licensed under the MIT License
 * https://opensource.org/license/mit/
*/
let file = null;
const dropHandler = (ev) => {
  console.log('ファイルがドロップされました。');

  // デフォルト動作でファイルが開くのを回避します。
  ev.preventDefault();

  if (ev.dataTransfer.items) {
    // ブラウザがChromeの場合、DataTransferItemListインターフェースを使用してファイルにアクセスします。
    for (let i = 0; i < ev.dataTransfer.items.length; i++) {
      // ドロップされたアイテムがファイルでない場合はスキップします。
      if (ev.dataTransfer.items[i].kind === 'file') {
        file = ev.dataTransfer.items[i].getAsFile();
        console.log('... file[' + i + '].name = ' + file.name);
      }
    }
  } else {
    // 旧式のブラウザの場合、DataTransferインターフェースを使って、ファイルにアクセスします。
    for (let i = 0; i < ev.dataTransfer.files.length; i++) {
      file = ev.dataTransfer.files[i];
      console.log('... file[' + i + '].name = ' + ev.dataTransfer.files[i].name);
    }
  }
  document.getElementById('file_name').innerText = file.name;
};
const dragOverHandler = (ev) => {
  console.log('ファイルがドロップゾーンに入りました。');

  // デフォルト動作でファイルが開くのを回避します。
  ev.preventDefault();
};
const APP_ID = KINTONE_APP_ID;
const registerExpense = async (ev) => {
  console.log('registerExpense関数内に入りました。');
  ev.preventDefault();
  const fileKeys = [];
  const param = {
    app: APP_ID,
    record: {
      description: {
        value: document.getElementById('description').value
      },
      cost: {
        value: document.getElementById('amount').value
      },
      date: {
        value: document.getElementById('date').value
      }
    }
  };
  try {
    const resp = await kintone.api(kintone.api.url('/k/v1/record.json', true), 'POST', param);
    // サクセス
    console.log(resp);
    console.log(`Record ID:${resp.id}`);
    const rec_id = resp.id;
    console.log(`File:${file}`);
    if (file) {
      uploadFile(rec_id);
    }
    alert(`レコードの登録に成功しました。レコードID: ${resp.id}`);
    resetForm();
  } catch (error) {
    // エラー
    alert(`レコードの登録に失敗しました。 ${error.message}`);
  }
};
const uploadFile = (rec_id) => {

  const formData = new FormData();
  formData.append('__REQUEST_TOKEN__', kintone.getRequestToken());

  formData.append('file', file, file.name);

  const url = kintone.api.url('/k/v1/file.json', true);
  const xhr = new XMLHttpRequest();
  xhr.open('POST', url);
  xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
  xhr.onload = () => {
    if (xhr.status === 200) {
      // サクセス
      console.log(JSON.parse(xhr.responseText));
      const key = {fileKey: JSON.parse(xhr.responseText).fileKey};
      updateRecord(rec_id, key);
    } else {
      // エラー
      console.log(JSON.parse(xhr.responseText));
    }
  };
  xhr.send(formData);
};
const updateRecord = async (rec_id, fileKey) => {
  const param = {
    app: APP_ID,
    id: rec_id,
    record: {
      receipt: {
        value: [fileKey]
      }
    }
  };
  const resp = await kintone.api(kintone.api.url('/k/v1/record.json', true), 'PUT', param);
  // サクセス
  console.log(resp);
};
const resetForm = () => {
  document.getElementById('description').value = '';
  document.getElementById('amount').value = '';
  document.getElementById('date').value = '';
  document.getElementById('file_name').innerText = '';
  file = null;
};

動作の確認

Design Portalにて上記の設定を保存し、左上のスイッチにてDesign Portalを有効化します。

kintoneのポータル画面を開き、ページを更新すると以下のような画面が表示されます。
必要事項を入力し、経費精算用のレシートのファイルをドロップして、登録ボタンをクリックすると上記で作成した経費精算アプリに新規レコードが登録されます。

経費精算アプリを開き、レコードが登録されていれば成功です。

他のウィジェットを表示させる

Kintone Portal Designerの「Export」ボタンをクリック後、表示されたサブメニューで「Export as JavaScript(Desktop)」をクリックします。
すると、作成したポータルデザインのJavaScriptファイルがダウンロードフォルダーにダウンロードされます。

次にポータル画面に戻り、ギアアイコンをクリックします。
「kintoneシステム管理」をクリックして、設定画面に入ります。

「カスタマイズ」ー「JavaScript/CSSでカスタマイズ」メニューをクリックして、カスタマイズ設定画面に入ります。

PC用のJavaScriptファイルの「アップロード」ボタンをクリックして、先ほどダウンロードしたポータルデザインのJavaScriptファイルをアップロードし、保存します。

Kintone Portal Designerの画面の戻り、Default Portalのスイッチをオフにします。

再びポータル画面に戻ると他のウィジェットも同時に表示されるようになります。

なお、表示されるウィジェットをカスタマイズする場合は、右上の「...」をクリックし、「ポータルの設定」を選択します。

「ポータルの表示するコンテンツ」より、表示したいコンテンツをチェックして、保存します。

コードの解説

こちらの関数は、ファイルが、divエレメントのClass名drop_zone内へドロップされた時に呼ばれる関数です。

 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const dropHandler = (ev) => {
  console.log('ファイルがドロップされました。');

  // デフォルト動作でファイルが開くのを回避します。
  ev.preventDefault();

  if (ev.dataTransfer.items) {
    // ブラウザがChromeの場合、DataTransferItemListインターフェースを使用してファイルにアクセスします。
    for (let i = 0; i < ev.dataTransfer.items.length; i++) {
      // ドロップされたアイテムがファイルでない場合はスキップします。
      if (ev.dataTransfer.items[i].kind === 'file') {
        file = ev.dataTransfer.items[i].getAsFile();
        console.log('... file[' + i + '].name = ' + file.name);
      }
    }
  } else {
    // 旧式のブラウザの場合、DataTransferインターフェースを使って、ファイルにアクセスします。
    for (let i = 0; i < ev.dataTransfer.files.length; i++) {
      file = ev.dataTransfer.files[i];
      console.log('... file[' + i + '].name = ' + ev.dataTransfer.files[i].name);
    }
  }
  document.getElementById('file_name').innerText = file.name;
};

こちらのメソッドで、ファイルをドロップ時に、ファイルが開くのを防止しています。

13
ev.preventDefault();

ev.dataTransfer.itemsインターフェースでドラッグされたすべてのデータのリストを所得して、タイプがファイルの場合のみデータの内容を取得します。
こちらのインターフェースは最新のブラウザーChrome等でサポートされています。

15
16
17
18
19
20
21
22
23
24
if (ev.dataTransfer.items) {
  // ブラウザがChromeの場合、DataTransferItemListインターフェースを使用してファイルにアクセスします。
  for (let i = 0; i < ev.dataTransfer.items.length; i++) {
    // ドロップされたアイテムがファイルでない場合はスキップします。
    if (ev.dataTransfer.items[i].kind === 'file') {
      file = ev.dataTransfer.items[i].getAsFile();
      console.log('... file[' + i + '].name = ' + file.name);
    }
  }
}

ev.dataTransfer.filesインターフェースにて、ドラッグされたファイルのリストを取得しています。
こちらのインターフェースは旧式のブラウザーでサポートされています。

25
26
27
28
29
// 旧式のブラウザの場合、DataTransferインターフェースを使って、ファイルにアクセスします。
for (let i = 0; i < ev.dataTransfer.files.length; i++) {
  file = ev.dataTransfer.files[i];
  console.log('... file[' + i + '].name = ' + ev.dataTransfer.files[i].name);
}

こちらのコードは、ファイルがdivエレメントのClass名drop_zone内へドラッグされた時に呼ばれる関数です。
こちらでも、ファイルが開くのを防止しています。

33
34
35
36
37
38
const dragOverHandler = (ev) => {
  console.log('ファイルがドロップゾーンに入りました。');

  // デフォルト動作でファイルが開くのを回避します。
  ev.preventDefault();
};

こちらの関数では、divエレメントのClass名、drop_zone内にドロップされたファイルおよび、入力された内容をkintoneへ新規レコードとして登録しています。
また、KINTONE_APP_IDには、お使いのkintoneで作成した経費精算アプリのIDを設定してください。

39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
const APP_ID = KINTONE_APP_ID;
const registerExpense = async (ev) => {
  console.log('registerExpense関数内に入りました。');
  ev.preventDefault();
  const fileKeys = [];
  const param = {
    app: APP_ID,
    record: {
      description: {
        value: document.getElementById('description').value
      },
      cost: {
        value: document.getElementById('amount').value
      },
      date: {
        value: document.getElementById('date').value
      }
    }
  };
  try {
    const resp = await kintone.api(kintone.api.url('/k/v1/record.json', true), 'POST', param);
    // サクセス
    console.log(resp);
    console.log(`Record ID:${resp.id}`);
    const rec_id = resp.id;
    console.log(`File:${file}`);
    if (file) {
      uploadFile(rec_id);
    }
    alert(`レコードの登録に成功しました。レコードID: ${resp.id}`);
    resetForm();
  } catch (error) {
    // エラー
    alert(`レコードの登録に失敗しました。 ${error.message}`);
  }
};

こちらのコードで、経費精算の各フィールドの値を取得し、レコードを新規作成しています。
こちらでは、ファイルのデータは保存していません。

44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
  const param = {
    app: APP_ID,
    record: {
      description: {
        value: document.getElementById('description').value
      },
      cost: {
        value: document.getElementById('amount').value
      },
      date: {
        value: document.getElementById('date').value
      }
    }
  };
  try {
    const resp = await kintone.api(kintone.api.url('/k/v1/record.json', true), 'POST', param);

新規レコード作成に成功した後、レコードIDを取得し、ファイルをアップロードする関数を呼び出します。

60
61
62
63
64
65
66
67
68
    // サクセス
    console.log(resp);
    console.log(`Record ID:${resp.id}`);
    const rec_id = resp.id;
    console.log(`File:${file}`);
    if (file) {
      uploadFile(rec_id);
    }
    alert(`レコードの登録に成功しました。レコードID: ${resp.id}`);

こちらの関数で、ファイルアップロードのkintone APIを呼び出し、ドロップされたファイルデータをkintoneにアップロードします。
アップロード成功の際に返されるFile Keyの値を取得します。
その後、レコードIDとFile Keyを紐づけるためにレコードを更新する関数を呼び出します。

75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
const uploadFile = (rec_id) => {

  const formData = new FormData();
  formData.append('__REQUEST_TOKEN__', kintone.getRequestToken());

  formData.append('file', file, file.name);

  const url = kintone.api.url('/k/v1/file.json', true);
  const xhr = new XMLHttpRequest();
  xhr.open('POST', url);
  xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
  xhr.onload = () => {
    if (xhr.status === 200) {
      // サクセス
      console.log(JSON.parse(xhr.responseText));
      const key = {fileKey: JSON.parse(xhr.responseText).fileKey};
      updateRecord(rec_id, key);
    } else {
      // エラー
      console.log(JSON.parse(xhr.responseText));
    }
  };
  xhr.send(formData);
};

レコードIDとFile Keyを紐づけるための関数です。

 99
100
101
102
103
104
105
106
107
108
109
110
111
112
const updateRecord = async (rec_id, fileKey) => {
  const param = {
    app: APP_ID,
    id: rec_id,
    record: {
      receipt: {
        value: [fileKey]
      }
    }
  };
  const resp = await kintone.api(kintone.api.url('/k/v1/record.json', true), 'PUT', param);
  // サクセス
  console.log(resp);
};

参照サイト

まとめ

普段、頻繁に使用するアプリは、本来kintoneポータルに表示してリンクをクリックすることで運用が可能でした。
しかし、該当のアプリのページに移行して運用する必要がありました。
Kintone Portal Designerをカスタマイズすることで、システム運用の改善につながります。
ポータル画面にいながら経費精算のようにレシートの画像ファイルをドラッグアンドドロップでアップロードし、該当のアプリにレコードを追加できます。

information

このTipsは、2021年10月版kintoneで動作を確認しています。