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 で動作を確認しています。