Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
2ff6cc474a
|
|||
|
b7d4cd40f6
|
|||
|
5f18c0b791
|
|||
|
94b7277b3a
|
|||
|
99251b4748
|
|||
|
b1540e8374
|
|||
|
a396952331
|
|||
|
43a55bbb2b
|
|||
|
db4e21607e
|
|||
|
c8a2c8087a
|
|||
|
dd1fd35238
|
|||
|
be0daf2097
|
|||
|
83f29b8e63
|
|||
|
5926c08da5
|
|||
|
e37ac9aa4a
|
15
.cspell.json
15
.cspell.json
@@ -5,6 +5,7 @@
|
|||||||
"words": [
|
"words": [
|
||||||
"blankspace",
|
"blankspace",
|
||||||
"docxtemplater",
|
"docxtemplater",
|
||||||
|
"errback",
|
||||||
"Heiti",
|
"Heiti",
|
||||||
"Kaku",
|
"Kaku",
|
||||||
"kintone",
|
"kintone",
|
||||||
@@ -13,11 +14,21 @@
|
|||||||
"Ligh",
|
"Ligh",
|
||||||
"moize",
|
"moize",
|
||||||
"officedocument",
|
"officedocument",
|
||||||
|
"OKUMURA",
|
||||||
"openxmlformats",
|
"openxmlformats",
|
||||||
|
"pcss",
|
||||||
"pizzip",
|
"pizzip",
|
||||||
"rspack",
|
"rspack",
|
||||||
|
"styl",
|
||||||
"SUBTABLE",
|
"SUBTABLE",
|
||||||
"wordprocessingml"
|
"wordprocessingml",
|
||||||
|
"Yoshihiro"
|
||||||
],
|
],
|
||||||
"ignorePaths": [".env"]
|
"ignorePaths": [
|
||||||
|
".env",
|
||||||
|
"dist",
|
||||||
|
"node_modules",
|
||||||
|
"scripts",
|
||||||
|
"plugin"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://json.schemastore.org/prettierrc",
|
|
||||||
"semi": true,
|
|
||||||
"tabWidth": 2,
|
|
||||||
"singleQuote": true,
|
|
||||||
"printWidth": 120,
|
|
||||||
"trailingComma": "all"
|
|
||||||
}
|
|
||||||
3
.vscode/extensions.json
vendored
3
.vscode/extensions.json
vendored
@@ -1,8 +1,7 @@
|
|||||||
{
|
{
|
||||||
"recommendations": [
|
"recommendations": [
|
||||||
"esbenp.prettier-vscode",
|
"biomejs.biome",
|
||||||
"streetsidesoftware.code-spell-checker",
|
"streetsidesoftware.code-spell-checker",
|
||||||
"dbaeumer.vscode-eslint",
|
|
||||||
"jawandarajbir.react-vscode-extension-pack"
|
"jawandarajbir.react-vscode-extension-pack"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
39
.vscode/settings.json
vendored
39
.vscode/settings.json
vendored
@@ -1,39 +1,40 @@
|
|||||||
{
|
{
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.biome": "explicit",
|
||||||
|
"quickfix.biome": "explicit",
|
||||||
"source.organizeImports": "explicit"
|
"source.organizeImports": "explicit"
|
||||||
},
|
},
|
||||||
|
"[css]": {
|
||||||
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
|
},
|
||||||
"[javascript]": {
|
"[javascript]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
},
|
},
|
||||||
"[javascriptreact]": {
|
"[javascriptreact]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
},
|
|
||||||
"[typescript]": {
|
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
||||||
},
|
|
||||||
"[typescriptreact]": {
|
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
||||||
},
|
|
||||||
"[html]": {
|
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
||||||
},
|
|
||||||
"[css]": {
|
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
||||||
},
|
},
|
||||||
"[json]": {
|
"[json]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
},
|
},
|
||||||
"[jsonc]": {
|
"[jsonc]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
},
|
},
|
||||||
|
"[typescript]": {
|
||||||
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
|
},
|
||||||
|
"[typescriptreact]": {
|
||||||
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
|
},
|
||||||
|
// Extensions - Biome
|
||||||
|
// - see: biome.json
|
||||||
// Exteions - cSpell
|
// Exteions - cSpell
|
||||||
// - see: .cspell.json
|
// - see: .cspell.json
|
||||||
|
// Extensions - ESLint
|
||||||
|
"eslint.enable": false,
|
||||||
// Extensions - HTML
|
// Extensions - HTML
|
||||||
"html.format.wrapLineLength": 0,
|
"html.format.wrapLineLength": 0,
|
||||||
// Extentions - Prettier
|
|
||||||
// - see: .prettierrc.json
|
|
||||||
// Extentions - Typescript
|
// Extentions - Typescript
|
||||||
"javascript.updateImportsOnFileMove.enabled": "always",
|
"javascript.updateImportsOnFileMove.enabled": "always",
|
||||||
"typescript.updateImportsOnFileMove.enabled": "always"
|
"typescript.updateImportsOnFileMove.enabled": "always",
|
||||||
}
|
}
|
||||||
|
|||||||
197
AGENTS.md
Normal file
197
AGENTS.md
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
# AGENTS.md - Development Guidelines for Kintone Word Output Plugin
|
||||||
|
|
||||||
|
This document provides guidelines and useful information for AI agents and developers working on the Kintone Word Output Plugin project. It includes setup instructions, coding standards, and best practices to ensure efficient and consistent development.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
- **Name**: Kintone Word Output Plugin
|
||||||
|
- **Purpose**: A Kintone plugin that formats and downloads data using Word (.docx) templates.
|
||||||
|
- **Tech Stack**:
|
||||||
|
- Frontend: React (TypeScript)
|
||||||
|
- Build Tool: rspack
|
||||||
|
- Linting/Formatting: Biome
|
||||||
|
- Template Engine: docxtemplater with angular-expressions
|
||||||
|
- Kintone Integration: @kintone/rest-api-client
|
||||||
|
- **Languages**: TypeScript, JavaScript
|
||||||
|
- **Package Manager**: npm
|
||||||
|
|
||||||
|
## Development Environment Setup
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- Node.js 18+
|
||||||
|
- npm (comes with Node.js)
|
||||||
|
|
||||||
|
### Initial Setup
|
||||||
|
1. Clone the repository
|
||||||
|
2. Run `npm install` to install dependencies
|
||||||
|
3. Run `npm run gen:key` to generate a private key for development
|
||||||
|
4. Run `npm start` to start the development server
|
||||||
|
|
||||||
|
### Useful Scripts
|
||||||
|
- `npm run build`: Development build
|
||||||
|
- `npm run build:prod`: Production build
|
||||||
|
- `npm run develop`: Watch mode build
|
||||||
|
- `npm run lint`: Run Biome linter
|
||||||
|
- `npm run format`: Format code with Biome
|
||||||
|
- `npm run cspell`: Check spelling
|
||||||
|
- `npm run upload`: Upload plugin to Kintone (requires env setup)
|
||||||
|
|
||||||
|
## Coding Standards
|
||||||
|
|
||||||
|
### TypeScript
|
||||||
|
- Use strict TypeScript settings (see tsconfig.json)
|
||||||
|
- Prefer interfaces over types for object definitions
|
||||||
|
- Use proper typing for Kintone API responses
|
||||||
|
- Avoid `any` type; use specific types or `unknown`
|
||||||
|
|
||||||
|
### React Components
|
||||||
|
- Use functional components with hooks
|
||||||
|
- Follow React best practices for state management
|
||||||
|
- Use TypeScript for prop types
|
||||||
|
- Implement error boundaries where appropriate
|
||||||
|
|
||||||
|
### File Structure
|
||||||
|
- `src/common/`: Shared utilities and components
|
||||||
|
- `src/config/`: Configuration UI components
|
||||||
|
- `src/desktop/`: Desktop app components and logic
|
||||||
|
- `src/types/`: TypeScript type definitions
|
||||||
|
- `plugin/`: Kintone plugin manifest and assets
|
||||||
|
- `scripts/`: Build and utility scripts
|
||||||
|
|
||||||
|
### Naming Conventions
|
||||||
|
- Use camelCase for variables and functions
|
||||||
|
- Use PascalCase for components and classes
|
||||||
|
- Use kebab-case for file names
|
||||||
|
- Prefix custom hooks with `use`
|
||||||
|
|
||||||
|
## Code Quality Tools
|
||||||
|
|
||||||
|
### Biome
|
||||||
|
- Used for linting and formatting
|
||||||
|
- Configuration in `biome.json`
|
||||||
|
- Run `npm run lint` to check code
|
||||||
|
- Run `npm run format` to auto-fix issues
|
||||||
|
|
||||||
|
### TypeScript Compiler
|
||||||
|
- Strict mode enabled
|
||||||
|
- Check for errors with `npm run build`
|
||||||
|
|
||||||
|
### Spell Checker
|
||||||
|
- Uses cspell for spell checking
|
||||||
|
- Configuration in cspell config files
|
||||||
|
- Run `npm run cspell` to check
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Current Testing Setup
|
||||||
|
- No automated tests are currently implemented
|
||||||
|
- Manual testing required for:
|
||||||
|
- Plugin installation in Kintone
|
||||||
|
- Template rendering with various data types
|
||||||
|
- Error handling scenarios
|
||||||
|
|
||||||
|
### Recommended Testing Approach
|
||||||
|
- Add unit tests for utility functions (e.g., data formatting)
|
||||||
|
- Add integration tests for template generation
|
||||||
|
- Test with different Kintone field types
|
||||||
|
- Test error scenarios (invalid templates, missing data)
|
||||||
|
|
||||||
|
## Build and Deployment
|
||||||
|
|
||||||
|
### Build Process
|
||||||
|
1. Pre-build: Generate keys and validate
|
||||||
|
2. Build with rspack (dev or prod mode)
|
||||||
|
3. Output: `dist/plugin.zip` for Kintone upload
|
||||||
|
|
||||||
|
### Deployment
|
||||||
|
- Use `npm run upload` for development
|
||||||
|
- Manual upload via Kintone Plugin Uploader for production
|
||||||
|
- Requires proper environment variables for authentication
|
||||||
|
|
||||||
|
## Kintone-Specific Considerations
|
||||||
|
|
||||||
|
### Plugin Architecture
|
||||||
|
- Separate config and desktop JS files
|
||||||
|
- Config handles settings UI
|
||||||
|
- Desktop handles record view functionality
|
||||||
|
- Uses Kintone's plugin API
|
||||||
|
|
||||||
|
### Data Handling
|
||||||
|
- Fields are mapped from Kintone records to template data
|
||||||
|
- Supports all Kintone field types
|
||||||
|
- Custom formatting for numbers, dates, calculations
|
||||||
|
- Handles subtable data as arrays
|
||||||
|
|
||||||
|
### Template Engine
|
||||||
|
- Uses docxtemplater for .docx processing
|
||||||
|
- Custom filters available (see generateWordFileData.ts)
|
||||||
|
- Supports loops, conditionals, and expressions
|
||||||
|
|
||||||
|
## Common Development Tasks
|
||||||
|
|
||||||
|
### Adding a New Filter
|
||||||
|
1. Add filter function in `generateWordFileData.ts`
|
||||||
|
2. Update filters object in expressionParser.configure()
|
||||||
|
3. Document in README.md
|
||||||
|
4. Test with sample templates
|
||||||
|
|
||||||
|
### Adding a New Field Type Support
|
||||||
|
1. Update `record2templateData` function
|
||||||
|
2. Handle formatting in appropriate helper functions
|
||||||
|
3. Add type checks and error handling
|
||||||
|
4. Test with sample data
|
||||||
|
|
||||||
|
### Updating UI Components
|
||||||
|
1. Use existing UI components from `src/common/ui/`
|
||||||
|
2. Follow i18n pattern for text
|
||||||
|
3. Test in both config and desktop contexts
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
- Build fails: Check Node.js version, run `npm install`
|
||||||
|
- Plugin not loading: Verify manifest.json, check browser console
|
||||||
|
- Template errors: Validate .docx file, check field codes
|
||||||
|
- Upload fails: Check environment variables, Kintone permissions
|
||||||
|
|
||||||
|
### Debug Tips
|
||||||
|
- Use browser dev tools for client-side debugging
|
||||||
|
- Check Kintone app logs for server-side issues
|
||||||
|
- Validate templates with docxtemplater documentation
|
||||||
|
- Test with minimal data sets first
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
### Pull Request Process
|
||||||
|
1. Create a feature branch
|
||||||
|
2. Make changes following coding standards
|
||||||
|
3. Run linting and build checks
|
||||||
|
4. Test manually in Kintone environment
|
||||||
|
5. Submit PR with clear description
|
||||||
|
|
||||||
|
### Code Review Checklist
|
||||||
|
- [ ] Code follows TypeScript and React best practices
|
||||||
|
- [ ] No linting errors
|
||||||
|
- [ ] Proper error handling
|
||||||
|
- [ ] Tested with various data scenarios
|
||||||
|
- [ ] Documentation updated if needed
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- [Kintone Developer Documentation](https://developer.kintone.io/)
|
||||||
|
- [docxtemplater Documentation](https://docxtemplater.com/)
|
||||||
|
- [Biome Documentation](https://biomejs.dev/)
|
||||||
|
- [React Documentation](https://react.dev/)
|
||||||
|
|
||||||
|
## AI Agent Guidelines
|
||||||
|
|
||||||
|
When working on this project:
|
||||||
|
- Always check existing code patterns before implementing new features
|
||||||
|
- Use the provided UI components and utilities
|
||||||
|
- Follow the established file structure
|
||||||
|
- Test changes in a Kintone environment
|
||||||
|
- Update documentation for any user-facing changes
|
||||||
|
- Prefer TypeScript over JavaScript for new code
|
||||||
|
- Use Biome for code quality checks
|
||||||
|
- Always use English for comments embedded in the code
|
||||||
|
- Respond to chat messages in the language used in the input
|
||||||
106
README.md
Normal file
106
README.md
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
# Kintone Word Output Plugin
|
||||||
|
|
||||||
|
A Kintone plugin that allows users to format and download data using Word template files (.docx). This plugin integrates with Kintone's record data and uses the docxtemplater library to populate templates with dynamic content.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Template-Based Output**: Use pre-designed Word (.docx) templates to format your Kintone data.
|
||||||
|
- **Data Source Flexibility**: Choose data from the current record or from another Kintone app.
|
||||||
|
- **Dynamic Content Population**: Leverage angular-expressions for complex data manipulation within templates.
|
||||||
|
- **Multi-Language Support**: Includes English and Japanese localization.
|
||||||
|
- **Error Handling**: Comprehensive error messages for configuration and runtime issues.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Download the plugin package from the [releases page](https://github.com/your-repo/kintone-plugin-docx/releases).
|
||||||
|
2. Upload the plugin to your Kintone environment via the Kintone Plugin Uploader.
|
||||||
|
3. Enable the plugin in your desired Kintone app.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
1. In the app settings, navigate to the plugin configuration.
|
||||||
|
2. Select the data source:
|
||||||
|
- **Own Record**: Use an attachment field in the current app that contains the Word template.
|
||||||
|
- **Record from Another App**: Specify the App ID, Record ID, and Attachment Field Code for the template.
|
||||||
|
3. Save the settings.
|
||||||
|
|
||||||
|
### Outputting Word Files
|
||||||
|
|
||||||
|
1. Open a record in the Kintone app where the plugin is enabled.
|
||||||
|
2. Click the "Output Word" button in the record view.
|
||||||
|
3. The plugin will generate and download a Word file based on the configured template and record data.
|
||||||
|
|
||||||
|
## Template Requirements
|
||||||
|
|
||||||
|
- Templates must be in .docx format.
|
||||||
|
- Use placeholders like `{{fieldCode}}` in the template to insert data.
|
||||||
|
- Supports angular-expressions for advanced formatting (e.g., `{{fieldCode | uppercase}}`).
|
||||||
|
- **Custom Filters**: The plugin provides the following custom filters for use in templates:
|
||||||
|
- `toUpperCase`: Converts a string to uppercase (e.g., `{{fieldCode | toUpperCase}}`).
|
||||||
|
- `toLowerCase`: Converts a string to lowercase (e.g., `{{fieldCode | toLowerCase}}`).
|
||||||
|
- `regexMatch`: Checks if a string matches a regex pattern (e.g., `{{fieldCode | regexMatch:'pattern'}}`).
|
||||||
|
- `regexFilter`: Filters an array of strings based on a regex pattern (e.g., `{{arrayField | regexFilter:'pattern'}}`).
|
||||||
|
- `includes`: Checks if an array or string includes a substring (e.g., `{{arrayField | includes:'value'}}`).
|
||||||
|
- `indexOf`: Returns the index of a substring in an array or string (e.g., `{{arrayField | indexOf:'value'}}`).
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Node.js (version 18 or higher)
|
||||||
|
- npm or yarn
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
1. Clone the repository:
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/your-repo/kintone-plugin-docx.git
|
||||||
|
cd kintone-plugin-docx
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Install dependencies:
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Generate private key for development:
|
||||||
|
```bash
|
||||||
|
npm run gen:key
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Start development server:
|
||||||
|
```bash
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
- Development build: `npm run build`
|
||||||
|
- Production build: `npm run build:prod`
|
||||||
|
|
||||||
|
### Upload to Kintone
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run upload
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- `@kintone/rest-api-client`: For interacting with Kintone REST API.
|
||||||
|
- `docxtemplater`: For processing Word templates.
|
||||||
|
- `angular-expressions`: For template expressions.
|
||||||
|
- `react`: For the plugin's UI components.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License. See the [LICENSE.txt](LICENSE.txt) file for details.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Contributions are welcome! Please open an issue or submit a pull request.
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For support, please check the [Kintone Developer Network](https://developer.kintone.io/) or open an issue in this repository.
|
||||||
44
biome.json
Normal file
44
biome.json
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://biomejs.dev/schemas/2.2.6/schema.json",
|
||||||
|
"vcs": {
|
||||||
|
"enabled": true,
|
||||||
|
"clientKind": "git",
|
||||||
|
"useIgnoreFile": true
|
||||||
|
},
|
||||||
|
"files": {
|
||||||
|
"ignoreUnknown": false,
|
||||||
|
"includes": ["src/**", "scripts/**"]
|
||||||
|
},
|
||||||
|
"formatter": {
|
||||||
|
"enabled": true,
|
||||||
|
"formatWithErrors": false,
|
||||||
|
"indentStyle": "space",
|
||||||
|
"lineEnding": "lf",
|
||||||
|
"indentWidth": 2,
|
||||||
|
"lineWidth": 120,
|
||||||
|
"attributePosition": "auto"
|
||||||
|
},
|
||||||
|
"linter": {
|
||||||
|
"enabled": true,
|
||||||
|
"domains": {
|
||||||
|
"react": "recommended"
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"recommended": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"javascript": {
|
||||||
|
"jsxRuntime": "reactClassic",
|
||||||
|
"formatter": {
|
||||||
|
"quoteStyle": "single"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"assist": {
|
||||||
|
"enabled": true,
|
||||||
|
"actions": {
|
||||||
|
"source": {
|
||||||
|
"organizeImports": "on"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import presetsPrettier from '@cybozu/eslint-config/flat/presets/react-typescript-prettier.js';
|
|
||||||
import globals from 'globals';
|
|
||||||
|
|
||||||
/** @type {import("eslint").Linter.Config[]} */
|
|
||||||
export default [
|
|
||||||
...presetsPrettier,
|
|
||||||
{
|
|
||||||
languageOptions: {
|
|
||||||
globals: {
|
|
||||||
...globals.node,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
'spaced-comment': ['error', 'always', { markers: ['/'] }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
4787
package-lock.json
generated
4787
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
65
package.json
65
package.json
@@ -1,51 +1,54 @@
|
|||||||
{
|
{
|
||||||
"name": "kintone-plugin-docx",
|
"name": "kintone-plugin-docx",
|
||||||
"version": "1.0.0",
|
"version": "2.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepare": "node scripts/prepare-private-key.js",
|
"gen": "run-s gen:*",
|
||||||
|
"gen:key": "node scripts/generate-private-key.js",
|
||||||
"start": "node scripts/npm-start.js",
|
"start": "node scripts/npm-start.js",
|
||||||
"develop": "npm run build -- --watch",
|
"develop": "npm run build -- --watch",
|
||||||
"build": "npm run prepare && cross-env NODE_ENV=development rspack build",
|
"prebuild": "npm run gen",
|
||||||
"build:prod": "npm run prepare && cross-env NODE_ENV=production rspack build",
|
"prebuild:prod": "npm run gen",
|
||||||
"dts-gen": "kintone-dts-gen",
|
"build": "cross-env NODE_ENV=development rspack build",
|
||||||
"lint": "eslint src --ext .js,.jsx,.ts,.tsx",
|
"build:prod": "cross-env NODE_ENV=production rspack build",
|
||||||
|
"cspell": "cspell .",
|
||||||
|
"lint": "biome lint .",
|
||||||
|
"format": "biome check --write .",
|
||||||
"upload": "env-cmd --silent kintone-plugin-uploader dist/plugin.zip --watch --waiting-dialog-ms 3000"
|
"upload": "env-cmd --silent kintone-plugin-uploader dist/plugin.zip --watch --waiting-dialog-ms 3000"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kintone/rest-api-client": "^5.7.4",
|
"@kintone/rest-api-client": "^6.0.0",
|
||||||
"angular-expressions": "^1.4.3",
|
"angular-expressions": "^1.5.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.18",
|
||||||
"docxtemplater": "^3.63.2",
|
"docxtemplater": "^3.66.7",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
|
"i18next": "^25.6.0",
|
||||||
"moize": "^6.1.6",
|
"moize": "^6.1.6",
|
||||||
"pizzip": "^3.2.0",
|
"pizzip": "^3.2.0",
|
||||||
"react": "^19.1.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-error-boundary": "^6.0.0",
|
"react-error-boundary": "^6.0.0",
|
||||||
|
"react-i18next": "^16.0.1",
|
||||||
"tiny-invariant": "^1.3.3"
|
"tiny-invariant": "^1.3.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cybozu/eslint-config": "^24.0.0",
|
"@biomejs/biome": "^2.2.6",
|
||||||
"@kintone/dts-gen": "^8.1.2",
|
"@kintone/dts-gen": "^9.0.0",
|
||||||
"@kintone/plugin-uploader": "^9.1.5",
|
"@kintone/plugin-uploader": "^10.0.0",
|
||||||
"@kintone/webpack-plugin-kintone-plugin": "^8.0.11",
|
"@kintone/webpack-plugin-kintone-plugin": "^9.0.0",
|
||||||
"@rspack/cli": "^1.3.13",
|
"@rspack/cli": "^1.5.8",
|
||||||
"@rspack/core": "^1.3.13",
|
"@rspack/core": "^1.5.8",
|
||||||
"@shin-chan/kypes": "^0.0.7",
|
|
||||||
"@swc/helpers": "^0.5.17",
|
|
||||||
"@types/file-saver": "^2.0.7",
|
"@types/file-saver": "^2.0.7",
|
||||||
"@types/react": "^19.1.6",
|
"@types/node": "^24.8.1",
|
||||||
"@types/react-dom": "^19.1.5",
|
"@types/react": "^19.2.2",
|
||||||
"cross-env": "^7.0.3",
|
"@types/react-dom": "^19.2.2",
|
||||||
"css-loader": "^7.1.2",
|
"cross-env": "^10.1.0",
|
||||||
"env-cmd": "^10.1.0",
|
"cspell": "^9.2.1",
|
||||||
"eslint": "^9.28.0",
|
"env-cmd": "^11.0.0",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"globals": "^16.4.0",
|
||||||
"globals": "^16.2.0",
|
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.6.2",
|
||||||
"style-loader": "^4.0.0",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.8.3"
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,21 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://raw.githubusercontent.com/kintone/js-sdk/%40kintone/plugin-manifest-validator%4010.2.0/packages/plugin-manifest-validator/manifest-schema.json",
|
"$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,
|
"manifest_version": 1,
|
||||||
"version": "1.0.0",
|
"version": "2.0.0",
|
||||||
"type": "APP",
|
"type": "APP",
|
||||||
"desktop": {
|
"desktop": {
|
||||||
"js": ["js/desktop.js"]
|
"js": ["js/desktop.js"],
|
||||||
|
"css": ["js/desktop.css"]
|
||||||
},
|
},
|
||||||
"mobile": {
|
"mobile": {
|
||||||
"js": ["js/desktop.js"]
|
"js": ["js/desktop.js"],
|
||||||
|
"css": ["js/desktop.css"]
|
||||||
},
|
},
|
||||||
"icon": "image/icon.png",
|
"icon": "image/icon.png",
|
||||||
"config": {
|
"config": {
|
||||||
"html": "html/config.html",
|
"html": "html/config.html",
|
||||||
"js": ["js/config.js"],
|
"js": ["js/config.js"],
|
||||||
"required_params": ["template"]
|
"css": ["js/config.css"]
|
||||||
},
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"en": "Word output plugin",
|
"en": "Word output plugin",
|
||||||
|
|||||||
@@ -1,99 +0,0 @@
|
|||||||
/* eslint-env node */
|
|
||||||
|
|
||||||
const path = require('path');
|
|
||||||
const KintonePlugin = require('@kintone/webpack-plugin-kintone-plugin');
|
|
||||||
|
|
||||||
const isProduction = process.env.NODE_ENV === 'production';
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
mode: isProduction ? 'production' : 'development',
|
|
||||||
devtool: isProduction ? false : 'inline-cheap-module-source-map',
|
|
||||||
entry: { config: './src/config/index.tsx', desktop: './src/desktop/index.tsx' },
|
|
||||||
output: {
|
|
||||||
path: path.resolve(__dirname, 'plugin', 'js'),
|
|
||||||
filename: '[name].js',
|
|
||||||
},
|
|
||||||
resolve: {
|
|
||||||
extensions: ['.ts', '.tsx', '.js'],
|
|
||||||
},
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
test: /\.(j|t)s$/,
|
|
||||||
exclude: [/[\\/]node_modules[\\/]/],
|
|
||||||
loader: 'builtin:swc-loader',
|
|
||||||
options: {
|
|
||||||
sourceMaps: !isProduction,
|
|
||||||
jsc: {
|
|
||||||
parser: {
|
|
||||||
syntax: 'typescript',
|
|
||||||
},
|
|
||||||
transform: {
|
|
||||||
react: {
|
|
||||||
runtime: 'automatic',
|
|
||||||
development: !isProduction,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
externalHelpers: true,
|
|
||||||
},
|
|
||||||
env: {
|
|
||||||
targets: 'Chrome >= 48',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.(j|t)sx$/,
|
|
||||||
loader: 'builtin:swc-loader',
|
|
||||||
exclude: [/[\\/]node_modules[\\/]/],
|
|
||||||
options: {
|
|
||||||
sourceMaps: !isProduction,
|
|
||||||
jsc: {
|
|
||||||
parser: {
|
|
||||||
syntax: 'typescript',
|
|
||||||
tsx: true,
|
|
||||||
},
|
|
||||||
transform: {
|
|
||||||
react: {
|
|
||||||
runtime: 'automatic',
|
|
||||||
development: !isProduction,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
externalHelpers: true,
|
|
||||||
},
|
|
||||||
env: {
|
|
||||||
targets: 'Chrome >= 48', // browser compatibility
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.css$/i,
|
|
||||||
use: ['style-loader', 'css-loader'],
|
|
||||||
exclude: /\.module\.css$/,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.module\.css$/i,
|
|
||||||
use: [
|
|
||||||
'style-loader',
|
|
||||||
{
|
|
||||||
loader: 'css-loader',
|
|
||||||
options: {
|
|
||||||
importLoaders: 1,
|
|
||||||
sourceMap: !isProduction,
|
|
||||||
modules: {
|
|
||||||
localIdentName: isProduction ? '[hash:base64]' : '[name]__[local]--[hash:base64:5]',
|
|
||||||
namedExport: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
new KintonePlugin({
|
|
||||||
manifestJSONPath: './plugin/manifest.json',
|
|
||||||
privateKeyPath: './private.ppk',
|
|
||||||
pluginZipPath: './dist/plugin.zip',
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
69
rspack.config.ts
Normal file
69
rspack.config.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { defineConfig } from '@rspack/cli';
|
||||||
|
import { rspack } from '@rspack/core';
|
||||||
|
|
||||||
|
import KintonePlugin from '@kintone/webpack-plugin-kintone-plugin';
|
||||||
|
|
||||||
|
const isDev = process.env.NODE_ENV === 'development';
|
||||||
|
|
||||||
|
// Target browsers, see: https://github.com/browserslist/browserslist
|
||||||
|
const targets = ['last 2 versions', '> 0.2%', 'not dead', 'Firefox ESR'];
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
mode: isDev ? 'development' : 'production',
|
||||||
|
devtool: isDev ? 'inline-cheap-module-source-map' : false,
|
||||||
|
entry: { config: './src/config/index.tsx', desktop: './src/desktop/index.tsx' },
|
||||||
|
output: {
|
||||||
|
path: './plugin/js',
|
||||||
|
filename: '[name].js',
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
extensions: ['...', '.ts', '.tsx'],
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.(tsx?)$/,
|
||||||
|
use: [
|
||||||
|
{
|
||||||
|
loader: 'builtin:swc-loader',
|
||||||
|
options: {
|
||||||
|
jsc: {
|
||||||
|
parser: {
|
||||||
|
syntax: 'typescript',
|
||||||
|
tsx: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
env: { targets },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
parser: {
|
||||||
|
'css/auto': {
|
||||||
|
namedExports: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new KintonePlugin({
|
||||||
|
manifestJSONPath: './plugin/manifest.json',
|
||||||
|
privateKeyPath: './private.ppk',
|
||||||
|
pluginZipPath: './dist/plugin.zip',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
optimization: {
|
||||||
|
minimizer: [
|
||||||
|
new rspack.SwcJsMinimizerRspackPlugin(),
|
||||||
|
new rspack.LightningCssMinimizerRspackPlugin({
|
||||||
|
minimizerOptions: { targets },
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
experiments: {
|
||||||
|
css: true,
|
||||||
|
},
|
||||||
|
performance: {
|
||||||
|
hints: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,7 +1,4 @@
|
|||||||
/* eslint-env node */
|
const fs = require('node:fs');
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const fs = require('fs');
|
|
||||||
const RSA = require('node-rsa');
|
const RSA = require('node-rsa');
|
||||||
|
|
||||||
const privateKeyFile = './private.ppk';
|
const privateKeyFile = './private.ppk';
|
||||||
@@ -1,6 +1,3 @@
|
|||||||
/* eslint-env node */
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const runAll = require('npm-run-all');
|
const runAll = require('npm-run-all');
|
||||||
|
|
||||||
runAll(['develop', 'upload'], {
|
runAll(['develop', 'upload'], {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import invariant from 'tiny-invariant';
|
import invariant from 'tiny-invariant';
|
||||||
|
|
||||||
@@ -21,8 +20,7 @@ const DynamicPortal: React.FC<DynamicPortalProps> = (props) => {
|
|||||||
el.parentNode.removeChild(el);
|
el.parentNode.removeChild(el);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
}, [el]);
|
||||||
}, []);
|
|
||||||
return ReactDOM.createPortal(children, el);
|
return ReactDOM.createPortal(children, el);
|
||||||
};
|
};
|
||||||
export default DynamicPortal;
|
export default DynamicPortal;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import KintonePluginAlert from './ui/KintonePluginAlert';
|
import KintonePluginAlert from './ui/KintonePluginAlert';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -8,11 +7,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;
|
||||||
|
|||||||
@@ -7,14 +7,16 @@
|
|||||||
background-color: rgba(0, 0, 0, 0.7);
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
}
|
}
|
||||||
.spinner-container {
|
|
||||||
|
.spinnerContainer {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
z-index: 10000;
|
z-index: 10000;
|
||||||
}
|
}
|
||||||
.spinner-container .spinner {
|
|
||||||
|
.spinnerContainer .spinner {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
width: 1em;
|
width: 1em;
|
||||||
height: 1em;
|
height: 1em;
|
||||||
@@ -24,6 +26,7 @@
|
|||||||
animation: mulShdSpin 1.1s infinite ease;
|
animation: mulShdSpin 1.1s infinite ease;
|
||||||
transform: translateZ(0);
|
transform: translateZ(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes mulShdSpin {
|
@keyframes mulShdSpin {
|
||||||
0%,
|
0%,
|
||||||
100% {
|
100% {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import DynamicPortal from './DynamicPortal';
|
import DynamicPortal from './DynamicPortal';
|
||||||
|
|
||||||
import styles from './Loading.module.css';
|
import styles from './Loading.module.css';
|
||||||
|
|||||||
41
src/common/config.ts
Normal file
41
src/common/config.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
export const PluginConfigDataSource = {
|
||||||
|
SELF: 'self',
|
||||||
|
APP: 'app',
|
||||||
|
} as const;
|
||||||
|
export type PluginConfigDataSource = (typeof PluginConfigDataSource)[keyof typeof PluginConfigDataSource];
|
||||||
|
|
||||||
|
export interface PluginConfigSelf {
|
||||||
|
dataSource: typeof PluginConfigDataSource.SELF;
|
||||||
|
fieldCode: string;
|
||||||
|
}
|
||||||
|
export interface PluginConfigApp {
|
||||||
|
dataSource: typeof PluginConfigDataSource.APP;
|
||||||
|
appId: string;
|
||||||
|
recordId: string;
|
||||||
|
fieldCode: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PluginConfig = PluginConfigSelf | PluginConfigApp;
|
||||||
|
|
||||||
|
const DEFAULT_CONFIG: PluginConfig = {
|
||||||
|
dataSource: PluginConfigDataSource.SELF,
|
||||||
|
fieldCode: '',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const saveConfig = (config: PluginConfig, callback?: () => void): void => {
|
||||||
|
kintone.plugin.app.setConfig(config, callback);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loadConfig = (pluginId: string): 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 || '' };
|
||||||
|
};
|
||||||
1
src/common/constants.ts
Normal file
1
src/common/constants.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const DOCX_CONTENT_TYPE = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
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.');
|
|
||||||
|
|
||||||
const KintoneUserLanguages = ['en', 'ja', 'zh', 'zh-TW', 'es', 'pt-BR', 'th'] as const;
|
|
||||||
export type KintoneUserLanguages = (typeof KintoneUserLanguages)[number];
|
|
||||||
export const LANGUAGE = kintone.getLoginUser().language as KintoneUserLanguages;
|
|
||||||
invariant(
|
|
||||||
KintoneUserLanguages.includes(LANGUAGE),
|
|
||||||
`Unsupported language: ${LANGUAGE}. Supported languages are: ${KintoneUserLanguages.join(', ')}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
export const DOCX_CONTENT_TYPE = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
|
|
||||||
10
src/common/kintoneLanguageDetector.ts
Normal file
10
src/common/kintoneLanguageDetector.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import type { LanguageDetectorModule } from 'i18next';
|
||||||
|
|
||||||
|
const KintoneLanguageDetector: LanguageDetectorModule = {
|
||||||
|
type: 'languageDetector',
|
||||||
|
// init: () => {},
|
||||||
|
detect: () => kintone.getLoginUser().language,
|
||||||
|
// cacheUserLanguage: () => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default KintoneLanguageDetector;
|
||||||
7
src/common/stringUtils.ts
Normal file
7
src/common/stringUtils.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export const naturalCompare = (a: string, b: string): number => {
|
||||||
|
const locales = new Set<string>([...navigator.languages, 'en-US', 'ja-JP']);
|
||||||
|
return new Intl.Collator(Array.from(locales), {
|
||||||
|
sensitivity: 'variant',
|
||||||
|
numeric: true,
|
||||||
|
}).compare(a, b);
|
||||||
|
};
|
||||||
5
src/common/types.ts
Normal file
5
src/common/types.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import type { KintoneFormFieldProperty } from '@kintone/rest-api-client';
|
||||||
|
|
||||||
|
export type KintoneFormFieldProperties = {
|
||||||
|
[fieldCode: string]: KintoneFormFieldProperty.OneOf;
|
||||||
|
};
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,11 +1,9 @@
|
|||||||
|
import clsx from 'clsx';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import clsx from 'clsx';
|
export type KintonePluginAlertProps = React.PropsWithChildren<{
|
||||||
|
|
||||||
interface KintonePluginAlertProps {
|
|
||||||
className?: string;
|
className?: string;
|
||||||
children: React.ReactNode;
|
}>;
|
||||||
}
|
|
||||||
|
|
||||||
const KintonePluginAlert: React.FC<KintonePluginAlertProps> = (props) => {
|
const KintonePluginAlert: React.FC<KintonePluginAlertProps> = (props) => {
|
||||||
const { className, children } = props;
|
const { className, children } = props;
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
|
import clsx from 'clsx';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import clsx from 'clsx';
|
export type KintonePluginButtonProps = React.PropsWithChildren<{
|
||||||
|
|
||||||
interface KintonePluginButtonProps {
|
|
||||||
className?: string;
|
className?: string;
|
||||||
variant: 'normal' | 'disabled' | 'dialog-ok' | 'dialog-cancel';
|
variant: 'normal' | 'disabled' | 'dialog-ok' | 'dialog-cancel' | 'add-row-image' | 'remove-row-image';
|
||||||
type?: 'button' | 'submit' | 'reset';
|
type?: 'button' | 'submit' | 'reset';
|
||||||
onClick?: React.MouseEventHandler<HTMLButtonElement>;
|
onClick?: React.MouseEventHandler<HTMLButtonElement>;
|
||||||
children: React.ReactNode;
|
}>;
|
||||||
}
|
|
||||||
|
|
||||||
const KintonePluginButton: React.FC<KintonePluginButtonProps> = (props) => {
|
const KintonePluginButton: React.FC<KintonePluginButtonProps> = (props) => {
|
||||||
const { className, variant, type, onClick, children } = props;
|
const { className, variant, type = 'button', onClick, children } = props;
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className={clsx(`kintoneplugin-button-${variant}`, className)}
|
className={clsx(`kintoneplugin-button-${variant}`, className)}
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
|
import clsx from 'clsx';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import clsx from 'clsx';
|
export type KintonePluginDescProps = React.PropsWithChildren<{
|
||||||
|
|
||||||
interface KintonePluginDescProps {
|
|
||||||
className?: string;
|
className?: string;
|
||||||
children: React.ReactNode;
|
}>;
|
||||||
}
|
|
||||||
|
|
||||||
const KintonePluginDesc: React.FC<KintonePluginDescProps> = (props) => {
|
const KintonePluginDesc: React.FC<KintonePluginDescProps> = (props) => {
|
||||||
const { className, children } = props;
|
const { className, children } = props;
|
||||||
|
|||||||
45
src/common/ui/KintonePluginInputText.tsx
Normal file
45
src/common/ui/KintonePluginInputText.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import clsx from 'clsx';
|
||||||
|
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<KintonePluginInputTextProps> = (props) => {
|
||||||
|
const { id, className, value, size, maxLength, disabled, required, placeholder, onChange } = props;
|
||||||
|
const handleOnChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
|
onChange(e.target.value);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className={clsx('kintoneplugin-input-outer', className)}>
|
||||||
|
<input
|
||||||
|
id={id}
|
||||||
|
className="kintoneplugin-input-text"
|
||||||
|
type="text"
|
||||||
|
value={value}
|
||||||
|
size={size}
|
||||||
|
maxLength={maxLength}
|
||||||
|
disabled={disabled}
|
||||||
|
required={required}
|
||||||
|
placeholder={placeholder}
|
||||||
|
aria-required={required}
|
||||||
|
aria-disabled={disabled}
|
||||||
|
aria-invalid={required && value === ''}
|
||||||
|
autoComplete="off"
|
||||||
|
autoCorrect="off"
|
||||||
|
autoCapitalize="off"
|
||||||
|
spellCheck={false}
|
||||||
|
onChange={handleOnChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default KintonePluginInputText;
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
|
import clsx from 'clsx';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import clsx from 'clsx';
|
export type KintonePluginLabelProps = React.PropsWithChildren<{
|
||||||
|
|
||||||
interface KintonePluginLabelProps {
|
|
||||||
className?: string;
|
className?: string;
|
||||||
children: React.ReactNode;
|
}>;
|
||||||
}
|
|
||||||
|
|
||||||
const KintonePluginLabel: React.FC<KintonePluginLabelProps> = (props) => {
|
const KintonePluginLabel: React.FC<KintonePluginLabelProps> = (props) => {
|
||||||
const { className, children } = props;
|
const { className, children } = props;
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
|
import clsx from 'clsx';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import clsx from 'clsx';
|
export type KintonePluginRequire = React.PropsWithChildren<{
|
||||||
|
|
||||||
interface KintonePluginRequire {
|
|
||||||
className?: string;
|
className?: string;
|
||||||
children: React.ReactNode;
|
}>;
|
||||||
}
|
|
||||||
|
|
||||||
const KintonePluginRequire: React.FC<KintonePluginRequire> = (props) => {
|
const KintonePluginRequire: React.FC<KintonePluginRequire> = (props) => {
|
||||||
const { className, children } = props;
|
const { className, children } = props;
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
|
import clsx from 'clsx';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import clsx from 'clsx';
|
export type KintonePluginRowProps = React.PropsWithChildren<{
|
||||||
|
|
||||||
interface KintonePluginRowProps {
|
|
||||||
className?: string;
|
className?: string;
|
||||||
children: React.ReactNode;
|
}>;
|
||||||
}
|
|
||||||
|
|
||||||
const KintonePluginRow: React.FC<KintonePluginRowProps> = (props) => {
|
const KintonePluginRow: React.FC<KintonePluginRowProps> = (props) => {
|
||||||
const { className, children } = props;
|
const { className, children } = props;
|
||||||
|
|||||||
@@ -1,33 +1,42 @@
|
|||||||
|
import clsx from 'clsx';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import clsx from 'clsx';
|
export type KintonePluginSelectOptionData = {
|
||||||
|
|
||||||
export interface KintonePluginSelectOptionData {
|
|
||||||
key: string;
|
|
||||||
value: string;
|
value: string;
|
||||||
label: string;
|
label: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
};
|
||||||
|
|
||||||
interface KintonePluginSelectProps {
|
export type KintonePluginSelectProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
defaultValue?: string;
|
value: string;
|
||||||
onChange?: (e: React.ChangeEvent<HTMLSelectElement>) => void;
|
onChange: (value: string) => void;
|
||||||
options: KintonePluginSelectOptionData[];
|
options: KintonePluginSelectOptionData[];
|
||||||
}
|
};
|
||||||
|
|
||||||
const KintonePluginSelect: React.FC<KintonePluginSelectProps> = (props) => {
|
const KintonePluginSelect: React.FC<KintonePluginSelectProps> = (props) => {
|
||||||
const { className, defaultValue, onChange, options } = props;
|
const { className, value, onChange, options } = props;
|
||||||
if (!options || options.length === 0) {
|
const optionWithKeys = React.useMemo(
|
||||||
return null; // Return null if no options are provided
|
() =>
|
||||||
|
options.map((option) => ({
|
||||||
|
...option,
|
||||||
|
key: crypto.randomUUID(),
|
||||||
|
})),
|
||||||
|
[options],
|
||||||
|
);
|
||||||
|
if (options.length === 0) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
const handleOnChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
onChange(e.target.value);
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<div className={clsx('kintoneplugin-select-outer', className)}>
|
<div className={clsx('kintoneplugin-select-outer', className)}>
|
||||||
<div className="kintoneplugin-select">
|
<div className="kintoneplugin-select">
|
||||||
<select defaultValue={defaultValue} onChange={onChange}>
|
<select value={value} onChange={handleOnChange}>
|
||||||
{options.map((option) => (
|
{optionWithKeys.map(({ key, value: itemValue, label, disabled }) => (
|
||||||
<option key={option.key} value={option.value} disabled={option.disabled}>
|
<option key={key} value={itemValue} disabled={disabled}>
|
||||||
{option.label}
|
{label}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
|
import clsx from 'clsx';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import clsx from 'clsx';
|
export type KintonePluginTitleProps = React.PropsWithChildren<{
|
||||||
|
|
||||||
interface KintonePluginTitleProps {
|
|
||||||
className?: string;
|
className?: string;
|
||||||
children: React.ReactNode;
|
}>;
|
||||||
}
|
|
||||||
|
|
||||||
const KintonePluginTitle: React.FC<KintonePluginTitleProps> = (props) => {
|
const KintonePluginTitle: React.FC<KintonePluginTitleProps> = (props) => {
|
||||||
const { className, children } = props;
|
const { className, children } = props;
|
||||||
|
|||||||
@@ -1,15 +1,34 @@
|
|||||||
|
import { KintoneRestAPIClient } from '@kintone/rest-api-client';
|
||||||
|
import moize from 'moize';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { ErrorBoundary } from 'react-error-boundary';
|
import { ErrorBoundary } from 'react-error-boundary';
|
||||||
|
import invariant from 'tiny-invariant';
|
||||||
import ErrorFallback from '../common/ErrorFallback';
|
import ErrorFallback from '../common/ErrorFallback';
|
||||||
import Loading from '../common/Loading';
|
import Loading from '../common/Loading';
|
||||||
|
import type { KintoneFormFieldProperties } from '../common/types';
|
||||||
import Settings from './Settings';
|
import Settings from './Settings';
|
||||||
|
|
||||||
const ConfigApp: React.FC = () => {
|
interface ConfigAppProps {
|
||||||
|
pluginId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cachedFormFieldsProperties = moize.promise(async (appId: number): Promise<KintoneFormFieldProperties> => {
|
||||||
|
const client = new KintoneRestAPIClient();
|
||||||
|
const { properties } = await client.app.getFormFields({ app: appId, preview: true });
|
||||||
|
return properties;
|
||||||
|
});
|
||||||
|
|
||||||
|
const ConfigApp: React.FC<ConfigAppProps> = (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 (
|
return (
|
||||||
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
||||||
<React.Suspense fallback={<Loading />}>
|
<React.Suspense fallback={<Loading />}>
|
||||||
<Settings />
|
<Settings pluginId={pluginId} appId={appId} propertiesPromise={propertiesPromise} />
|
||||||
</React.Suspense>
|
</React.Suspense>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
.settings .app {
|
||||||
|
display: flex;
|
||||||
|
gap: 2em;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
.buttons > *:not(:last-child) {
|
.buttons > *:not(:last-child) {
|
||||||
margin-right: 0.5em;
|
margin-right: 0.5em;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,89 +1,186 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { KintoneRestAPIClient } from '@kintone/rest-api-client';
|
import type { PluginConfig } from '../common/config';
|
||||||
import { Properties as KintoneFormFieldProperties } from '@kintone/rest-api-client/lib/src/client/types';
|
import { loadConfig, PluginConfigDataSource, saveConfig } from '../common/config';
|
||||||
import moize from 'moize';
|
import { naturalCompare } from '../common/stringUtils';
|
||||||
import invariant from 'tiny-invariant';
|
import type { KintoneFormFieldProperties } from '../common/types';
|
||||||
import { PLUGIN_ID } from '../common/global';
|
|
||||||
import KintonePluginAlert from '../common/ui/KintonePluginAlert';
|
import KintonePluginAlert from '../common/ui/KintonePluginAlert';
|
||||||
import KintonePluginButton from '../common/ui/KintonePluginButton';
|
import KintonePluginButton from '../common/ui/KintonePluginButton';
|
||||||
import KintonePluginDesc from '../common/ui/KintonePluginDesc';
|
import KintonePluginDesc from '../common/ui/KintonePluginDesc';
|
||||||
|
import KintonePluginInputText from '../common/ui/KintonePluginInputText';
|
||||||
import KintonePluginLabel from '../common/ui/KintonePluginLabel';
|
import KintonePluginLabel from '../common/ui/KintonePluginLabel';
|
||||||
import KintonePluginRequire from '../common/ui/KintonePluginRequire';
|
import KintonePluginRequire from '../common/ui/KintonePluginRequire';
|
||||||
import KintonePluginRow from '../common/ui/KintonePluginRow';
|
import KintonePluginRow from '../common/ui/KintonePluginRow';
|
||||||
import KintonePluginSelect, { 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> => {
|
interface SettingsProps {
|
||||||
const client = new KintoneRestAPIClient();
|
pluginId: string;
|
||||||
const { properties } = await client.app.getFormFields({ app: appId, lang: 'en', preview: false });
|
appId: number;
|
||||||
return properties;
|
propertiesPromise: Promise<KintoneFormFieldProperties>;
|
||||||
});
|
}
|
||||||
|
|
||||||
const Settings: React.FC = () => {
|
const Settings: React.FC<SettingsProps> = (props) => {
|
||||||
const config = kintone.plugin.app.getConfig(PLUGIN_ID);
|
const { pluginId, appId, propertiesPromise } = props;
|
||||||
const appId = kintone.app.getId();
|
const { t } = useTranslation();
|
||||||
invariant(appId, 'The app ID is not available. Please ensure you are on a Kintone app page.');
|
const config = loadConfig(pluginId);
|
||||||
const properties = React.use(cachedFormFieldsProperties(appId));
|
const properties = React.use(propertiesPromise);
|
||||||
const fileFields = Object.values(properties).filter((property) => property.type === 'FILE');
|
// 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[] = [
|
const options: KintonePluginSelectOptionData[] = [
|
||||||
{ key: '-', value: '', label: 'Select a File field', disabled: true }, // Default option
|
{ value: '', label: t('settings.self.messages.select-an-attachment-fields'), disabled: true }, // Default option
|
||||||
...fileFields.map((property) => ({
|
...fileFields.map((property) => ({
|
||||||
key: property.code,
|
|
||||||
value: property.code,
|
value: property.code,
|
||||||
label: property.label,
|
label: `${property.label} (${property.code})`,
|
||||||
})),
|
})),
|
||||||
];
|
];
|
||||||
|
const [fieldCode, setFieldCode] = React.useState<string>(
|
||||||
|
() =>
|
||||||
|
options.find((option) => config.dataSource === PluginConfigDataSource.SELF && option.value === config.fieldCode)
|
||||||
|
?.value ?? '',
|
||||||
|
);
|
||||||
|
|
||||||
const [template, setTemplate] = React.useState<string>(
|
// states for APP data source
|
||||||
() => options.find((option) => option.value === (config.template ?? ''))?.value ?? '',
|
const [otherAppId, setOtherAppId] = React.useState<string>(
|
||||||
|
config.dataSource === PluginConfigDataSource.APP ? config.appId : '',
|
||||||
|
);
|
||||||
|
const [otherRecordId, setOtherRecordId] = React.useState<string>(
|
||||||
|
config.dataSource === PluginConfigDataSource.APP ? config.recordId : '',
|
||||||
|
);
|
||||||
|
const [otherFieldCode, setOtherFieldCode] = React.useState<string>(
|
||||||
|
config.dataSource === PluginConfigDataSource.APP ? config.fieldCode : '',
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleOnSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
const handleOnSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
kintone.plugin.app.setConfig({ template }, () => {
|
let configToSave: PluginConfig;
|
||||||
alert('The plug-in settings have been saved. Please update the app!');
|
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}`;
|
window.location.href = `../../flow?app=${appId}`;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOnChangeTemplate = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
|
||||||
setTemplate(e.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOnClickCancel = () => {
|
const handleOnClickCancel = () => {
|
||||||
window.location.href = `../../${appId}/plugin/`;
|
window.location.href = `../../${appId}/plugin/`;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="settings">
|
<div className={styles.settings}>
|
||||||
<KintonePluginLabel>Settings for the Kintone Word output plugin</KintonePluginLabel>
|
<KintonePluginLabel>{t('title')}</KintonePluginLabel>
|
||||||
<form onSubmit={handleOnSubmit}>
|
<form onSubmit={handleOnSubmit}>
|
||||||
<KintonePluginRow>
|
<KintonePluginRow>
|
||||||
<KintonePluginTitle>
|
<KintonePluginTitle>
|
||||||
Template<KintonePluginRequire>*</KintonePluginRequire>
|
{t('settings.datasource.title')}
|
||||||
|
<KintonePluginRequire>*</KintonePluginRequire>
|
||||||
</KintonePluginTitle>
|
</KintonePluginTitle>
|
||||||
<KintonePluginDesc>Select a file field that contains the Word template file.</KintonePluginDesc>
|
<KintonePluginDesc>{t('settings.datasource.description')}</KintonePluginDesc>
|
||||||
{fileFields.length === 0 ? (
|
<KintonePluginSelect
|
||||||
<KintonePluginAlert>
|
value={String(dataSource)}
|
||||||
No file fields found in the app. Please add a file field to use this plugin.
|
onChange={(value) => {
|
||||||
</KintonePluginAlert>
|
const newDataSource = value as PluginConfigDataSource;
|
||||||
) : (
|
if (Object.values(PluginConfigDataSource).includes(newDataSource)) {
|
||||||
<KintonePluginSelect defaultValue={template} onChange={handleOnChangeTemplate} options={options} />
|
setDataSource(newDataSource);
|
||||||
)}
|
}
|
||||||
|
}}
|
||||||
|
options={dataSourceOptions.map((opt) => ({ value: String(opt.value), label: opt.label }))}
|
||||||
|
/>
|
||||||
</KintonePluginRow>
|
</KintonePluginRow>
|
||||||
|
{dataSource === PluginConfigDataSource.SELF && (
|
||||||
|
<KintonePluginRow>
|
||||||
|
<KintonePluginTitle>
|
||||||
|
{t('settings.self.title')}
|
||||||
|
<KintonePluginRequire>*</KintonePluginRequire>
|
||||||
|
</KintonePluginTitle>
|
||||||
|
<KintonePluginDesc>{t('settings.self.description')}</KintonePluginDesc>
|
||||||
|
{fileFields.length === 0 ? (
|
||||||
|
<KintonePluginAlert>{t('settings.self.errors.no-attachment-field-found')}</KintonePluginAlert>
|
||||||
|
) : (
|
||||||
|
<KintonePluginSelect value={fieldCode} onChange={(value) => setFieldCode(value)} options={options} />
|
||||||
|
)}
|
||||||
|
</KintonePluginRow>
|
||||||
|
)}
|
||||||
|
{dataSource === PluginConfigDataSource.APP && (
|
||||||
|
<KintonePluginRow>
|
||||||
|
<KintonePluginTitle>
|
||||||
|
{t('settings.app.title')}
|
||||||
|
<KintonePluginRequire>*</KintonePluginRequire>
|
||||||
|
</KintonePluginTitle>
|
||||||
|
<KintonePluginDesc>{t('settings.app.description')}</KintonePluginDesc>
|
||||||
|
<div className={styles.app}>
|
||||||
|
<label htmlFor="otherAppIdInput">
|
||||||
|
{t('settings.app.labels.app-id')} <KintonePluginRequire>*</KintonePluginRequire>
|
||||||
|
<br />
|
||||||
|
<KintonePluginInputText
|
||||||
|
id="otherAppIdInput"
|
||||||
|
value={otherAppId}
|
||||||
|
required
|
||||||
|
onChange={(value) => {
|
||||||
|
setOtherAppId(value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label htmlFor="otherRecordIdInput">
|
||||||
|
{t('settings.app.labels.record-id')} <KintonePluginRequire>*</KintonePluginRequire>
|
||||||
|
<br />
|
||||||
|
<KintonePluginInputText
|
||||||
|
id="otherRecordIdInput"
|
||||||
|
value={otherRecordId}
|
||||||
|
required
|
||||||
|
onChange={(value) => {
|
||||||
|
setOtherRecordId(value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label htmlFor="otherFieldCodeInput">
|
||||||
|
{t('settings.app.labels.field-code')} <KintonePluginRequire>*</KintonePluginRequire>
|
||||||
|
<br />
|
||||||
|
<KintonePluginInputText
|
||||||
|
id="otherFieldCodeInput"
|
||||||
|
value={otherFieldCode}
|
||||||
|
required
|
||||||
|
onChange={(value) => {
|
||||||
|
setOtherFieldCode(value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</KintonePluginRow>
|
||||||
|
)}
|
||||||
<KintonePluginRow className={styles.buttons}>
|
<KintonePluginRow className={styles.buttons}>
|
||||||
<KintonePluginButton variant="dialog-cancel" type="button" onClick={handleOnClickCancel}>
|
<KintonePluginButton variant="dialog-cancel" type="button" onClick={handleOnClickCancel}>
|
||||||
Cancel
|
{t('buttons.cancel')}
|
||||||
</KintonePluginButton>
|
</KintonePluginButton>
|
||||||
<KintonePluginButton variant="dialog-ok" type="submit">
|
<KintonePluginButton variant="dialog-ok" type="submit">
|
||||||
Save
|
{t('buttons.save')}
|
||||||
</KintonePluginButton>
|
</KintonePluginButton>
|
||||||
</KintonePluginRow>
|
</KintonePluginRow>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export default Settings;
|
export default Settings;
|
||||||
|
|||||||
27
src/config/i18n.ts
Normal file
27
src/config/i18n.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import i18n from 'i18next';
|
||||||
|
import { initReactI18next } from 'react-i18next';
|
||||||
|
import kintoneLanguageDetector from '../common/kintoneLanguageDetector';
|
||||||
|
|
||||||
|
import enTranslation from './locales/en.json';
|
||||||
|
import jaTranslation from './locales/ja.json';
|
||||||
|
|
||||||
|
i18n
|
||||||
|
.use(kintoneLanguageDetector)
|
||||||
|
.use(initReactI18next)
|
||||||
|
.init({
|
||||||
|
fallbackLng: 'en',
|
||||||
|
// debug: true,
|
||||||
|
interpolation: {
|
||||||
|
escapeValue: false, // not needed for react as it escapes by default
|
||||||
|
},
|
||||||
|
resources: {
|
||||||
|
en: {
|
||||||
|
translation: enTranslation,
|
||||||
|
},
|
||||||
|
ja: {
|
||||||
|
translation: jaTranslation,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default i18n;
|
||||||
@@ -1,16 +1,19 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import invariant from 'tiny-invariant';
|
import invariant from 'tiny-invariant';
|
||||||
import ConfigApp from './ConfigApp';
|
import ConfigApp from './ConfigApp';
|
||||||
|
|
||||||
import '../common/ui/51-modern-default.css';
|
import '../common/ui/51-modern-default.css';
|
||||||
|
|
||||||
const root = document.getElementById('plugin-config-root');
|
import './i18n';
|
||||||
invariant(root, 'The plugin configuration root element "plugin-config-root" is not found.');
|
|
||||||
|
|
||||||
ReactDOM.createRoot(root).render(
|
((PLUGIN_ID) => {
|
||||||
<React.StrictMode>
|
const root = document.getElementById('plugin-config-root');
|
||||||
<ConfigApp />
|
invariant(root, 'The plugin configuration root element "plugin-config-root" is not found.');
|
||||||
</React.StrictMode>,
|
|
||||||
);
|
ReactDOM.createRoot(root).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<ConfigApp pluginId={PLUGIN_ID} />
|
||||||
|
</React.StrictMode>,
|
||||||
|
);
|
||||||
|
})(kintone.$PLUGIN_ID);
|
||||||
|
|||||||
37
src/config/locales/en.json
Normal file
37
src/config/locales/en.json
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"title": "Settings for the Kintone Word output plugin",
|
||||||
|
"settings": {
|
||||||
|
"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 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": {
|
||||||
|
"save": "Save",
|
||||||
|
"cancel": "Cancel"
|
||||||
|
},
|
||||||
|
"on-saved": "The plug-in settings have been saved. Please update the app!"
|
||||||
|
}
|
||||||
37
src/config/locales/ja.json
Normal file
37
src/config/locales/ja.json
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"title": "Word出力プラグイン設定",
|
||||||
|
"settings": {
|
||||||
|
"datasource": {
|
||||||
|
"title": "WORD雛形ファイルのデータソース選択",
|
||||||
|
"description": "WORD雛形ファイルのデータソースを選択してください。",
|
||||||
|
"labels": {
|
||||||
|
"self": "自レコード",
|
||||||
|
"app": "他アプリのレコード"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"self": {
|
||||||
|
"title": "雛形ファイル",
|
||||||
|
"description": "Word雛形ファイルを保存する添付ファイルフィールドを選択してください。",
|
||||||
|
"messages": {
|
||||||
|
"select-an-attachment-fields": "(選択してください)"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"no-attachment-field-found": "このアプリに添付ファイルフィールドが見つかりません。フォームに添付ファイルフィールドを追加してください。"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"app": {
|
||||||
|
"title": "他アプリの添付ファイル情報",
|
||||||
|
"description": "アプリID・レコード番号・添付ファイルフィールドコードを入力してください。",
|
||||||
|
"labels": {
|
||||||
|
"app-id": "アプリID",
|
||||||
|
"record-id": "レコード番号",
|
||||||
|
"field-code": "添付ファイルフィールドコード"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"buttons": {
|
||||||
|
"save": "保存",
|
||||||
|
"cancel": "キャンセル"
|
||||||
|
},
|
||||||
|
"on-saved": "設定を保存しました。アプリを更新してください。"
|
||||||
|
}
|
||||||
@@ -1,20 +1,47 @@
|
|||||||
|
import { KintoneRestAPIClient } from '@kintone/rest-api-client';
|
||||||
|
import moize from 'moize';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { ErrorBoundary } from 'react-error-boundary';
|
import { ErrorBoundary } from 'react-error-boundary';
|
||||||
|
import { loadConfig, type PluginConfig } from '../common/config';
|
||||||
import ErrorFallback from '../common/ErrorFallback';
|
import ErrorFallback from '../common/ErrorFallback';
|
||||||
import Loading from '../common/Loading';
|
import Loading from '../common/Loading';
|
||||||
|
import type { KintoneFormFieldProperties } from '../common/types';
|
||||||
|
import { checkConfig } from './checkConfig';
|
||||||
import MenuPanel from './MenuPanel';
|
import MenuPanel from './MenuPanel';
|
||||||
|
|
||||||
interface DesktopAppProps {
|
interface DesktopAppProps {
|
||||||
|
pluginId: string;
|
||||||
event: kintone.events.AppRecordDetailShowEvent | kintone.events.MobileAppRecordDetailShowEvent;
|
event: kintone.events.AppRecordDetailShowEvent | kintone.events.MobileAppRecordDetailShowEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cachedFormFieldsProperties = moize.promise(async (appId: number): Promise<KintoneFormFieldProperties> => {
|
||||||
|
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<string> => {
|
||||||
|
return await checkConfig(config, record);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const DesktopApp: React.FC<DesktopAppProps> = (props) => {
|
const DesktopApp: React.FC<DesktopAppProps> = (props) => {
|
||||||
const { event } = props;
|
const { pluginId, event } = props;
|
||||||
|
|
||||||
|
const config = loadConfig(pluginId);
|
||||||
|
const propertiesPromise = cachedFormFieldsProperties(event.appId);
|
||||||
|
const fileKeyPromise = cachedConfigCheck(config, event.record);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
||||||
<React.Suspense fallback={<Loading />}>
|
<React.Suspense fallback={<Loading />}>
|
||||||
<MenuPanel event={event} />
|
<MenuPanel
|
||||||
|
pluginId={pluginId}
|
||||||
|
event={event}
|
||||||
|
propertiesPromise={propertiesPromise}
|
||||||
|
fileKeyPromise={fileKeyPromise}
|
||||||
|
/>
|
||||||
</React.Suspense>
|
</React.Suspense>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,237 +1,32 @@
|
|||||||
|
import { KintoneRestAPIClient } from '@kintone/rest-api-client';
|
||||||
|
import saveAs from 'file-saver';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { KintoneFormFieldProperty, KintoneRestAPIClient } from '@kintone/rest-api-client';
|
import type { KintoneFormFieldProperties } from '../common/types';
|
||||||
import { Properties as KintoneFormFieldProperties } from '@kintone/rest-api-client/lib/src/client/types';
|
|
||||||
import { KintoneRecord } from '@shin-chan/kypes/types/page';
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import Docxtemplater from 'docxtemplater';
|
|
||||||
import expressionParser from 'docxtemplater/expressions';
|
|
||||||
import { saveAs } from 'file-saver';
|
|
||||||
import moize from 'moize';
|
|
||||||
import PizZip from 'pizzip';
|
|
||||||
import invariant from 'tiny-invariant';
|
|
||||||
import { DOCX_CONTENT_TYPE, LANGUAGE, PLUGIN_ID } from '../common/global';
|
|
||||||
import KintonePluginAlert from '../common/ui/KintonePluginAlert';
|
|
||||||
import KintonePluginButton from '../common/ui/KintonePluginButton';
|
import KintonePluginButton from '../common/ui/KintonePluginButton';
|
||||||
|
import generateWordFileData from './generateWordFileData';
|
||||||
|
|
||||||
interface TemplateData {
|
export interface MenuPanelProps {
|
||||||
[key: string]: TemplateData | TemplateData[] | string | string[];
|
pluginId: string;
|
||||||
}
|
|
||||||
|
|
||||||
interface MenuPanelProps {
|
|
||||||
event: kintone.events.AppRecordDetailShowEvent | kintone.events.MobileAppRecordDetailShowEvent;
|
event: kintone.events.AppRecordDetailShowEvent | kintone.events.MobileAppRecordDetailShowEvent;
|
||||||
|
propertiesPromise: Promise<KintoneFormFieldProperties>;
|
||||||
|
fileKeyPromise: Promise<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cachedFormFieldsProperties = moize.promise(async (appId: number): Promise<KintoneFormFieldProperties> => {
|
|
||||||
const client = new KintoneRestAPIClient();
|
|
||||||
const { properties } = await client.app.getFormFields({ app: appId, lang: 'en', preview: false });
|
|
||||||
return properties;
|
|
||||||
});
|
|
||||||
|
|
||||||
const formatNumberValue = (value: string, digit: boolean, scale?: number): string => {
|
|
||||||
if (value !== '' && !isNaN(Number(value))) {
|
|
||||||
const numberValue = Number(value);
|
|
||||||
const options: Intl.NumberFormatOptions = {
|
|
||||||
useGrouping: digit,
|
|
||||||
};
|
|
||||||
if (scale != null && scale >= 0) {
|
|
||||||
options.minimumFractionDigits = scale;
|
|
||||||
options.maximumFractionDigits = scale;
|
|
||||||
}
|
|
||||||
return new Intl.NumberFormat('en-US', options).format(numberValue);
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatValueWithUnit = (
|
|
||||||
value: string,
|
|
||||||
property: KintoneFormFieldProperty.Number | KintoneFormFieldProperty.Calc,
|
|
||||||
): string => {
|
|
||||||
if (value !== '' && property.unit !== '') {
|
|
||||||
if (property.unitPosition === 'BEFORE') {
|
|
||||||
return `${property.unit} ${value}`;
|
|
||||||
} else if (property.unitPosition === 'AFTER') {
|
|
||||||
return `${value} ${property.unit}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatNumberRecordValue = (
|
|
||||||
value: string,
|
|
||||||
property: KintoneFormFieldProperty.Number | KintoneFormFieldProperty.Lookup,
|
|
||||||
): string => {
|
|
||||||
if ('lookup' in property) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
const scale = property.displayScale !== '' ? Number(property.displayScale) : undefined;
|
|
||||||
const digit = property.digit ?? false;
|
|
||||||
const formattedValue = formatNumberValue(value, digit, scale);
|
|
||||||
return formatValueWithUnit(formattedValue, property);
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatCalculatedRecordValue = (value: string, property: KintoneFormFieldProperty.Calc): string => {
|
|
||||||
const { format } = property;
|
|
||||||
if (value === '') {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
const scale = property.displayScale !== '' ? Number(property.displayScale) : undefined;
|
|
||||||
let formattedValue: string;
|
|
||||||
if (format === 'NUMBER') {
|
|
||||||
formattedValue = formatNumberValue(value, false, scale);
|
|
||||||
} else if (format === 'NUMBER_DIGIT') {
|
|
||||||
formattedValue = formatNumberValue(value, true, scale);
|
|
||||||
} else if (format === 'DATETIME') {
|
|
||||||
// "2025-05-30T00:00:00Z" -> "2025-05-30 9:00" (browser local timezone)
|
|
||||||
const date = new Date(value);
|
|
||||||
invariant(!isNaN(date.getTime()), `Expected value to be a valid date string, but got ${value}`);
|
|
||||||
formattedValue = dayjs(date).format('YYYY-MM-DD h:mm');
|
|
||||||
} else if (format === 'DATE') {
|
|
||||||
// "2025-05-30T00:00:00Z" -> "2025-05-30" (browser local timezone)
|
|
||||||
const date = new Date(value);
|
|
||||||
invariant(!isNaN(date.getTime()), `Expected value to be a valid date string, but got ${value}`);
|
|
||||||
formattedValue = dayjs(date).format('YYYY-MM-DD');
|
|
||||||
} else if (format === 'TIME') {
|
|
||||||
// "00:30" -> "0:30"
|
|
||||||
const [hours, minutes] = value.split(':').map((v) => Number(v));
|
|
||||||
invariant(
|
|
||||||
!isNaN(hours) && !isNaN(minutes),
|
|
||||||
`Expected hours and minutes to be numbers, but got hours: ${hours}, minutes: ${minutes}`,
|
|
||||||
);
|
|
||||||
formattedValue = `${hours}:${String(minutes).padStart(2, '0')}`;
|
|
||||||
} else if (format === 'HOUR_MINUTE') {
|
|
||||||
// "49:30" -> en:"49 hours 30 minutes", ja: "49時間30分"
|
|
||||||
const [hours, minutes] = value.split(':').map((v) => Number(v));
|
|
||||||
invariant(
|
|
||||||
!isNaN(hours) && !isNaN(minutes),
|
|
||||||
`Expected hours and minutes to be numbers, but got hours: ${hours}, minutes: ${minutes}`,
|
|
||||||
);
|
|
||||||
formattedValue = LANGUAGE === 'ja' ? `${hours}時間${minutes}分` : `${hours} hours ${minutes} minutes`;
|
|
||||||
} else if (format === 'DAY_HOUR_MINUTE') {
|
|
||||||
// "49:30" -> en: "2 days 1 hour 30 minutes", ja: "2日1時間30分
|
|
||||||
const [hours, minutes] = value.split(':').map((v) => Number(v));
|
|
||||||
invariant(
|
|
||||||
!isNaN(hours) && !isNaN(minutes),
|
|
||||||
`Expected hours and minutes to be numbers, but got hours: ${hours}, minutes: ${minutes}`,
|
|
||||||
);
|
|
||||||
const days = Math.floor(Number(hours) / 24);
|
|
||||||
const remainingHours = Number(hours) % 24;
|
|
||||||
formattedValue =
|
|
||||||
LANGUAGE === 'ja'
|
|
||||||
? `${days}日${remainingHours}時間${minutes}分`
|
|
||||||
: `${days} days ${remainingHours} hours ${minutes} minutes`;
|
|
||||||
} else {
|
|
||||||
// DATETIME, DATE, TIME
|
|
||||||
formattedValue = value;
|
|
||||||
}
|
|
||||||
return formatValueWithUnit(formattedValue, property);
|
|
||||||
};
|
|
||||||
|
|
||||||
const record2data = (properties: KintoneFormFieldProperties, record: Partial<KintoneRecord>): TemplateData => {
|
|
||||||
const data: TemplateData = {};
|
|
||||||
for (const key in record) {
|
|
||||||
if (Object.prototype.hasOwnProperty.call(record, key) && Object.prototype.hasOwnProperty.call(properties, key)) {
|
|
||||||
const item = record[key];
|
|
||||||
const property = properties[key];
|
|
||||||
if (item == null) continue;
|
|
||||||
const { type, value } = item;
|
|
||||||
if (value == null) {
|
|
||||||
data[key] = '';
|
|
||||||
} else if (type === 'CREATOR' || type === 'MODIFIER') {
|
|
||||||
data[key] = {
|
|
||||||
name: value.name,
|
|
||||||
code: value.code,
|
|
||||||
};
|
|
||||||
} else if (type === 'NUMBER') {
|
|
||||||
invariant(property.type === 'NUMBER', `Expected property type to be NUMBER, but got ${property.type}`);
|
|
||||||
data[key] = formatNumberRecordValue(value, property);
|
|
||||||
} else if (type === 'CALC') {
|
|
||||||
invariant(property.type === 'CALC', `Expected property type to be CALC, but got ${property.type}`);
|
|
||||||
data[key] = formatCalculatedRecordValue(value, property);
|
|
||||||
} else if (type === 'CHECK_BOX' || type === 'MULTI_SELECT' || type === 'CATEGORY') {
|
|
||||||
data[key] = value.map((v) => v);
|
|
||||||
} else if (
|
|
||||||
type === 'USER_SELECT' ||
|
|
||||||
type === 'ORGANIZATION_SELECT' ||
|
|
||||||
type === 'GROUP_SELECT' ||
|
|
||||||
type === 'STATUS_ASSIGNEE'
|
|
||||||
) {
|
|
||||||
data[key] = value.map((v) => {
|
|
||||||
return { name: v.name, code: v.code };
|
|
||||||
});
|
|
||||||
} else if (type === 'SUBTABLE') {
|
|
||||||
invariant(property.type === 'SUBTABLE', `Expected property type to be SUBTABLE, but got ${property.type}`);
|
|
||||||
data[key] = value.map((subRecord) => record2data(property.fields, subRecord.value));
|
|
||||||
} else if (type === 'FILE') {
|
|
||||||
invariant(property.type === 'FILE', `Expected property type to be FILE, but got ${property.type}`);
|
|
||||||
data[key] = value.map((file) => ({
|
|
||||||
name: file.name,
|
|
||||||
size: file.size,
|
|
||||||
contentType: file.contentType,
|
|
||||||
fileKey: file.fileKey,
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
// RECORD_NUMBER, __ID__, __REVISION__, CREATED_TIME, UPDATED_TIME
|
|
||||||
// SINGLE_LINE_TEXT, MULTI_LINE_TEXT, RICH_TEXT, RADIO_BUTTON, DROP_DOWN, DATE, TIME, DATETIME, LINK, STATUS
|
|
||||||
data[key] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
|
|
||||||
const MenuPanel: React.FC<MenuPanelProps> = (props) => {
|
const MenuPanel: React.FC<MenuPanelProps> = (props) => {
|
||||||
const { event } = props;
|
const { event, propertiesPromise, fileKeyPromise } = props;
|
||||||
const appId = event.appId;
|
const { t } = useTranslation();
|
||||||
const properties = React.use(cachedFormFieldsProperties(appId));
|
const language = kintone.getLoginUser().language;
|
||||||
const config = kintone.plugin.app.getConfig(PLUGIN_ID);
|
const properties = React.use(propertiesPromise);
|
||||||
const template: string = config.template ?? '';
|
const fileKey = React.use(fileKeyPromise);
|
||||||
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: Template field must be a file field.</KintonePluginAlert>;
|
|
||||||
}
|
|
||||||
if (record.value.length === 0) {
|
|
||||||
return <KintonePluginAlert>Word output plugin: Template field does not contain any files.</KintonePluginAlert>;
|
|
||||||
}
|
|
||||||
if (record.value.length > 1) {
|
|
||||||
return (
|
|
||||||
<KintonePluginAlert>
|
|
||||||
Word output plugin: Template field contains multiple files. Please ensure it contains only one file.
|
|
||||||
</KintonePluginAlert>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const { fileKey, contentType } = record.value[0];
|
|
||||||
if (contentType !== DOCX_CONTENT_TYPE) {
|
|
||||||
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>) => {
|
const handleOnClickOutputButton = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const client = new KintoneRestAPIClient();
|
const client = new KintoneRestAPIClient();
|
||||||
client.file
|
client.file
|
||||||
.downloadFile({ fileKey })
|
.downloadFile({ fileKey })
|
||||||
.then((content) => {
|
.then((content) => {
|
||||||
const zip = new PizZip(content);
|
const out = generateWordFileData(content, properties, event.record, language);
|
||||||
const doc = new Docxtemplater(zip, {
|
|
||||||
paragraphLoop: true,
|
|
||||||
linebreaks: true,
|
|
||||||
parser: expressionParser,
|
|
||||||
});
|
|
||||||
doc.render(record2data(properties, event.record));
|
|
||||||
const out = doc.getZip().generate({ type: 'blob', mimeType: DOCX_CONTENT_TYPE });
|
|
||||||
saveAs(out, 'output.docx');
|
saveAs(out, 'output.docx');
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -242,7 +37,7 @@ const MenuPanel: React.FC<MenuPanelProps> = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<KintonePluginButton variant="normal" onClick={handleOnClickOutputButton}>
|
<KintonePluginButton variant="normal" onClick={handleOnClickOutputButton}>
|
||||||
Word出力
|
{t('buttons.output')}
|
||||||
</KintonePluginButton>
|
</KintonePluginButton>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
78
src/desktop/checkConfig.ts
Normal file
78
src/desktop/checkConfig.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { KintoneRestAPIClient } from '@kintone/rest-api-client';
|
||||||
|
import i18n from 'i18next';
|
||||||
|
import {
|
||||||
|
type PluginConfig,
|
||||||
|
type PluginConfigApp,
|
||||||
|
PluginConfigDataSource,
|
||||||
|
type PluginConfigSelf,
|
||||||
|
} from '../common/config';
|
||||||
|
import { DOCX_CONTENT_TYPE } from '../common/constants';
|
||||||
|
|
||||||
|
const checkConfigSelf = (config: PluginConfigSelf, record: kintone.types.SavedFields): string => {
|
||||||
|
const { fieldCode } = config;
|
||||||
|
if (fieldCode === '') {
|
||||||
|
throw new Error(i18n.t('errors.template-field-is-required'));
|
||||||
|
}
|
||||||
|
const field = record[fieldCode];
|
||||||
|
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, 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 (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[fieldCode];
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const checkConfig = async (config: PluginConfig, record: kintone.types.SavedFields): Promise<string> => {
|
||||||
|
if (config.dataSource === PluginConfigDataSource.SELF) {
|
||||||
|
return checkConfigSelf(config, record);
|
||||||
|
} else if (config.dataSource === PluginConfigDataSource.APP) {
|
||||||
|
return checkConfigApp(config);
|
||||||
|
}
|
||||||
|
throw new Error(i18n.t('errors.unexpected-error-occurred'));
|
||||||
|
};
|
||||||
223
src/desktop/generateWordFileData.ts
Normal file
223
src/desktop/generateWordFileData.ts
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
import type { KintoneFormFieldProperty } from '@kintone/rest-api-client';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import Docxtemplater from 'docxtemplater';
|
||||||
|
import expressionParser from 'docxtemplater/expressions';
|
||||||
|
import PizZip from 'pizzip';
|
||||||
|
import invariant from 'tiny-invariant';
|
||||||
|
import { DOCX_CONTENT_TYPE } from '../common/constants';
|
||||||
|
import type { KintoneFormFieldProperties } from '../common/types';
|
||||||
|
|
||||||
|
interface TemplateData {
|
||||||
|
[key: string]: TemplateData | TemplateData[] | string | string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatNumberValue = (value: string, digit: boolean, scale?: number): string => {
|
||||||
|
if (value !== '' && !Number.isNaN(Number(value))) {
|
||||||
|
const numberValue = Number(value);
|
||||||
|
const options: Intl.NumberFormatOptions = {
|
||||||
|
useGrouping: digit,
|
||||||
|
};
|
||||||
|
if (scale != null && scale >= 0) {
|
||||||
|
options.minimumFractionDigits = scale;
|
||||||
|
options.maximumFractionDigits = scale;
|
||||||
|
}
|
||||||
|
return new Intl.NumberFormat('en-US', options).format(numberValue);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatValueWithUnit = (
|
||||||
|
value: string,
|
||||||
|
property: KintoneFormFieldProperty.Number | KintoneFormFieldProperty.Calc,
|
||||||
|
): string => {
|
||||||
|
if (value !== '' && property.unit !== '') {
|
||||||
|
if (property.unitPosition === 'BEFORE') {
|
||||||
|
return `${property.unit} ${value}`;
|
||||||
|
} else if (property.unitPosition === 'AFTER') {
|
||||||
|
return `${value} ${property.unit}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatNumberRecordValue = (
|
||||||
|
value: string,
|
||||||
|
property: KintoneFormFieldProperty.Number | KintoneFormFieldProperty.Lookup,
|
||||||
|
): string => {
|
||||||
|
if ('lookup' in property) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
const scale = property.displayScale !== '' ? Number(property.displayScale) : undefined;
|
||||||
|
const digit = property.digit ?? false;
|
||||||
|
const formattedValue = formatNumberValue(value, digit, scale);
|
||||||
|
return formatValueWithUnit(formattedValue, property);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatCalculatedRecordValue = (
|
||||||
|
value: string,
|
||||||
|
property: KintoneFormFieldProperty.Calc,
|
||||||
|
language: string,
|
||||||
|
): string => {
|
||||||
|
const { format } = property;
|
||||||
|
if (value === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const scale = property.displayScale !== '' ? Number(property.displayScale) : undefined;
|
||||||
|
let formattedValue: string;
|
||||||
|
if (format === 'NUMBER') {
|
||||||
|
formattedValue = formatNumberValue(value, false, scale);
|
||||||
|
} else if (format === 'NUMBER_DIGIT') {
|
||||||
|
formattedValue = formatNumberValue(value, true, scale);
|
||||||
|
} else if (format === 'DATETIME') {
|
||||||
|
// "2025-05-30T00:00:00Z" -> "2025-05-30 9:00" (browser local timezone)
|
||||||
|
const date = new Date(value);
|
||||||
|
invariant(!Number.isNaN(date.getTime()), `Expected value to be a valid date string, but got ${value}`);
|
||||||
|
formattedValue = dayjs(date).format('YYYY-MM-DD h:mm');
|
||||||
|
} else if (format === 'DATE') {
|
||||||
|
// "2025-05-30T00:00:00Z" -> "2025-05-30" (browser local timezone)
|
||||||
|
const date = new Date(value);
|
||||||
|
invariant(!Number.isNaN(date.getTime()), `Expected value to be a valid date string, but got ${value}`);
|
||||||
|
formattedValue = dayjs(date).format('YYYY-MM-DD');
|
||||||
|
} else if (format === 'TIME') {
|
||||||
|
// "00:30" -> "0:30"
|
||||||
|
const [hours, minutes] = value.split(':').map((v) => Number(v));
|
||||||
|
invariant(
|
||||||
|
!Number.isNaN(hours) && !Number.isNaN(minutes),
|
||||||
|
`Expected hours and minutes to be numbers, but got hours: ${hours}, minutes: ${minutes}`,
|
||||||
|
);
|
||||||
|
formattedValue = `${hours}:${String(minutes).padStart(2, '0')}`;
|
||||||
|
} else if (format === 'HOUR_MINUTE') {
|
||||||
|
// "49:30" -> en:"49 hours 30 minutes", ja: "49時間30分"
|
||||||
|
const [hours, minutes] = value.split(':').map((v) => Number(v));
|
||||||
|
invariant(
|
||||||
|
!Number.isNaN(hours) && !Number.isNaN(minutes),
|
||||||
|
`Expected hours and minutes to be numbers, but got hours: ${hours}, minutes: ${minutes}`,
|
||||||
|
);
|
||||||
|
formattedValue = language === 'ja' ? `${hours}時間${minutes}分` : `${hours} hours ${minutes} minutes`;
|
||||||
|
} else if (format === 'DAY_HOUR_MINUTE') {
|
||||||
|
// "49:30" -> en: "2 days 1 hour 30 minutes", ja: "2日1時間30分
|
||||||
|
const [hours, minutes] = value.split(':').map((v) => Number(v));
|
||||||
|
invariant(
|
||||||
|
!Number.isNaN(hours) && !Number.isNaN(minutes),
|
||||||
|
`Expected hours and minutes to be numbers, but got hours: ${hours}, minutes: ${minutes}`,
|
||||||
|
);
|
||||||
|
const days = Math.floor(Number(hours) / 24);
|
||||||
|
const remainingHours = Number(hours) % 24;
|
||||||
|
formattedValue =
|
||||||
|
language === 'ja'
|
||||||
|
? `${days}日${remainingHours}時間${minutes}分`
|
||||||
|
: `${days} days ${remainingHours} hours ${minutes} minutes`;
|
||||||
|
} else {
|
||||||
|
// DATETIME, DATE, TIME
|
||||||
|
formattedValue = value;
|
||||||
|
}
|
||||||
|
return formatValueWithUnit(formattedValue, property);
|
||||||
|
};
|
||||||
|
|
||||||
|
const record2templateData = (
|
||||||
|
properties: KintoneFormFieldProperties,
|
||||||
|
record: Partial<kintone.types.AllFields>,
|
||||||
|
language: string,
|
||||||
|
): TemplateData => {
|
||||||
|
const data: TemplateData = {};
|
||||||
|
for (const key in record) {
|
||||||
|
// biome-ignore lint/suspicious/noPrototypeBuiltins: avoid prototype pollution
|
||||||
|
if (Object.prototype.hasOwnProperty.call(record, key) && Object.prototype.hasOwnProperty.call(properties, key)) {
|
||||||
|
const item = record[key];
|
||||||
|
const property = properties[key];
|
||||||
|
if (item == null) continue;
|
||||||
|
const { type, value } = item;
|
||||||
|
if (value == null) {
|
||||||
|
data[key] = '';
|
||||||
|
} else if (type === 'CREATOR' || type === 'MODIFIER') {
|
||||||
|
data[key] = {
|
||||||
|
name: value.name,
|
||||||
|
code: value.code,
|
||||||
|
};
|
||||||
|
} else if (type === 'NUMBER') {
|
||||||
|
invariant(property.type === 'NUMBER', `Expected property type to be NUMBER, but got ${property.type}`);
|
||||||
|
data[key] = formatNumberRecordValue(value, property);
|
||||||
|
} else if (type === 'CALC') {
|
||||||
|
invariant(property.type === 'CALC', `Expected property type to be CALC, but got ${property.type}`);
|
||||||
|
data[key] = formatCalculatedRecordValue(value, property, language);
|
||||||
|
} else if (type === 'CHECK_BOX' || type === 'MULTI_SELECT' || type === 'CATEGORY') {
|
||||||
|
data[key] = value.map((v) => v);
|
||||||
|
} else if (
|
||||||
|
type === 'USER_SELECT' ||
|
||||||
|
type === 'ORGANIZATION_SELECT' ||
|
||||||
|
type === 'GROUP_SELECT' ||
|
||||||
|
type === 'STATUS_ASSIGNEE'
|
||||||
|
) {
|
||||||
|
data[key] = value.map((v) => {
|
||||||
|
return { name: v.name, code: v.code };
|
||||||
|
});
|
||||||
|
} else if (type === 'SUBTABLE') {
|
||||||
|
invariant(property.type === 'SUBTABLE', `Expected property type to be SUBTABLE, but got ${property.type}`);
|
||||||
|
data[key] = value.map((subRecord) => record2templateData(property.fields, subRecord.value, language));
|
||||||
|
} else if (type === 'FILE') {
|
||||||
|
invariant(property.type === 'FILE', `Expected property type to be FILE, but got ${property.type}`);
|
||||||
|
data[key] = value.map((file) => ({
|
||||||
|
name: file.name,
|
||||||
|
size: file.size,
|
||||||
|
contentType: file.contentType,
|
||||||
|
fileKey: file.fileKey,
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
// RECORD_NUMBER, __ID__, __REVISION__, CREATED_TIME, UPDATED_TIME
|
||||||
|
// SINGLE_LINE_TEXT, MULTI_LINE_TEXT, RICH_TEXT, RADIO_BUTTON, DROP_DOWN, DATE, TIME, DATETIME, LINK, STATUS
|
||||||
|
data[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateWordFileData = (
|
||||||
|
content: ArrayBuffer,
|
||||||
|
properties: KintoneFormFieldProperties,
|
||||||
|
record: kintone.types.AllFields,
|
||||||
|
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: parser,
|
||||||
|
nullGetter: () => '',
|
||||||
|
});
|
||||||
|
doc.render(record2templateData(properties, record, language));
|
||||||
|
return doc.getZip().generate({ type: 'blob', mimeType: DOCX_CONTENT_TYPE });
|
||||||
|
};
|
||||||
|
|
||||||
|
export default generateWordFileData;
|
||||||
27
src/desktop/i18n.ts
Normal file
27
src/desktop/i18n.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import i18n from 'i18next';
|
||||||
|
import { initReactI18next } from 'react-i18next';
|
||||||
|
import kintoneLanguageDetector from '../common/kintoneLanguageDetector';
|
||||||
|
|
||||||
|
import enTranslation from './locales/en.json';
|
||||||
|
import jaTranslation from './locales/ja.json';
|
||||||
|
|
||||||
|
i18n
|
||||||
|
.use(kintoneLanguageDetector)
|
||||||
|
.use(initReactI18next)
|
||||||
|
.init({
|
||||||
|
fallbackLng: 'en',
|
||||||
|
// debug: true,
|
||||||
|
interpolation: {
|
||||||
|
escapeValue: false, // not needed for react as it escapes by default
|
||||||
|
},
|
||||||
|
resources: {
|
||||||
|
en: {
|
||||||
|
translation: enTranslation,
|
||||||
|
},
|
||||||
|
ja: {
|
||||||
|
translation: jaTranslation,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default i18n;
|
||||||
@@ -1,32 +1,35 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import invariant from 'tiny-invariant';
|
import invariant from 'tiny-invariant';
|
||||||
import DesktopApp from './DesktopApp';
|
import DesktopApp from './DesktopApp';
|
||||||
|
|
||||||
import '../common/ui/51-modern-default.css';
|
import '../common/ui/51-modern-default.css';
|
||||||
|
|
||||||
kintone.events.on(
|
import './i18n';
|
||||||
['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');
|
((PLUGIN_ID) => {
|
||||||
spaceElement.appendChild(root);
|
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.',
|
||||||
|
);
|
||||||
|
|
||||||
ReactDOM.createRoot(root).render(
|
const root = document.createElement('div');
|
||||||
<React.StrictMode>
|
spaceElement.appendChild(root);
|
||||||
<DesktopApp event={event} />
|
|
||||||
</React.StrictMode>,
|
|
||||||
);
|
|
||||||
|
|
||||||
return event;
|
ReactDOM.createRoot(root).render(
|
||||||
},
|
<React.StrictMode>
|
||||||
);
|
<DesktopApp pluginId={PLUGIN_ID} event={event} />
|
||||||
|
</React.StrictMode>,
|
||||||
|
);
|
||||||
|
|
||||||
|
return event;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})(kintone.$PLUGIN_ID);
|
||||||
|
|||||||
18
src/desktop/locales/en.json
Normal file
18
src/desktop/locales/en.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "Word output plugin",
|
||||||
|
"errors": {
|
||||||
|
"template-field-is-required": "The template field is not configured. Please check the plugin settings.",
|
||||||
|
"template-field-is-not-available": "The template field is not available. Please check the plugin settings.",
|
||||||
|
"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": "The template field does not contain any files.",
|
||||||
|
"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}}'.",
|
||||||
|
"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.",
|
||||||
|
"unexpected-error-occurred": "An unexpected error occurred."
|
||||||
|
},
|
||||||
|
"buttons": {
|
||||||
|
"output": "Output Word"
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/desktop/locales/ja.json
Normal file
18
src/desktop/locales/ja.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "Word出力プラグイン",
|
||||||
|
"errors": {
|
||||||
|
"template-field-is-required": "雛形ファイルが設定されていません。プラグインの設定を見直してください。",
|
||||||
|
"template-field-is-not-available": "雛形ファイルに設定された添付ファイルフィールドが見つかりませんでした。プラグインの設定を見直してください。",
|
||||||
|
"template-field-must-be-an-attachment-field": "雛形ファイルに設定されたフィールド型が添付ファイルフィールドではありません。プラグインの設定を見直してください。",
|
||||||
|
"template-field-does-not-contain-any-files": "雛形ファイルが添付されていません。",
|
||||||
|
"template-field-contains-multiple-files": "雛形ファイルとして複数のファイルが添付されています。1つだけ添付してください。",
|
||||||
|
"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": "雛形ファイルを格納されたアプリのレコードが取得できません。プラグインの設定を見直してください。",
|
||||||
|
"unexpected-error-occurred": "予期しないエラーが発生しました。"
|
||||||
|
},
|
||||||
|
"buttons": {
|
||||||
|
"output": "Word出力"
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/kintone-env.d.ts
vendored
17
src/kintone-env.d.ts
vendored
@@ -1,17 +0,0 @@
|
|||||||
/// <reference types="@shin-chan/kypes" />
|
|
||||||
|
|
||||||
// CSS modules
|
|
||||||
type CSSModuleClasses = { readonly [key: string]: string };
|
|
||||||
|
|
||||||
declare module '*.module.css' {
|
|
||||||
const classes: CSSModuleClasses;
|
|
||||||
export default classes;
|
|
||||||
}
|
|
||||||
declare module '*.module.scss' {
|
|
||||||
const classes: CSSModuleClasses;
|
|
||||||
export default classes;
|
|
||||||
}
|
|
||||||
|
|
||||||
// CSS
|
|
||||||
declare module '*.css' {}
|
|
||||||
declare module '*.scss' {}
|
|
||||||
93
src/types/css-modules.d.ts
vendored
Normal file
93
src/types/css-modules.d.ts
vendored
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
// CSS modules
|
||||||
|
type CSSModuleClasses = { readonly [key: string]: string };
|
||||||
|
|
||||||
|
declare module '*.module.css' {
|
||||||
|
const classes: CSSModuleClasses;
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
declare module '*.module.scss' {
|
||||||
|
const classes: CSSModuleClasses;
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
declare module '*.module.sass' {
|
||||||
|
const classes: CSSModuleClasses;
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
declare module '*.module.less' {
|
||||||
|
const classes: CSSModuleClasses;
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
declare module '*.module.styl' {
|
||||||
|
const classes: CSSModuleClasses;
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
declare module '*.module.stylus' {
|
||||||
|
const classes: CSSModuleClasses;
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
declare module '*.module.pcss' {
|
||||||
|
const classes: CSSModuleClasses;
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
declare module '*.module.sss' {
|
||||||
|
const classes: CSSModuleClasses;
|
||||||
|
export default classes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CSS
|
||||||
|
declare module '*.css' {
|
||||||
|
/**
|
||||||
|
* @deprecated Use `import style from './style.css?inline'` instead.
|
||||||
|
*/
|
||||||
|
const css: string;
|
||||||
|
export default css;
|
||||||
|
}
|
||||||
|
declare module '*.scss' {
|
||||||
|
/**
|
||||||
|
* @deprecated Use `import style from './style.scss?inline'` instead.
|
||||||
|
*/
|
||||||
|
const css: string;
|
||||||
|
export default css;
|
||||||
|
}
|
||||||
|
declare module '*.sass' {
|
||||||
|
/**
|
||||||
|
* @deprecated Use `import style from './style.sass?inline'` instead.
|
||||||
|
*/
|
||||||
|
const css: string;
|
||||||
|
export default css;
|
||||||
|
}
|
||||||
|
declare module '*.less' {
|
||||||
|
/**
|
||||||
|
* @deprecated Use `import style from './style.less?inline'` instead.
|
||||||
|
*/
|
||||||
|
const css: string;
|
||||||
|
export default css;
|
||||||
|
}
|
||||||
|
declare module '*.styl' {
|
||||||
|
/**
|
||||||
|
* @deprecated Use `import style from './style.styl?inline'` instead.
|
||||||
|
*/
|
||||||
|
const css: string;
|
||||||
|
export default css;
|
||||||
|
}
|
||||||
|
declare module '*.stylus' {
|
||||||
|
/**
|
||||||
|
* @deprecated Use `import style from './style.stylus?inline'` instead.
|
||||||
|
*/
|
||||||
|
const css: string;
|
||||||
|
export default css;
|
||||||
|
}
|
||||||
|
declare module '*.pcss' {
|
||||||
|
/**
|
||||||
|
* @deprecated Use `import style from './style.pcss?inline'` instead.
|
||||||
|
*/
|
||||||
|
const css: string;
|
||||||
|
export default css;
|
||||||
|
}
|
||||||
|
declare module '*.sss' {
|
||||||
|
/**
|
||||||
|
* @deprecated Use `import style from './style.sss?inline'` instead.
|
||||||
|
*/
|
||||||
|
const css: string;
|
||||||
|
export default css;
|
||||||
|
}
|
||||||
30
src/types/cybozu.d.ts
vendored
Normal file
30
src/types/cybozu.d.ts
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
declare namespace cybozu.data {
|
||||||
|
namespace types {
|
||||||
|
type SchemaDataField = {
|
||||||
|
id: string;
|
||||||
|
label: string; // field name
|
||||||
|
properties: Record<string, string>;
|
||||||
|
type: string;
|
||||||
|
var: string; // field code
|
||||||
|
};
|
||||||
|
type SchemaDataTable = cybozu.data.types.SchemaDataField & {
|
||||||
|
fieldList: Record<string, cybozu.data.types.SchemaDataField>;
|
||||||
|
};
|
||||||
|
type SchemaDataSubtable = Record<string, cybozu.data.types.SchemaDataTable>;
|
||||||
|
type SchemaDataGroups = Array<{
|
||||||
|
table: cybozu.data.types.SchemaDataTable;
|
||||||
|
subTable: cybozu.data.types.SchemaDataSubtable;
|
||||||
|
}>;
|
||||||
|
type SchemaData = {
|
||||||
|
groups: cybozu.data.types.SchemaDataGroups;
|
||||||
|
revision: string;
|
||||||
|
table: cybozu.data.types.SchemaDataTable;
|
||||||
|
subTable: cybozu.data.types.SchemaDataSubtable;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
namespace page {
|
||||||
|
const FORM_DATA: {
|
||||||
|
schema: cybozu.data.types.SchemaData;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
117
src/types/kintone.d.ts
vendored
Normal file
117
src/types/kintone.d.ts
vendored
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
declare namespace kintone {
|
||||||
|
namespace fieldTypes {
|
||||||
|
interface Category {
|
||||||
|
type?: 'CATEGORY';
|
||||||
|
value: Array<string>;
|
||||||
|
}
|
||||||
|
interface Status {
|
||||||
|
type?: 'STATUS';
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
interface StatusAssignee {
|
||||||
|
type?: 'STATUS_ASSIGNEE';
|
||||||
|
value: kintone.fieldTypes.UserSelect['value'];
|
||||||
|
}
|
||||||
|
type InSubtableFieldTypes =
|
||||||
|
| kintone.fieldTypes.SingleLineText
|
||||||
|
| kintone.fieldTypes.RichText
|
||||||
|
| kintone.fieldTypes.MultiLineText
|
||||||
|
| kintone.fieldTypes.Number
|
||||||
|
| kintone.fieldTypes.Calc
|
||||||
|
| kintone.fieldTypes.RadioButton
|
||||||
|
| kintone.fieldTypes.DropDown
|
||||||
|
| kintone.fieldTypes.Date
|
||||||
|
| kintone.fieldTypes.Time
|
||||||
|
| kintone.fieldTypes.DateTime
|
||||||
|
| kintone.fieldTypes.Link
|
||||||
|
| kintone.fieldTypes.CheckBox
|
||||||
|
| kintone.fieldTypes.MultiSelect
|
||||||
|
| kintone.fieldTypes.UserSelect
|
||||||
|
| kintone.fieldTypes.OrganizationSelect
|
||||||
|
| kintone.fieldTypes.GroupSelect
|
||||||
|
| kintone.fieldTypes.File;
|
||||||
|
type SystemFieldTypes =
|
||||||
|
| kintone.fieldTypes.Id
|
||||||
|
| kintone.fieldTypes.Revision
|
||||||
|
| kintone.fieldTypes.Modifier
|
||||||
|
| kintone.fieldTypes.Creator
|
||||||
|
| kintone.fieldTypes.RecordNumber
|
||||||
|
| kintone.fieldTypes.UpdatedTime
|
||||||
|
| kintone.fieldTypes.CreatedTime;
|
||||||
|
type SubtableValueItem<T extends string> = {
|
||||||
|
id: string;
|
||||||
|
value: Record<T, kintone.fieldTypes.InSubtableFieldTypes>;
|
||||||
|
};
|
||||||
|
type Subtable<T extends string> = {
|
||||||
|
type: 'SUBTABLE';
|
||||||
|
value: Array<kintone.fieldTypes.SubtableValueItem<T>>;
|
||||||
|
};
|
||||||
|
type ExtraFieldTypes = kintone.fieldTypes.Category | kintone.fieldTypes.Status | kintone.fieldTypes.StatusAssignee;
|
||||||
|
type GenericFieldTypes = kintone.fieldTypes.InSubtableFieldTypes | kintone.fieldTypes.Subtable<string>;
|
||||||
|
type GenericSavedFieldTypes = kintone.fieldTypes.GenericFieldTypes | kintone.fieldTypes.SystemFieldTypes;
|
||||||
|
type AllFieldTypes = kintone.fieldTypes.GenericSavedFieldTypes | kintone.fieldTypes.ExtraFieldTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace types {
|
||||||
|
type GenericFields = Record<string, kintone.fieldTypes.GenericFieldTypes>;
|
||||||
|
type GenericSavedFields = Record<string, kintone.fieldTypes.GenericSavedFieldTypes>;
|
||||||
|
type AllFields = Record<string, kintone.fieldTypes.AllFieldTypes>;
|
||||||
|
|
||||||
|
type Fields = kintone.types.GenericFields;
|
||||||
|
type SavedFields = kintone.types.GenericSavedFields;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace events {
|
||||||
|
interface AppRecordIndexEditShowEvent {
|
||||||
|
type: 'app.record.index.edit.show';
|
||||||
|
appId: number;
|
||||||
|
recordId: number;
|
||||||
|
record: kintone.types.SavedFields;
|
||||||
|
}
|
||||||
|
interface AppRecordCreateShowEvent {
|
||||||
|
type: 'app.record.create.show';
|
||||||
|
appId: number;
|
||||||
|
record: kintone.types.Fields;
|
||||||
|
reuse: boolean;
|
||||||
|
}
|
||||||
|
interface AppRecordEditShowEvent {
|
||||||
|
type: 'app.record.edit.show';
|
||||||
|
appId: number;
|
||||||
|
recordId: number;
|
||||||
|
record: kintone.types.SavedFields;
|
||||||
|
}
|
||||||
|
interface AppRecordDetailShowEvent {
|
||||||
|
type: 'app.record.detail.show';
|
||||||
|
appId: number;
|
||||||
|
recordId: number;
|
||||||
|
record: kintone.types.SavedFields;
|
||||||
|
}
|
||||||
|
interface MobileAppRecordDetailShowEvent {
|
||||||
|
type: 'mobile.app.record.detail.show';
|
||||||
|
appId: number;
|
||||||
|
recordId: number;
|
||||||
|
record: kintone.types.SavedFields;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace app {
|
||||||
|
function getFormFields(): 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;
|
||||||
|
}
|
||||||
@@ -1,73 +1,29 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
/* Visit https://aka.ms/tsconfig.json to read more about this file */
|
"lib": ["DOM", "ES2020"],
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"target": "ES2020",
|
||||||
|
"noEmit": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
|
||||||
/* Basic Options */
|
/* modules */
|
||||||
// "incremental": true, /* Enable incremental compilation */
|
"module": "ESNext",
|
||||||
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
|
"resolveJsonModule": true,
|
||||||
"module": "es2015", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
|
"moduleResolution": "bundler",
|
||||||
"lib": ["dom", "es2020"], /* Specify library files to be included in the compilation. */
|
"esModuleInterop": true,
|
||||||
// "allowJs": true, /* Allow javascript files to be compiled. */
|
"allowImportingTsExtensions": true,
|
||||||
// "checkJs": true, /* Report errors in .js files. */
|
|
||||||
"jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
|
||||||
// "declaration": true, /* Generates corresponding '.d.ts' file. */
|
|
||||||
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
|
||||||
// "sourceMap": true, /* Generates corresponding '.map' file. */
|
|
||||||
// "outFile": "./", /* Concatenate and emit output to single file. */
|
|
||||||
"outDir": "./plugin", /* Redirect output structure to the directory. */
|
|
||||||
"rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
|
||||||
// "composite": true, /* Enable project compilation */
|
|
||||||
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
|
|
||||||
// "removeComments": true, /* Do not emit comments to output. */
|
|
||||||
// "noEmit": true, /* Do not emit outputs. */
|
|
||||||
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
|
||||||
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
|
||||||
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
|
||||||
|
|
||||||
/* Strict Type-Checking Options */
|
/* type checking */
|
||||||
"strict": true, /* Enable all strict type-checking options. */
|
"strict": true,
|
||||||
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
"noUnusedLocals": true,
|
||||||
// "strictNullChecks": true, /* Enable strict null checks. */
|
"noUnusedParameters": true
|
||||||
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
|
||||||
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
|
|
||||||
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
|
|
||||||
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
|
||||||
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
|
||||||
|
|
||||||
/* Additional Checks */
|
|
||||||
// "noUnusedLocals": true, /* Report errors on unused locals. */
|
|
||||||
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
|
||||||
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
|
||||||
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
|
||||||
|
|
||||||
/* Module Resolution Options */
|
|
||||||
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
|
||||||
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
|
||||||
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
|
||||||
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
|
||||||
// "typeRoots": [], /* List of folders to include type definitions from. */
|
|
||||||
// "types": [], /* Type declaration files to be included in compilation. */
|
|
||||||
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
|
||||||
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
|
||||||
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
|
||||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
|
||||||
|
|
||||||
/* Source Map Options */
|
|
||||||
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
|
||||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
|
||||||
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
|
|
||||||
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
|
|
||||||
|
|
||||||
/* Experimental Options */
|
|
||||||
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
|
||||||
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
|
||||||
|
|
||||||
/* Advanced Options */
|
|
||||||
"skipLibCheck": true, /* Skip type checking of declaration files. */
|
|
||||||
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
|
|
||||||
},
|
},
|
||||||
"include": [
|
"files": ["./node_modules/@kintone/dts-gen/kintone.d.ts"],
|
||||||
"src/**/*",
|
"include": ["src/**/*"],
|
||||||
"./node_modules/@kintone/dts-gen/kintone.d.ts"
|
"ts-node": {
|
||||||
]
|
"compilerOptions": {
|
||||||
|
"module": "CommonJS"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user