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

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;