first commit.
This commit is contained in:
7
.cspell.json
Normal file
7
.cspell.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2",
|
||||||
|
"language": "en,en-gb",
|
||||||
|
"ignoreWords": [],
|
||||||
|
"words": ["blankspace", "Heiti", "jscoverage", "Kaku", "kintone", "kintoneplugin", "kypes", "Ligh", "moize"],
|
||||||
|
"ignorePaths": [".env"]
|
||||||
|
}
|
3
.env.example
Normal file
3
.env.example
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
KINTONE_BASE_URL="https://sample.kintone.com"
|
||||||
|
KINTONE_USERNAME="user01"
|
||||||
|
KINTONE_PASSWORD="XXXXXXXXXXXXXXXX"
|
167
.gitignore
vendored
Normal file
167
.gitignore
vendored
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
# Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,node
|
||||||
|
# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,node
|
||||||
|
|
||||||
|
### Node ###
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Snowpack dependency directory (https://snowpack.dev/)
|
||||||
|
web_modules/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
.stylelintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
.cache/
|
||||||
|
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
# public
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# vuepress v2.x temp and cache directory
|
||||||
|
.temp
|
||||||
|
|
||||||
|
# Docusaurus cache and generated files
|
||||||
|
.docusaurus
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
|
.vscode-test
|
||||||
|
|
||||||
|
# yarn v2
|
||||||
|
.yarn/cache
|
||||||
|
.yarn/unplugged
|
||||||
|
.yarn/build-state.yml
|
||||||
|
.yarn/install-state.gz
|
||||||
|
.pnp.*
|
||||||
|
|
||||||
|
### Node Patch ###
|
||||||
|
# Serverless Webpack directories
|
||||||
|
.webpack/
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
|
||||||
|
# SvelteKit build / generate output
|
||||||
|
.svelte-kit
|
||||||
|
|
||||||
|
### VisualStudioCode ###
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
!.vscode/*.code-snippets
|
||||||
|
|
||||||
|
# Local History for Visual Studio Code
|
||||||
|
.history/
|
||||||
|
|
||||||
|
# Built Visual Studio Code Extensions
|
||||||
|
*.vsix
|
||||||
|
|
||||||
|
### VisualStudioCode Patch ###
|
||||||
|
# Ignore all local history of files
|
||||||
|
.history
|
||||||
|
.ionide
|
||||||
|
|
||||||
|
# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,node
|
||||||
|
|
||||||
|
# customize
|
||||||
|
private.ppk
|
||||||
|
plugin/js/
|
8
.prettierrc.json
Normal file
8
.prettierrc.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/prettierrc",
|
||||||
|
"semi": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 120,
|
||||||
|
"trailingComma": "all"
|
||||||
|
}
|
8
.vscode/extensions.json
vendored
Normal file
8
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"streetsidesoftware.code-spell-checker",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"jawandarajbir.react-vscode-extension-pack"
|
||||||
|
]
|
||||||
|
}
|
39
.vscode/settings.json
vendored
Normal file
39
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.organizeImports": "explicit"
|
||||||
|
},
|
||||||
|
"[javascript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[javascriptreact]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[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]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[jsonc]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
// Exteions - cSpell
|
||||||
|
// - see: .cspell.json
|
||||||
|
// Extensions - HTML
|
||||||
|
"html.format.wrapLineLength": 0,
|
||||||
|
// Extentions - Prettier
|
||||||
|
// - see: .prettierrc.json
|
||||||
|
// Extentions - Typescript
|
||||||
|
"javascript.updateImportsOnFileMove.enabled": "always",
|
||||||
|
"typescript.updateImportsOnFileMove.enabled": "always"
|
||||||
|
}
|
21
LICENSE.txt
Normal file
21
LICENSE.txt
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 Yoshihiro OKUMURA
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
17
eslint.config.mjs
Normal file
17
eslint.config.mjs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
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: ['/'] }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
11335
package-lock.json
generated
Normal file
11335
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
49
package.json
Normal file
49
package.json
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"name": "kintone-plugin-filelookup",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"prepare": "node scripts/prepare-private-key.js",
|
||||||
|
"start": "node scripts/npm-start.js",
|
||||||
|
"develop": "npm run build -- --watch",
|
||||||
|
"build": "npm run prepare && cross-env NODE_ENV=development rspack build",
|
||||||
|
"build:prod": "npm run prepare && cross-env NODE_ENV=production rspack build",
|
||||||
|
"dts-gen": "kintone-dts-gen",
|
||||||
|
"lint": "eslint src --ext .js,.jsx,.ts,.tsx",
|
||||||
|
"upload": "env-cmd --silent kintone-plugin-uploader dist/plugin.zip --watch --waiting-dialog-ms 3000"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@kintone/rest-api-client": "^5.7.4",
|
||||||
|
"angular-expressions": "^1.4.3",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"dayjs": "^1.11.13",
|
||||||
|
"immer": "^10.1.1",
|
||||||
|
"moize": "^6.1.6",
|
||||||
|
"nanoid": "^5.1.5",
|
||||||
|
"react": "^19.1.0",
|
||||||
|
"react-dom": "^19.1.0",
|
||||||
|
"react-error-boundary": "^6.0.0",
|
||||||
|
"tiny-invariant": "^1.3.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@cybozu/eslint-config": "^24.0.0",
|
||||||
|
"@kintone/dts-gen": "^8.1.2",
|
||||||
|
"@kintone/plugin-uploader": "^9.1.5",
|
||||||
|
"@kintone/webpack-plugin-kintone-plugin": "^8.0.11",
|
||||||
|
"@rspack/cli": "^1.3.15",
|
||||||
|
"@rspack/core": "^1.3.15",
|
||||||
|
"@shin-chan/kypes": "^0.0.7",
|
||||||
|
"@swc/helpers": "^0.5.17",
|
||||||
|
"@types/react": "^19.1.8",
|
||||||
|
"@types/react-dom": "^19.1.6",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"css-loader": "^7.1.2",
|
||||||
|
"env-cmd": "^10.1.0",
|
||||||
|
"eslint": "^9.29.0",
|
||||||
|
"eslint-plugin-react": "^7.37.5",
|
||||||
|
"globals": "^16.2.0",
|
||||||
|
"npm-run-all": "^4.1.5",
|
||||||
|
"prettier": "^3.5.3",
|
||||||
|
"style-loader": "^4.0.0",
|
||||||
|
"typescript": "^5.8.3"
|
||||||
|
}
|
||||||
|
}
|
1
plugin/html/config.html
Normal file
1
plugin/html/config.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
<div id="plugin-config-root"></div>
|
BIN
plugin/image/icon.png
Normal file
BIN
plugin/image/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.6 KiB |
26
plugin/manifest.json
Normal file
26
plugin/manifest.json
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"$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,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "APP",
|
||||||
|
"desktop": {
|
||||||
|
"js": ["js/desktop.js"]
|
||||||
|
},
|
||||||
|
"mobile": {
|
||||||
|
"js": ["js/desktop.js"]
|
||||||
|
},
|
||||||
|
"icon": "image/icon.png",
|
||||||
|
"config": {
|
||||||
|
"html": "html/config.html",
|
||||||
|
"js": ["js/config.js"],
|
||||||
|
"required_params": ["lookup"]
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"ja": "Lookupファイル同期プラグイン",
|
||||||
|
"en": "Lookup file sync plugin"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"en": "Synchronize files using the Lookup field",
|
||||||
|
"ja": "Lookupフィールドを利用してファイルの同期を行います。"
|
||||||
|
}
|
||||||
|
}
|
99
rspack.config.js
Normal file
99
rspack.config.js
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
/* 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',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
16
scripts/npm-start.js
Normal file
16
scripts/npm-start.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
/* eslint-env node */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const runAll = require('npm-run-all');
|
||||||
|
|
||||||
|
runAll(['develop', 'upload'], {
|
||||||
|
parallel: true,
|
||||||
|
stdout: process.stdout,
|
||||||
|
stdin: process.stdin,
|
||||||
|
}).catch(({ results }) => {
|
||||||
|
results
|
||||||
|
.filter(({ code }) => code)
|
||||||
|
.forEach(({ name }) => {
|
||||||
|
console.log(`"npm run ${name}" was failed`);
|
||||||
|
});
|
||||||
|
});
|
18
scripts/prepare-private-key.js
Normal file
18
scripts/prepare-private-key.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
/* eslint-env node */
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const RSA = require('node-rsa');
|
||||||
|
|
||||||
|
const privateKeyFile = './private.ppk';
|
||||||
|
|
||||||
|
if (!fs.existsSync(privateKeyFile)) {
|
||||||
|
const key = new RSA({ b: 1024 });
|
||||||
|
const privateKey = key.exportKey('pkcs1-private');
|
||||||
|
fs.writeFile(privateKeyFile, privateKey, (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(err);
|
||||||
|
process.exitCode = 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
28
src/common/DynamicPortal.tsx
Normal file
28
src/common/DynamicPortal.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import invariant from 'tiny-invariant';
|
||||||
|
|
||||||
|
interface DynamicPortalProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DynamicPortal: React.FC<DynamicPortalProps> = (props) => {
|
||||||
|
const { children } = props;
|
||||||
|
const [el] = React.useState(() => document.createElement('div'));
|
||||||
|
React.useEffect(() => {
|
||||||
|
const body = document.querySelector('body');
|
||||||
|
invariant(body, 'The body element is not available. Please ensure this code runs in a browser context.');
|
||||||
|
if (el.parentNode == null) {
|
||||||
|
body.appendChild(el);
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
if (el.parentNode) {
|
||||||
|
el.parentNode.removeChild(el);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
return ReactDOM.createPortal(children, el);
|
||||||
|
};
|
||||||
|
export default DynamicPortal;
|
18
src/common/ErrorFallback.tsx
Normal file
18
src/common/ErrorFallback.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import KintonePluginAlert from './ui/KintonePluginAlert';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
error: Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ErrorFallback: React.FC<Props> = (props) => {
|
||||||
|
const { error } = props;
|
||||||
|
return (
|
||||||
|
<KintonePluginAlert>
|
||||||
|
<p>Something went wrong:</p>
|
||||||
|
<pre>{error.message}</pre>
|
||||||
|
</KintonePluginAlert>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default ErrorFallback;
|
117
src/common/Loading.module.css
Normal file
117
src/common/Loading.module.css
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
.overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
.spinner-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
z-index: 10000;
|
||||||
|
}
|
||||||
|
.spinner-container .spinner {
|
||||||
|
font-size: 10px;
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
border-radius: 50%;
|
||||||
|
position: relative;
|
||||||
|
text-indent: -9999em;
|
||||||
|
animation: mulShdSpin 1.1s infinite ease;
|
||||||
|
transform: translateZ(0);
|
||||||
|
}
|
||||||
|
@keyframes mulShdSpin {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
box-shadow:
|
||||||
|
0em -2.6em 0em 0em #ffffff,
|
||||||
|
1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2),
|
||||||
|
2.5em 0em 0 0em rgba(255, 255, 255, 0.2),
|
||||||
|
1.75em 1.75em 0 0em rgba(255, 255, 255, 0.2),
|
||||||
|
0em 2.5em 0 0em rgba(255, 255, 255, 0.2),
|
||||||
|
-1.8em 1.8em 0 0em rgba(255, 255, 255, 0.2),
|
||||||
|
-2.6em 0em 0 0em rgba(255, 255, 255, 0.5),
|
||||||
|
-1.8em -1.8em 0 0em rgba(255, 255, 255, 0.7);
|
||||||
|
}
|
||||||
|
12.5% {
|
||||||
|
box-shadow:
|
||||||
|
0em -2.6em 0em 0em rgba(255, 255, 255, 0.7),
|
||||||
|
1.8em -1.8em 0 0em #ffffff,
|
||||||
|
2.5em 0em 0 0em rgba(255, 255, 255, 0.2),
|
||||||
|
1.75em 1.75em 0 0em rgba(255, 255, 255, 0.2),
|
||||||
|
0em 2.5em 0 0em rgba(255, 255, 255, 0.2),
|
||||||
|
-1.8em 1.8em 0 0em rgba(255, 255, 255, 0.2),
|
||||||
|
-2.6em 0em 0 0em rgba(255, 255, 255, 0.2),
|
||||||
|
-1.8em -1.8em 0 0em rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
box-shadow:
|
||||||
|
0em -2.6em 0em 0em rgba(255, 255, 255, 0.5),
|
||||||
|
1.8em -1.8em 0 0em rgba(255, 255, 255, 0.7),
|
||||||
|
2.5em 0em 0 0em #ffffff,
|
||||||
|
1.75em 1.75em 0 0em rgba(255, 255, 255, 0.2),
|
||||||
|
0em 2.5em 0 0em rgba(255, 255, 255, 0.2),
|
||||||
|
-1.8em 1.8em 0 0em rgba(255, 255, 255, 0.2),
|
||||||
|
-2.6em 0em 0 0em rgba(255, 255, 255, 0.2),
|
||||||
|
-1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
37.5% {
|
||||||
|
box-shadow:
|
||||||
|
0em -2.6em 0em 0em rgba(255, 255, 255, 0.2),
|
||||||
|
1.8em -1.8em 0 0em rgba(255, 255, 255, 0.5),
|
||||||
|
2.5em 0em 0 0em rgba(255, 255, 255, 0.7),
|
||||||
|
1.75em 1.75em 0 0em #ffffff,
|
||||||
|
0em 2.5em 0 0em rgba(255, 255, 255, 0.2),
|
||||||
|
-1.8em 1.8em 0 0em rgba(255, 255, 255, 0.2),
|
||||||
|
-2.6em 0em 0 0em rgba(255, 255, 255, 0.2),
|
||||||
|
-1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
box-shadow:
|
||||||
|
0em -2.6em 0em 0em rgba(255, 255, 255, 0.2),
|
||||||
|
1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2),
|
||||||
|
2.5em 0em 0 0em rgba(255, 255, 255, 0.5),
|
||||||
|
1.75em 1.75em 0 0em rgba(255, 255, 255, 0.7),
|
||||||
|
0em 2.5em 0 0em #ffffff,
|
||||||
|
-1.8em 1.8em 0 0em rgba(255, 255, 255, 0.2),
|
||||||
|
-2.6em 0em 0 0em rgba(255, 255, 255, 0.2),
|
||||||
|
-1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
62.5% {
|
||||||
|
box-shadow:
|
||||||
|
0em -2.6em 0em 0em rgba(255, 255, 255, 0.2),
|
||||||
|
1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2),
|
||||||
|
2.5em 0em 0 0em rgba(255, 255, 255, 0.2),
|
||||||
|
1.75em 1.75em 0 0em rgba(255, 255, 255, 0.5),
|
||||||
|
0em 2.5em 0 0em rgba(255, 255, 255, 0.7),
|
||||||
|
-1.8em 1.8em 0 0em #ffffff,
|
||||||
|
-2.6em 0em 0 0em rgba(255, 255, 255, 0.2),
|
||||||
|
-1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
box-shadow:
|
||||||
|
0em -2.6em 0em 0em rgba(255, 255, 255, 0.2),
|
||||||
|
1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2),
|
||||||
|
2.5em 0em 0 0em rgba(255, 255, 255, 0.2),
|
||||||
|
1.75em 1.75em 0 0em rgba(255, 255, 255, 0.2),
|
||||||
|
0em 2.5em 0 0em rgba(255, 255, 255, 0.5),
|
||||||
|
-1.8em 1.8em 0 0em rgba(255, 255, 255, 0.7),
|
||||||
|
-2.6em 0em 0 0em #ffffff,
|
||||||
|
-1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
87.5% {
|
||||||
|
box-shadow:
|
||||||
|
0em -2.6em 0em 0em rgba(255, 255, 255, 0.2),
|
||||||
|
1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2),
|
||||||
|
2.5em 0em 0 0em rgba(255, 255, 255, 0.2),
|
||||||
|
1.75em 1.75em 0 0em rgba(255, 255, 255, 0.2),
|
||||||
|
0em 2.5em 0 0em rgba(255, 255, 255, 0.2),
|
||||||
|
-1.8em 1.8em 0 0em rgba(255, 255, 255, 0.5),
|
||||||
|
-2.6em 0em 0 0em rgba(255, 255, 255, 0.7),
|
||||||
|
-1.8em -1.8em 0 0em #ffffff;
|
||||||
|
}
|
||||||
|
}
|
18
src/common/Loading.tsx
Normal file
18
src/common/Loading.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import DynamicPortal from './DynamicPortal';
|
||||||
|
|
||||||
|
import styles from './Loading.module.css';
|
||||||
|
|
||||||
|
const Loading: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<DynamicPortal>
|
||||||
|
<div className={styles.overlay}>
|
||||||
|
<div className={styles.spinnerContainer}>
|
||||||
|
<div className={styles.spinner}>Loading..</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DynamicPortal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default Loading;
|
44
src/common/config.ts
Normal file
44
src/common/config.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { PLUGIN_ID } from './global';
|
||||||
|
|
||||||
|
export interface PluginConfigLookupItem {
|
||||||
|
lookupFieldCode: string;
|
||||||
|
srcAttachmentFieldCode: string;
|
||||||
|
destAttachmentFieldCode: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PluginConfigLookup = PluginConfigLookupItem[];
|
||||||
|
|
||||||
|
export interface PluginConfig {
|
||||||
|
lookup: PluginConfigLookup;
|
||||||
|
}
|
||||||
|
|
||||||
|
const encodeConfigLookup = (rows: PluginConfigLookup): string => {
|
||||||
|
return JSON.stringify(rows);
|
||||||
|
};
|
||||||
|
|
||||||
|
const decodeConfigLookup = (lookup: string): PluginConfigLookup | null => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(lookup) as PluginConfigLookup;
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loadPluginConfigLookup = (): PluginConfigLookup | null => {
|
||||||
|
const config = kintone.plugin.app.getConfig(PLUGIN_ID);
|
||||||
|
return decodeConfigLookup(config.lookup);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loadPluginConfig = (): PluginConfig | null => {
|
||||||
|
const config = kintone.plugin.app.getConfig(PLUGIN_ID);
|
||||||
|
const lookup = decodeConfigLookup(config.lookup);
|
||||||
|
return lookup ? { lookup } : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const savePluginConfigLookup = (lookup: PluginConfigLookup, callback: () => void) => {
|
||||||
|
kintone.plugin.app.setConfig({ lookup: encodeConfigLookup(lookup) }, callback);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const savePluginConfig = (config: PluginConfig, callback: () => void) => {
|
||||||
|
kintone.plugin.app.setConfig({ lookup: encodeConfigLookup(config.lookup) }, callback);
|
||||||
|
};
|
77
src/common/context.ts
Normal file
77
src/common/context.ts
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import { KintoneFormFieldProperty, KintoneRestAPIClient } from '@kintone/rest-api-client';
|
||||||
|
import {
|
||||||
|
AppID as KintoneAppId,
|
||||||
|
Properties as KintoneFormFieldProperties,
|
||||||
|
} from '@kintone/rest-api-client/lib/src/client/types';
|
||||||
|
import { PluginConfigLookup } from './config';
|
||||||
|
|
||||||
|
export interface PluginContext {
|
||||||
|
appId: KintoneAppId;
|
||||||
|
formFieldsProperties: Record<KintoneAppId, KintoneFormFieldProperties>;
|
||||||
|
attachmentFields: Record<KintoneAppId, KintoneFormFieldProperty.File[]>;
|
||||||
|
lookupFields: KintoneFormFieldProperty.Lookup[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const filterAttachmentFields = (properties: KintoneFormFieldProperties): KintoneFormFieldProperty.File[] => {
|
||||||
|
return Object.values(properties).filter((property) => property.type === 'FILE');
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterLookupFields = (properties: KintoneFormFieldProperties): KintoneFormFieldProperty.Lookup[] => {
|
||||||
|
return Object.values(properties).filter((property) => 'lookup' in property);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFormFieldsProperties = async (appId: KintoneAppId): Promise<KintoneFormFieldProperties> => {
|
||||||
|
const client = new KintoneRestAPIClient();
|
||||||
|
const { properties } = await client.app.getFormFields({ app: appId, lang: 'en', preview: false });
|
||||||
|
return properties;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPluginContext = async (appId: KintoneAppId): Promise<PluginContext> => {
|
||||||
|
const context: PluginContext = {
|
||||||
|
appId: appId,
|
||||||
|
formFieldsProperties: {},
|
||||||
|
attachmentFields: {},
|
||||||
|
lookupFields: [],
|
||||||
|
};
|
||||||
|
const properties = await getFormFieldsProperties(appId);
|
||||||
|
context.formFieldsProperties[appId] = properties;
|
||||||
|
context.attachmentFields[appId] = filterAttachmentFields(properties);
|
||||||
|
const lookupFields = filterLookupFields(properties);
|
||||||
|
const relatedAppIds = lookupFields
|
||||||
|
.map(({ lookup }) => lookup.relatedApp.app)
|
||||||
|
.filter((value, index, self) => self.indexOf(value) === index);
|
||||||
|
await Promise.all(
|
||||||
|
relatedAppIds.map(async (relatedAppId) => {
|
||||||
|
const relatedProperties = await getFormFieldsProperties(relatedAppId);
|
||||||
|
context.formFieldsProperties[relatedAppId] = relatedProperties;
|
||||||
|
context.attachmentFields[relatedAppId] = filterAttachmentFields(relatedProperties);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
context.lookupFields = lookupFields.filter((field) => {
|
||||||
|
const relatedKeyField = context.formFieldsProperties[field.lookup.relatedApp.app][field.lookup.relatedKeyField];
|
||||||
|
return (
|
||||||
|
relatedKeyField.type === 'RECORD_NUMBER' ||
|
||||||
|
((relatedKeyField.type === 'SINGLE_LINE_TEXT' || relatedKeyField.type === 'NUMBER') &&
|
||||||
|
'unique' in relatedKeyField &&
|
||||||
|
relatedKeyField.unique)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const filterConfigByPluginContext = (
|
||||||
|
config: PluginConfigLookup | null,
|
||||||
|
context: PluginContext,
|
||||||
|
): PluginConfigLookup | null => {
|
||||||
|
const filtered = config?.filter((item) => {
|
||||||
|
const lookupField = context.lookupFields.find((f) => f.code === item.lookupFieldCode);
|
||||||
|
return (
|
||||||
|
lookupField != null &&
|
||||||
|
context.attachmentFields[lookupField.lookup.relatedApp.app]?.find(
|
||||||
|
(f) => f.code === item.srcAttachmentFieldCode,
|
||||||
|
) != null &&
|
||||||
|
context.attachmentFields[context.appId]?.find((f) => f.code === item.destAttachmentFieldCode) != null
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return filtered != null && filtered.length > 0 ? filtered : null;
|
||||||
|
};
|
12
src/common/global.ts
Normal file
12
src/common/global.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
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(', ')}`,
|
||||||
|
);
|
669
src/common/ui/51-modern-default.css
Normal file
669
src/common/ui/51-modern-default.css
Normal file
File diff suppressed because one or more lines are too long
17
src/common/ui/KintonePluginAlert.tsx
Normal file
17
src/common/ui/KintonePluginAlert.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
export type KintonePluginAlertProps = React.PropsWithChildren<{
|
||||||
|
className?: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
const KintonePluginAlert: React.FC<KintonePluginAlertProps> = (props) => {
|
||||||
|
const { className, children } = props;
|
||||||
|
return (
|
||||||
|
<div className={clsx('kintoneplugin-alert', className)} role="alert">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default KintonePluginAlert;
|
25
src/common/ui/KintonePluginButton.tsx
Normal file
25
src/common/ui/KintonePluginButton.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
export type KintonePluginButtonProps = React.PropsWithChildren<{
|
||||||
|
className?: string;
|
||||||
|
variant: 'normal' | 'disabled' | 'dialog-ok' | 'dialog-cancel' | 'add-row-image' | 'remove-row-image';
|
||||||
|
type?: 'button' | 'submit' | 'reset';
|
||||||
|
onClick?: React.MouseEventHandler<HTMLButtonElement>;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
const KintonePluginButton: React.FC<KintonePluginButtonProps> = (props) => {
|
||||||
|
const { className, variant, type = 'button', onClick, children } = props;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={clsx(`kintoneplugin-button-${variant}`, className)}
|
||||||
|
type={type}
|
||||||
|
onClick={onClick}
|
||||||
|
disabled={variant === 'disabled'}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default KintonePluginButton;
|
13
src/common/ui/KintonePluginDesc.tsx
Normal file
13
src/common/ui/KintonePluginDesc.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
export type KintonePluginDescProps = React.PropsWithChildren<{
|
||||||
|
className?: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
const KintonePluginDesc: React.FC<KintonePluginDescProps> = (props) => {
|
||||||
|
const { className, children } = props;
|
||||||
|
return <div className={clsx('kintoneplugin-desc', className)}>{children}</div>;
|
||||||
|
};
|
||||||
|
export default KintonePluginDesc;
|
36
src/common/ui/KintonePluginInputCheckbox.tsx
Normal file
36
src/common/ui/KintonePluginInputCheckbox.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
|
||||||
|
export type KintonePluginInputCheckboxProps = {
|
||||||
|
className?: string;
|
||||||
|
label: string;
|
||||||
|
checked: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
onChange: (checked: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const KintonePluginInputCheckbox: React.FC<KintonePluginInputCheckboxProps> = (props) => {
|
||||||
|
const { className, label, checked, disabled, onChange } = props;
|
||||||
|
const id = nanoid();
|
||||||
|
const handleOnChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
|
onChange(e.target.checked);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className={clsx('kintoneplugin-input-checkbox', className)}>
|
||||||
|
<span className="kintoneplugin-input-checkbox-item">
|
||||||
|
<input
|
||||||
|
id={id}
|
||||||
|
className="kintoneplugin-input-text"
|
||||||
|
type="text"
|
||||||
|
checked={checked}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={handleOnChange}
|
||||||
|
/>
|
||||||
|
<label htmlFor={id}>{label}</label>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default KintonePluginInputCheckbox;
|
47
src/common/ui/KintonePluginInputRadio.tsx
Normal file
47
src/common/ui/KintonePluginInputRadio.tsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
|
||||||
|
export type KintonePluginInputRadioItem = {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type KintonePluginInputRadioProps = {
|
||||||
|
className?: string;
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
items: KintonePluginInputRadioItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const KintonePluginInputRadio: React.FC<KintonePluginInputRadioProps> = (props) => {
|
||||||
|
const { className, value, onChange, items } = props;
|
||||||
|
if (!items || items.length === 0) {
|
||||||
|
return null; // Return null if no items are provided
|
||||||
|
}
|
||||||
|
const id = nanoid();
|
||||||
|
const handleOnChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
|
onChange(e.target.value);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className={clsx('kintoneplugin-input-radio', className)}>
|
||||||
|
{items.map(({ value: itemValue, label, disabled }, idx) => (
|
||||||
|
<span key={`${id}-k${idx}`} className="kintoneplugin-input-radio-item">
|
||||||
|
<input
|
||||||
|
id={`${id}-${idx}`}
|
||||||
|
type="radio"
|
||||||
|
name={id}
|
||||||
|
value={itemValue}
|
||||||
|
checked={value === itemValue}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={handleOnChange}
|
||||||
|
/>
|
||||||
|
<label htmlFor={`${id}-${idx}`}>{label}</label>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default KintonePluginInputRadio;
|
29
src/common/ui/KintonePluginInputText.tsx
Normal file
29
src/common/ui/KintonePluginInputText.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
export type KintonePluginInputTextProps = {
|
||||||
|
className?: string;
|
||||||
|
value: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const KintonePluginInputText: React.FC<KintonePluginInputTextProps> = (props) => {
|
||||||
|
const { className, value, disabled, onChange } = props;
|
||||||
|
const handleOnChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||||
|
onChange(e.target.value);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className={clsx('kintoneplugin-input-outer', className)}>
|
||||||
|
<input
|
||||||
|
className="kintoneplugin-input-text"
|
||||||
|
type="text"
|
||||||
|
value={value}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={handleOnChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default KintonePluginInputText;
|
13
src/common/ui/KintonePluginLabel.tsx
Normal file
13
src/common/ui/KintonePluginLabel.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
export type KintonePluginLabelProps = React.PropsWithChildren<{
|
||||||
|
className?: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
const KintonePluginLabel: React.FC<KintonePluginLabelProps> = (props) => {
|
||||||
|
const { className, children } = props;
|
||||||
|
return <div className={clsx('kintoneplugin-label', className)}>{children}</div>;
|
||||||
|
};
|
||||||
|
export default KintonePluginLabel;
|
13
src/common/ui/KintonePluginRequire.tsx
Normal file
13
src/common/ui/KintonePluginRequire.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
export type KintonePluginRequire = React.PropsWithChildren<{
|
||||||
|
className?: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
const KintonePluginRequire: React.FC<KintonePluginRequire> = (props) => {
|
||||||
|
const { className, children } = props;
|
||||||
|
return <span className={clsx('kintoneplugin-require', className)}>{children}</span>;
|
||||||
|
};
|
||||||
|
export default KintonePluginRequire;
|
13
src/common/ui/KintonePluginRow.tsx
Normal file
13
src/common/ui/KintonePluginRow.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
export type KintonePluginRowProps = React.PropsWithChildren<{
|
||||||
|
className?: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
const KintonePluginRow: React.FC<KintonePluginRowProps> = (props) => {
|
||||||
|
const { className, children } = props;
|
||||||
|
return <div className={clsx('kintoneplugin-row', className)}>{children}</div>;
|
||||||
|
};
|
||||||
|
export default KintonePluginRow;
|
42
src/common/ui/KintonePluginSelect.tsx
Normal file
42
src/common/ui/KintonePluginSelect.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
|
||||||
|
export type KintonePluginSelectOptionData = {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type KintonePluginSelectProps = {
|
||||||
|
className?: string;
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
options: KintonePluginSelectOptionData[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const KintonePluginSelect: React.FC<KintonePluginSelectProps> = (props) => {
|
||||||
|
const { className, value, onChange, options } = props;
|
||||||
|
if (!options || options.length === 0) {
|
||||||
|
return null; // Return null if no options are provided
|
||||||
|
}
|
||||||
|
const key = nanoid();
|
||||||
|
const handleOnChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
onChange(e.target.value);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className={clsx('kintoneplugin-select-outer', className)}>
|
||||||
|
<div className="kintoneplugin-select">
|
||||||
|
<select value={value} onChange={handleOnChange}>
|
||||||
|
{options.map(({ value: itemValue, label, disabled }, idx) => (
|
||||||
|
<option key={`${key}-k${idx}`} value={itemValue} disabled={disabled}>
|
||||||
|
{label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default KintonePluginSelect;
|
13
src/common/ui/KintonePluginTable.tsx
Normal file
13
src/common/ui/KintonePluginTable.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
export type KintonePluginTableProps = React.PropsWithChildren<{
|
||||||
|
className?: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
const KintonePluginTable: React.FC<KintonePluginTableProps> = (props) => {
|
||||||
|
const { className, children } = props;
|
||||||
|
return <table className={clsx('kintoneplugin-table', className)}>{children}</table>;
|
||||||
|
};
|
||||||
|
export default KintonePluginTable;
|
23
src/common/ui/KintonePluginTableTd.tsx
Normal file
23
src/common/ui/KintonePluginTableTd.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
export type KintonePluginTableTdProps = React.PropsWithChildren<{
|
||||||
|
className?: string;
|
||||||
|
variant: 'control' | 'operation';
|
||||||
|
}>;
|
||||||
|
|
||||||
|
const KintonePluginTableTd: React.FC<KintonePluginTableTdProps> = (props) => {
|
||||||
|
const { className, variant, children } = props;
|
||||||
|
if (variant === 'control') {
|
||||||
|
return (
|
||||||
|
<td className={className}>
|
||||||
|
<div className="kintoneplugin-table-td-control">
|
||||||
|
<div className="kintoneplugin-table-td-control-value">{children}</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <td className={clsx('kintoneplugin-table-td-operation', className)}>{children}</td>;
|
||||||
|
};
|
||||||
|
export default KintonePluginTableTd;
|
21
src/common/ui/KintonePluginTableTh.tsx
Normal file
21
src/common/ui/KintonePluginTableTh.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
export type KintonePluginTableThProps = React.PropsWithChildren<{
|
||||||
|
className?: string;
|
||||||
|
variant: 'title' | 'blankspace';
|
||||||
|
}>;
|
||||||
|
|
||||||
|
const KintonePluginTableTh: React.FC<KintonePluginTableThProps> = (props) => {
|
||||||
|
const { className, variant, children } = props;
|
||||||
|
if (variant === 'title') {
|
||||||
|
return (
|
||||||
|
<th className={clsx('kintoneplugin-table-th', className)}>
|
||||||
|
<span className="title">{children}</span>
|
||||||
|
</th>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <th className={clsx('kintoneplugin-table-th-blankspace', className)}>{children}</th>;
|
||||||
|
};
|
||||||
|
export default KintonePluginTableTh;
|
13
src/common/ui/KintonePluginTitle.tsx
Normal file
13
src/common/ui/KintonePluginTitle.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
export type KintonePluginTitleProps = React.PropsWithChildren<{
|
||||||
|
className?: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
const KintonePluginTitle: React.FC<KintonePluginTitleProps> = (props) => {
|
||||||
|
const { className, children } = props;
|
||||||
|
return <div className={clsx('kintoneplugin-title', className)}>{children}</div>;
|
||||||
|
};
|
||||||
|
export default KintonePluginTitle;
|
17
src/config/ConfigApp.tsx
Normal file
17
src/config/ConfigApp.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { ErrorBoundary } from 'react-error-boundary';
|
||||||
|
import ErrorFallback from '../common/ErrorFallback';
|
||||||
|
import Loading from '../common/Loading';
|
||||||
|
import Settings from './Settings';
|
||||||
|
|
||||||
|
const ConfigApp: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
||||||
|
<React.Suspense fallback={<Loading />}>
|
||||||
|
<Settings />
|
||||||
|
</React.Suspense>
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default ConfigApp;
|
3
src/config/Settings.module.css
Normal file
3
src/config/Settings.module.css
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.buttons > *:not(:last-child) {
|
||||||
|
margin-right: 0.5em;
|
||||||
|
}
|
290
src/config/Settings.tsx
Normal file
290
src/config/Settings.tsx
Normal file
@ -0,0 +1,290 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { produce } from 'immer';
|
||||||
|
import invariant from 'tiny-invariant';
|
||||||
|
import {
|
||||||
|
loadPluginConfigLookup,
|
||||||
|
PluginConfigLookup,
|
||||||
|
PluginConfigLookupItem,
|
||||||
|
savePluginConfigLookup,
|
||||||
|
} from '../common/config';
|
||||||
|
import { filterConfigByPluginContext, getPluginContext, PluginContext } from '../common/context';
|
||||||
|
import KintonePluginAlert from '../common/ui/KintonePluginAlert';
|
||||||
|
import KintonePluginButton from '../common/ui/KintonePluginButton';
|
||||||
|
import KintonePluginDesc from '../common/ui/KintonePluginDesc';
|
||||||
|
import KintonePluginLabel from '../common/ui/KintonePluginLabel';
|
||||||
|
import KintonePluginRequire from '../common/ui/KintonePluginRequire';
|
||||||
|
import KintonePluginRow from '../common/ui/KintonePluginRow';
|
||||||
|
import KintonePluginSelect, { KintonePluginSelectOptionData } from '../common/ui/KintonePluginSelect';
|
||||||
|
import KintonePluginTable from '../common/ui/KintonePluginTable';
|
||||||
|
import KintonePluginTableTd from '../common/ui/KintonePluginTableTd';
|
||||||
|
import KintonePluginTableTh from '../common/ui/KintonePluginTableTh';
|
||||||
|
import KintonePluginTitle from '../common/ui/KintonePluginTitle';
|
||||||
|
|
||||||
|
import moize from 'moize';
|
||||||
|
import styles from './Settings.module.css';
|
||||||
|
|
||||||
|
const hasDuplicate = <T,>(items: T[], func: (a: T, b: T) => boolean = (a, b) => a === b): boolean => {
|
||||||
|
return items.find((item, idx) => items.some((item2, idx2) => func(item, item2) && idx !== idx2)) != null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasDuplicateDestAttachmentFields = (rows: PluginConfigLookup): boolean => {
|
||||||
|
return hasDuplicate<PluginConfigLookupItem>(rows, (a, b) => a.destAttachmentFieldCode === b.destAttachmentFieldCode);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface FieldSelectTableRowProps {
|
||||||
|
context: PluginContext;
|
||||||
|
row: PluginConfigLookupItem;
|
||||||
|
onUpdate: (item: PluginConfigLookupItem) => void;
|
||||||
|
onClickAddRow: () => void;
|
||||||
|
onClickRemoveRow: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FieldSelectTableRow: React.FC<FieldSelectTableRowProps> = (props) => {
|
||||||
|
const { context, row, onUpdate, onClickAddRow, onClickRemoveRow } = props;
|
||||||
|
const defaultOption: KintonePluginSelectOptionData = React.useMemo(() => {
|
||||||
|
return { value: '', label: '--', disabled: true };
|
||||||
|
}, []);
|
||||||
|
const lookupOptions: KintonePluginSelectOptionData[] = React.useMemo(() => {
|
||||||
|
return [
|
||||||
|
defaultOption,
|
||||||
|
...context.lookupFields.map((property) => ({
|
||||||
|
value: property.code,
|
||||||
|
label: `${property.label} (${property.code})`,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
}, [context.lookupFields, defaultOption]);
|
||||||
|
|
||||||
|
const srcAppId = context.lookupFields.find((field) => field.code === row.lookupFieldCode)?.lookup.relatedApp.app;
|
||||||
|
const srcFileOptions: KintonePluginSelectOptionData[] = React.useMemo(() => {
|
||||||
|
return [
|
||||||
|
defaultOption,
|
||||||
|
...(srcAppId != null
|
||||||
|
? context.attachmentFields[srcAppId].map((property) => ({
|
||||||
|
value: property.code,
|
||||||
|
label: `${property.label} (${property.code})`,
|
||||||
|
}))
|
||||||
|
: []),
|
||||||
|
];
|
||||||
|
}, [srcAppId, context.attachmentFields, defaultOption]);
|
||||||
|
|
||||||
|
const destFileOptions: KintonePluginSelectOptionData[] = React.useMemo(() => {
|
||||||
|
return [
|
||||||
|
defaultOption,
|
||||||
|
...(srcAppId != null
|
||||||
|
? context.attachmentFields[context.appId].map((property) => ({
|
||||||
|
value: property.code,
|
||||||
|
label: `${property.label} (${property.code})`,
|
||||||
|
}))
|
||||||
|
: []),
|
||||||
|
];
|
||||||
|
}, [context.appId, context.attachmentFields, defaultOption, srcAppId]);
|
||||||
|
|
||||||
|
const handleOnChangeLookup = (code: string) => {
|
||||||
|
const draft = {
|
||||||
|
lookupFieldCode: code,
|
||||||
|
srcAttachmentFieldCode: '',
|
||||||
|
destAttachmentFieldCode: '',
|
||||||
|
};
|
||||||
|
onUpdate(draft);
|
||||||
|
};
|
||||||
|
const handleOnChangeSrcFile = (code: string) => {
|
||||||
|
const draft = {
|
||||||
|
lookupFieldCode: row.lookupFieldCode,
|
||||||
|
srcAttachmentFieldCode: code,
|
||||||
|
destAttachmentFieldCode: '',
|
||||||
|
};
|
||||||
|
onUpdate(draft);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOnChangeDestFile = (code: string) => {
|
||||||
|
const draft = {
|
||||||
|
lookupFieldCode: row.lookupFieldCode,
|
||||||
|
srcAttachmentFieldCode: row.srcAttachmentFieldCode,
|
||||||
|
destAttachmentFieldCode: code,
|
||||||
|
};
|
||||||
|
onUpdate(draft);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOnClickAddRow: React.MouseEventHandler<HTMLButtonElement> = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onClickAddRow();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOnClickRemoveRow: React.MouseEventHandler<HTMLButtonElement> = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onClickRemoveRow();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr>
|
||||||
|
<KintonePluginTableTd variant="control">
|
||||||
|
<KintonePluginSelect value={row.lookupFieldCode} onChange={handleOnChangeLookup} options={lookupOptions} />
|
||||||
|
</KintonePluginTableTd>
|
||||||
|
<KintonePluginTableTd variant="control">
|
||||||
|
<KintonePluginSelect
|
||||||
|
value={row.srcAttachmentFieldCode}
|
||||||
|
onChange={handleOnChangeSrcFile}
|
||||||
|
options={srcFileOptions}
|
||||||
|
/>
|
||||||
|
</KintonePluginTableTd>
|
||||||
|
<KintonePluginTableTd variant="control">
|
||||||
|
<KintonePluginSelect
|
||||||
|
value={row.destAttachmentFieldCode}
|
||||||
|
onChange={handleOnChangeDestFile}
|
||||||
|
options={destFileOptions}
|
||||||
|
/>
|
||||||
|
</KintonePluginTableTd>
|
||||||
|
<KintonePluginTableTd variant="operation">
|
||||||
|
<KintonePluginButton variant="add-row-image" onClick={handleOnClickAddRow} />
|
||||||
|
<KintonePluginButton variant="remove-row-image" onClick={handleOnClickRemoveRow} />
|
||||||
|
</KintonePluginTableTd>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cachedPluginContext = moize.promise(getPluginContext);
|
||||||
|
|
||||||
|
const Settings: React.FC = () => {
|
||||||
|
const appId = kintone.app.getId();
|
||||||
|
invariant(appId, 'The app ID is not available. Please ensure you are on a Kintone app page.');
|
||||||
|
const context = React.use(cachedPluginContext(appId));
|
||||||
|
|
||||||
|
const [mappings, setMappings] = React.useState<PluginConfigLookup>(
|
||||||
|
() =>
|
||||||
|
filterConfigByPluginContext(loadPluginConfigLookup(), context) ?? [
|
||||||
|
{ lookupFieldCode: '', srcAttachmentFieldCode: '', destAttachmentFieldCode: '' },
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const [error, setError] = React.useState<string>('');
|
||||||
|
|
||||||
|
const handleOnUpdateFieldSelect = (idx: number, item: PluginConfigLookupItem) => {
|
||||||
|
setError('');
|
||||||
|
setMappings(
|
||||||
|
produce((draft) => {
|
||||||
|
draft[idx] = item;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOnAddFieldSelect = (idx: number) => {
|
||||||
|
setError('');
|
||||||
|
if (mappings.length < context.attachmentFields[context.appId].length) {
|
||||||
|
setMappings(
|
||||||
|
produce((draft) => {
|
||||||
|
draft.splice(idx + 1, 0, {
|
||||||
|
lookupFieldCode: '',
|
||||||
|
srcAttachmentFieldCode: '',
|
||||||
|
destAttachmentFieldCode: '',
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOnRemoveFieldSelect = (idx: number) => {
|
||||||
|
setError('');
|
||||||
|
if (mappings.length === 1) {
|
||||||
|
setMappings(
|
||||||
|
produce((draft) => {
|
||||||
|
draft[idx] = {
|
||||||
|
lookupFieldCode: '',
|
||||||
|
srcAttachmentFieldCode: '',
|
||||||
|
destAttachmentFieldCode: '',
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else if (mappings.length > 1) {
|
||||||
|
setMappings(
|
||||||
|
produce((draft) => {
|
||||||
|
draft.splice(idx, 1);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOnSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (mappings.find((item) => item.destAttachmentFieldCode === '') != null) {
|
||||||
|
setError('Incomplete field mapping found');
|
||||||
|
} else if (hasDuplicateDestAttachmentFields(mappings)) {
|
||||||
|
setError('Same destination attachment fields found');
|
||||||
|
} else {
|
||||||
|
setError('');
|
||||||
|
savePluginConfigLookup(mappings, () => {
|
||||||
|
alert('The plug-in settings have been saved. Please update the app!');
|
||||||
|
window.location.href = `../../flow?app=${appId}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOnClickCancel = () => {
|
||||||
|
setError('');
|
||||||
|
window.location.href = `../../${appId}/plugin/`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="settings">
|
||||||
|
<KintonePluginLabel>Settings for the Kintone Lookup File Sync plugin</KintonePluginLabel>
|
||||||
|
{error !== '' && (
|
||||||
|
<KintonePluginRow>
|
||||||
|
<KintonePluginAlert>{error}</KintonePluginAlert>
|
||||||
|
</KintonePluginRow>
|
||||||
|
)}
|
||||||
|
<form onSubmit={handleOnSubmit}>
|
||||||
|
<KintonePluginRow>
|
||||||
|
<KintonePluginTitle>
|
||||||
|
Field Mappings<KintonePluginRequire>*</KintonePluginRequire>
|
||||||
|
</KintonePluginTitle>
|
||||||
|
<KintonePluginDesc>
|
||||||
|
Select lookup and attachment fields that used for the file synchronization.
|
||||||
|
</KintonePluginDesc>
|
||||||
|
{context.lookupFields.length === 0 ? (
|
||||||
|
<KintonePluginAlert>
|
||||||
|
No lookup fields found in the app. Please add a lookup field to use this plugin.
|
||||||
|
</KintonePluginAlert>
|
||||||
|
) : (
|
||||||
|
<KintonePluginTable>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<KintonePluginTableTh variant="title">Lookup Field</KintonePluginTableTh>
|
||||||
|
<KintonePluginTableTh variant="title">Source Attachment Field (in Related App)</KintonePluginTableTh>
|
||||||
|
<KintonePluginTableTh variant="title">Destination Attachment Field</KintonePluginTableTh>
|
||||||
|
<KintonePluginTableTh variant="blankspace" />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{mappings.map((row, idx) => (
|
||||||
|
<FieldSelectTableRow
|
||||||
|
key={idx}
|
||||||
|
context={context}
|
||||||
|
row={row}
|
||||||
|
onUpdate={(draft) => {
|
||||||
|
handleOnUpdateFieldSelect(idx, draft);
|
||||||
|
}}
|
||||||
|
onClickAddRow={() => {
|
||||||
|
handleOnAddFieldSelect(idx);
|
||||||
|
}}
|
||||||
|
onClickRemoveRow={() => {
|
||||||
|
handleOnRemoveFieldSelect(idx);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</KintonePluginTable>
|
||||||
|
)}
|
||||||
|
</KintonePluginRow>
|
||||||
|
<KintonePluginRow className={styles.buttons}>
|
||||||
|
<KintonePluginButton variant="dialog-cancel" type="button" onClick={handleOnClickCancel}>
|
||||||
|
Cancel
|
||||||
|
</KintonePluginButton>
|
||||||
|
<KintonePluginButton variant="dialog-ok" type="submit">
|
||||||
|
Save
|
||||||
|
</KintonePluginButton>
|
||||||
|
</KintonePluginRow>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default Settings;
|
16
src/config/index.tsx
Normal file
16
src/config/index.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import invariant from 'tiny-invariant';
|
||||||
|
import ConfigApp from './ConfigApp';
|
||||||
|
|
||||||
|
import '../common/ui/51-modern-default.css';
|
||||||
|
|
||||||
|
const root = document.getElementById('plugin-config-root');
|
||||||
|
invariant(root, 'The plugin configuration root element "plugin-config-root" is not found.');
|
||||||
|
|
||||||
|
ReactDOM.createRoot(root).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<ConfigApp />
|
||||||
|
</React.StrictMode>,
|
||||||
|
);
|
149
src/desktop/index.tsx
Normal file
149
src/desktop/index.tsx
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
import { KintoneRestAPIClient } from '@kintone/rest-api-client';
|
||||||
|
import invariant from 'tiny-invariant';
|
||||||
|
import { loadPluginConfigLookup } from '../common/config';
|
||||||
|
import { filterConfigByPluginContext, getPluginContext } from '../common/context';
|
||||||
|
|
||||||
|
interface UpdateRecordAttachmentFieldValueItem {
|
||||||
|
fileKey: string;
|
||||||
|
}
|
||||||
|
type UpdateRecordAttachmentFieldValue = UpdateRecordAttachmentFieldValueItem[];
|
||||||
|
|
||||||
|
kintone.events.on(
|
||||||
|
['app.record.create.show', 'app.record.edit.show', 'mobile.app.record.create.show', 'mobile.app.record.edit.show'],
|
||||||
|
function (
|
||||||
|
event:
|
||||||
|
| kintone.events.AppRecordCreateShowEvent
|
||||||
|
| kintone.events.AppRecordEditShowEvent
|
||||||
|
| kintone.events.MobileAppRecordCreateShowEvent
|
||||||
|
| kintone.events.MobileAppRecordEditShowEvent,
|
||||||
|
) {
|
||||||
|
const mappings = loadPluginConfigLookup();
|
||||||
|
mappings?.forEach((mapping) => {
|
||||||
|
if (mapping.destAttachmentFieldCode in event.record) {
|
||||||
|
(event.record[mapping.destAttachmentFieldCode] as any).disabled = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return event;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
kintone.events.on(
|
||||||
|
[
|
||||||
|
'app.record.create.submit.success',
|
||||||
|
'app.record.edit.submit.success',
|
||||||
|
'mobile.app.record.create.submit.success',
|
||||||
|
'mobile.app.record.edit.submit.success',
|
||||||
|
],
|
||||||
|
async (
|
||||||
|
event:
|
||||||
|
| kintone.events.AppRecordCreateSubmitSuccessEvent
|
||||||
|
| kintone.events.AppRecordEditSubmitSuccessEvent
|
||||||
|
| kintone.events.MobileAppRecordCreateSubmitSuccessEvent
|
||||||
|
| kintone.events.MobileAppRecordEditSubmitSuccessEvent,
|
||||||
|
) => {
|
||||||
|
const context = await getPluginContext(event.appId);
|
||||||
|
const mappings = filterConfigByPluginContext(loadPluginConfigLookup(), context);
|
||||||
|
if (mappings == null) {
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
const client = new KintoneRestAPIClient();
|
||||||
|
await Promise.all<void>(
|
||||||
|
mappings.map(async (mapping) => {
|
||||||
|
const lookupField = event.record[mapping.lookupFieldCode];
|
||||||
|
invariant(
|
||||||
|
lookupField?.type === 'NUMBER' || lookupField?.type === 'SINGLE_LINE_TEXT',
|
||||||
|
'The field type of lookup field code must be number or single line text field.',
|
||||||
|
);
|
||||||
|
const lookupFieldProperty = context.lookupFields.find((p) => p.code === mapping.lookupFieldCode);
|
||||||
|
invariant(lookupFieldProperty != null, 'The property of lookup field not found.');
|
||||||
|
const destAttachmentField = event.record[mapping.destAttachmentFieldCode];
|
||||||
|
invariant(
|
||||||
|
destAttachmentField?.type === 'FILE',
|
||||||
|
'The field type of destination attachment field code must be attachment field.',
|
||||||
|
);
|
||||||
|
const updateFileKeys: UpdateRecordAttachmentFieldValue = [];
|
||||||
|
if (lookupField.value !== '') {
|
||||||
|
const res = await client.record
|
||||||
|
.getRecords({
|
||||||
|
app: lookupFieldProperty.lookup.relatedApp.app,
|
||||||
|
query: `${lookupFieldProperty.lookup.relatedKeyField} = "${lookupField.value?.replace(/[\\"]/g, '\\$&')}"`,
|
||||||
|
totalCount: true,
|
||||||
|
})
|
||||||
|
.catch(() => null);
|
||||||
|
if (res != null && res.totalCount === '1') {
|
||||||
|
const srcRecordId = res.records[0].$id;
|
||||||
|
invariant(srcRecordId.type === '__ID__', 'The field of Source record ID not found.');
|
||||||
|
const srcAttachmentField = res.records[0][mapping.srcAttachmentFieldCode];
|
||||||
|
invariant(
|
||||||
|
srcAttachmentField.type === 'FILE',
|
||||||
|
'The field type of source attachment field code must be attachment field.',
|
||||||
|
);
|
||||||
|
destAttachmentField.value.forEach((destFileInfo) => {
|
||||||
|
if (
|
||||||
|
srcAttachmentField.value.find(
|
||||||
|
(srcFileInfo) =>
|
||||||
|
destFileInfo.name === srcFileInfo.name &&
|
||||||
|
destFileInfo.size === srcFileInfo.size &&
|
||||||
|
destFileInfo.contentType === srcFileInfo.contentType,
|
||||||
|
) != null
|
||||||
|
) {
|
||||||
|
updateFileKeys.push({ fileKey: destFileInfo.fileKey });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await Promise.all<void>(
|
||||||
|
srcAttachmentField.value
|
||||||
|
.filter((srcFileInfo) => {
|
||||||
|
return (
|
||||||
|
destAttachmentField.value.find(
|
||||||
|
(destFileInfo) =>
|
||||||
|
destFileInfo.name === srcFileInfo.name &&
|
||||||
|
destFileInfo.size === srcFileInfo.size &&
|
||||||
|
destFileInfo.contentType === srcFileInfo.contentType,
|
||||||
|
) == null
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.map(async (srcFileInfo) => {
|
||||||
|
return client.file
|
||||||
|
.downloadFile({
|
||||||
|
fileKey: srcFileInfo.fileKey,
|
||||||
|
})
|
||||||
|
.then((fileData) => {
|
||||||
|
return client.file.uploadFile({
|
||||||
|
file: {
|
||||||
|
name: srcFileInfo.name,
|
||||||
|
data: new Blob([fileData], { type: srcFileInfo.contentType }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then((fileKey) => {
|
||||||
|
updateFileKeys.push(fileKey);
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
destAttachmentField.value.length !== updateFileKeys.length ||
|
||||||
|
destAttachmentField.value.every(
|
||||||
|
(v) =>
|
||||||
|
destAttachmentField.value.filter((e) => e.fileKey === v.fileKey).length !==
|
||||||
|
updateFileKeys.filter((e) => e.fileKey === v.fileKey).length,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
await client.record.updateRecord({
|
||||||
|
app: event.appId,
|
||||||
|
id: event.recordId,
|
||||||
|
record: {
|
||||||
|
[mapping.destAttachmentFieldCode]: {
|
||||||
|
value: updateFileKeys,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
).catch((e) => {
|
||||||
|
console.log(e);
|
||||||
|
});
|
||||||
|
return event;
|
||||||
|
},
|
||||||
|
);
|
17
src/kintone-env.d.ts
vendored
Normal file
17
src/kintone-env.d.ts
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
/// <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' {}
|
73
tsconfig.json
Normal file
73
tsconfig.json
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
/* Visit https://aka.ms/tsconfig.json to read more about this file */
|
||||||
|
|
||||||
|
/* Basic Options */
|
||||||
|
// "incremental": true, /* Enable incremental compilation */
|
||||||
|
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
|
||||||
|
"module": "es2015", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
|
||||||
|
"lib": ["dom", "es2020"], /* Specify library files to be included in the compilation. */
|
||||||
|
// "allowJs": true, /* Allow javascript files to be compiled. */
|
||||||
|
// "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 */
|
||||||
|
"strict": true, /* Enable all strict type-checking options. */
|
||||||
|
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
||||||
|
// "strictNullChecks": true, /* Enable strict null checks. */
|
||||||
|
// "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": [
|
||||||
|
"src/**/*",
|
||||||
|
"./node_modules/@kintone/dts-gen/kintone.d.ts"
|
||||||
|
]
|
||||||
|
}
|
Reference in New Issue
Block a user