mobile対応追加
This commit is contained in:
@@ -0,0 +1,328 @@
|
||||
import type { FieldLayout, FieldsJoinMapping, JoinTable, Record, RecordForParameter, SavedData, WhereCondition } from "@/types/model";
|
||||
import { type OneOf, isType } from "./field-types-mobile";
|
||||
import type { ConditionValue } from "./conditions";
|
||||
declare var KintoneRestAPIClient: typeof import("@kintone/rest-api-client").KintoneRestAPIClient;
|
||||
export class KintoneIndexEventHandler {
|
||||
private config: SavedData<FieldLayout>;
|
||||
private currentApp: string;
|
||||
constructor(config: SavedData<FieldLayout>, currentApp: string) {
|
||||
this.config = config;
|
||||
this.currentApp = currentApp;
|
||||
}
|
||||
|
||||
public init(): void {
|
||||
this.addButtonToView();
|
||||
}
|
||||
|
||||
// ボタン追加
|
||||
private addButtonToView(): void {
|
||||
const headerSpace =kintone.mobile.app.getHeaderSpaceElement();
|
||||
if (!headerSpace) {
|
||||
throw new Error('このページではヘッダー要素が利用できません。');
|
||||
};
|
||||
|
||||
// ボタン追加
|
||||
if (document.getElementById('btn-data-fetch')) return;
|
||||
const kuc = Kucs['1.18.0'];
|
||||
const button = new kuc.Button({
|
||||
text: this.config.buttonName,
|
||||
type: "submit",
|
||||
id: 'btn-data-fetch',
|
||||
});
|
||||
button.addEventListener('click', () => this.handleButtonClick());
|
||||
headerSpace.appendChild(button);
|
||||
}
|
||||
|
||||
// ボタンクリック
|
||||
private handleButtonClick = async (): Promise<void> => {
|
||||
const spinner = this.showSpinner();
|
||||
try {
|
||||
console.log('データ収集開始...');
|
||||
await this.execDataFectch();
|
||||
spinner.close();
|
||||
location.reload();
|
||||
} catch (error) {
|
||||
spinner.close();
|
||||
const detailError = (error instanceof Error) ? "\n詳細:" + error.message : "";
|
||||
const errorMsg = `データ収集中処理中例外発生しました。${detailError}`;
|
||||
console.error(errorMsg, error);
|
||||
window.alert(errorMsg);
|
||||
}
|
||||
}
|
||||
|
||||
private showSpinner() {
|
||||
const kuc = Kucs['1.18.0'];
|
||||
const spinner = new kuc.Spinner({
|
||||
text: 'データ収集中',
|
||||
container: document.body
|
||||
});
|
||||
spinner.open();
|
||||
return spinner;
|
||||
}
|
||||
|
||||
/**
|
||||
* 検索データ取得&作成処理
|
||||
*/
|
||||
private execDataFectch = async (): Promise<void> => {
|
||||
const mainTable = this.config.joinTables[0];
|
||||
const mainRecords = await this.fetchDataFromApp(mainTable);
|
||||
// フィールド結合
|
||||
let mainData = mainRecords.map((record) => {
|
||||
const rightRecord: Record = {};
|
||||
mainTable.fieldsMapping.forEach((mapping => {
|
||||
rightRecord[this.fieldCode(mapping.rightField)] = record[this.fieldCode(mapping.leftField)]
|
||||
}));
|
||||
return rightRecord;
|
||||
});
|
||||
const joinTables = this.config.joinTables.filter((table, index) => index > 0);
|
||||
for (const table of joinTables) {
|
||||
const subDatas = await this.fetchDataFromApp(table);
|
||||
mainData = this.leftJoin(mainData, subDatas, table);
|
||||
// console.log("LeftJoin", mainData);
|
||||
};
|
||||
//現在のデータをクリアする
|
||||
await this.deleteCurrentRecords();
|
||||
//データを更新する
|
||||
await this.saveDataToCurrentApp(mainData);
|
||||
}
|
||||
/**
|
||||
* Appからデータを取得する
|
||||
* @param joinTable 対象アプリかられレコードを取得する
|
||||
* @returns
|
||||
*/
|
||||
private fetchDataFromApp = async (joinTable: JoinTable<FieldLayout>): Promise<Record[]> => {
|
||||
// Filter 条件作成
|
||||
const filter = this.getWhereCondition(joinTable.whereConditions);
|
||||
//取得列を設定する
|
||||
const fetchFields = joinTable.fieldsMapping.map(map => this.fieldCode(map.leftField));
|
||||
if (joinTable.table) {
|
||||
fetchFields.push(joinTable.table);
|
||||
}
|
||||
const onFields =joinTable.onConditions.map(cond=>this.fieldCode(cond.leftField));
|
||||
onFields.forEach(fld=>{
|
||||
if(!fetchFields.includes(fld)){
|
||||
fetchFields.push(fld);
|
||||
}
|
||||
});
|
||||
// KintoneRESTAPI
|
||||
const client = new KintoneRestAPIClient();
|
||||
const records = await client.record.getAllRecords({
|
||||
app: joinTable.app,
|
||||
fields: fetchFields,
|
||||
condition: filter
|
||||
});
|
||||
//console.log("Data Fetch", records);
|
||||
//SubTableが含まれる場合、フラットなデータに変換する
|
||||
return this.convertToFlatDatas(records, joinTable.table);
|
||||
|
||||
}
|
||||
/**
|
||||
* 絞り込み条件式作成
|
||||
* @param whereCondifions
|
||||
* @returns
|
||||
*/
|
||||
private getWhereCondition(whereCondifions: WhereCondition<FieldLayout>[]): string {
|
||||
const conds = whereCondifions
|
||||
.filter((cond) => this.fieldCode(cond.field) !== '');
|
||||
const condition = conds.map((cond) => {
|
||||
let condition = cond.condition;
|
||||
if ("subField" in cond.field && cond.field.subField) {
|
||||
condition = this.mapConditionForSubField(cond.condition);
|
||||
}
|
||||
const condValue = this.getConditionValue(cond.field as OneOf, condition, cond.data);
|
||||
return `${this.fieldCode(cond.field)} ${condition} ${condValue}`;
|
||||
}).join(' and ');
|
||||
return condition;
|
||||
}
|
||||
/**
|
||||
* サブフィールドの演算子対応
|
||||
* @param condition
|
||||
* @returns
|
||||
*/
|
||||
private mapConditionForSubField(condition: ConditionValue): ConditionValue {
|
||||
switch (condition) {
|
||||
case "=":
|
||||
return "in";
|
||||
case "!=":
|
||||
return "not in";
|
||||
default:
|
||||
return condition; // 既存の条件をそのまま使用
|
||||
}
|
||||
}
|
||||
private getConditionValue(field: OneOf, condition: ConditionValue, data: string): string {
|
||||
if (!data) return "";
|
||||
if (isType.NUMBER(field) || isType.RECORD_NUMBER(field)) {
|
||||
// For numbers, return as is
|
||||
return data;
|
||||
} else if (isType.DATE(field)) {
|
||||
// If field is DATE, format as "yyyy-MM-dd" unless it's a reserved function
|
||||
if (/^\d{4}-\d{2}-\d{2}$/.test(data) || data.match(/^\w+\(.*\)$/)) {
|
||||
return `"${data}"`;
|
||||
}
|
||||
const date = new Date(data);
|
||||
return `"${date.toISOString().split('T')[0]}"`;
|
||||
} else if (isType.DATETIME(field) || isType.CREATED_TIME(field) || isType.UPDATED_TIME(field)) {
|
||||
// If field is DATETIME, format as "yyyy-MM-ddTHH:mm:ssZ"
|
||||
if (data.match(/^\w+\(.*\)$/)) {
|
||||
return `"${data}"`;
|
||||
}
|
||||
const dateTime = new Date(data);
|
||||
return `"${dateTime.toISOString()}"`;
|
||||
} else if ((condition === "in" || condition === "not in")) {
|
||||
if (data.includes(",")) {
|
||||
// Handle "in" and "not in" with comma-separated strings
|
||||
const items = data.split(",").map(item => `"${item.trim()}"`);
|
||||
return `(${items.join(",")})`;
|
||||
} else {
|
||||
return `("${data}")`;
|
||||
}
|
||||
} else {
|
||||
// Default case for other types (treat as text)
|
||||
return `"${data}"`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* fieldからコードを取得する
|
||||
* @param field
|
||||
* @returns
|
||||
*/
|
||||
private fieldCode(field: any): string {
|
||||
if (!field) {
|
||||
return "";
|
||||
}
|
||||
if (typeof field === 'string' && field) {
|
||||
return field;
|
||||
} else if (typeof field === 'object' && 'code' in field) {
|
||||
return field.code;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* ネストされたサブテーブルデータをフラットなデータに変換し、親レコードを複製します。
|
||||
* @param records レコードの配列
|
||||
* @returns 変換後のフラットなレコード配列
|
||||
*/
|
||||
private convertToFlatDatas(records: Record[], subTable: string): Record[] {
|
||||
if (!subTable) {
|
||||
return records;
|
||||
}
|
||||
const flattenedData: Record[] = [];
|
||||
records.forEach((record) => {
|
||||
// テーブルフィールドが存在するかを確認
|
||||
if (record[subTable]?.type === "SUBTABLE" && record[subTable].value.length > 0) {
|
||||
// サブテーブル内の各レコードを処理
|
||||
record[subTable].value.forEach((nested: Record) => {
|
||||
// 親レコードのコピーを作成
|
||||
const flatRecord = { ...record };
|
||||
|
||||
// サブテーブルフィールドを抽出してフラットな構造に追加
|
||||
Object.entries(nested.value).forEach(([key, field]) => {
|
||||
flatRecord[key] = { value: field.value, type: field.type };
|
||||
});
|
||||
|
||||
// テーブルフィールドを削除
|
||||
delete flatRecord[subTable];
|
||||
|
||||
// 結果の配列に追加
|
||||
flattenedData.push(flatRecord);
|
||||
});
|
||||
} else {
|
||||
// サブテーブルが空の場合、親レコードをそのまま追加
|
||||
const flatRecord = { ...record };
|
||||
delete flatRecord[subTable];
|
||||
flattenedData.push(flatRecord);
|
||||
}
|
||||
});
|
||||
// console.log("FlatDatas=>", flattenedData);
|
||||
return flattenedData;
|
||||
}
|
||||
|
||||
/**
|
||||
* データLeftJoin処理
|
||||
* @param mainData
|
||||
* @param subData
|
||||
* @param onConditions
|
||||
* @param fieldsMapping
|
||||
* @returns
|
||||
*/
|
||||
private leftJoin(
|
||||
mainData: Record[],
|
||||
subData: Record[],
|
||||
joinTable: JoinTable<FieldLayout>
|
||||
): Record[] {
|
||||
const joinedRecords: Record[] = [];
|
||||
mainData.forEach((mainRecord) => {
|
||||
const matchedRecords = subData.filter((subRecord) =>
|
||||
joinTable.onConditions.every(
|
||||
(cond) => mainRecord[this.fieldCode(cond.rightField)]?.value === subRecord[this.fieldCode(cond.leftField)]?.value
|
||||
)
|
||||
);
|
||||
|
||||
// マッチ出来ない場合、LEFTの列のみ返す
|
||||
if (!matchedRecords) {
|
||||
joinedRecords.push(mainRecord);
|
||||
} else {
|
||||
matchedRecords.forEach((matchedRecord) => {
|
||||
// フィールド結合
|
||||
const combinedRecord: Record = { ...mainRecord };
|
||||
joinTable.fieldsMapping.forEach((mapping) => {
|
||||
combinedRecord[this.fieldCode(mapping.rightField)] = matchedRecord[this.fieldCode(mapping.leftField)];
|
||||
});
|
||||
joinedRecords.push(combinedRecord);
|
||||
});
|
||||
}
|
||||
});
|
||||
return joinedRecords;
|
||||
}
|
||||
|
||||
/**
|
||||
* 現在アプリのすべてレコードを削除する
|
||||
*/
|
||||
private async deleteCurrentRecords(): Promise<void> {
|
||||
const client = new KintoneRestAPIClient();
|
||||
const currentRecords = await client.record.getAllRecords({
|
||||
app: this.currentApp,
|
||||
fields: ["$id"],
|
||||
});
|
||||
const deleteRecords = currentRecords.map(record => {
|
||||
return { id: record.$id.value as string }
|
||||
});
|
||||
await client.record.deleteAllRecords({
|
||||
app: this.currentApp,
|
||||
records: deleteRecords
|
||||
});
|
||||
client.record.addAllRecords
|
||||
}
|
||||
/**
|
||||
* 結合後のデータを現在のアプリに挿入する
|
||||
* @param records
|
||||
*/
|
||||
private async saveDataToCurrentApp(records: Record[]): Promise<void> {
|
||||
try {
|
||||
const client = new KintoneRestAPIClient();
|
||||
const result = await client.record.addAllRecords({
|
||||
app: this.currentApp,
|
||||
records: this.convertForUpdate(records)
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('データ作成時エラーが発生しました:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recordを更新時の形式を変換する
|
||||
* @param resords
|
||||
* @returns
|
||||
*/
|
||||
private convertForUpdate(resords: Record[]): RecordForParameter[] {
|
||||
return resords.map((record) =>
|
||||
Object.fromEntries(
|
||||
Object.entries(record).map(([fieldCode, { value }]) => [fieldCode, { value }])
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
55
vue-project/my-kintone-plugin/src/js/field-types-mobile.ts
Normal file
55
vue-project/my-kintone-plugin/src/js/field-types-mobile.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
declare var KintoneRestAPIClient: typeof import("@kintone/rest-api-client").KintoneRestAPIClient;
|
||||
const client = new KintoneRestAPIClient();
|
||||
export type Properties = Awaited<ReturnType<typeof client.app.getFormFields>>['properties'];
|
||||
export type Layout = Awaited<ReturnType<typeof client.app.getFormLayout>>['layout'];
|
||||
|
||||
export type OneOf = Properties[string];
|
||||
export type FieldType = OneOf['type'];
|
||||
|
||||
const typeNames = [
|
||||
'RECORD_NUMBER',
|
||||
'CREATOR',
|
||||
'CREATED_TIME',
|
||||
'MODIFIER',
|
||||
'UPDATED_TIME',
|
||||
'CATEGORY',
|
||||
'STATUS',
|
||||
'STATUS_ASSIGNEE',
|
||||
'SINGLE_LINE_TEXT',
|
||||
'NUMBER',
|
||||
'CALC',
|
||||
'MULTI_LINE_TEXT',
|
||||
'RICH_TEXT',
|
||||
'LINK',
|
||||
'CHECK_BOX',
|
||||
'RADIO_BUTTON',
|
||||
'DROP_DOWN',
|
||||
'MULTI_SELECT',
|
||||
'FILE',
|
||||
'DATE',
|
||||
'TIME',
|
||||
'DATETIME',
|
||||
'USER_SELECT',
|
||||
'ORGANIZATION_SELECT',
|
||||
'GROUP_SELECT',
|
||||
'GROUP',
|
||||
'REFERENCE_TABLE',
|
||||
'SUBTABLE',
|
||||
] as const satisfies readonly FieldType[];
|
||||
|
||||
export const types = typeNames.reduce(
|
||||
(acc, name) => {
|
||||
acc[name] = name;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<(typeof typeNames)[number], (typeof typeNames)[number]>,
|
||||
);
|
||||
|
||||
type ExtractOneOf<T extends FieldType> = Extract<OneOf, { type: T }>;
|
||||
function createTypeGuard<T extends FieldType>(type: T) {
|
||||
return (value: OneOf): value is ExtractOneOf<T> => value?.type === type;
|
||||
}
|
||||
|
||||
export const isType = Object.fromEntries(
|
||||
typeNames.map((typeName) => [typeName, createTypeGuard(typeName as FieldType)]),
|
||||
) as { [K in (typeof typeNames)[number]]: (value: OneOf) => value is ExtractOneOf<K> };
|
||||
@@ -1,22 +1,32 @@
|
||||
import type { Field, FieldLayout, SavedData } from "@/types/model";
|
||||
import { KintoneIndexEventHandler } from "./KintoneIndexEventHandler.mobile";
|
||||
|
||||
(function (PLUGIN_ID) {
|
||||
kintone.events.on('mobile.app.record.index.show', () => {
|
||||
const spaceEl = kintone.mobile.app.getHeaderSpaceElement();
|
||||
if (spaceEl === null) {
|
||||
throw new Error('The header element is unavailable on this page.');
|
||||
kintone.events.on('mobile.app.record.index.show', (event) => {
|
||||
try{
|
||||
const setting = kintone.plugin.app.getConfig(PLUGIN_ID);
|
||||
const config:SavedData<FieldLayout> = getConfig(setting);
|
||||
const currentApp = kintone.mobile.app.getId()?.toString();
|
||||
if(!currentApp) return;
|
||||
const handler = new KintoneIndexEventHandler(config,currentApp);
|
||||
handler.init();
|
||||
}catch(error){
|
||||
const detailError =(error instanceof Error) ? "\n詳細:" + error.message : "";
|
||||
const errorMsg = `データ収集中処理中例外発生しました。${ detailError }`;
|
||||
event.error = errorMsg;
|
||||
}
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
const headingEl = document.createElement('h3');
|
||||
const messageEl = document.createElement('p');
|
||||
|
||||
const config = kintone.plugin.app.getConfig(PLUGIN_ID);
|
||||
messageEl.textContent = config.message;
|
||||
messageEl.classList.add('plugin-space-message');
|
||||
headingEl.textContent = 'Hello kintone plugin!';
|
||||
headingEl.classList.add('plugin-space-heading');
|
||||
|
||||
fragment.appendChild(headingEl);
|
||||
fragment.appendChild(messageEl);
|
||||
spaceEl.appendChild(fragment);
|
||||
return event;
|
||||
});
|
||||
/**
|
||||
* Config設定値を変換する
|
||||
* @param setting
|
||||
* @returns
|
||||
*/
|
||||
function getConfig(setting:any):SavedData<FieldLayout>{
|
||||
const config:SavedData<FieldLayout>={
|
||||
buttonName:setting.buttonName,
|
||||
joinTables:JSON.parse(setting.joinTables)
|
||||
}
|
||||
return config;
|
||||
}
|
||||
})(kintone.$PLUGIN_ID);
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
},
|
||||
"mobile": {
|
||||
"js": [
|
||||
"js/KintoneRestAPIClient.min.js",
|
||||
"js/kuc.min.js",
|
||||
"js/mobile.js"
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user