first commit
This commit is contained in:
24
src/desktop/DesktopApp.tsx
Normal file
24
src/desktop/DesktopApp.tsx
Normal 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
128
src/desktop/MenuPanel.tsx
Normal 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
34
src/desktop/index.tsx
Normal 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;
|
||||
},
|
||||
);
|
Reference in New Issue
Block a user