diff --git a/package-lock.json b/package-lock.json index 6223f61..6070611 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,9 +12,8 @@ "angular-expressions": "^1.4.3", "clsx": "^2.1.1", "core-js": "^3.42.0", - "docxtemplater": "^3.63.0", + "docxtemplater": "^3.63.2", "file-saver": "^2.0.5", - "kintone-pretty-fields": "^0.10.5", "moize": "^6.1.6", "pizzip": "^3.2.0", "react": "^19.1.0", @@ -32,7 +31,7 @@ "@shin-chan/kypes": "^0.0.7", "@swc/helpers": "^0.5.17", "@types/file-saver": "^2.0.7", - "@types/react": "^19.1.5", + "@types/react": "^19.1.6", "@types/react-dom": "^19.1.5", "cross-env": "^7.0.3", "css-loader": "^7.1.2", @@ -1633,9 +1632,9 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "19.1.5", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.5.tgz", - "integrity": "sha512-piErsCVVbpMMT2r7wbawdZsq4xMvIAhQuac2gedQHysu1TZYEigE6pnFfgZT+/jQnrRuF5r+SHzuehFjfRjr4g==", + "version": "19.1.6", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.6.tgz", + "integrity": "sha512-JeG0rEWak0N6Itr6QUx+X60uQmN+5t3j9r/OVDtWzFXKaj6kD1BwJzOksD0FF6iWxZlbE1kB0q9vtnU2ekqa1Q==", "dev": true, "license": "MIT", "dependencies": { @@ -4016,9 +4015,9 @@ } }, "node_modules/docxtemplater": { - "version": "3.63.0", - "resolved": "https://registry.npmjs.org/docxtemplater/-/docxtemplater-3.63.0.tgz", - "integrity": "sha512-ePkPXWBxcHxmja4mGY6qqqzRPHL6KnWxP+6T3xry9mrA1SNUKrQB2GO4JQSMSmGTNmgxuStBQ5pSG+ge+met8Q==", + "version": "3.63.2", + "resolved": "https://registry.npmjs.org/docxtemplater/-/docxtemplater-3.63.2.tgz", + "integrity": "sha512-e/euKWiDeEXWB7g+eUAkZEduRp/Tf+kqpHO5KHindzAuBht/9q7oEOaOKQo5RCtSMCYjAZj+jXdXKPUPR6XIeQ==", "license": "MIT", "dependencies": { "@xmldom/xmldom": "^0.9.8" @@ -4027,18 +4026,6 @@ "node": ">=0.10" } }, - "node_modules/dotenv": { - "version": "16.5.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", - "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -6899,26 +6886,6 @@ "node": ">=0.10.0" } }, - "node_modules/kintone-pretty-fields": { - "version": "0.10.5", - "resolved": "https://registry.npmjs.org/kintone-pretty-fields/-/kintone-pretty-fields-0.10.5.tgz", - "integrity": "sha512-5vstgXuakpxzLVv7np72ot9FzjENS9BwE2xjBpxoRJmqoCrbdj4Vszr8KVe4gZdTxzhlvg0Ssfig87zXLNnjXg==", - "license": "MIT", - "dependencies": { - "@kintone/rest-api-client": "^5.5.2", - "kintone-typeguard": "^0.15.2" - } - }, - "node_modules/kintone-typeguard": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/kintone-typeguard/-/kintone-typeguard-0.15.2.tgz", - "integrity": "sha512-QFIrk3bKd0+vGgz7VRPWvl4fVw3Qrqgi7mr6S0/qPUUyrIDy3IZjxBbtcEUUr9bGmCefHZchQ8ZU49XgurRmSA==", - "license": "MIT", - "dependencies": { - "@kintone/rest-api-client": "^5.5.2", - "dotenv": "^16.4.5" - } - }, "node_modules/language-subtag-registry": { "version": "0.3.23", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", diff --git a/package.json b/package.json index b65c600..d2dbfc0 100644 --- a/package.json +++ b/package.json @@ -16,9 +16,8 @@ "angular-expressions": "^1.4.3", "clsx": "^2.1.1", "core-js": "^3.42.0", - "docxtemplater": "^3.63.0", + "docxtemplater": "^3.63.2", "file-saver": "^2.0.5", - "kintone-pretty-fields": "^0.10.5", "moize": "^6.1.6", "pizzip": "^3.2.0", "react": "^19.1.0", @@ -36,7 +35,7 @@ "@shin-chan/kypes": "^0.0.7", "@swc/helpers": "^0.5.17", "@types/file-saver": "^2.0.7", - "@types/react": "^19.1.5", + "@types/react": "^19.1.6", "@types/react-dom": "^19.1.5", "cross-env": "^7.0.3", "css-loader": "^7.1.2", diff --git a/src/common/global.ts b/src/common/global.ts index 2211669..1ed240a 100644 --- a/src/common/global.ts +++ b/src/common/global.ts @@ -2,3 +2,6 @@ import invariant from 'tiny-invariant'; export const PLUGIN_ID = kintone.$PLUGIN_ID; invariant(PLUGIN_ID, 'The PLUGIN_ID is not available. Please ensure you are on a Kintone plugin page.'); + +export const LANGUAGE = kintone.getLoginUser().language; +invariant(LANGUAGE, 'The LANGUAGE is not available. Please ensure you are on a Kintone plugin page.'); diff --git a/src/config/Settings.tsx b/src/config/Settings.tsx index b27d46f..8ca3806 100644 --- a/src/config/Settings.tsx +++ b/src/config/Settings.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { KintoneRestAPIClient } from '@kintone/rest-api-client'; -import { kintonePrettyFields } from 'kintone-pretty-fields'; +import { Properties as KintoneFormFieldProperties } from '@kintone/rest-api-client/lib/src/client/types'; import moize from 'moize'; import invariant from 'tiny-invariant'; import { PLUGIN_ID } from '../common/global'; @@ -16,28 +16,30 @@ import KintonePluginTitle from '../common/ui/KintonePluginTitle'; import styles from './Settings.module.css'; -const cachedFields = moize.promise(async (appId: number) => { +const cachedFormFieldsProperties = moize.promise(async (appId: number): Promise => { const client = new KintoneRestAPIClient(); - const fields = await kintonePrettyFields.getFields({ client, app: appId, lang: 'en', preview: false }); - return fields; + const { properties } = await client.app.getFormFields({ app: appId, lang: 'en', preview: false }); + return properties; }); const Settings: React.FC = () => { const config = kintone.plugin.app.getConfig(PLUGIN_ID); const appId = kintone.app.getId(); invariant(appId, 'The app ID is not available. Please ensure you are on a Kintone app page.'); - const { fields } = React.use(cachedFields(appId)); - const fileFields = fields.filter(kintonePrettyFields.isFile); + const properties = React.use(cachedFormFieldsProperties(appId)); + const fileFields = Object.values(properties).filter((property) => property.type === 'FILE'); const options: KintonePluginSelectOptionData[] = [ { key: '-', value: '', label: 'Select a File field', disabled: true }, // Default option - ...fileFields.map((field) => ({ - key: field.code, - value: field.code, - label: field.label, + ...fileFields.map((property) => ({ + key: property.code, + value: property.code, + label: property.label, })), ]; - const [template, setTemplate] = React.useState(config.template ?? ''); + const [template, setTemplate] = React.useState( + () => options.find((option) => option.value === (config.template ?? ''))?.value ?? '', + ); const handleOnSubmit = (e: React.FormEvent) => { e.preventDefault(); diff --git a/src/desktop/MenuPanel.tsx b/src/desktop/MenuPanel.tsx index 72329da..ae507b1 100644 --- a/src/desktop/MenuPanel.tsx +++ b/src/desktop/MenuPanel.tsx @@ -1,12 +1,15 @@ import React from 'react'; import { KintoneRestAPIClient } from '@kintone/rest-api-client'; +import { Properties as KintoneFormFieldProperties } from '@kintone/rest-api-client/lib/src/client/types'; import { KintoneRecord } from '@shin-chan/kypes/types/page'; import Docxtemplater from 'docxtemplater'; import expressionParser from 'docxtemplater/expressions'; import { saveAs } from 'file-saver'; +import moize from 'moize'; import PizZip from 'pizzip'; -import { PLUGIN_ID } from '../common/global'; +import invariant from 'tiny-invariant'; +import { LANGUAGE, PLUGIN_ID } from '../common/global'; import KintonePluginAlert from '../common/ui/KintonePluginAlert'; import KintonePluginButton from '../common/ui/KintonePluginButton'; @@ -22,15 +25,23 @@ interface MenuPanelProps { event: kintone.events.AppRecordDetailShowEvent | kintone.events.MobileAppRecordDetailShowEvent; } -const record2data = (record: Partial): TemplateData => { +const cachedFormFieldsProperties = moize.promise(async (appId: number): Promise => { + const client = new KintoneRestAPIClient(); + const { properties } = await client.app.getFormFields({ app: appId, lang: 'en', preview: false }); + return properties; +}); + +const record2data = (properties: KintoneFormFieldProperties, record: Partial): TemplateData => { const data: TemplateData = {}; for (const key in record) { - if (Object.prototype.hasOwnProperty.call(record, key)) { - const field = record[key]; - if (field == null) continue; - const { type, value } = field; - if (value == null) continue; - if (type === 'CREATOR' || type === 'MODIFIER') { + if (Object.prototype.hasOwnProperty.call(record, key) && Object.prototype.hasOwnProperty.call(properties, key)) { + const item = record[key]; + const property = properties[key]; + if (item == null) continue; + const { type, value } = item; + if (value == null) { + data[key] = ''; + } else if (type === 'CREATOR' || type === 'MODIFIER') { data[key] = { name: value.name, code: value.code, @@ -47,7 +58,45 @@ const record2data = (record: Partial): TemplateData => { return { name: v.name, code: v.code }; }); } else if (type === 'SUBTABLE') { - data[key] = value.map((subRecord) => record2data(subRecord.value)); + invariant(property.type === 'SUBTABLE', `Expected property type to be SUBTABLE, but got ${property.type}`); + data[key] = value.map((subRecord) => record2data(property.fields, subRecord.value)); + } else if (type === 'CALC') { + invariant(property.type === 'CALC', `Expected property type to be CALC, but got ${property.type}`); + if (value === '') { + data[key] = ''; + } else if (property.format === 'NUMBER_DIGIT') { + // "1234567" -> "1,234,567" + data[key] = Number(value).toLocaleString('en-US'); + } else if (property.format === 'HOUR_MINUTE') { + // "49:30" -> en:"49 hours 30 minutes", ja: "49時間30分" + const [hours, minutes] = value.split(':').map((v) => Number(v)); + invariant( + !isNaN(hours) && !isNaN(minutes), + `Expected hours and minutes to be numbers, but got hours: ${hours}, minutes: ${minutes}`, + ); + if (LANGUAGE === 'ja') { + data[key] = `${hours}時間${minutes}分`; + } else { + data[key] = `${hours} hours ${minutes} minutes`; + } + } else if (property.format === 'DAY_HOUR_MINUTE') { + // "49:30" -> en: "2 days 1 hour 30 minutes", ja: "2日1時間30分 + const [hours, minutes] = value.split(':').map((v) => Number(v)); + invariant( + !isNaN(hours) && !isNaN(minutes), + `Expected hours and minutes to be numbers, but got hours: ${hours}, minutes: ${minutes}`, + ); + const days = Math.floor(Number(hours) / 24); + const remainingHours = Number(hours) % 24; + if (LANGUAGE === 'ja') { + data[key] = `${days}日${remainingHours}時間${minutes}分`; + } else { + data[key] = `${days} days ${remainingHours} hours ${minutes} minutes`; + } + } else { + // NUMBER, DATETIME, DATE, TIME + data[key] = value; + } } else { data[key] = value; } @@ -58,6 +107,8 @@ const record2data = (record: Partial): TemplateData => { const MenuPanel: React.FC = (props) => { const { event } = props; + const appId = event.appId; + const properties = React.use(cachedFormFieldsProperties(appId)); const config = kintone.plugin.app.getConfig(PLUGIN_ID); const template: string = config.template ?? ''; if (template === '') { @@ -94,11 +145,6 @@ const MenuPanel: React.FC = (props) => { } const handleOnClickOutputButton = (e: React.MouseEvent) => { e.preventDefault(); - const appId = event.type === 'app.record.detail.show' ? kintone.app.getId() : kintone.mobile.app.getId(); - if (!appId) { - alert('The app ID is not available. Please ensure you are on a Kintone app page.'); - return; - } const client = new KintoneRestAPIClient(); client.file .downloadFile({ fileKey }) @@ -109,7 +155,7 @@ const MenuPanel: React.FC = (props) => { linebreaks: true, parser: expressionParser, }); - doc.render(record2data(event.record)); + doc.render(record2data(properties, event.record)); const out = doc.getZip().generate({ type: 'blob', mimeType: DOCX_CONTENTTYPE }); saveAs(out, 'output.docx'); })