レコードの複製
kintone の標準機能にもレコードの再利用(コピー)機能はありますが、「特定のフィールドだけ値を変えてコピーしたい」「ボタン一つで複製を完了させたい」「サブテーブルの内容も含めて完全にコピーしたい」といった業務要件には対応しきれないことがあります。
JavaScript カスタマイズと REST API を組み合わせれば、より柔軟なレコード複製を実現できます。
この記事では、レコード詳細画面にコピーボタンを設置し、REST API を使ってレコードを複製する方法を、基本パターンから応用パターンまで紹介します。
基本: 現在のレコードを複製する
コピーボタンの設置と基本的な複製
レコード詳細画面のスペースフィールドに「レコードをコピー」ボタンを設置し、クリック時に現在のレコードを複製する基本パターンです。
(() => {
'use strict';
// コピー対象外のシステムフィールド
const EXCLUDED_FIELDS = [
'$id',
'$revision',
'作成者',
'更新者',
'作成日時',
'更新日時',
'レコード番号',
];
/**
* レコードオブジェクトからコピー用のデータを生成する
* @param { Object } record - kintone のレコードオブジェクト
* @returns { Object } コピー用レコードデータ
*/
const buildCopyRecord = (record) => {
const copyRecord = {};
Object.entries(record).forEach(([fieldCode, field]) => {
// システムフィールドを除外
if (EXCLUDED_FIELDS.includes(fieldCode)) return;
// ステータス・カテゴリーフィールドを除外
if (['STATUS', 'STATUS_ASSIGNEE', 'CATEGORY'].includes(field.type)) return;
// CALC(計算)フィールドを除外(自動計算されるため)
if (field.type === 'CALC') return;
copyRecord[fieldCode] = { value: field.value };
});
return copyRecord;
};
const events = [
'app.record.detail.show',
'mobile.app.record.detail.show',
];
kintone.events.on(events, (event) => {
// スペースフィールドにボタンを設置
const space = kintone.app.record.getSpaceElement('コピーボタン');
if (!space || space.childNodes.length > 0) return event;
const button = document.createElement('button');
button.textContent = 'レコードをコピー';
button.className = 'kintoneplugin-button-dialog-ok';
button.style.marginTop = '8px';
button.addEventListener('click', async () => {
if (!confirm('このレコードを複製しますか?')) return;
try {
button.disabled = true;
button.textContent = 'コピー中...';
const record = event.record;
const appId = kintone.app.getId();
const copyRecord = buildCopyRecord(record);
// REST API でレコードを追加
const response = await kintone.api(
kintone.api.url('/k/v1/record.json', true),
'POST',
{ app: appId, record: copyRecord }
);
alert(`レコードを複製しました(ID: ${response.id})`);
// 新しいレコードの詳細画面に遷移
const baseUrl = location.pathname.replace(/\/show.*/, '');
location.href = `${baseUrl}/show#record=${response.id}`;
} catch (error) {
console.error('レコードのコピーに失敗しました', error);
alert('レコードのコピーに失敗しました: ' + error.message);
} finally {
button.disabled = false;
button.textContent = 'レコードをコピー';
}
});
space.appendChild(button);
return event;
});
})();
上記のコードを動作させるには、フォーム設定でスペースフィールドを配置し、要素 ID に コピーボタン
を設定する必要があります。
特定フィールドの値を変更してコピー
ステータスやフラグを初期化する
コピー元のレコードには「承認済み」や「完了」のステータスが設定されていることがあります。コピー時にこれらを初期値に戻すパターンです。
(() => {
'use strict';
const EXCLUDED_FIELDS = [
'$id', '$revision', '作成者', '更新者', '作成日時', '更新日時', 'レコード番号',
];
// コピー時に値をリセットするフィールド
const RESET_FIELDS = {
'対応ステータス': '未対応',
'承認フラグ': [],
'担当者': [],
'対応日': '',
'備考': '',
};
const buildCopyRecord = (record) => {
const copyRecord = {};
Object.entries(record).forEach(([fieldCode, field]) => {
if (EXCLUDED_FIELDS.includes(fieldCode)) return;
if (['STATUS', 'STATUS_ASSIGNEE', 'CATEGORY', 'CALC'].includes(field.type)) return;
// リセット対象のフィールドは指定した初期値を設定
if (fieldCode in RESET_FIELDS) {
copyRecord[fieldCode] = { value: RESET_FIELDS[fieldCode] };
return;
}
copyRecord[fieldCode] = { value: field.value };
});
return copyRecord;
};
const events = ['app.record.detail.show'];
kintone.events.on(events, (event) => {
const space = kintone.app.record.getSpaceElement('コピーボタン');
if (!space || space.childNodes.length > 0) return event;
const button = document.createElement('button');
button.textContent = 'ステータスを初期化してコピー';
button.className = 'kintoneplugin-button-dialog-ok';
button.addEventListener('click', async () => {
if (!confirm('ステータスを初期化してレコードを複製しますか?')) return;
try {
button.disabled = true;
const copyRecord = buildCopyRecord(event.record);
const response = await kintone.api(
kintone.api.url('/k/v1/record.json', true),
'POST',
{ app: kintone.app.getId(), record: copyRecord }
);
alert(`レコードを複製しました(ID: ${response.id})`);
location.reload();
} catch (error) {
console.error(error);
alert('コピーに失敗しました: ' + error.message);
} finally {
button.disabled = false;
}
});
space.appendChild(button);
return event;
});
})();
RESET_FIELDS
オブジェクトでリセット対象とその初期値を定義しておくと、フィールドの追加・変更が容易です。ドロップダウンフィールドの場合は
'未対応' のように文字列、チェックボックスやユーザー選択の場合は [](空配列)を指定します。
サブテーブルを含むレコードの複製
サブテーブルを含むレコードをコピーする場合、テーブルの各行から id を除去する必要があります。id は既存の行を識別するためのものであり、新しいレコードに含めると意図しない動作になる可能性があります。
/**
* サブテーブルフィールドからコピー用のデータを生成する
* 各行の id を除去し、value のみを保持する
* @param { Object[] } tableValue - サブテーブルの value 配列
* @returns { Object[] } コピー用のサブテーブルデータ
*/
const copySubtable = (tableValue) => {
return tableValue.map((row) => {
const newRow = {};
Object.entries(row.value).forEach(([fieldCode, field]) => {
// CALC フィールドはコピー不要
if (field.type === 'CALC') return;
newRow[fieldCode] = { value: field.value };
});
return { value: newRow };
});
};
/**
* レコードオブジェクトからコピー用のデータを生成する(サブテーブル対応版)
* @param { Object } record - kintone のレコードオブジェクト
* @returns { Object } コピー用レコードデータ
*/
const buildCopyRecord = (record) => {
const EXCLUDED_FIELDS = [
'$id', '$revision', '作成者', '更新者', '作成日時', '更新日時', 'レコード番号',
];
const copyRecord = {};
Object.entries(record).forEach(([fieldCode, field]) => {
if (EXCLUDED_FIELDS.includes(fieldCode)) return;
if (['STATUS', 'STATUS_ASSIGNEE', 'CATEGORY', 'CALC'].includes(field.type)) return;
// サブテーブルの場合は id を除去してコピー
if (field.type === 'SUBTABLE') {
copyRecord[fieldCode] = { value: copySubtable(field.value) };
return;
}
copyRecord[fieldCode] = { value: field.value };
});
return copyRecord;
};
添付ファイルフィールド(FILE
タイプ)を含むレコードをコピーする場合、ファイルの実体はコピーされません。ファイルキー(fileKey)はコピー元のレコードに紐づいているため、コピー先で参照しても正しく動作しない場合があります。ファイルも含めて完全にコピーしたい場合は、ファイル
API でダウンロードしてから再アップロードする必要があります。
複数レコードの一括コピー
一覧画面でチェックボックスを使って選択したレコードを、別のアプリに一括コピーするパターンです。
(() => {
'use strict';
const DEST_APP_ID = 20; // コピー先のアプリID
// コピー対象のフィールドマッピング(コピー元 → コピー先)
const FIELD_MAPPING = {
'案件名': '案件名',
'顧客名': '顧客名',
'金額': '金額',
'担当者': '担当者',
};
const events = ['app.record.index.show'];
kintone.events.on(events, (event) => {
if (document.getElementById('bulk-copy-button')) return event;
const headerSpace = kintone.app.getHeaderMenuSpaceElement();
if (!headerSpace) return event;
const button = document.createElement('button');
button.id = 'bulk-copy-button';
button.textContent = '選択レコードを別アプリにコピー';
button.className = 'kintoneplugin-button-dialog-ok';
button.addEventListener('click', async () => {
// 一覧画面で表示されているレコードを取得
const records = event.records;
if (!records || records.length === 0) {
alert('コピー対象のレコードがありません');
return;
}
if (!confirm(`${records.length}件のレコードをコピーしますか?`)) return;
try {
button.disabled = true;
button.textContent = 'コピー中...';
// コピー用のレコードデータを生成
const copyRecords = records.map((record) => {
const newRecord = {};
Object.entries(FIELD_MAPPING).forEach(([srcField, destField]) => {
if (record[srcField]) {
newRecord[destField] = { value: record[srcField].value };
}
});
return newRecord;
});
// 100件ずつ分割して登録(REST API の上限)
for (let i = 0; i < copyRecords.length; i += 100) {
const chunk = copyRecords.slice(i, i + 100);
await kintone.api(
kintone.api.url('/k/v1/records.json', true),
'POST',
{ app: DEST_APP_ID, records: chunk }
);
}
alert(`${records.length}件のレコードをコピーしました`);
} catch (error) {
console.error(error);
alert('コピーに失敗しました: ' + error.message);
} finally {
button.disabled = false;
button.textContent = '選択レコードを別アプリにコピー';
}
});
headerSpace.appendChild(button);
return event;
});
})();
FIELD_MAPPING
でコピー元とコピー先のフィールドコードを対応付けることで、フィールドコードが異なるアプリ間でもコピーが可能です。
まとめ
buildCopyRecord関数でシステムフィールドやステータスフィールドを自動的に除外してコピーデータを生成できる- サブテーブルを含むレコードのコピーでは、各行の
idを除去する必要がある RESET_FIELDSオブジェクトを使えば、コピー時に特定フィールドの値を初期化できる- 添付ファイルフィールドの実体はコピーされないため、ファイル API による再アップロードが必要
- 100 件を超えるレコードの一括コピーは、100 件ずつ分割してリクエストする
FIELD_MAPPINGを定義すれば、異なるフィールドコードを持つアプリ間のコピーも可能