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

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;