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,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;
},
);