refactor: enhance configuration handling and error checking for template fields in the Word output plugin
This commit is contained in:
@@ -5,6 +5,7 @@
|
|||||||
"words": [
|
"words": [
|
||||||
"blankspace",
|
"blankspace",
|
||||||
"docxtemplater",
|
"docxtemplater",
|
||||||
|
"errback",
|
||||||
"Heiti",
|
"Heiti",
|
||||||
"Kaku",
|
"Kaku",
|
||||||
"kintone",
|
"kintone",
|
||||||
|
@@ -8,11 +8,6 @@ interface Props {
|
|||||||
|
|
||||||
const ErrorFallback: React.FC<Props> = (props) => {
|
const ErrorFallback: React.FC<Props> = (props) => {
|
||||||
const { error } = props;
|
const { error } = props;
|
||||||
return (
|
return <KintonePluginAlert>{error.message}</KintonePluginAlert>;
|
||||||
<KintonePluginAlert>
|
|
||||||
<p>Something went wrong:</p>
|
|
||||||
<pre>{error.message}</pre>
|
|
||||||
</KintonePluginAlert>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
export default ErrorFallback;
|
export default ErrorFallback;
|
||||||
|
35
src/common/config.ts
Normal file
35
src/common/config.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
export const PluginConfigType = {
|
||||||
|
SELF: 0,
|
||||||
|
APP: 1,
|
||||||
|
URL: 2,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export interface PluginConfigSelf {
|
||||||
|
type: typeof PluginConfigType.SELF;
|
||||||
|
template: string;
|
||||||
|
}
|
||||||
|
export interface PluginConfigApp {
|
||||||
|
type: typeof PluginConfigType.APP;
|
||||||
|
appId: string;
|
||||||
|
template: string;
|
||||||
|
recordId: string;
|
||||||
|
}
|
||||||
|
export interface PluginConfigUrl {
|
||||||
|
type: typeof PluginConfigType.URL;
|
||||||
|
template: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PluginConfig = PluginConfigSelf | PluginConfigApp | PluginConfigUrl;
|
||||||
|
|
||||||
|
const DEFAULT_CONFIG: PluginConfig = {
|
||||||
|
type: PluginConfigType.SELF,
|
||||||
|
template: '',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const saveConfig = (config: PluginConfig, callback?: () => void): void => {
|
||||||
|
kintone.plugin.app.setConfig(config, callback);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loadConfig = (pluginId: string): PluginConfig => {
|
||||||
|
return { ...DEFAULT_CONFIG, ...(kintone.plugin.app.getConfig(pluginId) as PluginConfig) };
|
||||||
|
};
|
@@ -3,6 +3,7 @@ import moize from 'moize';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import invariant from 'tiny-invariant';
|
import invariant from 'tiny-invariant';
|
||||||
|
import { PluginConfigType, saveConfig } from '../common/config';
|
||||||
import { naturalCompare } from '../common/stringUtils';
|
import { naturalCompare } from '../common/stringUtils';
|
||||||
import type { KintoneFormFieldProperties } from '../common/types';
|
import type { KintoneFormFieldProperties } from '../common/types';
|
||||||
import KintonePluginAlert from '../common/ui/KintonePluginAlert';
|
import KintonePluginAlert from '../common/ui/KintonePluginAlert';
|
||||||
@@ -13,7 +14,6 @@ import KintonePluginRequire from '../common/ui/KintonePluginRequire';
|
|||||||
import KintonePluginRow from '../common/ui/KintonePluginRow';
|
import KintonePluginRow from '../common/ui/KintonePluginRow';
|
||||||
import KintonePluginSelect, { type KintonePluginSelectOptionData } from '../common/ui/KintonePluginSelect';
|
import KintonePluginSelect, { type KintonePluginSelectOptionData } from '../common/ui/KintonePluginSelect';
|
||||||
import KintonePluginTitle from '../common/ui/KintonePluginTitle';
|
import KintonePluginTitle from '../common/ui/KintonePluginTitle';
|
||||||
|
|
||||||
import styles from './Settings.module.css';
|
import styles from './Settings.module.css';
|
||||||
|
|
||||||
const cachedFormFieldsProperties = moize.promise(async (appId: number): Promise<KintoneFormFieldProperties> => {
|
const cachedFormFieldsProperties = moize.promise(async (appId: number): Promise<KintoneFormFieldProperties> => {
|
||||||
@@ -50,7 +50,7 @@ const Settings: React.FC<SettingsProps> = (props) => {
|
|||||||
|
|
||||||
const handleOnSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
const handleOnSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
kintone.plugin.app.setConfig({ template }, () => {
|
saveConfig({ type: PluginConfigType.SELF, template }, () => {
|
||||||
alert(t('on-saved'));
|
alert(t('on-saved'));
|
||||||
window.location.href = `../../flow?app=${appId}`;
|
window.location.href = `../../flow?app=${appId}`;
|
||||||
});
|
});
|
||||||
|
@@ -3,10 +3,10 @@ import saveAs from 'file-saver';
|
|||||||
import moize from 'moize';
|
import moize from 'moize';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { DOCX_CONTENT_TYPE } from '../common/constants';
|
import { loadConfig, type PluginConfig, PluginConfigType } from '../common/config';
|
||||||
import type { KintoneFormFieldProperties } from '../common/types';
|
import type { KintoneFormFieldProperties } from '../common/types';
|
||||||
import KintonePluginAlert from '../common/ui/KintonePluginAlert';
|
|
||||||
import KintonePluginButton from '../common/ui/KintonePluginButton';
|
import KintonePluginButton from '../common/ui/KintonePluginButton';
|
||||||
|
import { checkConfig } from './checkConfig';
|
||||||
import generateWordFileData from './generateWordFileData';
|
import generateWordFileData from './generateWordFileData';
|
||||||
|
|
||||||
export interface MenuPanelProps {
|
export interface MenuPanelProps {
|
||||||
@@ -20,65 +20,39 @@ const cachedFormFieldsProperties = moize.promise(async (appId: number): Promise<
|
|||||||
return properties;
|
return properties;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const cachedConfigCheck = moize.promise(
|
||||||
|
async (config: PluginConfig, record: kintone.types.SavedFields): Promise<string> => {
|
||||||
|
return checkConfig(config, record);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const downloadFile = async (config: PluginConfig, template: string): Promise<ArrayBuffer> => {
|
||||||
|
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<MenuPanelProps> = (props) => {
|
const MenuPanel: React.FC<MenuPanelProps> = (props) => {
|
||||||
const { pluginId, event } = props;
|
const { pluginId, event } = props;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const appId = event.appId;
|
const appId = event.appId;
|
||||||
const properties = React.use(cachedFormFieldsProperties(appId));
|
|
||||||
const language = kintone.getLoginUser().language;
|
const language = kintone.getLoginUser().language;
|
||||||
const config = kintone.plugin.app.getConfig(pluginId);
|
const config = loadConfig(pluginId);
|
||||||
const template: string = config.template ?? '';
|
const template = React.use(cachedConfigCheck(config, event.record));
|
||||||
|
const properties = React.use(cachedFormFieldsProperties(appId));
|
||||||
if (template === '') {
|
|
||||||
return (
|
|
||||||
<KintonePluginAlert>
|
|
||||||
{t('name')}: {t('errors.template-field-is-not-set')}
|
|
||||||
</KintonePluginAlert>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const record = event.record[template];
|
|
||||||
if (record == null) {
|
|
||||||
return (
|
|
||||||
<KintonePluginAlert>
|
|
||||||
{t('name')}: {t('errors.template-field-is-not-available')}
|
|
||||||
</KintonePluginAlert>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (record.type !== 'FILE') {
|
|
||||||
return (
|
|
||||||
<KintonePluginAlert>
|
|
||||||
{t('name')}: {t('errors.template-field-must-be-an-attachment-field')}
|
|
||||||
</KintonePluginAlert>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (record.value.length === 0) {
|
|
||||||
return (
|
|
||||||
<KintonePluginAlert>
|
|
||||||
{t('name')}: {t('errors.template-field-does-not-contain-any-files')}
|
|
||||||
</KintonePluginAlert>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (record.value.length > 1) {
|
|
||||||
return (
|
|
||||||
<KintonePluginAlert>
|
|
||||||
{t('name')}: {t('errors.template-field-contains-multiple-files')}
|
|
||||||
</KintonePluginAlert>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const { fileKey, contentType } = record.value[0];
|
|
||||||
if (contentType !== DOCX_CONTENT_TYPE) {
|
|
||||||
return (
|
|
||||||
<KintonePluginAlert>
|
|
||||||
{t('name')}: {t('errors.template-file-must-be-a-docx', { contentType })}
|
|
||||||
</KintonePluginAlert>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleOnClickOutputButton = (e: React.MouseEvent<HTMLButtonElement>) => {
|
const handleOnClickOutputButton = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const client = new KintoneRestAPIClient();
|
downloadFile(config, template)
|
||||||
client.file
|
|
||||||
.downloadFile({ fileKey })
|
|
||||||
.then((content) => {
|
.then((content) => {
|
||||||
const out = generateWordFileData(content, properties, event.record, language);
|
const out = generateWordFileData(content, properties, event.record, language);
|
||||||
saveAs(out, 'output.docx');
|
saveAs(out, 'output.docx');
|
||||||
|
92
src/desktop/checkConfig.ts
Normal file
92
src/desktop/checkConfig.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import { KintoneRestAPIClient } from '@kintone/rest-api-client';
|
||||||
|
import i18n from 'i18next';
|
||||||
|
import {
|
||||||
|
type PluginConfig,
|
||||||
|
type PluginConfigApp,
|
||||||
|
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 === '') {
|
||||||
|
throw new Error(i18n.t('errors.template-field-is-required'));
|
||||||
|
}
|
||||||
|
const field = record[template];
|
||||||
|
if (!field) {
|
||||||
|
throw new Error(i18n.t('errors.template-field-is-not-available'));
|
||||||
|
}
|
||||||
|
if (field.type !== 'FILE') {
|
||||||
|
throw new Error(i18n.t('errors.template-field-must-be-an-attachment-field'));
|
||||||
|
}
|
||||||
|
const files = Array.isArray(field.value) ? (field.value as kintone.fieldTypes.File['value']) : [];
|
||||||
|
if (files.length === 0) {
|
||||||
|
throw new Error(i18n.t('errors.template-field-does-not-contain-any-files'));
|
||||||
|
}
|
||||||
|
if (files.length > 1) {
|
||||||
|
throw new Error(i18n.t('errors.template-field-contains-multiple-files'));
|
||||||
|
}
|
||||||
|
if (files[0].contentType !== DOCX_CONTENT_TYPE) {
|
||||||
|
throw new Error(i18n.t('errors.template-file-must-be-a-docx', { contentType: files[0].contentType }));
|
||||||
|
}
|
||||||
|
return files[0].fileKey;
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkConfigApp = async (config: PluginConfigApp): Promise<string> => {
|
||||||
|
const { appId, template, 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 === '') {
|
||||||
|
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];
|
||||||
|
if (!field) {
|
||||||
|
throw new Error(i18n.t('errors.template-field-is-not-available'));
|
||||||
|
}
|
||||||
|
if (field.type !== 'FILE') {
|
||||||
|
throw new Error(i18n.t('errors.template-field-must-be-an-attachment-field'));
|
||||||
|
}
|
||||||
|
const files = Array.isArray(field.value) ? (field.value as kintone.fieldTypes.File['value']) : [];
|
||||||
|
if (files.length === 0) {
|
||||||
|
throw new Error(i18n.t('errors.template-field-does-not-contain-any-files'));
|
||||||
|
}
|
||||||
|
if (files.length > 1) {
|
||||||
|
throw new Error(i18n.t('errors.template-field-contains-multiple-files'));
|
||||||
|
}
|
||||||
|
if (files[0].contentType !== DOCX_CONTENT_TYPE) {
|
||||||
|
throw new Error(i18n.t('errors.template-file-must-be-a-docx', { contentType: files[0].contentType }));
|
||||||
|
}
|
||||||
|
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<string> => {
|
||||||
|
if (config.type === PluginConfigType.SELF) {
|
||||||
|
return checkConfigSelf(config, record);
|
||||||
|
} else if (config.type === PluginConfigType.APP) {
|
||||||
|
return checkConfigApp(config);
|
||||||
|
} else if (config.type === PluginConfigType.URL) {
|
||||||
|
return checkConfigUrl(config);
|
||||||
|
}
|
||||||
|
throw new Error(i18n.t('errors.unexpected-error-occurred'));
|
||||||
|
};
|
@@ -1,12 +1,17 @@
|
|||||||
{
|
{
|
||||||
"name": "Word output plugin",
|
"name": "Word output plugin",
|
||||||
"errors": {
|
"errors": {
|
||||||
"template-field-is-not-set": "Template field is not set. Please configure the plugin.",
|
"template-field-is-required": "The template field is not configured. Please check the plugin settings.",
|
||||||
"template-field-is-not-available": "Template field is not available in this app.",
|
"template-field-is-not-available": "The template field is not available. Please check the plugin settings.",
|
||||||
"template-field-must-be-an-attachment-field": "Template field must be an attachment field.",
|
"template-field-must-be-an-attachment-field": "The template field must be an attachment field. Please check the plugin settings.",
|
||||||
"template-field-does-not-contain-any-files": "Template field does not contain any files.",
|
"template-field-does-not-contain-any-files": "The template field does not contain any files.",
|
||||||
"template-field-contains-multiple-files": "Template field contains multiple files. Please ensure it contains only one file.",
|
"template-field-contains-multiple-files": "The template field contains multiple files. Please attach only one file.",
|
||||||
"template-file-must-be-a-docx": "The template file must be a Word (.docx) file. 'Mime-Type: {{contentType}}'."
|
"template-file-must-be-a-docx": "The template file must be a Word (.docx) file. 'Mime-Type: {{contentType}}'.",
|
||||||
|
"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": {
|
"buttons": {
|
||||||
"output": "Output Word"
|
"output": "Output Word"
|
||||||
|
@@ -1,12 +1,17 @@
|
|||||||
{
|
{
|
||||||
"name": "Word出力プラグイン",
|
"name": "Word出力プラグイン",
|
||||||
"errors": {
|
"errors": {
|
||||||
"template-field-is-not-set": "雛形ファイルが設定されていません。プラグインの設定を見直してください。",
|
"template-field-is-required": "雛形ファイルが設定されていません。プラグインの設定を見直してください。",
|
||||||
"template-field-is-not-available": "雛形ファイルに設定された添付ファイルフィールドが見つかりませんでした。プラグインの設定を見直してください。",
|
"template-field-is-not-available": "雛形ファイルに設定された添付ファイルフィールドが見つかりませんでした。プラグインの設定を見直してください。",
|
||||||
"template-field-must-be-an-attachment-field": "雛形ファイルに設定されたフィールド型が添付ファイルフィールドではありません。プラグインの設定を見直してください。",
|
"template-field-must-be-an-attachment-field": "雛形ファイルに設定されたフィールド型が添付ファイルフィールドではありません。プラグインの設定を見直してください。",
|
||||||
"template-field-does-not-contain-any-files": "雛形ファイルが添付されていません。",
|
"template-field-does-not-contain-any-files": "雛形ファイルが添付されていません。",
|
||||||
"template-field-contains-multiple-files": "雛形ファイルとして複数登録されています。1つのみを指定してください。",
|
"template-field-contains-multiple-files": "雛形ファイルとして複数のファイルが添付されています。1つだけ添付してください。",
|
||||||
"template-file-must-be-a-docx": "雛形ファイルがWord形式(.docx)ではありません。'Mime-Type: {{contentType}}'"
|
"template-file-must-be-a-docx": "雛形ファイルがWord形式(.docx)ではありません。'Mime-Type: {{contentType}}'",
|
||||||
|
"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": {
|
"buttons": {
|
||||||
"output": "Word出力"
|
"output": "Word出力"
|
||||||
|
16
src/types/kintone.d.ts
vendored
16
src/types/kintone.d.ts
vendored
@@ -98,4 +98,20 @@ declare namespace kintone {
|
|||||||
function getFormFields(): Promise<unknown>;
|
function getFormFields(): Promise<unknown>;
|
||||||
function getFormLayout(): Promise<unknown>;
|
function getFormLayout(): Promise<unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function proxy(
|
||||||
|
url: string,
|
||||||
|
method: string,
|
||||||
|
headers: Record<string, string>,
|
||||||
|
data: Record<string, unknown> | string,
|
||||||
|
): Promise<[string, number, Record<string, string>]>;
|
||||||
|
|
||||||
|
function proxy(
|
||||||
|
url: string,
|
||||||
|
method: string,
|
||||||
|
headers: Record<string, string>,
|
||||||
|
data: Record<string, unknown> | string,
|
||||||
|
callback: (resp: [string, number, Record<string, string>]) => void,
|
||||||
|
errback: (err: Error) => void,
|
||||||
|
): void;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user