first commit

This commit is contained in:
2025-05-27 17:02:32 +09:00
commit 191fe24ed2
36 changed files with 13550 additions and 0 deletions

View File

@ -0,0 +1,18 @@
import React from 'react';
import KintonePluginAlert from './ui/KintonePluginAlert';
interface Props {
error: Error;
}
const ErrorFallback: React.FC<Props> = (props) => {
const { error } = props;
return (
<KintonePluginAlert>
<p>Something went wrong:</p>
<pre>{error.message}</pre>
</KintonePluginAlert>
);
};
export default ErrorFallback;

196
src/common/Loading.css Normal file
View File

@ -0,0 +1,196 @@
.loading {
margin: 100px auto;
font-size: 25px;
width: 1em;
height: 1em;
border-radius: 50%;
position: relative;
text-indent: -9999em;
-webkit-animation: loadingKeyframes 1.1s infinite ease;
animation: loadingKeyframes 1.1s infinite ease;
-webkit-transform: translateZ(0);
-ms-transform: translateZ(0);
transform: translateZ(0);
}
@-webkit-keyframes loadingKeyframes {
0%,
100% {
box-shadow:
0em -2.6em 0em 0em #000000,
1.8em -1.8em 0 0em rgba(0, 0, 0, 0.2),
2.5em 0em 0 0em rgba(0, 0, 0, 0.2),
1.75em 1.75em 0 0em rgba(0, 0, 0, 0.2),
0em 2.5em 0 0em rgba(0, 0, 0, 0.2),
-1.8em 1.8em 0 0em rgba(0, 0, 0, 0.2),
-2.6em 0em 0 0em rgba(0, 0, 0, 0.5),
-1.8em -1.8em 0 0em rgba(0, 0, 0, 0.7);
}
12.5% {
box-shadow:
0em -2.6em 0em 0em rgba(0, 0, 0, 0.7),
1.8em -1.8em 0 0em #000000,
2.5em 0em 0 0em rgba(0, 0, 0, 0.2),
1.75em 1.75em 0 0em rgba(0, 0, 0, 0.2),
0em 2.5em 0 0em rgba(0, 0, 0, 0.2),
-1.8em 1.8em 0 0em rgba(0, 0, 0, 0.2),
-2.6em 0em 0 0em rgba(0, 0, 0, 0.2),
-1.8em -1.8em 0 0em rgba(0, 0, 0, 0.5);
}
25% {
box-shadow:
0em -2.6em 0em 0em rgba(0, 0, 0, 0.5),
1.8em -1.8em 0 0em rgba(0, 0, 0, 0.7),
2.5em 0em 0 0em #000000,
1.75em 1.75em 0 0em rgba(0, 0, 0, 0.2),
0em 2.5em 0 0em rgba(0, 0, 0, 0.2),
-1.8em 1.8em 0 0em rgba(0, 0, 0, 0.2),
-2.6em 0em 0 0em rgba(0, 0, 0, 0.2),
-1.8em -1.8em 0 0em rgba(0, 0, 0, 0.2);
}
37.5% {
box-shadow:
0em -2.6em 0em 0em rgba(0, 0, 0, 0.2),
1.8em -1.8em 0 0em rgba(0, 0, 0, 0.5),
2.5em 0em 0 0em rgba(0, 0, 0, 0.7),
1.75em 1.75em 0 0em #000000,
0em 2.5em 0 0em rgba(0, 0, 0, 0.2),
-1.8em 1.8em 0 0em rgba(0, 0, 0, 0.2),
-2.6em 0em 0 0em rgba(0, 0, 0, 0.2),
-1.8em -1.8em 0 0em rgba(0, 0, 0, 0.2);
}
50% {
box-shadow:
0em -2.6em 0em 0em rgba(0, 0, 0, 0.2),
1.8em -1.8em 0 0em rgba(0, 0, 0, 0.2),
2.5em 0em 0 0em rgba(0, 0, 0, 0.5),
1.75em 1.75em 0 0em rgba(0, 0, 0, 0.7),
0em 2.5em 0 0em #000000,
-1.8em 1.8em 0 0em rgba(0, 0, 0, 0.2),
-2.6em 0em 0 0em rgba(0, 0, 0, 0.2),
-1.8em -1.8em 0 0em rgba(0, 0, 0, 0.2);
}
62.5% {
box-shadow:
0em -2.6em 0em 0em rgba(0, 0, 0, 0.2),
1.8em -1.8em 0 0em rgba(0, 0, 0, 0.2),
2.5em 0em 0 0em rgba(0, 0, 0, 0.2),
1.75em 1.75em 0 0em rgba(0, 0, 0, 0.5),
0em 2.5em 0 0em rgba(0, 0, 0, 0.7),
-1.8em 1.8em 0 0em #000000,
-2.6em 0em 0 0em rgba(0, 0, 0, 0.2),
-1.8em -1.8em 0 0em rgba(0, 0, 0, 0.2);
}
75% {
box-shadow:
0em -2.6em 0em 0em rgba(0, 0, 0, 0.2),
1.8em -1.8em 0 0em rgba(0, 0, 0, 0.2),
2.5em 0em 0 0em rgba(0, 0, 0, 0.2),
1.75em 1.75em 0 0em rgba(0, 0, 0, 0.2),
0em 2.5em 0 0em rgba(0, 0, 0, 0.5),
-1.8em 1.8em 0 0em rgba(0, 0, 0, 0.7),
-2.6em 0em 0 0em #000000,
-1.8em -1.8em 0 0em rgba(0, 0, 0, 0.2);
}
87.5% {
box-shadow:
0em -2.6em 0em 0em rgba(0, 0, 0, 0.2),
1.8em -1.8em 0 0em rgba(0, 0, 0, 0.2),
2.5em 0em 0 0em rgba(0, 0, 0, 0.2),
1.75em 1.75em 0 0em rgba(0, 0, 0, 0.2),
0em 2.5em 0 0em rgba(0, 0, 0, 0.2),
-1.8em 1.8em 0 0em rgba(0, 0, 0, 0.5),
-2.6em 0em 0 0em rgba(0, 0, 0, 0.7),
-1.8em -1.8em 0 0em #000000;
}
}
@keyframes loadingKeyframes {
0%,
100% {
box-shadow:
0em -2.6em 0em 0em #000000,
1.8em -1.8em 0 0em rgba(0, 0, 0, 0.2),
2.5em 0em 0 0em rgba(0, 0, 0, 0.2),
1.75em 1.75em 0 0em rgba(0, 0, 0, 0.2),
0em 2.5em 0 0em rgba(0, 0, 0, 0.2),
-1.8em 1.8em 0 0em rgba(0, 0, 0, 0.2),
-2.6em 0em 0 0em rgba(0, 0, 0, 0.5),
-1.8em -1.8em 0 0em rgba(0, 0, 0, 0.7);
}
12.5% {
box-shadow:
0em -2.6em 0em 0em rgba(0, 0, 0, 0.7),
1.8em -1.8em 0 0em #000000,
2.5em 0em 0 0em rgba(0, 0, 0, 0.2),
1.75em 1.75em 0 0em rgba(0, 0, 0, 0.2),
0em 2.5em 0 0em rgba(0, 0, 0, 0.2),
-1.8em 1.8em 0 0em rgba(0, 0, 0, 0.2),
-2.6em 0em 0 0em rgba(0, 0, 0, 0.2),
-1.8em -1.8em 0 0em rgba(0, 0, 0, 0.5);
}
25% {
box-shadow:
0em -2.6em 0em 0em rgba(0, 0, 0, 0.5),
1.8em -1.8em 0 0em rgba(0, 0, 0, 0.7),
2.5em 0em 0 0em #000000,
1.75em 1.75em 0 0em rgba(0, 0, 0, 0.2),
0em 2.5em 0 0em rgba(0, 0, 0, 0.2),
-1.8em 1.8em 0 0em rgba(0, 0, 0, 0.2),
-2.6em 0em 0 0em rgba(0, 0, 0, 0.2),
-1.8em -1.8em 0 0em rgba(0, 0, 0, 0.2);
}
37.5% {
box-shadow:
0em -2.6em 0em 0em rgba(0, 0, 0, 0.2),
1.8em -1.8em 0 0em rgba(0, 0, 0, 0.5),
2.5em 0em 0 0em rgba(0, 0, 0, 0.7),
1.75em 1.75em 0 0em #000000,
0em 2.5em 0 0em rgba(0, 0, 0, 0.2),
-1.8em 1.8em 0 0em rgba(0, 0, 0, 0.2),
-2.6em 0em 0 0em rgba(0, 0, 0, 0.2),
-1.8em -1.8em 0 0em rgba(0, 0, 0, 0.2);
}
50% {
box-shadow:
0em -2.6em 0em 0em rgba(0, 0, 0, 0.2),
1.8em -1.8em 0 0em rgba(0, 0, 0, 0.2),
2.5em 0em 0 0em rgba(0, 0, 0, 0.5),
1.75em 1.75em 0 0em rgba(0, 0, 0, 0.7),
0em 2.5em 0 0em #000000,
-1.8em 1.8em 0 0em rgba(0, 0, 0, 0.2),
-2.6em 0em 0 0em rgba(0, 0, 0, 0.2),
-1.8em -1.8em 0 0em rgba(0, 0, 0, 0.2);
}
62.5% {
box-shadow:
0em -2.6em 0em 0em rgba(0, 0, 0, 0.2),
1.8em -1.8em 0 0em rgba(0, 0, 0, 0.2),
2.5em 0em 0 0em rgba(0, 0, 0, 0.2),
1.75em 1.75em 0 0em rgba(0, 0, 0, 0.5),
0em 2.5em 0 0em rgba(0, 0, 0, 0.7),
-1.8em 1.8em 0 0em #000000,
-2.6em 0em 0 0em rgba(0, 0, 0, 0.2),
-1.8em -1.8em 0 0em rgba(0, 0, 0, 0.2);
}
75% {
box-shadow:
0em -2.6em 0em 0em rgba(0, 0, 0, 0.2),
1.8em -1.8em 0 0em rgba(0, 0, 0, 0.2),
2.5em 0em 0 0em rgba(0, 0, 0, 0.2),
1.75em 1.75em 0 0em rgba(0, 0, 0, 0.2),
0em 2.5em 0 0em rgba(0, 0, 0, 0.5),
-1.8em 1.8em 0 0em rgba(0, 0, 0, 0.7),
-2.6em 0em 0 0em #000000,
-1.8em -1.8em 0 0em rgba(0, 0, 0, 0.2);
}
87.5% {
box-shadow:
0em -2.6em 0em 0em rgba(0, 0, 0, 0.2),
1.8em -1.8em 0 0em rgba(0, 0, 0, 0.2),
2.5em 0em 0 0em rgba(0, 0, 0, 0.2),
1.75em 1.75em 0 0em rgba(0, 0, 0, 0.2),
0em 2.5em 0 0em rgba(0, 0, 0, 0.2),
-1.8em 1.8em 0 0em rgba(0, 0, 0, 0.5),
-2.6em 0em 0 0em rgba(0, 0, 0, 0.7),
-1.8em -1.8em 0 0em #000000;
}
}

8
src/common/Loading.tsx Normal file
View File

@ -0,0 +1,8 @@
import React from 'react';
import './Loading.css';
const Loading: React.FC = () => {
return <div className="loading">Loading...</div>;
};
export default Loading;

4
src/common/global.ts Normal file
View File

@ -0,0 +1,4 @@
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.');

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,18 @@
import React from 'react';
import clsx from 'clsx';
interface KintonePluginAlertProps {
className?: string;
children: React.ReactNode;
}
const KintonePluginAlert: React.FC<KintonePluginAlertProps> = (props) => {
const { className, children } = props;
return (
<div className={clsx('kintoneplugin-alert', className)} role="alert">
{children}
</div>
);
};
export default KintonePluginAlert;

View File

@ -0,0 +1,26 @@
import React from 'react';
import clsx from 'clsx';
interface KintonePluginButtonProps {
className?: string;
variant: 'normal' | 'disabled' | 'dialog-ok' | 'dialog-cancel';
type?: 'button' | 'submit' | 'reset';
onClick?: React.MouseEventHandler<HTMLButtonElement>;
children: React.ReactNode;
}
const KintonePluginButton: React.FC<KintonePluginButtonProps> = (props) => {
const { className, variant, type, onClick, children } = props;
return (
<button
className={clsx(`kintoneplugin-button-${variant}`, className)}
type={type}
onClick={onClick}
disabled={variant === 'disabled'}
>
{children}
</button>
);
};
export default KintonePluginButton;

View File

@ -0,0 +1,14 @@
import React from 'react';
import clsx from 'clsx';
interface KintonePluginDescProps {
className?: string;
children: React.ReactNode;
}
const KintonePluginDesc: React.FC<KintonePluginDescProps> = (props) => {
const { className, children } = props;
return <div className={clsx('kintoneplugin-desc', className)}>{children}</div>;
};
export default KintonePluginDesc;

View File

@ -0,0 +1,14 @@
import React from 'react';
import clsx from 'clsx';
interface KintonePluginLabelProps {
className?: string;
children: React.ReactNode;
}
const KintonePluginLabel: React.FC<KintonePluginLabelProps> = (props) => {
const { className, children } = props;
return <div className={clsx('kintoneplugin-label', className)}>{children}</div>;
};
export default KintonePluginLabel;

View File

@ -0,0 +1,14 @@
import React from 'react';
import clsx from 'clsx';
interface KintonePluginRequire {
className?: string;
children: React.ReactNode;
}
const KintonePluginRequire: React.FC<KintonePluginRequire> = (props) => {
const { className, children } = props;
return <span className={clsx('kintoneplugin-require', className)}>{children}</span>;
};
export default KintonePluginRequire;

View File

@ -0,0 +1,14 @@
import React from 'react';
import clsx from 'clsx';
interface KintonePluginRowProps {
className?: string;
children: React.ReactNode;
}
const KintonePluginRow: React.FC<KintonePluginRowProps> = (props) => {
const { className, children } = props;
return <div className={clsx('kintoneplugin-row', className)}>{children}</div>;
};
export default KintonePluginRow;

View File

@ -0,0 +1,38 @@
import React from 'react';
import clsx from 'clsx';
export interface KintonePluginSelectOptionData {
key: string;
value: string;
label: string;
disabled?: boolean;
}
interface KintonePluginSelectProps {
className?: string;
defaultValue?: string;
onChange?: (e: React.ChangeEvent<HTMLSelectElement>) => void;
options: KintonePluginSelectOptionData[];
}
const KintonePluginSelect: React.FC<KintonePluginSelectProps> = (props) => {
const { className, defaultValue, onChange, options } = props;
if (!options || options.length === 0) {
return null; // Return null if no options are provided
}
return (
<div className={clsx('kintoneplugin-select-outer', className)}>
<div className="kintoneplugin-select">
<select defaultValue={defaultValue} onChange={onChange}>
{options.map((option) => (
<option key={option.key} value={option.value} disabled={option.disabled}>
{option.label}
</option>
))}
</select>
</div>
</div>
);
};
export default KintonePluginSelect;

View File

@ -0,0 +1,14 @@
import React from 'react';
import clsx from 'clsx';
interface KintonePluginTitleProps {
className?: string;
children: React.ReactNode;
}
const KintonePluginTitle: React.FC<KintonePluginTitleProps> = (props) => {
const { className, children } = props;
return <div className={clsx('kintoneplugin-title', className)}>{children}</div>;
};
export default KintonePluginTitle;

17
src/config/ConfigApp.tsx Normal file
View File

@ -0,0 +1,17 @@
import React from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import ErrorFallback from '../common/ErrorFallback';
import Loading from '../common/Loading';
import Settings from './Settings';
const ConfigApp: React.FC = () => {
return (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<React.Suspense fallback={<Loading />}>
<Settings />
</React.Suspense>
</ErrorBoundary>
);
};
export default ConfigApp;

View File

@ -0,0 +1,3 @@
.buttons > *:not(:last-child) {
margin-right: 0.5em;
}

87
src/config/Settings.tsx Normal file
View File

@ -0,0 +1,87 @@
import React from 'react';
import { KintoneRestAPIClient } from '@kintone/rest-api-client';
import { kintonePrettyFields } from 'kintone-pretty-fields';
import moize from 'moize';
import invariant from 'tiny-invariant';
import { PLUGIN_ID } from '../common/global';
import KintonePluginAlert from '../common/ui/KintonePluginAlert';
import KintonePluginButton from '../common/ui/KintonePluginButton';
import KintonePluginDesc from '../common/ui/KintonePluginDesc';
import KintonePluginLabel from '../common/ui/KintonePluginLabel';
import KintonePluginRequire from '../common/ui/KintonePluginRequire';
import KintonePluginRow from '../common/ui/KintonePluginRow';
import KintonePluginSelect, { KintonePluginSelectOptionData } from '../common/ui/KintonePluginSelect';
import KintonePluginTitle from '../common/ui/KintonePluginTitle';
import styles from './Settings.module.css';
const cachedFields = moize.promise(async (appId: number) => {
const client = new KintoneRestAPIClient();
const fields = await kintonePrettyFields.getFields({ client, app: appId, lang: 'en', preview: false });
return fields;
});
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 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,
})),
];
const [template, setTemplate] = React.useState<string>(config.template ?? '');
const handleOnSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
kintone.plugin.app.setConfig({ template }, () => {
alert('The plug-in settings have been saved. Please update the app!');
window.location.href = `../../flow?app=${appId}`;
});
};
const handleOnChangeTemplate = (e: React.ChangeEvent<HTMLSelectElement>) => {
setTemplate(e.target.value);
};
const handleOnClickCancel = () => {
window.location.href = `../../${appId}/plugin/`;
};
return (
<section className="settings">
<KintonePluginLabel>Settings for the Kintone Word output plugin</KintonePluginLabel>
<form onSubmit={handleOnSubmit}>
<KintonePluginRow>
<KintonePluginTitle>
Template<KintonePluginRequire>*</KintonePluginRequire>
</KintonePluginTitle>
<KintonePluginDesc>Select a file field that contains the WORD template file.</KintonePluginDesc>
{fileFields.length === 0 ? (
<KintonePluginAlert>
No file fields found in the app. Please add a file field to use this plugin.
</KintonePluginAlert>
) : (
<KintonePluginSelect defaultValue={template} onChange={handleOnChangeTemplate} options={options} />
)}
</KintonePluginRow>
<KintonePluginRow className={styles.buttons}>
<KintonePluginButton variant="dialog-cancel" type="button" onClick={handleOnClickCancel}>
Cancel
</KintonePluginButton>
<KintonePluginButton variant="dialog-ok" type="submit">
Save
</KintonePluginButton>
</KintonePluginRow>
</form>
</section>
);
};
export default Settings;

16
src/config/index.tsx Normal file
View File

@ -0,0 +1,16 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import invariant from 'tiny-invariant';
import ConfigApp from './ConfigApp';
import '../common/ui/51-modern-default.css';
const root = document.getElementById('plugin-config-root');
invariant(root, 'The plugin configuration root element "plugin-config-root" is not found.');
ReactDOM.createRoot(root).render(
<React.StrictMode>
<ConfigApp />
</React.StrictMode>,
);

View File

@ -0,0 +1,24 @@
import React from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import ErrorFallback from '../common/ErrorFallback';
import Loading from '../common/Loading';
import MenuPanel from './MenuPanel';
import '@shin-chan/kypes';
interface DesktopAppProps {
event: kintone.events.AppRecordDetailShowEvent | kintone.events.MobileAppRecordDetailShowEvent;
}
const DesktopApp: React.FC<DesktopAppProps> = (props) => {
const { event } = props;
return (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<React.Suspense fallback={<Loading />}>
<MenuPanel event={event} />
</React.Suspense>
</ErrorBoundary>
);
};
export default DesktopApp;

128
src/desktop/MenuPanel.tsx Normal file
View File

@ -0,0 +1,128 @@
import React from 'react';
import { KintoneRestAPIClient } from '@kintone/rest-api-client';
import { KintoneRecord } from '@shin-chan/kypes/types/page';
import Docxtemplater from 'docxtemplater';
import expressionParser from 'docxtemplater/expressions';
import { saveAs } from 'file-saver';
import PizZip from 'pizzip';
import { PLUGIN_ID } from '../common/global';
import KintonePluginAlert from '../common/ui/KintonePluginAlert';
import KintonePluginButton from '../common/ui/KintonePluginButton';
import '@shin-chan/kypes';
interface TemplateData {
[key: string]: TemplateData | TemplateData[] | string | string[];
}
const DOCX_CONTENTTYPE = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
interface MenuPanelProps {
event: kintone.events.AppRecordDetailShowEvent | kintone.events.MobileAppRecordDetailShowEvent;
}
const record2data = (record: Partial<KintoneRecord>): 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') {
data[key] = {
name: value.name,
code: value.code,
};
} else if (type === 'CATEGORY' || type === 'CHECK_BOX' || type === 'MULTI_SELECT') {
data[key] = value.map((v) => v);
} else if (
type === 'STATUS_ASSIGNEE' ||
type === 'USER_SELECT' ||
type === 'GROUP_SELECT' ||
type === 'ORGANIZATION_SELECT'
) {
data[key] = value.map((v) => {
return { name: v.name, code: v.code };
});
} else if (type === 'SUBTABLE') {
data[key] = value.map((subRecord) => record2data(subRecord.value));
} else {
data[key] = value;
}
}
}
return data;
};
const MenuPanel: React.FC<MenuPanelProps> = (props) => {
const { event } = props;
const config = kintone.plugin.app.getConfig(PLUGIN_ID);
const template: string = config.template ?? '';
if (template === '') {
return (
<KintonePluginAlert>
WORD output plugin: Template field is not set. Please configure the plugin.
</KintonePluginAlert>
);
}
const record = event.record[template];
if (record == null) {
return <KintonePluginAlert>WORD output plugin: Template field is not available in this app.</KintonePluginAlert>;
}
if (record.type !== 'FILE') {
return <KintonePluginAlert>WORD output plugin: The template field must be a file field.</KintonePluginAlert>;
}
if (record.value.length === 0) {
return <KintonePluginAlert>WORD output plugin: The template field does not contain any files.</KintonePluginAlert>;
}
if (record.value.length > 1) {
return (
<KintonePluginAlert>
WORD output plugin: The template field contains multiple files. Only the first file will be used.
</KintonePluginAlert>
);
}
const { fileKey, contentType } = record.value[0];
if (contentType !== DOCX_CONTENTTYPE) {
return (
<KintonePluginAlert>
WORD output plugin: The template file must be a DOCX file. The current file type is {contentType}.
</KintonePluginAlert>
);
}
const handleOnClickOutputButton = (e: React.MouseEvent<HTMLButtonElement>) => {
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 })
.then((content) => {
const zip = new PizZip(content);
const doc = new Docxtemplater(zip, {
paragraphLoop: true,
linebreaks: true,
parser: expressionParser,
});
doc.render(record2data(event.record));
const out = doc.getZip().generate({ type: 'blob', mimeType: DOCX_CONTENTTYPE });
saveAs(out, 'output.docx');
})
.catch((error) => {
console.error('Error downloading file:', error);
alert('Failed to download the WORD template file.');
});
};
return (
<KintonePluginButton variant="normal" onClick={handleOnClickOutputButton}>
WORD出力
</KintonePluginButton>
);
};
export default MenuPanel;

34
src/desktop/index.tsx Normal file
View File

@ -0,0 +1,34 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import invariant from 'tiny-invariant';
import DesktopApp from './DesktopApp';
import '@shin-chan/kypes';
import '../common/ui/51-modern-default.css';
kintone.events.on(
['app.record.detail.show', 'mobile.app.record.detail.show'],
async (event: kintone.events.AppRecordDetailShowEvent | kintone.events.MobileAppRecordDetailShowEvent) => {
const spaceElement =
event.type === 'app.record.detail.show'
? kintone.app.record.getHeaderMenuSpaceElement()
: kintone.mobile.app.getHeaderSpaceElement();
invariant(
spaceElement,
'The header menu space element is not available. Please ensure you are on a Kintone app record detail page.',
);
const root = document.createElement('div');
spaceElement.appendChild(root);
ReactDOM.createRoot(root).render(
<React.StrictMode>
<DesktopApp event={event} />
</React.StrictMode>,
);
return event;
},
);