From 94b7277b3a5ef51114712baf9eeb9214fa2661cc Mon Sep 17 00:00:00 2001 From: Yoshihiro OKUMURA Date: Thu, 2 Oct 2025 13:26:46 +0900 Subject: [PATCH] feat: Upgrade plugin version to 2.0.0 and refactor configuration handling - Updated version numbers in package.json, package-lock.json, and manifest.json to 2.0.0. - Refactored configuration types to use data sources (SELF, APP) instead of type-based configurations. - Enhanced settings UI to allow selection between using the current record or another app's record. - Improved error handling and validation for configuration settings. - Added new input component for better handling of text inputs in settings. - Updated localization files for new settings structure and error messages. - Optimized API calls with memoization for fetching form field properties and checking configurations. --- package-lock.json | 72 +++++------ package.json | 16 +-- plugin/manifest.json | 5 +- src/common/config.ts | 38 +++--- src/common/ui/KintonePluginInputText.tsx | 46 +++++++ src/config/ConfigApp.tsx | 17 ++- src/config/Settings.module.css | 7 + src/config/Settings.tsx | 155 ++++++++++++++++++----- src/config/locales/en.json | 21 ++- src/config/locales/ja.json | 19 ++- src/desktop/DesktopApp.tsx | 29 ++++- src/desktop/MenuPanel.tsx | 44 ++----- src/desktop/checkConfig.ts | 32 ++--- src/desktop/generateWordFileData.ts | 33 ++++- src/desktop/locales/en.json | 1 - src/desktop/locales/ja.json | 1 - 16 files changed, 374 insertions(+), 162 deletions(-) create mode 100644 src/common/ui/KintonePluginInputText.tsx diff --git a/package-lock.json b/package-lock.json index 59d9b2b..ab3c45b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "kintone-plugin-docx", - "version": "1.0.2", + "version": "2.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "kintone-plugin-docx", - "version": "1.0.2", + "version": "2.0.0", "dependencies": { "@kintone/rest-api-client": "^5.7.5", "angular-expressions": "^1.5.1", @@ -14,11 +14,11 @@ "dayjs": "^1.11.18", "docxtemplater": "^3.66.4", "file-saver": "^2.0.5", - "i18next": "^25.5.2", + "i18next": "^25.5.3", "moize": "^6.1.6", "pizzip": "^3.2.0", - "react": "^19.1.1", - "react-dom": "^19.1.1", + "react": "^19.2.0", + "react-dom": "^19.2.0", "react-error-boundary": "^6.0.0", "react-i18next": "^16.0.0", "tiny-invariant": "^1.3.3" @@ -31,9 +31,9 @@ "@rspack/cli": "^1.5.8", "@rspack/core": "^1.5.8", "@types/file-saver": "^2.0.7", - "@types/node": "^24.6.0", - "@types/react": "^19.1.16", - "@types/react-dom": "^19.1.9", + "@types/node": "^24.6.2", + "@types/react": "^19.2.0", + "@types/react-dom": "^19.2.0", "cross-env": "^10.1.0", "cspell": "^9.2.1", "env-cmd": "^11.0.0", @@ -41,7 +41,7 @@ "npm-run-all": "^4.1.5", "prettier": "^3.6.2", "ts-node": "^10.9.2", - "typescript": "^5.9.2" + "typescript": "^5.9.3" } }, "node_modules/@babel/code-frame": { @@ -2428,9 +2428,9 @@ } }, "node_modules/@types/node": { - "version": "24.6.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.6.0.tgz", - "integrity": "sha512-F1CBxgqwOMc4GKJ7eY22hWhBVQuMYTtqI8L0FcszYcpYX0fzfDGpez22Xau8Mgm7O9fI+zA/TYIdq3tGWfweBA==", + "version": "24.6.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.6.2.tgz", + "integrity": "sha512-d2L25Y4j+W3ZlNAeMKcy7yDsK425ibcAOO2t7aPTz6gNMH0z2GThtwENCDc0d/Pw9wgyRqE5Px1wkV7naz8ang==", "dev": true, "license": "MIT", "dependencies": { @@ -2469,9 +2469,9 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "19.1.16", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.16.tgz", - "integrity": "sha512-WBM/nDbEZmDUORKnh5i1bTnAz6vTohUf9b8esSMu+b24+srbaxa04UbJgWx78CVfNXA20sNu0odEIluZDFdCog==", + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.0.tgz", + "integrity": "sha512-1LOH8xovvsKsCBq1wnT4ntDUdCJKmnEakhsuoUSy6ExlHCkGP2hqnatagYTgFk6oeL0VU31u7SNjunPN+GchtA==", "dev": true, "license": "MIT", "dependencies": { @@ -2479,13 +2479,13 @@ } }, "node_modules/@types/react-dom": { - "version": "19.1.9", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.9.tgz", - "integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==", + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.0.tgz", + "integrity": "sha512-brtBs0MnE9SMx7px208g39lRmC5uHZs96caOJfTjFcYSLHNamvaSMfJNagChVNkup2SdtOxKX1FDBkRSJe1ZAg==", "dev": true, "license": "MIT", "peerDependencies": { - "@types/react": "^19.0.0" + "@types/react": "^19.2.0" } }, "node_modules/@types/retry": { @@ -7337,9 +7337,9 @@ } }, "node_modules/i18next": { - "version": "25.5.2", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.5.2.tgz", - "integrity": "sha512-lW8Zeh37i/o0zVr+NoCHfNnfvVw+M6FQbRp36ZZ/NyHDJ3NJVpp2HhAUyU9WafL5AssymNoOjMRB48mmx2P6Hw==", + "version": "25.5.3", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.5.3.tgz", + "integrity": "sha512-joFqorDeQ6YpIXni944upwnuHBf5IoPMuqAchGVeQLdWC2JOjxgM9V8UGLhNIIH/Q8QleRxIi0BSRQehSrDLcg==", "funding": [ { "type": "individual", @@ -9906,24 +9906,24 @@ } }, "node_modules/react": { - "version": "19.1.1", - "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", - "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", + "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.1.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", - "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==", + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", + "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", "dependencies": { - "scheduler": "^0.26.0" + "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.1.1" + "react": "^19.2.0" } }, "node_modules/react-error-boundary": { @@ -10491,9 +10491,9 @@ "license": "MIT" }, "node_modules/scheduler": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", - "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "license": "MIT" }, "node_modules/schema-utils": { @@ -11936,9 +11936,9 @@ "license": "MIT" }, "node_modules/typescript": { - "version": "5.9.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", - "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", "bin": { diff --git a/package.json b/package.json index 881a5b7..ef839a0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "kintone-plugin-docx", - "version": "1.0.2", + "version": "2.0.0", "scripts": { "gen": "run-s gen:*", "gen:key": "node scripts/generate-private-key.js", @@ -22,11 +22,11 @@ "dayjs": "^1.11.18", "docxtemplater": "^3.66.4", "file-saver": "^2.0.5", - "i18next": "^25.5.2", + "i18next": "^25.5.3", "moize": "^6.1.6", "pizzip": "^3.2.0", - "react": "^19.1.1", - "react-dom": "^19.1.1", + "react": "^19.2.0", + "react-dom": "^19.2.0", "react-error-boundary": "^6.0.0", "react-i18next": "^16.0.0", "tiny-invariant": "^1.3.3" @@ -39,9 +39,9 @@ "@rspack/cli": "^1.5.8", "@rspack/core": "^1.5.8", "@types/file-saver": "^2.0.7", - "@types/node": "^24.6.0", - "@types/react": "^19.1.16", - "@types/react-dom": "^19.1.9", + "@types/node": "^24.6.2", + "@types/react": "^19.2.0", + "@types/react-dom": "^19.2.0", "cross-env": "^10.1.0", "cspell": "^9.2.1", "env-cmd": "^11.0.0", @@ -49,6 +49,6 @@ "npm-run-all": "^4.1.5", "prettier": "^3.6.2", "ts-node": "^10.9.2", - "typescript": "^5.9.2" + "typescript": "^5.9.3" } } diff --git a/plugin/manifest.json b/plugin/manifest.json index bb34d86..f102644 100644 --- a/plugin/manifest.json +++ b/plugin/manifest.json @@ -1,7 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/kintone/js-sdk/%40kintone/plugin-manifest-validator%4010.2.0/packages/plugin-manifest-validator/manifest-schema.json", "manifest_version": 1, - "version": "1.0.2", + "version": "2.0.0", "type": "APP", "desktop": { "js": ["js/desktop.js"], @@ -15,8 +15,7 @@ "config": { "html": "html/config.html", "js": ["js/config.js"], - "css": ["js/config.css"], - "required_params": ["template"] + "css": ["js/config.css"] }, "name": { "en": "Word output plugin", diff --git a/src/common/config.ts b/src/common/config.ts index a43a1a8..a409a86 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -1,29 +1,25 @@ -export const PluginConfigType = { - SELF: 0, - APP: 1, - URL: 2, +export const PluginConfigDataSource = { + SELF: 'self', + APP: 'app', } as const; +export type PluginConfigDataSource = (typeof PluginConfigDataSource)[keyof typeof PluginConfigDataSource]; export interface PluginConfigSelf { - type: typeof PluginConfigType.SELF; - template: string; + dataSource: typeof PluginConfigDataSource.SELF; + fieldCode: string; } export interface PluginConfigApp { - type: typeof PluginConfigType.APP; + dataSource: typeof PluginConfigDataSource.APP; appId: string; - template: string; recordId: string; -} -export interface PluginConfigUrl { - type: typeof PluginConfigType.URL; - template: string; + fieldCode: string; } -export type PluginConfig = PluginConfigSelf | PluginConfigApp | PluginConfigUrl; +export type PluginConfig = PluginConfigSelf | PluginConfigApp; const DEFAULT_CONFIG: PluginConfig = { - type: PluginConfigType.SELF, - template: '', + dataSource: PluginConfigDataSource.SELF, + fieldCode: '', } as const; export const saveConfig = (config: PluginConfig, callback?: () => void): void => { @@ -31,5 +27,15 @@ export const saveConfig = (config: PluginConfig, callback?: () => void): void => }; export const loadConfig = (pluginId: string): PluginConfig => { - return { ...DEFAULT_CONFIG, ...(kintone.plugin.app.getConfig(pluginId) as PluginConfig) }; + const config = kintone.plugin.app.getConfig(pluginId); + if (!config || Object.keys(config).length === 0) { + // initial state + return { ...DEFAULT_CONFIG }; + } + if (config.dataSource != null) { + // current version of config format + return config as PluginConfig; + } + // old version of config format + return { dataSource: PluginConfigDataSource.SELF, fieldCode: config.template || '' }; }; diff --git a/src/common/ui/KintonePluginInputText.tsx b/src/common/ui/KintonePluginInputText.tsx new file mode 100644 index 0000000..de06fd1 --- /dev/null +++ b/src/common/ui/KintonePluginInputText.tsx @@ -0,0 +1,46 @@ +import clsx from 'clsx'; +// biome-ignore lint/style/useImportType: React is required in scope for the old JSX transform. +import React from 'react'; + +export type KintonePluginInputTextProps = { + id?: string; + className?: string; + value: string; + size?: number; + maxLength?: number; + disabled?: boolean; + required?: boolean; + placeholder?: string; + onChange: (value: string) => void; +}; + +const KintonePluginInputText: React.FC = (props) => { + const { id, className, value, size, maxLength, disabled, required, placeholder, onChange } = props; + const handleOnChange: React.ChangeEventHandler = (e) => { + onChange(e.target.value); + }; + return ( +
+ +
+ ); +}; +export default KintonePluginInputText; diff --git a/src/config/ConfigApp.tsx b/src/config/ConfigApp.tsx index 018c982..facec64 100644 --- a/src/config/ConfigApp.tsx +++ b/src/config/ConfigApp.tsx @@ -1,19 +1,34 @@ +import { KintoneRestAPIClient } from '@kintone/rest-api-client'; +import moize from 'moize'; import React from 'react'; import { ErrorBoundary } from 'react-error-boundary'; +import invariant from 'tiny-invariant'; import ErrorFallback from '../common/ErrorFallback'; import Loading from '../common/Loading'; +import type { KintoneFormFieldProperties } from '../common/types'; import Settings from './Settings'; interface ConfigAppProps { pluginId: string; } +const cachedFormFieldsProperties = moize.promise(async (appId: number): Promise => { + const client = new KintoneRestAPIClient(); + const { properties } = await client.app.getFormFields({ app: appId, preview: true }); + return properties; +}); + const ConfigApp: React.FC = (props) => { const { pluginId } = props; + + const appId = kintone.app.getId(); + invariant(appId, 'The app ID is not available. Please ensure you are on a Kintone app page.'); + const propertiesPromise = cachedFormFieldsProperties(appId); + return ( }> - + ); diff --git a/src/config/Settings.module.css b/src/config/Settings.module.css index 7e86197..1a0a3ad 100644 --- a/src/config/Settings.module.css +++ b/src/config/Settings.module.css @@ -1,3 +1,10 @@ +.settings .app { + display: flex; + gap: 2em; + align-items: center; + margin-bottom: 1em; +} + .buttons > *:not(:last-child) { margin-right: 0.5em; } diff --git a/src/config/Settings.tsx b/src/config/Settings.tsx index 71fdab3..cbb523c 100644 --- a/src/config/Settings.tsx +++ b/src/config/Settings.tsx @@ -1,85 +1,176 @@ -import { KintoneRestAPIClient } from '@kintone/rest-api-client'; -import moize from 'moize'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import invariant from 'tiny-invariant'; -import { PluginConfigType, saveConfig } from '../common/config'; +import type { PluginConfig } from '../common/config'; +import { loadConfig, PluginConfigDataSource, saveConfig } from '../common/config'; import { naturalCompare } from '../common/stringUtils'; import type { KintoneFormFieldProperties } from '../common/types'; import KintonePluginAlert from '../common/ui/KintonePluginAlert'; import KintonePluginButton from '../common/ui/KintonePluginButton'; import KintonePluginDesc from '../common/ui/KintonePluginDesc'; +import KintonePluginInputText from '../common/ui/KintonePluginInputText'; import KintonePluginLabel from '../common/ui/KintonePluginLabel'; import KintonePluginRequire from '../common/ui/KintonePluginRequire'; import KintonePluginRow from '../common/ui/KintonePluginRow'; import KintonePluginSelect, { type KintonePluginSelectOptionData } from '../common/ui/KintonePluginSelect'; import KintonePluginTitle from '../common/ui/KintonePluginTitle'; -import styles from './Settings.module.css'; -const cachedFormFieldsProperties = moize.promise(async (appId: number): Promise => { - const client = new KintoneRestAPIClient(); - const { properties } = await client.app.getFormFields({ app: appId, preview: true }); - return properties; -}); +import styles from './Settings.module.css'; interface SettingsProps { pluginId: string; + appId: number; + propertiesPromise: Promise; } const Settings: React.FC = (props) => { - const { pluginId } = props; + const { pluginId, appId, propertiesPromise } = props; const { t } = useTranslation(); - const config = kintone.plugin.app.getConfig(pluginId); - const appId = kintone.app.getId(); - invariant(appId, 'The app ID is not available. Please ensure you are on a Kintone app page.'); - const properties = React.use(cachedFormFieldsProperties(appId)); + const config = loadConfig(pluginId); + const properties = React.use(propertiesPromise); + // data source options + const dataSourceOptions = [ + { value: PluginConfigDataSource.SELF, label: t('settings.datasource.labels.self') }, + { value: PluginConfigDataSource.APP, label: t('settings.datasource.labels.app') }, + ]; + const [dataSource, setDataSource] = React.useState< + (typeof PluginConfigDataSource)[keyof typeof PluginConfigDataSource] + >(config.dataSource); + + // states for SELF data source const fileFields = Object.values(properties) .filter((property) => property.type === 'FILE') .sort((a, b) => naturalCompare(`${a.label} (${a.code})`, `${b.label} (${b.code})`)); const options: KintonePluginSelectOptionData[] = [ - { value: '', label: t('settings.template.messages.select-an-attachment-fields'), disabled: true }, // Default option + { value: '', label: t('settings.self.messages.select-an-attachment-fields'), disabled: true }, // Default option ...fileFields.map((property) => ({ value: property.code, label: `${property.label} (${property.code})`, })), ]; + const [fieldCode, setFieldCode] = React.useState( + () => + options.find((option) => config.dataSource === PluginConfigDataSource.SELF && option.value === config.fieldCode) + ?.value ?? '', + ); - const [template, setTemplate] = React.useState( - () => options.find((option) => option.value === (config.template ?? ''))?.value ?? '', + // states for APP data source + const [otherAppId, setOtherAppId] = React.useState( + config.dataSource === PluginConfigDataSource.APP ? config.appId : '', + ); + const [otherRecordId, setOtherRecordId] = React.useState( + config.dataSource === PluginConfigDataSource.APP ? config.recordId : '', + ); + const [otherFieldCode, setOtherFieldCode] = React.useState( + config.dataSource === PluginConfigDataSource.APP ? config.fieldCode : '', ); const handleOnSubmit = (e: React.FormEvent) => { e.preventDefault(); - saveConfig({ type: PluginConfigType.SELF, template }, () => { + let configToSave: PluginConfig; + if (dataSource === PluginConfigDataSource.SELF) { + configToSave = { dataSource: PluginConfigDataSource.SELF, fieldCode }; + } else if (dataSource === PluginConfigDataSource.APP) { + configToSave = { + dataSource: PluginConfigDataSource.APP, + appId: otherAppId, + fieldCode: otherFieldCode, + recordId: otherRecordId, + }; + } else { + return; + } + saveConfig(configToSave, () => { alert(t('on-saved')); window.location.href = `../../flow?app=${appId}`; }); }; - const handleOnChangeTemplate = (value: string) => { - setTemplate(value); - }; - const handleOnClickCancel = () => { window.location.href = `../../${appId}/plugin/`; }; return ( -
+
{t('title')}
- {t('settings.template.title')} + {t('settings.datasource.title')} * - {t('settings.template.description')} - {fileFields.length === 0 ? ( - {t('settings.template.errors.no-attachment-field-found')} - ) : ( - - )} + {t('settings.datasource.description')} + { + const newDataSource = value as PluginConfigDataSource; + if (Object.values(PluginConfigDataSource).includes(newDataSource)) { + setDataSource(newDataSource); + } + }} + options={dataSourceOptions.map((opt) => ({ value: String(opt.value), label: opt.label }))} + /> + {dataSource === PluginConfigDataSource.SELF && ( + + + {t('settings.self.title')} + * + + {t('settings.self.description')} + {fileFields.length === 0 ? ( + {t('settings.self.errors.no-attachment-field-found')} + ) : ( + setFieldCode(value)} options={options} /> + )} + + )} + {dataSource === PluginConfigDataSource.APP && ( + + + {t('settings.app.title')} + * + + {t('settings.app.description')} +
+ + + +
+
+ )} {t('buttons.cancel')} @@ -89,7 +180,7 @@ const Settings: React.FC = (props) => {
-
+ ); }; export default Settings; diff --git a/src/config/locales/en.json b/src/config/locales/en.json index ce380c3..2da1da2 100644 --- a/src/config/locales/en.json +++ b/src/config/locales/en.json @@ -1,15 +1,32 @@ { "title": "Settings for the Kintone Word output plugin", "settings": { - "template": { + "datasource": { + "title": "Select data source for Word template file", + "description": "Please select the data source for the Word template file.", + "labels": { + "self": "Own record", + "app": "Record from another app" + } + }, + "self": { "title": "Template", - "description": "Select a attachment field that contains the Word template file.", + "description": "Select an attachment field that contains the Word template file.", "messages": { "select-an-attachment-fields": "(Select an attachment field)" }, "errors": { "no-attachment-field-found": "No attachment fields found in the app. Please add an attachment field to use this plugin." } + }, + "app": { + "title": "Attachment file info from another app", + "description": "Enter App ID, Record ID, and Attachment Field Code.", + "labels": { + "app-id": "App ID", + "record-id": "Record ID", + "field-code": "Attachment Field Code" + } } }, "buttons": { diff --git a/src/config/locales/ja.json b/src/config/locales/ja.json index 37d2667..f54887b 100644 --- a/src/config/locales/ja.json +++ b/src/config/locales/ja.json @@ -1,7 +1,15 @@ { "title": "Word出力プラグイン設定", "settings": { - "template": { + "datasource": { + "title": "WORD雛形ファイルのデータソース選択", + "description": "WORD雛形ファイルのデータソースを選択してください。", + "labels": { + "self": "自レコード", + "app": "他アプリのレコード" + } + }, + "self": { "title": "雛形ファイル", "description": "Word雛形ファイルを保存する添付ファイルフィールドを選択してください。", "messages": { @@ -10,6 +18,15 @@ "errors": { "no-attachment-field-found": "このアプリに添付ファイルフィールドが見つかりません。フォームに添付ファイルフィールドを追加してください。" } + }, + "app": { + "title": "他アプリの添付ファイル情報", + "description": "アプリID・レコード番号・添付ファイルフィールドコードを入力してください。", + "labels": { + "app-id": "アプリID", + "record-id": "レコード番号", + "field-code": "添付ファイルフィールドコード" + } } }, "buttons": { diff --git a/src/desktop/DesktopApp.tsx b/src/desktop/DesktopApp.tsx index 5a15824..4db4714 100644 --- a/src/desktop/DesktopApp.tsx +++ b/src/desktop/DesktopApp.tsx @@ -1,7 +1,12 @@ +import { KintoneRestAPIClient } from '@kintone/rest-api-client'; +import moize from 'moize'; import React from 'react'; import { ErrorBoundary } from 'react-error-boundary'; +import { loadConfig, type PluginConfig } from '../common/config'; import ErrorFallback from '../common/ErrorFallback'; import Loading from '../common/Loading'; +import type { KintoneFormFieldProperties } from '../common/types'; +import { checkConfig } from './checkConfig'; import MenuPanel from './MenuPanel'; interface DesktopAppProps { @@ -9,12 +14,34 @@ interface DesktopAppProps { event: kintone.events.AppRecordDetailShowEvent | kintone.events.MobileAppRecordDetailShowEvent; } +const cachedFormFieldsProperties = moize.promise(async (appId: number): Promise => { + const client = new KintoneRestAPIClient(); + const { properties } = await client.app.getFormFields({ app: appId, preview: false }); + return properties; +}); + +const cachedConfigCheck = moize.promise( + async (config: PluginConfig, record: kintone.types.SavedFields): Promise => { + return await checkConfig(config, record); + }, +); + const DesktopApp: React.FC = (props) => { const { pluginId, event } = props; + + const config = loadConfig(pluginId); + const propertiesPromise = cachedFormFieldsProperties(event.appId); + const fileKeyPromise = cachedConfigCheck(config, event.record); + return ( }> - + ); diff --git a/src/desktop/MenuPanel.tsx b/src/desktop/MenuPanel.tsx index a1487f0..fb88a16 100644 --- a/src/desktop/MenuPanel.tsx +++ b/src/desktop/MenuPanel.tsx @@ -1,58 +1,30 @@ import { KintoneRestAPIClient } from '@kintone/rest-api-client'; import saveAs from 'file-saver'; -import moize from 'moize'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { loadConfig, type PluginConfig, PluginConfigType } from '../common/config'; import type { KintoneFormFieldProperties } from '../common/types'; import KintonePluginButton from '../common/ui/KintonePluginButton'; -import { checkConfig } from './checkConfig'; import generateWordFileData from './generateWordFileData'; export interface MenuPanelProps { pluginId: string; event: kintone.events.AppRecordDetailShowEvent | kintone.events.MobileAppRecordDetailShowEvent; + propertiesPromise: Promise; + fileKeyPromise: Promise; } -const cachedFormFieldsProperties = moize.promise(async (appId: number): Promise => { - const client = new KintoneRestAPIClient(); - const { properties } = await client.app.getFormFields({ app: appId, preview: false }); - return properties; -}); - -const cachedConfigCheck = moize.promise( - async (config: PluginConfig, record: kintone.types.SavedFields): Promise => { - return checkConfig(config, record); - }, -); - -const downloadFile = async (config: PluginConfig, template: string): Promise => { - if (config.type === PluginConfigType.SELF || config.type === PluginConfigType.APP) { - const client = new KintoneRestAPIClient(); - const content = await client.file.downloadFile({ fileKey: template }); - return content; - } else { - // PluginConfigType.URL - const [body, status, _headers] = await kintone.proxy(template, 'GET', {}, {}); - if (status !== 200) { - throw new Error(`Failed to download the file from URL: ${template}`); - } - return new Uint16Array(Array.from(String(body), (c) => c.charCodeAt(0))).buffer; - } -}; - const MenuPanel: React.FC = (props) => { - const { pluginId, event } = props; + const { event, propertiesPromise, fileKeyPromise } = props; const { t } = useTranslation(); - const appId = event.appId; const language = kintone.getLoginUser().language; - const config = loadConfig(pluginId); - const template = React.use(cachedConfigCheck(config, event.record)); - const properties = React.use(cachedFormFieldsProperties(appId)); + const properties = React.use(propertiesPromise); + const fileKey = React.use(fileKeyPromise); const handleOnClickOutputButton = (e: React.MouseEvent) => { e.preventDefault(); - downloadFile(config, template) + const client = new KintoneRestAPIClient(); + client.file + .downloadFile({ fileKey }) .then((content) => { const out = generateWordFileData(content, properties, event.record, language); saveAs(out, 'output.docx'); diff --git a/src/desktop/checkConfig.ts b/src/desktop/checkConfig.ts index 1462132..3e06c44 100644 --- a/src/desktop/checkConfig.ts +++ b/src/desktop/checkConfig.ts @@ -3,18 +3,17 @@ import i18n from 'i18next'; import { type PluginConfig, type PluginConfigApp, + PluginConfigDataSource, type PluginConfigSelf, - PluginConfigType, - type PluginConfigUrl, } from '../common/config'; import { DOCX_CONTENT_TYPE } from '../common/constants'; const checkConfigSelf = (config: PluginConfigSelf, record: kintone.types.SavedFields): string => { - const { template } = config; - if (template === '') { + const { fieldCode } = config; + if (fieldCode === '') { throw new Error(i18n.t('errors.template-field-is-required')); } - const field = record[template]; + const field = record[fieldCode]; if (!field) { throw new Error(i18n.t('errors.template-field-is-not-available')); } @@ -35,21 +34,21 @@ const checkConfigSelf = (config: PluginConfigSelf, record: kintone.types.SavedFi }; const checkConfigApp = async (config: PluginConfigApp): Promise => { - const { appId, template, recordId } = config; + const { appId, fieldCode, recordId } = config; if (appId === '') { throw new Error(i18n.t('errors.app-id-is-required')); } if (recordId === '') { throw new Error(i18n.t('errors.record-id-is-required')); } - if (template === '') { + if (fieldCode === '') { throw new Error(i18n.t('errors.template-field-is-required')); } const client = new KintoneRestAPIClient(); const { record } = await client.record.getRecord({ app: appId, id: recordId }).catch((_error) => { throw new Error(i18n.t('errors.record-is-not-available')); }); - const field = record[template]; + const field = record[fieldCode]; if (!field) { throw new Error(i18n.t('errors.template-field-is-not-available')); } @@ -69,24 +68,11 @@ const checkConfigApp = async (config: PluginConfigApp): Promise => { return files[0].fileKey; }; -const checkConfigUrl = (config: PluginConfigUrl): string => { - const { template } = config; - if (template === '') { - throw new Error(i18n.t('errors.template-field-is-required')); - } - if (!template.startsWith('http://') && !template.startsWith('https://')) { - throw new Error(i18n.t('errors.template-field-must-be-a-valid-url')); - } - return template; -}; - export const checkConfig = async (config: PluginConfig, record: kintone.types.SavedFields): Promise => { - if (config.type === PluginConfigType.SELF) { + if (config.dataSource === PluginConfigDataSource.SELF) { return checkConfigSelf(config, record); - } else if (config.type === PluginConfigType.APP) { + } else if (config.dataSource === PluginConfigDataSource.APP) { return checkConfigApp(config); - } else if (config.type === PluginConfigType.URL) { - return checkConfigUrl(config); } throw new Error(i18n.t('errors.unexpected-error-occurred')); }; diff --git a/src/desktop/generateWordFileData.ts b/src/desktop/generateWordFileData.ts index 6b70c0b..948fb9f 100644 --- a/src/desktop/generateWordFileData.ts +++ b/src/desktop/generateWordFileData.ts @@ -179,10 +179,41 @@ const generateWordFileData = ( language: string, ) => { const zip = new PizZip(content); + const parser = expressionParser.configure({ + filters: { + toUpperCase: (str: string): string => str.toUpperCase(), + toLowerCase: (str: string): string => str.toLowerCase(), + regexMatch: (str: string, regex: string): boolean | string[] => { + const pattern = new RegExp(regex); + return pattern.test(str); + }, + regexFilter: (arr: string[], regex: string): string[] => { + const pattern = new RegExp(regex); + if (Array.isArray(arr)) { + return arr.filter((item) => pattern.test(item)); + } + return []; + }, + includes: (arr: string[] | string, str: string): boolean => { + if (Array.isArray(arr)) { + return arr.includes(str); + } else { + return arr === str; + } + }, + indexOf: (arr: string[] | string, str: string): number => { + if (Array.isArray(arr)) { + return arr.indexOf(str); + } else { + return arr === str ? 0 : -1; + } + }, + }, + }); const doc = new Docxtemplater(zip, { paragraphLoop: true, linebreaks: true, - parser: expressionParser, + parser: parser, nullGetter: () => '', }); doc.render(record2templateData(properties, record, language)); diff --git a/src/desktop/locales/en.json b/src/desktop/locales/en.json index 8803749..d1a80e6 100644 --- a/src/desktop/locales/en.json +++ b/src/desktop/locales/en.json @@ -10,7 +10,6 @@ "app-id-is-required": "The app ID for the template file is required. Please check the plugin settings.", "record-id-is-required": "The record ID for the template file is required. Please check the plugin settings.", "record-is-not-available": "The record is not available. Please check the plugin settings.", - "template-field-must-be-a-valid-url": "The template field must be a valid URL. Please check the plugin settings.", "unexpected-error-occurred": "An unexpected error occurred." }, "buttons": { diff --git a/src/desktop/locales/ja.json b/src/desktop/locales/ja.json index d942bdc..39356fe 100644 --- a/src/desktop/locales/ja.json +++ b/src/desktop/locales/ja.json @@ -10,7 +10,6 @@ "app-id-is-required": "雛形ファイルを格納するアプリIDが設定されていません。プラグインの設定を見直してください。", "record-id-is-required": "雛形ファイルを格納するアプリのレコードIDが設定されていません。プラグインの設定を見直してください。", "record-is-not-available": "雛形ファイルを格納されたアプリのレコードが取得できません。プラグインの設定を見直してください。", - "template-field-must-be-a-valid-url": "テンプレートフィールドは有効なURLである必要があります。プラグインの設定を見直してください。", "unexpected-error-occurred": "予期しないエラーが発生しました。" }, "buttons": {