first commit.

This commit is contained in:
2025-06-17 14:03:55 +09:00
commit ddae42619e
45 changed files with 13680 additions and 0 deletions

7
.cspell.json Normal file
View 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
View File

@ -0,0 +1,3 @@
KINTONE_BASE_URL="https://sample.kintone.com"
KINTONE_USERNAME="user01"
KINTONE_PASSWORD="XXXXXXXXXXXXXXXX"

167
.gitignore vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

49
package.json Normal file
View 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
View File

@ -0,0 +1 @@
<div id="plugin-config-root"></div>

BIN
plugin/image/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

26
plugin/manifest.json Normal file
View 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
View 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
View 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`);
});
});

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

View 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;

View File

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

View 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
View 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
View 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
View 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
View 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(', ')}`,
);

File diff suppressed because one or more lines are too long

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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
View File

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

View File

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

290
src/config/Settings.tsx Normal file
View 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
View File

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

149
src/desktop/index.tsx Normal file
View 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
View 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
View 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"
]
}