48 Commits

Author SHA1 Message Date
d5ed64511e auto generate /dist file 2025-02-08 09:52:46 +08:00
88343bbfdf fix multi input 2025-02-08 09:50:48 +08:00
2023ca601f changed project name 2025-02-07 18:57:23 +09:00
dec0e73dbb change project name 2025-02-07 17:55:45 +09:00
c88affc53a Merge branch 'master' into bugfix 2025-02-07 16:36:00 +09:00
41ba930473 Fix code format 2025-02-07 12:02:35 +08:00
cc0e34a651 bugfix 2025-02-06 21:44:13 +08:00
03a43789ac add comment 2025-02-06 15:51:57 +08:00
78a269b503 fix dropdown items 2025-02-05 22:34:40 +08:00
86669d3a28 コメント修正 2025-02-05 22:36:55 +09:00
b277fb23aa Read.me修正 2025-02-05 09:43:24 +09:00
f33887df8a Merge branch 'feature-muiltiSelect' 2025-02-05 09:18:08 +09:00
0aab989b32 リリース環境 2025-02-05 09:17:27 +09:00
ebfd49b130 merged 2025-02-04 10:36:55 +09:00
f13781ebd1 障害対応 2025-02-04 10:31:49 +09:00
4761f416f4 障害対応 2025-02-04 10:29:56 +09:00
10d0c2ef74 plugin test done 2025-02-03 23:37:35 +09:00
a3595b1368 複数チェック追加 2025-01-31 17:09:21 +09:00
c585013611 css 2025-01-27 23:43:20 +08:00
4b8d8506aa css adjust 2025-01-27 23:41:29 +08:00
4a229e5e59 bugifx 2025-01-27 22:47:35 +08:00
8d4ab48b89 conditons (FROM_TODAY) 2025-01-27 17:48:07 +08:00
dfcb522951 conditons (but FROM_TODAY) 2025-01-27 14:23:08 +08:00
6a0a28418f mobile対応追加 2025-01-27 14:48:38 +09:00
88a83878d2 kintone側実装完了。PVC初回提出版 2025-01-27 08:19:35 +09:00
bf4a55dd26 fix bug 2025-01-26 21:48:54 +08:00
defc671ad3 condition修正 2025-01-26 21:16:34 +09:00
e927b9d5af merge to kintone exec 2025-01-26 20:18:17 +09:00
18c6433b2f condition merged 2025-01-26 17:27:08 +09:00
85f8bd9526 fix 2025-01-26 16:18:21 +08:00
53da9bebb3 add subtable 2025-01-26 16:11:59 +08:00
4f4482e20d データ取得処理条件式対応 2025-01-26 16:42:56 +09:00
0abea3628a add some conditions 2025-01-26 11:22:16 +08:00
d57aac4613 app test case 2025-01-26 12:04:29 +09:00
62ec4b84fc finish join/mapping 2025-01-26 01:11:38 +08:00
54d384e7da Merge branch 'master' of https://dev.azure.com/alicorn-dev/deta-fetch-plugin/_git/deta-fetch-plugin 2025-01-25 21:31:54 +09:00
e58a4c7293 merge 2025-01-25 21:31:47 +09:00
662b18548f fix 2025-01-25 20:30:58 +08:00
f3bb622fd5 Merge branch 'master' of https://dev.azure.com/alicorn-dev/deta-fetch-plugin/_git/deta-fetch-plugin 2025-01-25 17:48:18 +09:00
7fb31b5c53 fix error 2025-01-24 23:23:16 +08:00
ccbcbf5259 fix join UI 2025-01-24 23:17:12 +08:00
ba897b00b9 Merge remote-tracking branch 'refs/remotes/origin/master' 2025-01-24 12:09:32 +09:00
46f2f5268c 'テスト仕様書(プラグイン部分)' 2025-01-24 12:06:25 +09:00
31b8f8a344 add field types 2025-01-24 02:18:31 +08:00
eaa9ec2fea fix ts 2025-01-24 01:49:10 +08:00
36b90ed45b Merge branch 'master' of https://dev.azure.com/alicorn-dev/deta-fetch-plugin/_git/deta-fetch-plugin 2025-01-23 16:05:19 +09:00
529d656d1b Merge branch 'master' of https://dev.azure.com/alicorn-dev/deta-fetch-plugin/_git/deta-fetch-plugin 2025-01-23 01:04:41 +09:00
acd3498832 Kintone側実装 2025-01-23 01:04:21 +09:00
115 changed files with 12580 additions and 6600 deletions

View File

@@ -1,15 +0,0 @@
{
// IntelliSense を使用して利用可能な属性を学べます。
// 既存の属性の説明をホバーして表示します。
// 詳細情報は次を確認してください: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "localhost に対して Chrome を起動する",
"url": "http://localhost:8080",
"webRoot": "${workspaceFolder}"
}
]
}

Binary file not shown.

Binary file not shown.

View File

@@ -1,2 +0,0 @@
n<EFBFBD>mR<10><> ӹE<D3B9><45><EFBFBD>H<EFBFBD>ޥu<DEA5><75><EFBFBD><02><><EFBFBD><EFBFBD>?u<>OL<4F>luG<75><47><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>$v
/9R{t<><74>M<>v#>T<>D<EFBFBD>F<EFBFBD>0<EFBFBD><><7F>L<EFBFBD>kC<6B>i!<05>J<EFBFBD><10>o

Binary file not shown.

View File

@@ -1,7 +0,0 @@
.settings-heading {
padding: 1em 0;
}
.kintoneplugin-input-text {
width: 20em;
}

View File

@@ -1,16 +0,0 @@
<section class="settings">
<h2 class="settings-heading">Settings for data fetch pluging</h2>
<p class="kintoneplugin-desc">This message is displayed on the app page after the app has been updated.</p>
<form class="js-submit-settings">
<p class="kintoneplugin-row">
<label for="message">
Message:
<input type="text" class="js-text-message kintoneplugin-input-text">
</label>
</p>
<p class="kintoneplugin-row">
<button type="button" class="js-cancel-button kintoneplugin-button-dialog-cancel">Cancel</button>
<button class="kintoneplugin-button-dialog-ok">Save</button>
</p>
</form>
</section>

View File

@@ -1,25 +0,0 @@
(function (PLUGIN_ID) {
const formEl = document.querySelector('.js-submit-settings');
const cancelButtonEl = document.querySelector('.js-cancel-button');
const messageEl = document.querySelector('.js-text-message');
if (!(formEl && cancelButtonEl && messageEl)) {
throw new Error('Required elements do not exist.');
}
const config = kintone.plugin.app.getConfig(PLUGIN_ID);
if (config.message) {
messageEl.value = config.message;
}
formEl.addEventListener('submit', (e) => {
e.preventDefault();
kintone.plugin.app.setConfig({ message: messageEl.value }, () => {
alert('The plug-in settings have been saved. Please update the app!');
window.location.href = '../../flow?app=' + kintone.app.getId();
});
});
cancelButtonEl.addEventListener('click', () => {
window.location.href = '../../' + kintone.app.getId() + '/plugin/';
});
})(kintone.$PLUGIN_ID);

View File

@@ -1,23 +0,0 @@
(function (PLUGIN_ID) {
kintone.events.on('app.record.index.show', () => {
alert('app.record.index.show run by pluging');
const spaceEl = kintone.app.getHeaderSpaceElement();
if (spaceEl === null) {
throw new Error('The header element is unavailable on this page.');
}
const fragment = document.createDocumentFragment();
const headingEl = document.createElement('h3');
const messageEl = document.createElement('p');
const config = kintone.plugin.app.getConfig(PLUGIN_ID);
messageEl.textContent = config.message;
messageEl.classList.add('plugin-space-message');
headingEl.textContent = 'Hello kintone plugin!';
headingEl.classList.add('plugin-space-heading');
fragment.appendChild(headingEl);
fragment.appendChild(messageEl);
spaceEl.appendChild(fragment);
});
})(kintone.$PLUGIN_ID);

View File

@@ -1,22 +0,0 @@
(function (PLUGIN_ID) {
kintone.events.on('mobile.app.record.index.show', () => {
const spaceEl = kintone.mobile.app.getHeaderSpaceElement();
if (spaceEl === null) {
throw new Error('The header element is unavailable on this page.');
}
const fragment = document.createDocumentFragment();
const headingEl = document.createElement('h3');
const messageEl = document.createElement('p');
const config = kintone.plugin.app.getConfig(PLUGIN_ID);
messageEl.textContent = config.message;
messageEl.classList.add('plugin-space-message');
headingEl.textContent = 'Hello kintone plugin!';
headingEl.classList.add('plugin-space-heading');
fragment.appendChild(headingEl);
fragment.appendChild(messageEl);
spaceEl.appendChild(fragment);
});
})(kintone.$PLUGIN_ID);

View File

@@ -1,45 +0,0 @@
{
"$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": 2,
"type": "APP",
"desktop": {
"js": [
"js/desktop.js"
],
"css": [
"css/51-modern-default.css",
"css/desktop.css"
]
},
"icon": "image/icon.png",
"config": {
"html": "html/config.html",
"js": [
"js/config.js"
],
"css": [
"css/51-modern-default.css",
"css/config.css"
],
"required_params": [
"message"
]
},
"name": {
"en": "data fetch pluging",
"ja": "データ取得プラグイン"
},
"description": {
"en": "create search data pluging",
"ja": "検索結果のデータを生成するプラグインです"
},
"mobile": {
"js": [
"js/mobile.js"
],
"css": [
"css/mobile.css"
]
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +0,0 @@
{
"name": "data-fetch-pluging",
"version": "0.1.1",
"scripts": {
"build": "kintone-plugin-packer --ppk private.ppk --out dist/plugin.zip src",
"develop": "npm run build -- --watch",
"lint": "eslint src",
"start": "npm run develop",
"test":"kintone-plugin-packer --help"
},
"devDependencies": {
"@cybozu/eslint-config": "^23.0.0",
"@kintone/plugin-packer": "^8.1.3",
"eslint": "^8.57.0"
}
}

View File

@@ -1,15 +0,0 @@
"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

@@ -1,7 +0,0 @@
.settings-heading {
padding: 1em 0;
}
.kintoneplugin-input-text {
width: 20em;
}

View File

@@ -1,10 +0,0 @@
.plugin-space-heading {
font-size: 1.5rem;
margin: 0.8rem;
}
.plugin-space-message {
display: inline-block;
font-size: 1.2em;
margin: 0.8rem;
margin-top: 0;
}

View File

@@ -1,16 +0,0 @@
<section class="settings">
<h2 class="settings-heading">Settings for data fetch pluging</h2>
<p class="kintoneplugin-desc">This message is displayed on the app page after the app has been updated.</p>
<form class="js-submit-settings">
<p class="kintoneplugin-row">
<label for="message">
Message:
<input type="text" class="js-text-message kintoneplugin-input-text">
</label>
</p>
<p class="kintoneplugin-row">
<button type="button" class="js-cancel-button kintoneplugin-button-dialog-cancel">Cancel</button>
<button class="kintoneplugin-button-dialog-ok">Save</button>
</p>
</form>
</section>

View File

@@ -1,25 +0,0 @@
(function (PLUGIN_ID) {
const formEl = document.querySelector('.js-submit-settings');
const cancelButtonEl = document.querySelector('.js-cancel-button');
const messageEl = document.querySelector('.js-text-message');
if (!(formEl && cancelButtonEl && messageEl)) {
throw new Error('Required elements do not exist.');
}
const config = kintone.plugin.app.getConfig(PLUGIN_ID);
if (config.message) {
messageEl.value = config.message;
}
formEl.addEventListener('submit', (e) => {
e.preventDefault();
kintone.plugin.app.setConfig({ message: messageEl.value }, () => {
alert('The plug-in settings have been saved. Please update the app!');
window.location.href = '../../flow?app=' + kintone.app.getId();
});
});
cancelButtonEl.addEventListener('click', () => {
window.location.href = '../../' + kintone.app.getId() + '/plugin/';
});
})(kintone.$PLUGIN_ID);

View File

@@ -1,22 +0,0 @@
(function (PLUGIN_ID) {
kintone.events.on('app.record.index.show', () => {
const spaceEl = kintone.app.getHeaderSpaceElement();
if (spaceEl === null) {
throw new Error('The header element is unavailable on this page.');
}
const fragment = document.createDocumentFragment();
const headingEl = document.createElement('h3');
const messageEl = document.createElement('p');
const config = kintone.plugin.app.getConfig(PLUGIN_ID);
messageEl.textContent = config.message;
messageEl.classList.add('plugin-space-message');
headingEl.textContent = 'Hello kintone plugin!';
headingEl.classList.add('plugin-space-heading');
fragment.appendChild(headingEl);
fragment.appendChild(messageEl);
spaceEl.appendChild(fragment);
});
})(kintone.$PLUGIN_ID);

View File

@@ -1,22 +0,0 @@
(function (PLUGIN_ID) {
kintone.events.on('mobile.app.record.index.show', () => {
const spaceEl = kintone.mobile.app.getHeaderSpaceElement();
if (spaceEl === null) {
throw new Error('The header element is unavailable on this page.');
}
const fragment = document.createDocumentFragment();
const headingEl = document.createElement('h3');
const messageEl = document.createElement('p');
const config = kintone.plugin.app.getConfig(PLUGIN_ID);
messageEl.textContent = config.message;
messageEl.classList.add('plugin-space-message');
headingEl.textContent = 'Hello kintone plugin!';
headingEl.classList.add('plugin-space-heading');
fragment.appendChild(headingEl);
fragment.appendChild(messageEl);
spaceEl.appendChild(fragment);
});
})(kintone.$PLUGIN_ID);

Binary file not shown.

View File

@@ -22,3 +22,4 @@ dist-ssr
*.njsproj
*.sln
*.sw?
data

View File

@@ -0,0 +1,41 @@
# data-fetch-plugin
1. コマンド:
- `package.json`ファイルを開き、`scripts`内の`upload`コマンドのパラメータを接続する必要があるkintoneドメインに変更してください。
package.json:
```json
"upload": "kintone-plugin-uploader --base-url https://{YOUR-KINTONE-DOMAIN}.cybozu.com --username {YOUR-USERID} --password {YOUR-PASSWORD} dist/plugin.zip ",
```
- `npm run build` を実行すると、`dist` ディレクトリにパッケージファイルを生成し、`plugin.zip` が作成されます。
- `npm run upload` を実行すると、`plugin.zip` がkintoneにアップロードされます。
- `npm run build-upload` を実行すると、上記両方同時実行されます。
2. Vue3.0を使用した開発:
- 設定ページは `components/Config.vue` にて開発します。
- Desktopページは `js/desktop.ts` にて開発します。
- Mobileページは `js/desktop.ts` にて開発します。
3. 依存環境作成:
- 最新のNode.js と npm のインストール
- Yarnのインストール
```bash
npm install -g yarn
```
- 依存環境をインストール
```bash
yarn
```
- ビルド
```bash
yarn build
```
- pluginアップロード
```bash
yarn upload
```
- ビルド&アップロード
```
yarn build-upload
```
---

View File

@@ -8,6 +8,7 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
Config: typeof import('./src/components/Config.vue')['default']
ErrorDialog: typeof import('./src/components/basic/ErrorDialog.vue')['default']
PluginDropdown: typeof import('./src/components/basic/PluginDropdown.vue')['default']
PluginInput: typeof import('./src/components/basic/PluginInput.vue')['default']
PluginLabel: typeof import('./src/components/basic/PluginLabel.vue')['default']
@@ -20,5 +21,7 @@ declare module 'vue' {
TableCombobox: typeof import('./src/components/basic/TableCombobox.vue')['default']
TableCondition: typeof import('./src/components/basic/conditions/TableCondition.vue')['default']
TableConditionValue: typeof import('./src/components/basic/conditions/TableConditionValue.vue')['default']
TableConditionValueDateTime: typeof import('./src/components/basic/conditions/TableConditionValueDateTime.vue')['default']
TableConditionValueMultiInput: typeof import('./src/components/basic/conditions/TableConditionValueMultiInput.vue')['default']
}
}

View File

@@ -0,0 +1,4 @@
<script type="module" crossorigin src="/src/js/config.js"></script>
<section class="settings">
<div id="app"></div>
</section>

Binary file not shown.

View File

@@ -0,0 +1,260 @@
/* 辅助类 */
.flex-row {
display: flex;
}
.hidden {
visibility: hidden;
}
.border {
border: 1px solid #e3e7e8;
}
/* config 页面 */
#app {
width: 60vw;
min-width: 1030px;
}
/* 最上面的说明 */
.settings-heading {
padding: 1em 0;
}
/* label 样式 */
.kintoneplugin-label {
padding-left: 20px;
line-height: 40px;
}
/* laebl input 单行的情况 */
.flex-row .kintoneplugin-label {
margin: 0;
width: 8.5em;
}
/* 遮罩 */
#main-area {
position: relative;
}
#main-area .kuc-spinner-1-18-0__mask {
position: absolute;
background-color: white;
}
#main-area .kuc-spinner-1-18-0__spinner {
position: absolute;
}
#main-area .kuc-spinner-1-18-0__spinner__loader {
fill: #3498db;
}
/* 表格内容垂直居中 */
.table-area {
margin: 0;
align-items: center;
}
/* 整体边框相关样式 */
.header-row {
padding: 24px 0;
margin: 0;
border-bottom: none;
}
.table-main-area {
flex: 1;
border-right: 1px solid #e3e7e8;
padding-top: 24px;
}
.table-area {
border-bottom: none;
}
.footer-row {
padding: 24px 0;
margin-bottom: 32px;
text-align: right;
/* border-top: none; */
}
/* 底部按钮空间 */
.save-btn {
margin-left: 16px;
margin-right: 24px;
}
/* 输入框宽度 */
.kuc-text-input {
--kuc-text-input-width: max(16vw, 200px);
--kuc-dropdown-toggle-width: max(16vw, 200px);
--kuc-combobox-toggle-width: max(16vw, 200px);
}
.plugin-kuc-table .kuc-text-input {
--kuc-text-input-width: max(15vw, 200px);
--kuc-dropdown-toggle-width: max(15vw, 200px);
--kuc-combobox-toggle-width: max(15vw, 200px);
}
@media screen and (max-width: 1840px) {
.plugin-kuc-table .kuc-text-input {
--kuc-text-input-width: max(13vw, 200px);
--kuc-dropdown-toggle-width: max(13vw, 200px);
--kuc-combobox-toggle-width: max(13vw, 200px);
}
}
@media screen and (max-width: 1760px) {
.plugin-kuc-table .kuc-text-input {
--kuc-text-input-width: max(12vw, 200px);
--kuc-dropdown-toggle-width: max(12vw, 200px);
--kuc-combobox-toggle-width: max(12vw, 200px);
}
}
/* 统一 kintone +/- 按钮样式 */
.kuc-action-button {
width: 24px;
height: 24px;
background: transparent;
border: 1px solid transparent;
padding: 2px;
cursor: pointer;
margin: 0 4px;
}
.kuc-action-button.remove {
margin-right: 8px;
}
.kuc-action-button.add {
margin-left: 8px;
}
.kuc-action-button:focus {
border: 1px solid #3498db;
outline: none;
}
.kuc-action-button.remove:hover path {
fill: #e74c3c;
}
/* 覆盖表格样式 */
.plugin-kuc-table > table > tbody > tr > td {
border-left-color: rgba(0, 0, 0, 0);
border-right-color: rgba(0, 0, 0, 0);
vertical-align: middle;
}
.plugin-kuc-table > table > tbody > tr > td:nth-last-child(2) {
border-right-color: #e3e7e8;
}
.plugin-kuc-table > table > tbody > tr > td:first-child {
border-left-color: #e3e7e8;
}
.plugin-kuc-table > table > tbody > tr > td[class$="table__body__row__action"] {
height: 55px;
align-items: center;
}
.table-option > table > tbody > tr > td[class$="table__body__row__action"] {
height: 40px;
align-items: center;
}
.plugin-kuc-table:not(.condition-table) > table > tbody > tr > td:nth-child(2) {
--kuc-table-header-1-width: 30px;
text-align: center;
}
.condition-table > table > tbody > tr > td[style]:not(:first-child),
.condition-table > table > thead > tr > th[style]:not(:first-child) {
padding-left: 0;
}
/* 絞り込み条件选择相关样式 */
.row-connector-area {
margin: 0 1em;
}
.condition-combobox-short {
--kuc-combobox-toggle-width: 168px;
}
/* .condition-combobox-short {
--kuc-combobox-toggle-width: 140px
}
.condition-combobox-short[data-val='!='] {
--kuc-combobox-toggle-width: 168px
}
.condition-combobox-short[data-val='like'] {
--kuc-combobox-toggle-width: 200px
}
.condition-combobox-short[data-val='in'] {
--kuc-combobox-toggle-width: 185px
}
.condition-combobox-short[data-val='not like'] {
--kuc-combobox-toggle-width: 225px
}
.condition-combobox-short[data-val='not in'] {
--kuc-combobox-toggle-width: 200px
} */
.kuc-text-input-placeholder-width {
--kuc-text-input-width: 258px;
}
.datetime-condition-combobox {
--kuc-combobox-toggle-width: 130px;
}
.datetime-condition-combobox.mid {
--kuc-combobox-toggle-width: 112px;
}
.datetime-condition-combobox.short {
--kuc-combobox-toggle-width: 92px;
}
.datetime-condition-combobox + * {
margin-left: 8px;
}
.datetime-condition-combobox li[value^='\-'] {
user-select: none;
margin: 8px 0;
cursor: default;
padding: 0;
height: 1px;
background-color: #eee;
list-style: none;
pointer-events: none;
}
.week-all-combobox {
--kuc-combobox-toggle-width: 140px;
}
.week-combobox {
--kuc-combobox-toggle-width: 72px;
}
.month-all-combobox {
--kuc-combobox-toggle-width: 100px;
}
.month-combobox {
--kuc-combobox-toggle-width: 86px;
}
.from-today-input {
--kuc-text-input-width: 75px;
--kuc-combobox-toggle-width: 75px;
}
.from-today-input.input {
--kuc-text-input-width: 50px;
--kuc-combobox-toggle-width: 50px;
}
/* .from-today-input error */
.condition-table.plugin-kuc-table > table > tbody > tr > td {
vertical-align: top;
}
.from-today-input.input div[class^="kuc-base-error"] {
position: absolute;
left: 0;
right: 0;
}
.from-today-input.input.error {
margin-bottom: 20px;
}
.table-option td {
padding: 1px;
margin: 0;
border: none;
}
.dialog-action-bar{
text-align: right;
}

View File

@@ -0,0 +1,4 @@
<script type="module" crossorigin src="/src/js/config.js"></script>
<section class="settings">
<div id="app"></div>
</section>

View File

Before

Width:  |  Height:  |  Size: 110 B

After

Width:  |  Height:  |  Size: 110 B

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -5,6 +5,7 @@
"type": "APP",
"desktop": {
"js": [
"js/KintoneRestAPIClient.min.js",
"js/kuc.min.js",
"js/desktop.js"
],
@@ -37,6 +38,7 @@
},
"mobile": {
"js": [
"js/KintoneRestAPIClient.min.js",
"js/kuc.min.js",
"js/mobile.js"
],

File diff suppressed because one or more lines are too long

View File

Before

Width:  |  Height:  |  Size: 110 B

After

Width:  |  Height:  |  Size: 110 B

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,166 @@
<template>
<h2 class="settings-heading">{{ $t('config.title') }}</h2>
<p class="kintoneplugin-desc">{{ $t('config.desc') }}</p>
<plugin-row class="header-row border">
<plugin-input v-model="data.buttonName" placeholder="ボタン名を入力してください" label="集約ボタン名" />
</plugin-row>
<div id="main-area" ref="mainArea">
<plugin-table-area v-for="joinTable in data.joinTables" :table="joinTable" :key="joinTable.id" />
</div>
<plugin-row class="footer-row border">
<kuc-button text="キャンセル" type="normal" @click="cancel" />
<kuc-button :disabled="!canSave" text="保存する" class="save-btn" type="submit" @click="save" />
</plugin-row>
<kuc-spinner :container="mainArea" ref="spinner"></kuc-spinner>
<error-dialog :message="errMessage" :show="showError" @update:show="(value) => showError = value"></error-dialog>
</template>
<script setup lang="ts">
import {
createEmptyJoinTable,
loadApps,
loadAppFieldsAndLayout,
EMPTY_OPTION,
getEmptyOnCondition,
getMeta,
} from '@/js/helper';
import { isType, type OneOf, type Properties } from '@/js/kintone-rest-api-client';
import type { CachedData, FieldsInfo, JoinTable, SavedData } from '@/types/model';
import type { Spinner } from 'kintone-ui-component';
import { onMounted, watch, provide, reactive, ref, shallowRef, nextTick } from 'vue';
const props = defineProps<{ pluginId: string }>();
const loading = ref(false);
const canSave = ref(true);
const data: SavedData = reactive({
buttonName: '',
joinTables: [createEmptyJoinTable()],
});
const showError = ref(false);
const errMessage = ref("");
const cachedData: CachedData = reactive({
apps: [EMPTY_OPTION],
currentAppFields: { fields: {}, layout: [] } as FieldsInfo,
});
provide('savedData', data);
provide('canSave', (data: boolean) => {
canSave.value = data;
});
provide('cachedData', cachedData);
const mainArea = shallowRef<HTMLElement | null>(null);
const spinner = shallowRef<Spinner | null>(null);
onMounted(async () => {
nextTick(async () => {
spinner.value?.close(); // fix bug: kuc-spinner will not auto amount to target HTML element when init loading
const savedData = kintone.plugin.app.getConfig(props.pluginId);
loading.value = true;
cachedData.apps = await loadApps();
cachedData.currentAppFields = await loadAppFieldsAndLayout();
if (savedData?.joinTablesForConfig) {
const newJoinTables = JSON.parse(savedData.joinTablesForConfig);
data.joinTables.length = 0;
data.joinTables.push(...newJoinTables);
}
data.buttonName = savedData?.buttonName || '集約';
loading.value = false;
});
});
watch(loading, (load) => {
load ? spinner.value?.open() : spinner.value?.close();
});
watch(
() => data.joinTables.length,
(newLength) => {
console.log(data.joinTables);
if (newLength === 1) {
data.joinTables[0].onConditions = [getEmptyOnCondition()];
}
},
);
/**
* 保存データのバリデーション関数
*/
function validate(data: SavedData<string>): boolean {
// ボタン名が空の場合、エラーを表示
if (!data.buttonName.trim()) {
errMessage.value = 'ボタン名を入力してください。';
return false;
}
for (const joinTable of data.joinTables) {
// 取得元アプリが空の場合、エラーを表示
if (!joinTable.app.trim()) {
errMessage.value = '取得元アプリを指定してください。';
return false;
}
// 取得フィールドのマッピングが1つ未満の場合、エラーを表示
if (
joinTable.fieldsMapping.length < 1 ||
!joinTable.fieldsMapping[0].leftField?.trim() ||
!joinTable.fieldsMapping[0].rightField?.trim()
) {
errMessage.value = '取得フィールドを1つ以上設定してください。';
return false;
}
}
return true;
}
function save() {
if(!validate(data)){
showError.value=true;
return;
}
const currentAppMeta = cachedData.currentAppFields.fields;
const convertJoinTables = JSON.parse(JSON.stringify(data.joinTables)) as JoinTable<OneOf | string>[];
convertJoinTables.forEach((item) => {
const meta = getMeta(item.meta as Properties, item.table);
if (!meta) return;
// Process onConditions
item.onConditions.forEach((condition) => {
condition.leftField = meta[condition.leftField as string] || condition.leftField;
condition.rightField = currentAppMeta[condition.rightField as string] || condition.rightField;
});
// Process fieldsMapping
item.fieldsMapping.forEach((mapping) => {
mapping.leftField = meta[mapping.leftField as string] || mapping.leftField;
mapping.rightField = currentAppMeta[mapping.rightField as string] || mapping.rightField;
});
// Process whereConditions
item.whereConditions.forEach((condition) => {
condition.field = meta[condition.field as string] || condition.field;
});
delete item.meta;
});
data.joinTables.forEach((item) => {
delete item.meta;
});
kintone.plugin.app.setConfig({
buttonName: data.buttonName,
joinTables: JSON.stringify(convertJoinTables),
joinTablesForConfig: JSON.stringify(data.joinTables || []),
});
}
function cancel() {
window.location.href = `../../${kintone.app.getId()}/plugin/`;
}
</script>

View File

@@ -0,0 +1,79 @@
<template>
<div ref="dialogContainer">
</div>
</template>
<script setup lang="ts">
import { Button, Dialog } from "kintone-ui-component";
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
const props = defineProps<{
message: string;
show: boolean;
}>();
const isDialogVisible = ref(props.show);
watch(
() => props.message,
(newMessage) => {
if (dialog.value) {
dialog.value.content=newMessage;
}
}
);
watch(
() => props.show,
(newValue) => {
if (dialog.value) {
if (newValue) {
dialog.value.open();
} else {
dialog.value.close();
}
}
}
);
const emit = defineEmits(["update:show"]);
const dialog = ref<Dialog | null>(null);
const dialogContainer = ref<HTMLDivElement | null>(null);
const closeDialog = () => {
if (dialog.value) {
dialog.value.close();
}
emit("update:show", false);
}
onMounted(() => {
if (!dialogContainer.value) return;
const okButton = new Button({ text: "OK", type: "normal" });
okButton.addEventListener("click", closeDialog);
const footerDiv = document.createElement("div");
footerDiv.className="dialog-action-bar";
footerDiv.appendChild(okButton);
// 创建 Dialog 实例
dialog.value = new Dialog({
header: "エラー情報",
content: props.message,
icon: "error",
container: dialogContainer.value,
footer: footerDiv,
footerVisible:true
});
if (props.show) {
dialog.value.open();
}
});
</script>

View File

@@ -7,7 +7,7 @@
<script setup lang="ts">
import type { KucEvent } from '@/types/my-kintone';
import type { DropdownItem } from 'kintone-ui-component';
import type { ComboboxChangeEventDetail, DropdownItem } from 'kintone-ui-component';
import { defineProps, defineEmits } from 'vue';
const props = defineProps<{
@@ -21,7 +21,7 @@ const emit = defineEmits<{
(e: 'update:modelValue', value: string): void;
}>();
const updateValue = (event: KucEvent) => {
emit('update:modelValue', event.detail.value);
const updateValue = ({ detail }: KucEvent<ComboboxChangeEventDetail>) => {
emit('update:modelValue', detail.value || '');
};
</script>

View File

@@ -7,6 +7,7 @@
<script setup lang="ts">
import type { KucEvent } from '@/types/my-kintone';
import type { TextInputEventDetail } from 'kintone-ui-component';
import { defineProps, defineEmits } from 'vue';
const props = defineProps<{
@@ -19,7 +20,7 @@ const emit = defineEmits<{
(e: 'update:modelValue', value: string): void;
}>();
const updateValue = (event: KucEvent) => {
emit('update:modelValue', event.detail.value);
const updateValue = ({ detail }: KucEvent<TextInputEventDetail>) => {
emit('update:modelValue', detail.value || '');
};
</script>

View File

@@ -16,15 +16,15 @@
</plugin-row>
<plugin-row class="flex-row" v-if="isJoinConditionShown(table)">
<plugin-label label="連結条件" />
<plugin-table-connect-row connector="=" :modelValue="table.onConditions" />
<plugin-table-connect-row connector="=" type="connect" :modelValue="table.onConditions" />
</plugin-row>
<plugin-row class="flex-row">
<plugin-label label="取得フィールド" />
<plugin-table-connect-row connector="→" :modelValue="table.fieldsMapping" />
<plugin-table-connect-row connector="→" type="mapping" :modelValue="table.fieldsMapping" />
</plugin-row>
<plugin-row class="flex-row">
<plugin-label label="絞込条件" />
<plugin-table-condition-row :modelValue="table.whereConditions"/>
<plugin-table-condition-row :modelValue="table.whereConditions" />
</plugin-row>
</div>
<div class="table-action-area">
@@ -43,7 +43,6 @@ import {
} from '@/js/helper';
import { types } from '@/js/kintone-rest-api-client';
import type { CachedData, CachedSelectedAppData, FieldsInfo, JoinTable, SavedData } from '@/types/model';
import type { KucEvent } from '@/types/my-kintone';
import { computed, inject, provide, reactive, ref, watch } from 'vue';
const savedData = inject<SavedData>('savedData') as SavedData;
@@ -86,13 +85,14 @@ watch(
const fields = await loadAppFieldsAndLayout(newVal);
tableOptions.value = getTableFieldsDropdownItems(fields, types.SUBTABLE);
selectedAppData.appFields = fields;
props.table.meta = fields.fields;
!!oldVal && resetTable(props.table);
loading.value = false;
},
{ immediate: true },
);
const selectTable = (e: KucEvent) => {
const selectTable = () => {
resetConditions(props.table);
};

View File

@@ -1,16 +1,14 @@
<template>
<kuc-table :class-name.camel="['plugin-kuc-table']" :columns="columns" :data="modelValue" />
<kuc-table className="plugin-kuc-table condition-table" :columns="columns" :data="modelValue" />
</template>
<script setup lang="ts">
import type { CachedData, CachedSelectedAppData, SavedData, WhereCondition } from '@/types/model';
import { defineProps, inject, computed, render, h, reactive } from 'vue';
import type { CachedData, CachedSelectedAppData, JoinTable, SavedData, WhereCondition } from '@/types/model';
import { defineProps, inject, computed, render, h, reactive, watch } from 'vue';
import TableCombobox from './TableCombobox.vue';
import { generateId, getFieldsDropdownItems, search } from '@/js/helper';
import type { ConditionValue } from '@/js/conditions';
import TableCondition from './conditions/TableCondition.vue';
import TableConditionValue from './conditions/TableConditionValue.vue';
import type { KucTableEvent } from '@/types/my-kintone';
const props = defineProps<{
modelValue: WhereCondition[];
@@ -19,7 +17,20 @@ const props = defineProps<{
const savedData = inject<SavedData>('savedData') as SavedData;
const cachedData = inject<CachedData>('cachedData') as CachedData;
const selectedAppData = inject<CachedSelectedAppData>('selectedAppData') as CachedSelectedAppData;
const table = computed(() => selectedAppData.table.table);
const canSave = inject<(canSave: boolean) => void>('canSave') as (canSave: boolean) => void;
watch(
() => props.modelValue,
(newValue, oldValue) => {
console.log(newValue);
console.log(oldValue);
},
{
deep: true,
immediate: true,
},
);
const columns = reactive([
{
@@ -33,19 +44,21 @@ const columns = reactive([
const vnode = h(TableCombobox, {
items: computed(() =>
getFieldsDropdownItems(selectedAppData.appFields, {
subTableCode: table.value,
subTableCode: '', //table.value,
baseFilter: undefined,
defaultLabel: 'すべてのレコード',
needAllSubTableField: true,
}),
),
modelValue: (search(props.modelValue, rowData.id) as WhereCondition)?.field || '',
modelValue: computed(() => (search(props.modelValue, rowData.id) as WhereCondition)?.field || ''),
selectedAppData,
dataList: props.modelValue,
id: rowData.id,
'onUpdate:modelValue': (data) => {
const obj = (data.obj as WhereCondition);
const obj = data.obj as WhereCondition;
if (obj) {
obj.field = data.value;
obj.condition = '',
obj.condition = '';
obj.data = '';
}
},
@@ -60,15 +73,12 @@ const columns = reactive([
render: (cellData: string, rowData: WhereCondition) => {
const container = document.createElement('div');
const vnode = h(TableCondition, {
modelValue: (search(props.modelValue, rowData.id) as WhereCondition)?.condition || '',
modelValue: computed(() => (search(props.modelValue, rowData.id) as WhereCondition)?.condition || ''),
selectedAppData,
id: rowData.id,
whereConditions: props.modelValue,
'onUpdate:modelValue': ({obj, value}) => {
if (obj) {
obj.condition = value;
obj.data = '';
}
'onUpdate:modelValue': ({ obj, value }) => {
obj && (obj.condition = value);
},
});
render(vnode, container);
@@ -81,12 +91,13 @@ const columns = reactive([
render: (cellData: string, rowData: WhereCondition) => {
const container = document.createElement('div');
const vnode = h(TableConditionValue, {
modelValue: (search(props.modelValue, rowData.id) as WhereCondition)?.data || '',
modelValue: computed(() => (search(props.modelValue, rowData.id) as WhereCondition)?.data || ''),
selectedAppData,
canSave,
id: rowData.id,
whereConditions: props.modelValue,
'onUpdate:modelValue': ({obj, value}) => {
obj && (obj.data = value);
'onUpdate:modelValue': ({ obj, value }) => {
obj && (obj.data = value);
},
});
render(vnode, container);
@@ -94,5 +105,4 @@ const columns = reactive([
},
},
]);
</script>

View File

@@ -0,0 +1,118 @@
<template>
<kuc-table className='plugin-kuc-table' :columns="columns" :data="modelValue"/>
</template>
<script setup lang="ts">
import type { CachedData, CachedSelectedAppData, FieldsJoinMapping, WhereCondition } from '@/types/model';
import { defineProps, inject, computed, reactive, render, h } from 'vue';
import { generateId, getFieldObj, getFieldsDropdownItems, search } from '@/js/helper';
import { getLeftAvailableJoinType, getRightAvailableJoinType, isLeftJoinForceDisable, isRightJoinForceDisable, } from '@/js/join';
import { getLeftAvailableMappingType, getRightAvailableMappingType } from '@/js/mapping';
import TableCombobox from './TableCombobox.vue';
import { type FieldType, type OneOf } from '@/js/kintone-rest-api-client';
const props = defineProps<{
connector: string;
modelValue: FieldsJoinMapping[];
type: 'connect' | 'mapping';
}>();
const cachedData = inject<CachedData>('cachedData') as CachedData;
const selectedAppData = inject<CachedSelectedAppData>('selectedAppData') as CachedSelectedAppData;
const table = computed(() => selectedAppData.table.table);
const filterFunc = {
connect: {
left: (right?: OneOf | '') => getLeftAvailableJoinType(right),
right: (left?: OneOf | '') => getRightAvailableJoinType(left),
},
mapping: {
left: (right?: OneOf | '') => getLeftAvailableMappingType(right),
right: (left?: OneOf | '') => getRightAvailableMappingType(left),
},
};
const columns = reactive([
{
title: '取得元アプリのフィールド',
field: 'leftField',
render: (cellData: string, rowData: WhereCondition) => {
if (!rowData.id) {
rowData.id = generateId();
}
const container = document.createElement('div');
const vnode = h(TableCombobox, {
items: computed(() => {
const dependFilterField = getField('rightField', rowData.id);
return getFieldsDropdownItems(selectedAppData.appFields, {
subTableCode: table.value,
baseFilter: filterFunc[props.type].left() as FieldType[],
filterType: filterFunc[props.type].left(dependFilterField),
dependFilterField,
defaultDisableCallback: isLeftJoinForceDisable,
});
}),
modelValue: computed(() => (search(props.modelValue, rowData.id) as FieldsJoinMapping)?.leftField || ''),
selectedAppData,
dataList: props.modelValue,
id: rowData.id,
'onUpdate:modelValue': (data) => {
if (data.obj) {
(data.obj as FieldsJoinMapping).leftField = data.value;
}
},
});
render(vnode, container);
return container;
},
},
{
title: '',
field: 'connector',
render: () => {
return props.connector;
},
},
{
title: 'このアプリのフィールド',
field: 'rightField',
render: (cellData: string, rowData: WhereCondition) => {
if (!rowData.id) {
rowData.id = generateId();
}
const container = document.createElement('div');
const vnode = h(TableCombobox, {
items: computed(() => {
const dependFilterField = getField('leftField', rowData.id);
return getFieldsDropdownItems(cachedData.currentAppFields, {
subTableCode: '', // subtable not allowed for current app
baseFilter: filterFunc[props.type].right() as FieldType[],
filterType: filterFunc[props.type].right(dependFilterField),
dependFilterField,
defaultDisableCallback: props.type === 'connect' ? isRightJoinForceDisable : undefined,
});
}),
modelValue: computed(() => (search(props.modelValue, rowData.id) as FieldsJoinMapping)?.rightField || ''),
selectedAppData,
dataList: props.modelValue,
id: rowData.id,
'onUpdate:modelValue': (data) => {
if (data.obj) {
(data.obj as FieldsJoinMapping).rightField = data.value;
}
},
});
render(vnode, container);
return container;
},
},
]);
function getField(key: 'leftField' | 'rightField', id: string) {
const dataRow = search(props.modelValue, id) as FieldsJoinMapping | undefined;
const fieldCode = dataRow ? dataRow[key] || '' : '';
const targetFieldMap = key === 'leftField' ? selectedAppData.appFields : cachedData.currentAppFields;
const targetTable = key === 'leftField' ? table.value : '';
return getFieldObj(fieldCode, targetFieldMap, targetTable);
}
</script>

View File

@@ -1,7 +1,8 @@
<template>
<kuc-combobox
className="kuc-text-input"
:items="items.value"
:value="modelValue"
:value="modelValue.value"
@change="updateValue"
:key="componentKey"
:disabled="selectedAppData.loading == undefined ? false : selectedAppData.loading"
@@ -12,12 +13,12 @@
import { search } from '@/js/helper';
import type { CachedSelectedAppData } from '@/types/model';
import type { KucEvent } from '@/types/my-kintone';
import type { DropdownItem } from 'kintone-ui-component';
import type { ComboboxChangeEventDetail, DropdownItem } from 'kintone-ui-component';
import { defineProps, defineEmits, type Ref, watch, ref } from 'vue';
const props = defineProps<{
items: Ref<DropdownItem[]>;
modelValue: string;
modelValue: Ref<string>;
dataList: any[];
id: string;
selectedAppData: CachedSelectedAppData;
@@ -28,7 +29,7 @@ const componentKey = ref(0);
watch(
() => props.items.value,
() => {
if (!props.modelValue) return;
if (!props.modelValue.value) return;
componentKey.value += 1;
},
{
@@ -45,7 +46,7 @@ const emit = defineEmits<{
(e: 'update:modelValue', data: EmitData): void;
}>();
const updateValue = (event: KucEvent) => {
emit('update:modelValue', { obj: search(props.dataList, props.id), value: event.detail.value });
const updateValue = ({ detail }: KucEvent<ComboboxChangeEventDetail>) => {
emit('update:modelValue', { obj: search(props.dataList, props.id), value: detail.value || '' });
};
</script>

View File

@@ -0,0 +1,66 @@
<template>
<kuc-combobox
v-if="items?.length"
:items="items"
:value="value"
@change.stop="updateValue"
:disabled="selectedAppData.loading == undefined ? false : selectedAppData.loading"
className="condition-combobox-short"
:data-val="value"
/>
</template>
<script setup lang="ts">
import { getAvailableCondition, type ConditionValue } from '@/js/conditions';
import { search } from '@/js/helper';
import type { CachedSelectedAppData, WhereCondition } from '@/types/model';
import type { KucEvent } from '@/types/my-kintone';
import type { ComboboxChangeEventDetail } from 'kintone-ui-component';
import { defineProps, defineEmits, computed, watch, ref, type Ref } from 'vue';
const props = defineProps<{
modelValue: Ref<string>;
selectedAppData: CachedSelectedAppData;
whereConditions: WhereCondition[];
id: string;
}>();
const whereCondition = computed(() => search(props.whereConditions, props.id) as WhereCondition | undefined);
const items = computed(() =>
getAvailableCondition(
whereCondition.value?.field || '',
props.selectedAppData.appFields,
props.selectedAppData.table.table,
),
);
const value = ref(props.modelValue.value);
watch(
() => items,
() => {
if (whereCondition.value?.condition === '') {
// select first option
const option = items.value?.[0] || { value: '' };
value.value = option.value;
updateValue({ detail: option });
}
},
{ deep: true },
);
type EmitData = {
obj?: WhereCondition;
value: ConditionValue;
};
const emit = defineEmits<{
(e: 'update:modelValue', data: EmitData): void;
}>();
const updateValue = ({ detail }: KucEvent<ComboboxChangeEventDetail>) => {
value.value = detail.value || '';
emit('update:modelValue', { obj: whereCondition.value, value: detail.value as ConditionValue });
};
</script>

View File

@@ -0,0 +1,142 @@
<template>
<kuc-text
v-if="valueType === 'kuc-text'"
:value="modelValue.value"
@change="updateValue"
:className="needPlaceholderWidthClass"
:placeholder="placeholder"
:disabled="selectedAppData.loading == undefined ? false : selectedAppData.loading"
/>
<kuc-combobox
v-else-if="valueType === 'kuc-combobox'"
:value="modelValue.value"
@change="updateValue"
:disabled="selectedAppData.loading == undefined ? false : selectedAppData.loading"
/>
<kuc-time-picker
v-else-if="valueType === 'kuc-time'"
:value="modelValue.value"
@change="updateValue"
:disabled="selectedAppData.loading == undefined ? false : selectedAppData.loading"
/>
<table-condition-value-date-time
v-else-if="valueType === 'datetime' || valueType === 'date'"
:time="valueType === 'datetime'"
:value="modelValue.value as string"
@change="updateValue"
:disabled="selectedAppData.loading == undefined ? false : selectedAppData.loading"
/>
<kuc-multi-choice
v-else-if="isMultiChoice"
:value="multiChoice"
:items="multiChoiceItems"
@change="updateMultiChoice"
:disabled="selectedAppData.loading == undefined ? false : selectedAppData.loading"
/>
<table-condition-value-multi-input
v-else-if="isMultiInput"
:value="multiInput"
@change="updateMultiInput"
:disabled="selectedAppData.loading == undefined ? false : selectedAppData.loading"
/>
</template>
<script setup lang="ts">
import { getComponent, multiValueComponent } from '@/js/conditions';
import { getFieldObj, isStringArray, search } from '@/js/helper';
import { isType } from '@/js/kintone-rest-api-client';
import { isSelectType } from '@/js/mapping';
import type { CachedSelectedAppData, StringValue, WhereCondition } from '@/types/model';
import type { KucEvent } from '@/types/my-kintone';
import type {
ComboboxChangeEventDetail,
TextInputEventDetail,
MultiChoiceChangeEventDetail,
} from 'kintone-ui-component';
import { defineProps, defineEmits, computed, type Ref, inject, provide, ref, watch, watchEffect } from 'vue';
const props = defineProps<{
modelValue: Ref<StringValue>;
selectedAppData: CachedSelectedAppData;
whereConditions: WhereCondition[];
id: string;
canSave: (canSave: boolean) => void;
}>();
provide('canSave', props.canSave);
const whereCondition = computed(() => search(props.whereConditions, props.id) as WhereCondition | undefined);
const needPlaceholderWidthClass = computed(() => (placeholder.value ? 'kuc-text-input-placeholder-width' : ''));
const placeholder = computed(() => {
const field = getFieldObj(whereCondition.value?.field || '', props.selectedAppData.appFields, '');
if (isType.GROUP_SELECT(field)) {
return 'グループコードをカンマ区切りで指定';
} else if (isType.ORGANIZATION_SELECT(field)) {
return '組織コードをカンマ区切りで指定';
} else if (isType.USER_SELECT(field) || isType.CREATOR(field) || isType.MODIFIER(field)) {
return 'ログイン名をカンマ区切りで指定';
}
return '';
});
const valueType = computed(() => {
const field = getFieldObj(whereCondition.value?.field || '', props.selectedAppData.appFields, '');
return getComponent(whereCondition.value?.condition || '', field);
});
type EmitData = {
obj?: WhereCondition;
value: StringValue;
};
const emit = defineEmits<{
(e: 'update:modelValue', data: EmitData): void;
}>();
const updateValue = (event: KucEvent<ComboboxChangeEventDetail | TextInputEventDetail>) => {
emit('update:modelValue', { obj: whereCondition.value, value: event.detail.value || '' });
};
// multi choice
const isMultiChoice = computed(() => valueType.value === multiValueComponent.multiChoice);
const multiChoice = computed(() => {
if (!isMultiChoice.value) {
return props.modelValue.value;
}
return isStringArray(props.modelValue.value) ? props.modelValue.value : [];
});
const multiChoiceItems = computed(() => {
if (!isMultiChoice.value) {
return [];
}
const field = getFieldObj(whereCondition.value?.field || '', props.selectedAppData.appFields, '');
const items = [{ label: '--', value: '' }];
if (field && isSelectType(field)) {
const multiOpts = Object.values(field.options).map((opt) => ({ label: opt.label, value: opt.label }));
items.push(...multiOpts);
}
return items;
});
const updateMultiChoice = (event: KucEvent<MultiChoiceChangeEventDetail>) => {
emit('update:modelValue', { obj: whereCondition.value, value: event.detail.value || [] });
};
// multi input
const isMultiInput = computed(() => valueType.value === multiValueComponent.multiInput);
const multiInput = computed(() => {
if (!isMultiInput.value) {
return props.modelValue.value as string[];
}
return isStringArray(props.modelValue.value) ? props.modelValue.value as string[] : ['', ''];
});
const updateMultiInput = (event: KucEvent<string[]>) => {
emit('update:modelValue', { obj: whereCondition.value, value: event.detail || ['', ''] });
};
</script>

View File

@@ -0,0 +1,267 @@
<template>
<div style="display: flex; position: relative">
<kuc-combobox
:value="funcValue"
:items="options"
@change.stop="updateFuncValue"
:disabled="disabled"
:className="shortConditionClass"
:key="time"
/>
<template v-if="isInput()">
<kuc-datetime-picker v-if="time" :value="inputValue" @change.stop="updateValue" :disabled="disabled" />
<kuc-date-picker v-else :value="inputValue" @change.stop="updateValue" :disabled="disabled" />
</template>
<kuc-combobox
v-else-if="isWeek()"
:items="weekOptions"
:value="selectValue"
@change.stop="updateValue"
:disabled="disabled"
:className="weekClassName"
/>
<kuc-combobox
v-else-if="isMonth()"
:items="monthOptions"
:value="selectValue"
@change.stop="updateValue"
:disabled="disabled"
:className="monthClassName"
/>
<template v-if="isFromToday()">
<kuc-text
:error="fromTodayError"
:value="inputValue"
@change.stop="updateFromTodayValue"
:disabled="disabled"
:className="fromTodayError ? 'from-today-input input error' : 'from-today-input input'"
/>
<kuc-combobox
:items="fromOptions"
:value="selectValue"
@change.stop="updateFromTodaySelectValue"
:disabled="disabled"
className="from-today-input"
/>
<kuc-combobox
:items="additionOptions"
:value="additionSelectValue"
@change.stop="updateFromTodayAdditionValue"
:disabled="disabled"
className="from-today-input addition"
/>
</template>
</div>
</template>
<script setup lang="ts">
import { dateFuncMap, getDateFuncList, type DateFuncKey } from '@/js/conditions';
import type { KucEvent } from '@/types/my-kintone';
import type { ComboboxChangeEventDetail } from 'kintone-ui-component';
import { defineProps, defineEmits, computed, ref, watch, inject, type Ref } from 'vue';
const props = defineProps<{
value: string;
time: boolean;
disabled: boolean;
}>();
const canSave = inject<(canSave: boolean) => void>('canSave');
const inputValue = ref('');
const selectValue = ref('');
const additionSelectValue = ref('');
const funcValue = ref('');
const fromTodayError = ref('');
watch(
() => fromTodayError.value,
() => {
canSave?.(!fromTodayError.value);
},
);
const isInput = (func = funcValue.value) => func === dateFuncMap[''].value;
const isWeek = (func = funcValue.value) => func.includes('WEEK');
const isMonth = (func = funcValue.value) => func.includes('MONTH');
const isFromToday = (func = funcValue.value) => func.includes('FROM_TODAY');
const weekClassName = computed(() => {
if (isWeek()) {
return selectValue.value === DEFAULT_WEEK_MONTH ? 'week-all-combobox' : 'week-combobox';
}
return '';
});
const monthClassName = computed(() => {
if (isMonth()) {
return selectValue.value === DEFAULT_WEEK_MONTH ? 'month-all-combobox' : 'month-combobox';
}
return '';
});
const shortConditionClass = computed(() => {
const className = 'datetime-condition-combobox';
if (isInput()) { return className }
if (isFromToday() || funcValue.value === dateFuncMap['NOW'].value) {
return className + ' mid';
} else {
return className + ' short';
}
});
const regex = /^(?<func>\w+)\((?<val>.*)\)$/;
watch(
() => props.value,
(current, before) => {
if (props.value === '') {
// select default one when empty
funcValue.value = dateFuncMap[''].value;
selectValue.value = '';
inputValue.value = '';
return;
}
const match = props.value.match(regex);
funcValue.value = dateFuncMap[(match?.groups?.func || '') as DateFuncKey].value;
const value = match?.groups?.val || (props.value.includes('%') ? '' : props.value);
// TODO set values is this method but isFromToday
if (isInput()) {
inputValue.value = value;
selectValue.value = '';
} else if (isWeek() || isMonth()) {
inputValue.value = '';
selectValue.value = value || DEFAULT_WEEK_MONTH;
} else if (isFromToday() && !(before && isFromToday(before))) {
// only called when first open page
const split = value.split(', ');
inputValue.value = split[0] === '0' ? '' : split[0].replace('-', '');
selectValue.value = split[1] || fromOptions[0].value;
additionSelectValue.value = split[0] ? (split[0].startsWith('-') ? '-' : '+') : '+';
}
},
{ immediate: true },
);
const options = computed(() => {
return getDateFuncList(props.time).map((item) => {
return { label: typeof item.label === 'function' ? item.label(props.time) : item.label, value: item.value };
});
});
const emit = defineEmits<{
(e: 'change', data: KucEvent<{ value: string }>): void;
}>();
const updateValue = (event: KucEvent<ComboboxChangeEventDetail>) => {
emit('change', { detail: { value: buildResult({ value: event.detail.value }) } });
};
const updateFuncValue = (event: KucEvent<ComboboxChangeEventDetail>) => {
selectValue.value = '';
inputValue.value = '';
emit('change', { detail: { value: buildResult({ func: event.detail.value }) } });
};
const updateFromTodayValue = (event: KucEvent<ComboboxChangeEventDetail>) => {
const value = buildFromTodayResult({ value: event.detail.value });
if (!value) return;
inputValue.value = event.detail.value as string;
emit('change', { detail: { value } });
};
const updateFromTodaySelectValue = (event: KucEvent<ComboboxChangeEventDetail>) => {
const value = buildFromTodayResult({ select: event.detail.value });
if (!value) return;
selectValue.value = event.detail.value as string;
emit('change', { detail: { value } });
};
const updateFromTodayAdditionValue = (event: KucEvent<ComboboxChangeEventDetail>) => {
const value = buildFromTodayResult({ addition: event.detail.value });
if (!value) return;
additionSelectValue.value = event.detail.value as string;
emit('change', { detail: { value } });
};
type FromTodayParam = {
value?: string;
select?: string;
addition?: string;
};
function buildFromTodayResult({
value = inputValue.value || '0',
select = selectValue.value || 'DAYS',
addition = additionSelectValue.value || '-',
}: FromTodayParam) {
if (value?.match(/^-?[0-9]+$/)) {
fromTodayError.value = '';
let res = value;
if (value && value !== '0' && addition === '-') {
if (value.startsWith('-')) {
res = res.replace('-', '');
} else {
res = '-' + res;
}
}
return funcValue.value.replace('%s', select).replace('%d', res);
} else if (!fromTodayError.value) {
inputValue.value = value || '';
fromTodayError.value = '整数値を指定してください。';
return;
}
}
type Param = {
func?: string;
value?: string;
};
function buildResult({ func = funcValue.value, value }: Param) {
let val = value || inputValue.value;
if (isWeek(func) || isMonth(func)) {
val = value || selectValue.value;
val = val === DEFAULT_WEEK_MONTH ? '' : val;
} else if (isFromToday(func)) {
return func.replace('%d', val || '0').replace('%s', val || 'DAYS');
}
return func.replace('%s', val || '');
}
const DEFAULT_WEEK_MONTH = '_';
const fromOptions = [
{ label: '日', value: 'DAYS' },
{ label: '周', value: 'WEEKS' },
{ label: '月', value: 'MONTHS' },
{ label: '年', value: 'YEARS' },
];
const additionOptions = [
{ label: '前', value: '-' },
{ label: '後', value: '+' },
];
const weekOptions = [
{ label: 'すべての曜日', value: '_' },
{ label: '日', value: 'SUNDAY' },
{ label: '月', value: 'MONDAY' },
{ label: '火', value: 'TUESDAY' },
{ label: '水', value: 'WEDNESDAY' },
{ label: '木', value: 'THURSDAY' },
{ label: '金', value: 'FRIDAY' },
{ label: '土', value: 'SATURDAY' },
];
const monthOptions = [{ label: 'すべて', value: '_' }];
for (let i = 1; i <= 31; i++) {
monthOptions.push({ label: i.toString() + '日', value: i.toString() });
}
monthOptions.push({ label: '末日', value: 'LAST' });
</script>

View File

@@ -0,0 +1,45 @@
<template>
<kuc-table
className="table-option"
:columns="columns"
:data="data"
@change.stop="updateValue"
:headerVisible="false"
/>
</template>
<script setup lang="ts">
import type { KucEvent } from '@/types/my-kintone';
import { Table, Text, type TableChangeEventDetail } from 'kintone-ui-component';
import { defineProps, defineEmits, ref, watch, h, render, reactive } from 'vue';
interface MultiItem {
value: string;
}
const props = defineProps<{
value: string[];
}>();
const data = ref<MultiItem[]>((props.value || ['', '']).map((x) => ({ value: x })));
const columns = reactive([
{
title: '',
field: 'value',
render: (cellData: string) => {
return new Text({ value: cellData });
},
},
]);
const emit = defineEmits<{
(e: 'change', data: KucEvent<string[]>): void;
}>();
const updateValue = (event: KucEvent<TableChangeEventDetail<MultiItem>>) => {
data.value = event.detail.data || [{ value: '' }, { value: '' }];
const multiData = event.detail.data ? event.detail.data.map((x) => x.value) : [];
emit('change', { detail: [...multiData] });
};
</script>

View File

@@ -0,0 +1,260 @@
/* 辅助类 */
.flex-row {
display: flex;
}
.hidden {
visibility: hidden;
}
.border {
border: 1px solid #e3e7e8;
}
/* config 页面 */
#app {
width: 60vw;
min-width: 1030px;
}
/* 最上面的说明 */
.settings-heading {
padding: 1em 0;
}
/* label 样式 */
.kintoneplugin-label {
padding-left: 20px;
line-height: 40px;
}
/* laebl input 单行的情况 */
.flex-row .kintoneplugin-label {
margin: 0;
width: 8.5em;
}
/* 遮罩 */
#main-area {
position: relative;
}
#main-area .kuc-spinner-1-18-0__mask {
position: absolute;
background-color: white;
}
#main-area .kuc-spinner-1-18-0__spinner {
position: absolute;
}
#main-area .kuc-spinner-1-18-0__spinner__loader {
fill: #3498db;
}
/* 表格内容垂直居中 */
.table-area {
margin: 0;
align-items: center;
}
/* 整体边框相关样式 */
.header-row {
padding: 24px 0;
margin: 0;
border-bottom: none;
}
.table-main-area {
flex: 1;
border-right: 1px solid #e3e7e8;
padding-top: 24px;
}
.table-area {
border-bottom: none;
}
.footer-row {
padding: 24px 0;
margin-bottom: 32px;
text-align: right;
/* border-top: none; */
}
/* 底部按钮空间 */
.save-btn {
margin-left: 16px;
margin-right: 24px;
}
/* 输入框宽度 */
.kuc-text-input {
--kuc-text-input-width: max(16vw, 200px);
--kuc-dropdown-toggle-width: max(16vw, 200px);
--kuc-combobox-toggle-width: max(16vw, 200px);
}
.plugin-kuc-table .kuc-text-input {
--kuc-text-input-width: max(15vw, 200px);
--kuc-dropdown-toggle-width: max(15vw, 200px);
--kuc-combobox-toggle-width: max(15vw, 200px);
}
@media screen and (max-width: 1840px) {
.plugin-kuc-table .kuc-text-input {
--kuc-text-input-width: max(13vw, 200px);
--kuc-dropdown-toggle-width: max(13vw, 200px);
--kuc-combobox-toggle-width: max(13vw, 200px);
}
}
@media screen and (max-width: 1760px) {
.plugin-kuc-table .kuc-text-input {
--kuc-text-input-width: max(12vw, 200px);
--kuc-dropdown-toggle-width: max(12vw, 200px);
--kuc-combobox-toggle-width: max(12vw, 200px);
}
}
/* 统一 kintone +/- 按钮样式 */
.kuc-action-button {
width: 24px;
height: 24px;
background: transparent;
border: 1px solid transparent;
padding: 2px;
cursor: pointer;
margin: 0 4px;
}
.kuc-action-button.remove {
margin-right: 8px;
}
.kuc-action-button.add {
margin-left: 8px;
}
.kuc-action-button:focus {
border: 1px solid #3498db;
outline: none;
}
.kuc-action-button.remove:hover path {
fill: #e74c3c;
}
/* 覆盖表格样式 */
.plugin-kuc-table > table > tbody > tr > td {
border-left-color: rgba(0, 0, 0, 0);
border-right-color: rgba(0, 0, 0, 0);
vertical-align: middle;
}
.plugin-kuc-table > table > tbody > tr > td:nth-last-child(2) {
border-right-color: #e3e7e8;
}
.plugin-kuc-table > table > tbody > tr > td:first-child {
border-left-color: #e3e7e8;
}
.plugin-kuc-table > table > tbody > tr > td[class$="table__body__row__action"] {
height: 55px;
align-items: center;
}
.table-option > table > tbody > tr > td[class$="table__body__row__action"] {
height: 40px;
align-items: center;
}
.plugin-kuc-table:not(.condition-table) > table > tbody > tr > td:nth-child(2) {
--kuc-table-header-1-width: 30px;
text-align: center;
}
.condition-table > table > tbody > tr > td[style]:not(:first-child),
.condition-table > table > thead > tr > th[style]:not(:first-child) {
padding-left: 0;
}
/* 絞り込み条件选择相关样式 */
.row-connector-area {
margin: 0 1em;
}
.condition-combobox-short {
--kuc-combobox-toggle-width: 168px;
}
/* .condition-combobox-short {
--kuc-combobox-toggle-width: 140px
}
.condition-combobox-short[data-val='!='] {
--kuc-combobox-toggle-width: 168px
}
.condition-combobox-short[data-val='like'] {
--kuc-combobox-toggle-width: 200px
}
.condition-combobox-short[data-val='in'] {
--kuc-combobox-toggle-width: 185px
}
.condition-combobox-short[data-val='not like'] {
--kuc-combobox-toggle-width: 225px
}
.condition-combobox-short[data-val='not in'] {
--kuc-combobox-toggle-width: 200px
} */
.kuc-text-input-placeholder-width {
--kuc-text-input-width: 258px;
}
.datetime-condition-combobox {
--kuc-combobox-toggle-width: 130px;
}
.datetime-condition-combobox.mid {
--kuc-combobox-toggle-width: 112px;
}
.datetime-condition-combobox.short {
--kuc-combobox-toggle-width: 92px;
}
.datetime-condition-combobox + * {
margin-left: 8px;
}
.datetime-condition-combobox li[value^='\-'] {
user-select: none;
margin: 8px 0;
cursor: default;
padding: 0;
height: 1px;
background-color: #eee;
list-style: none;
pointer-events: none;
}
.week-all-combobox {
--kuc-combobox-toggle-width: 140px;
}
.week-combobox {
--kuc-combobox-toggle-width: 72px;
}
.month-all-combobox {
--kuc-combobox-toggle-width: 100px;
}
.month-combobox {
--kuc-combobox-toggle-width: 86px;
}
.from-today-input {
--kuc-text-input-width: 75px;
--kuc-combobox-toggle-width: 75px;
}
.from-today-input.input {
--kuc-text-input-width: 50px;
--kuc-combobox-toggle-width: 50px;
}
/* .from-today-input error */
.condition-table.plugin-kuc-table > table > tbody > tr > td {
vertical-align: top;
}
.from-today-input.input div[class^="kuc-base-error"] {
position: absolute;
left: 0;
right: 0;
}
.from-today-input.input.error {
margin-bottom: 20px;
}
.table-option td {
padding: 1px;
margin: 0;
border: none;
}
.dialog-action-bar{
text-align: right;
}

View File

@@ -1,10 +1,11 @@
.plugin-space-heading {
font-size: 1.5rem;
margin: 0.8rem;
}
.plugin-space-message {
display: inline-block;
font-size: 1.2em;
margin: 0.8rem;
margin-top: 0;
}
.plugin-space-heading {
font-size: 1.5rem;
margin: 0.8rem;
}

View File

@@ -0,0 +1,6 @@
export default {
config: {
title: 'Data Fetch Plugin Settings',
desc: 'Set the aggregation button name, data source app, fetch fields, filter conditions, and linking conditions, then save.',
},
};

View File

@@ -0,0 +1,6 @@
export default {
config: {
title: 'データ取得プラグインの設定',
desc: '集約ボタン名とデータ取得元アプリ、取得フィールド、絞込条件や連結条件を設定後、保存してください。',
},
};

View File

@@ -0,0 +1,349 @@
import type { FieldLayout, FieldsJoinMapping, JoinTable, Record, RecordForParameter, SavedData,StringValue, WhereCondition } from "@/types/model";
import { type OneOf, isType } from "./field-types-mobile";
import type { ConditionValue } from "./conditions";
declare var KintoneRestAPIClient: typeof import("@kintone/rest-api-client").KintoneRestAPIClient;
export class KintoneIndexEventHandler {
private config: SavedData<FieldLayout>;
private currentApp: string;
constructor(config: SavedData<FieldLayout>, currentApp: string) {
this.config = config;
this.currentApp = currentApp;
}
public init(): void {
this.addButtonToView();
}
// ボタン追加
private addButtonToView(): void {
const headerSpace =kintone.mobile.app.getHeaderSpaceElement();
if (!headerSpace) {
throw new Error('このページではヘッダー要素が利用できません。');
};
// ボタン追加
if (document.getElementById('btn-data-fetch')) return;
const kuc = Kucs['1.18.0'];
const button = new kuc.MobileButton({
text: this.config.buttonName,
type: "submit",
id: 'btn-data-fetch',
});
button.addEventListener('click', () => this.handleButtonClick());
headerSpace.appendChild(button);
}
// ボタンクリック
private handleButtonClick = async (): Promise<void> => {
const spinner = this.showSpinner();
try {
console.log('データ収集開始...');
await this.execDataFectch();
spinner.close();
location.reload();
} catch (error) {
spinner.close();
const detailError = (error instanceof Error) ? "\n詳細:" + error.message : "";
const errorMsg = `データ収集中処理中例外発生しました。${detailError}`;
console.error(errorMsg, error);
window.alert(errorMsg);
}
}
private showSpinner() {
const kuc = Kucs['1.18.0'];
const spinner = new kuc.Spinner({
text: 'データ収集中',
container: document.body
});
spinner.open();
return spinner;
}
/**
* 検索データ取得&作成処理
*/
private execDataFectch = async (): Promise<void> => {
const mainTable = this.config.joinTables[0];
const mainRecords = await this.fetchDataFromApp(mainTable);
// フィールド結合
let mainData = mainRecords.map((record) => {
const rightRecord: Record = {};
mainTable.fieldsMapping.forEach((mapping => {
rightRecord[this.fieldCode(mapping.rightField)] = record[this.fieldCode(mapping.leftField)]
}));
return rightRecord;
});
const joinTables = this.config.joinTables.filter((table, index) => index > 0);
for (const table of joinTables) {
const subDatas = await this.fetchDataFromApp(table);
mainData = this.leftJoin(mainData, subDatas, table);
// console.log("LeftJoin", mainData);
};
//現在のデータをクリアする
await this.deleteCurrentRecords();
//データを更新する
await this.saveDataToCurrentApp(mainData);
}
/**
* Appからデータを取得する
* @param joinTable 対象アプリかられレコードを取得する
* @returns
*/
private fetchDataFromApp = async (joinTable: JoinTable<FieldLayout>): Promise<Record[]> => {
// Filter 条件作成
const filter = this.getWhereCondition(joinTable.whereConditions);
//取得列を設定する
const fetchFields = joinTable.fieldsMapping.map(map => this.fieldCode(map.leftField));
if (joinTable.table) {
fetchFields.push(joinTable.table);
}
const onFields =joinTable.onConditions.map(cond=>this.fieldCode(cond.leftField));
onFields.forEach(fld=>{
if(!fetchFields.includes(fld)){
fetchFields.push(fld);
}
});
// KintoneRESTAPI
const client = new KintoneRestAPIClient();
const records = await client.record.getAllRecords({
app: joinTable.app,
fields: fetchFields,
condition: filter
});
//console.log("Data Fetch", records);
//SubTableが含まれる場合、フラットなデータに変換する
return this.convertToFlatDatas(records, joinTable.table);
}
/**
* 絞り込み条件式作成
* @param whereCondifions
* @returns
*/
private getWhereCondition(whereCondifions: WhereCondition<FieldLayout>[]): string {
const conds = whereCondifions
.filter((cond) => this.fieldCode(cond.field) !== '');
const condition = conds.map((cond) => {
let condition = cond.condition;
if ("subField" in cond.field && cond.field.subField) {
condition = this.mapConditionForSubField(cond.condition);
}
const condValue = this.getConditionValue(cond.field as OneOf, condition, cond.data);
return `${this.fieldCode(cond.field)} ${condition} ${condValue}`;
}).join(' and ');
return condition;
}
/**
* サブフィールドの演算子対応
* @param condition
* @returns
*/
private mapConditionForSubField(condition: ConditionValue): ConditionValue {
switch (condition) {
case "=":
return "in";
case "!=":
return "not in";
default:
return condition; // 既存の条件をそのまま使用
}
}
/**
* 条件比較値を変換する
* @param field
* @param condition
* @param value
* @returns
*/
private getConditionValue(field: OneOf, condition: ConditionValue, value: StringValue): string {
if (!value) return "\"\"";
if(this.isStringArray(value)){
//マルチデータの場合
const items = (value as string[]).map(item => `"${item.trim()}"`);
return `(${items.join(",")})`;
}
const data = value as string;
if (isType.NUMBER(field) || isType.RECORD_NUMBER(field)) {
// For numbers, return as is
return data;
} else if (isType.DATE(field)) {
// If field is DATE, format as "yyyy-MM-dd" unless it's a reserved function
if (/^\d{4}-\d{2}-\d{2}$/.test(data)){
return `"${data}"`;
}else if(data.match(/^\w+\(.*\)$/)){
return data;
}
const date = new Date(data);
return `"${date.toISOString().split('T')[0]}"`;
} else if (isType.DATETIME(field) || isType.CREATED_TIME(field) || isType.UPDATED_TIME(field)) {
// 関数を使用する場合
if (data.match(/^\w+\(.*\)$/)) {
return data;
}
const dateTime = new Date(data);
return `"${dateTime.toISOString()}"`;
} else if ((condition === "in" || condition === "not in")) {
if (data.includes(",")) {
// Handle "in" and "not in" with comma-separated strings
const items = data.split(",").map(item => `"${item.trim()}"`);
return `(${items.join(",")})`;
} else {
return `("${data}")`;
}
} else {
// Default case for other types (treat as text)
return `"${data}"`;
}
}
private isStringArray=(value:any)=>{
if(Array.isArray(value) && value.every(x=>typeof x ==='string')){
return true;
}
return false;
}
/**
* fieldからコードを取得する
* @param field
* @returns
*/
private fieldCode(field: any): string {
if (!field) {
return "";
}
if (typeof field === 'string' && field) {
return field;
} else if (typeof field === 'object' && 'code' in field) {
return field.code;
}
return "";
}
/**
* ネストされたサブテーブルデータをフラットなデータに変換し、親レコードを複製します。
* @param records レコードの配列
* @returns 変換後のフラットなレコード配列
*/
private convertToFlatDatas(records: Record[], subTable: string): Record[] {
if (!subTable) {
return records;
}
const flattenedData: Record[] = [];
records.forEach((record) => {
// テーブルフィールドが存在するかを確認
if (record[subTable]?.type === "SUBTABLE" && record[subTable].value.length > 0) {
// サブテーブル内の各レコードを処理
record[subTable].value.forEach((nested: Record) => {
// 親レコードのコピーを作成
const flatRecord = { ...record };
// サブテーブルフィールドを抽出してフラットな構造に追加
Object.entries(nested.value).forEach(([key, field]) => {
flatRecord[key] = { value: field.value, type: field.type };
});
// テーブルフィールドを削除
delete flatRecord[subTable];
// 結果の配列に追加
flattenedData.push(flatRecord);
});
} else {
// サブテーブルが空の場合、親レコードをそのまま追加
const flatRecord = { ...record };
delete flatRecord[subTable];
flattenedData.push(flatRecord);
}
});
// console.log("FlatDatas=>", flattenedData);
return flattenedData;
}
/**
* データLeftJoin処理
* @param mainData
* @param subData
* @param onConditions
* @param fieldsMapping
* @returns
*/
private leftJoin(
mainData: Record[],
subData: Record[],
joinTable: JoinTable<FieldLayout>
): Record[] {
const joinedRecords: Record[] = [];
mainData.forEach((mainRecord) => {
const matchedRecords = subData.filter((subRecord) =>
joinTable.onConditions.every(
(cond) => mainRecord[this.fieldCode(cond.rightField)]?.value === subRecord[this.fieldCode(cond.leftField)]?.value
)
);
// マッチ出来ない場合、LEFTの列のみ返す
if (!matchedRecords || matchedRecords.length==0) {
joinedRecords.push(mainRecord);
} else {
matchedRecords.forEach((matchedRecord) => {
// フィールド結合
const combinedRecord: Record = { ...mainRecord };
joinTable.fieldsMapping.forEach((mapping) => {
combinedRecord[this.fieldCode(mapping.rightField)] = matchedRecord[this.fieldCode(mapping.leftField)];
});
joinedRecords.push(combinedRecord);
});
}
});
return joinedRecords;
}
/**
* 現在アプリのすべてレコードを削除する
*/
private async deleteCurrentRecords(): Promise<void> {
const client = new KintoneRestAPIClient();
const currentRecords = await client.record.getAllRecords({
app: this.currentApp,
fields: ["$id"],
});
const deleteRecords = currentRecords.map(record => {
return { id: record.$id.value as string }
});
await client.record.deleteAllRecords({
app: this.currentApp,
records: deleteRecords
});
client.record.addAllRecords
}
/**
* 結合後のデータを現在のアプリに挿入する
* @param records
*/
private async saveDataToCurrentApp(records: Record[]): Promise<void> {
try {
const client = new KintoneRestAPIClient();
const result = await client.record.addAllRecords({
app: this.currentApp,
records: this.convertForUpdate(records)
});
} catch (error) {
console.error('データ作成時エラーが発生しました:', error);
throw error;
}
}
/**
* Recordを更新時の形式を変換する
* @param resords
* @returns
*/
private convertForUpdate(resords: Record[]): RecordForParameter[] {
return resords.map((record) =>
Object.fromEntries(
Object.entries(record).map(([fieldCode, { value }]) => [fieldCode, { value }])
)
);
}
}

View File

@@ -0,0 +1,342 @@
import type { FieldLayout, FieldsJoinMapping, JoinTable, Record, RecordForParameter, SavedData, StringValue,WhereCondition } from "@/types/model";
import { type OneOf, isType } from "./field-types";
import type { ConditionValue } from "./conditions";
declare var KintoneRestAPIClient: typeof import("@kintone/rest-api-client").KintoneRestAPIClient;
export class KintoneIndexEventHandler {
private config: SavedData<FieldLayout>;
private currentApp: string;
constructor(config: SavedData<FieldLayout>, currentApp: string) {
this.config = config;
this.currentApp = currentApp;
}
public init(): void {
this.addButtonToView();
}
// ボタン追加
private addButtonToView(): void {
const headerSpace = kintone.app.getHeaderMenuSpaceElement();
if (!headerSpace) {
throw new Error('このページではヘッダー要素が利用できません。');
};
// ボタン追加
if (document.getElementById('btn-data-fetch')) return;
const kuc = Kucs['1.18.0'];
const button = new kuc.Button({
text: this.config.buttonName,
type: "submit",
id: 'btn-data-fetch',
});
button.addEventListener('click', () => this.handleButtonClick());
headerSpace.appendChild(button);
}
// ボタンクリック
private handleButtonClick = async (): Promise<void> => {
const spinner = this.showSpinner();
try {
console.log('データ収集開始...');
await this.execDataFectch();
spinner.close();
location.reload();
} catch (error) {
spinner.close();
const detailError = (error instanceof Error) ? "\n詳細:" + error.message : "";
const errorMsg = `データ収集中処理中例外発生しました。${detailError}`;
console.error(errorMsg, error);
window.alert(errorMsg);
}
}
private showSpinner() {
const kuc = Kucs['1.18.0'];
const spinner = new kuc.Spinner({
text: 'データ収集中',
container: document.body
});
spinner.open();
return spinner;
}
/**
* 検索データ取得&作成処理
*/
private execDataFectch = async (): Promise<void> => {
const mainTable = this.config.joinTables[0];
const mainRecords = await this.fetchDataFromApp(mainTable);
// フィールド結合
let mainData = mainRecords.map((record) => {
const rightRecord: Record = {};
mainTable.fieldsMapping.forEach((mapping => {
rightRecord[this.fieldCode(mapping.rightField)] = record[this.fieldCode(mapping.leftField)]
}));
return rightRecord;
});
const joinTables = this.config.joinTables.filter((table, index) => index > 0);
for (const table of joinTables) {
const subDatas = await this.fetchDataFromApp(table);
mainData = this.leftJoin(mainData, subDatas, table);
// console.log("LeftJoin", mainData);
};
//現在のデータをクリアする
await this.deleteCurrentRecords();
//データを更新する
await this.saveDataToCurrentApp(mainData);
}
/**
* Appからデータを取得する
* @param joinTable 対象アプリかられレコードを取得する
* @returns
*/
private fetchDataFromApp = async (joinTable: JoinTable<FieldLayout>): Promise<Record[]> => {
// Filter 条件作成
const filter = this.getWhereCondition(joinTable.whereConditions);
//取得列を設定する
const fetchFields = joinTable.fieldsMapping.map(map => this.fieldCode(map.leftField));
if (joinTable.table) {
fetchFields.push(joinTable.table);
}
const onFields =joinTable.onConditions.map(cond=>this.fieldCode(cond.leftField));
onFields.forEach(fld=>{
if(!fetchFields.includes(fld)){
fetchFields.push(fld);
}
});
// KintoneRESTAPI
const client = new KintoneRestAPIClient();
const records = await client.record.getAllRecords({
app: joinTable.app,
fields: fetchFields,
condition: filter
});
//console.log("Data Fetch", records);
//SubTableが含まれる場合、フラットなデータに変換する
return this.convertToFlatDatas(records, joinTable.table);
}
/**
* 絞り込み条件式作成
* @param whereCondifions
* @returns
*/
private getWhereCondition(whereCondifions: WhereCondition<FieldLayout>[]): string {
const conds = whereCondifions
.filter((cond) => this.fieldCode(cond.field) !== '');
const condition = conds.map((cond) => {
let condition = cond.condition;
if ("subField" in cond.field && cond.field.subField) {
condition = this.mapConditionForSubField(cond.condition);
}
const condValue = this.getConditionValue(cond.field as OneOf, condition, cond.data);
return `${this.fieldCode(cond.field)} ${condition} ${condValue}`;
}).join(' and ');
return condition;
}
/**
* サブフィールドの演算子対応
* @param condition
* @returns
*/
private mapConditionForSubField(condition: ConditionValue): ConditionValue {
switch (condition) {
case "=":
return "in";
case "!=":
return "not in";
default:
return condition; // 既存の条件をそのまま使用
}
}
private getConditionValue(field: OneOf, condition: ConditionValue, value: StringValue): string {
if (!value) return "\"\"";
if(this.isStringArray(value)){
//マルチデータの場合
const items = (value as string[]).map(item => `"${item.trim()}"`);
return `(${items.join(",")})`;
}
const data = value as string;
if (isType.NUMBER(field) || isType.RECORD_NUMBER(field)) {
// For numbers, return as is
return data;
} else if (isType.DATE(field)) {
// If field is DATE, format as "yyyy-MM-dd" unless it's a reserved function
if (/^\d{4}-\d{2}-\d{2}$/.test(data)){
return `"${data}"`;
}else if(data.match(/^\w+\(.*\)$/)){
return data;
}
const date = new Date(data);
return `"${date.toISOString().split('T')[0]}"`;
} else if (isType.DATETIME(field) || isType.CREATED_TIME(field) || isType.UPDATED_TIME(field)) {
// If field is DATETIME, format as "yyyy-MM-ddTHH:mm:ssZ"
if (data.match(/^\w+\(.*\)$/)) {
return data;
}
const dateTime = new Date(data);
return `"${dateTime.toISOString()}"`;
} else if ((condition === "in" || condition === "not in")) {
if (data.includes(",")) {
// Handle "in" and "not in" with comma-separated strings
const items = data.split(",").map(item => `"${item.trim()}"`);
return `(${items.join(",")})`;
} else {
return `("${data}")`;
}
} else {
// Default case for other types (treat as text)
return `"${data}"`;
}
}
private isStringArray=(value:any)=>{
if(Array.isArray(value) && value.every(x=>typeof x ==='string')){
return true;
}
return false;
}
/**
* fieldからコードを取得する
* @param field
* @returns
*/
private fieldCode(field: any): string {
if (!field) {
return "";
}
if (typeof field === 'string' && field) {
return field;
} else if (typeof field === 'object' && 'code' in field) {
return field.code;
}
return "";
}
/**
* ネストされたサブテーブルデータをフラットなデータに変換し、親レコードを複製します。
* @param records レコードの配列
* @returns 変換後のフラットなレコード配列
*/
private convertToFlatDatas(records: Record[], subTable: string): Record[] {
if (!subTable) {
return records;
}
const flattenedData: Record[] = [];
records.forEach((record) => {
// テーブルフィールドが存在するかを確認
if (record[subTable]?.type === "SUBTABLE" && record[subTable].value.length > 0) {
// サブテーブル内の各レコードを処理
record[subTable].value.forEach((nested: Record) => {
// 親レコードのコピーを作成
const flatRecord = { ...record };
// サブテーブルフィールドを抽出してフラットな構造に追加
Object.entries(nested.value).forEach(([key, field]) => {
flatRecord[key] = { value: field.value, type: field.type };
});
// テーブルフィールドを削除
delete flatRecord[subTable];
// 結果の配列に追加
flattenedData.push(flatRecord);
});
} else {
// サブテーブルが空の場合、親レコードをそのまま追加
const flatRecord = { ...record };
delete flatRecord[subTable];
flattenedData.push(flatRecord);
}
});
// console.log("FlatDatas=>", flattenedData);
return flattenedData;
}
/**
* データLeftJoin処理
* @param mainData
* @param subData
* @param onConditions
* @param fieldsMapping
* @returns
*/
private leftJoin(
mainData: Record[],
subData: Record[],
joinTable: JoinTable<FieldLayout>
): Record[] {
const joinedRecords: Record[] = [];
mainData.forEach((mainRecord) => {
const matchedRecords = subData.filter((subRecord) =>
joinTable.onConditions.every(
(cond) => mainRecord[this.fieldCode(cond.rightField)]?.value === subRecord[this.fieldCode(cond.leftField)]?.value
)
);
// マッチ出来ない場合、LEFTの列のみ返す
if (!matchedRecords || matchedRecords.length==0) {
joinedRecords.push(mainRecord);
} else {
matchedRecords.forEach((matchedRecord) => {
// フィールド結合
const combinedRecord: Record = { ...mainRecord };
joinTable.fieldsMapping.forEach((mapping) => {
combinedRecord[this.fieldCode(mapping.rightField)] = matchedRecord[this.fieldCode(mapping.leftField)];
});
joinedRecords.push(combinedRecord);
});
}
});
return joinedRecords;
}
/**
* 現在アプリのすべてレコードを削除する
*/
private async deleteCurrentRecords(): Promise<void> {
const client = new KintoneRestAPIClient();
const currentRecords = await client.record.getAllRecords({
app: this.currentApp,
fields: ["$id"],
});
const deleteRecords = currentRecords.map(record => {
return { id: record.$id.value as string }
});
await client.record.deleteAllRecords({
app: this.currentApp,
records: deleteRecords
});
client.record.addAllRecords
}
/**
* 結合後のデータを現在のアプリに挿入する
* @param records
*/
private async saveDataToCurrentApp(records: Record[]): Promise<void> {
try {
const client = new KintoneRestAPIClient();
const result = await client.record.addAllRecords({
app: this.currentApp,
records: this.convertForUpdate(records)
});
} catch (error) {
console.error('データ作成時エラーが発生しました:', error);
throw error;
}
}
/**
* Recordを更新時の形式を変換する
* @param resords
* @returns
*/
private convertForUpdate(resords: Record[]): RecordForParameter[] {
return resords.map((record) =>
Object.fromEntries(
Object.entries(record).map(([fieldCode, { value }]) => [fieldCode, { value }])
)
);
}
}

View File

@@ -0,0 +1,217 @@
import type { FieldsInfo } from '@/types/model';
import { isType, type FieldType, type OneOf } from './kintone-rest-api-client';
import { getFieldObj } from './helper';
// conditionValue = '' | 'eq' | 'ne'
// conditionItem = { value: 'eq', label: '=(等しい)', type: 'input', func: (a: string, b: string) => a === b }
// = conditionMap[conditionValue]
export type ConditionValue = '' | '=' | '!=' | '>=' | '<=' | '<' | '>' | 'like' | 'not like' | 'in' | 'not in';
type ConditionItem = {
value: ConditionValue;
label: string | ((field: OneOf) => string);
type: ComponentType | ((field: OneOf) => ComponentType);
};
export const conditionList: ConditionItem[] = [
{ value: '=', label: '=(等しい)', type: (field) => dateTimeComponentMap[field.type] || 'input' },
{ value: '!=', label: '≠ (等しくない)', type: (field) => dateTimeComponentMap[field.type] || 'input' },
{
value: '<=',
label: (field) => (isDateTimeType(field) ? '≦ (以前)' : '≦ (以下)'),
type: (field) => dateTimeComponentMap[field.type] || 'input',
},
{ value: '<', label: '< (より前)', type: (field) => dateTimeComponentMap[field.type] || 'input' },
{
value: '>=',
label: (field) => (isDateTimeType(field) ? '≧ (以降)' : '≧ (以上)'),
type: (field) => dateTimeComponentMap[field.type] || 'input',
},
{ value: '>', label: '> (より後)', type: (field) => dateTimeComponentMap[field.type] || 'input' },
{ value: 'like', label: '次のキーワードを含む', type: 'input' },
{ value: 'not like', label: '次のキーワードを含まない', type: 'input' },
{
value: 'in',
label: (field) => (isMultiInputType(field) ? '次のいずれかと等しい' : '次のいずれかを含む'),
type: (field) => multiValueComponentMap[field.type] || 'input',
},
{
value: 'not in',
label: (field) => (isMultiInputType(field) ? '次のいずれとも等しくない' : '次のいずれも含まない'),
type: (field) => multiValueComponentMap[field.type] || 'input',
},
];
// search from conditionList
// conditionItem = conditionMap[conditionValue]
export const conditionMap: Record<ConditionValue, ConditionItem> = conditionList.reduce(
(map, item) => {
map[item.value] = item;
return map;
},
{} as Record<ConditionValue, ConditionItem>,
);
type FieldConditions = Partial<Record<FieldType, ConditionValue[]>>;
const textCondition: ConditionValue[] = ['=', '!=', 'in', 'like', 'not like'];
const numberCondition: ConditionValue[] = ['=', '!=', '<=', '>='];
const timeCondition: ConditionValue[] = ['=', '!=', '<=', '>=', '<', '>'];
const containsCondition: ConditionValue[] = ['in', 'not in'];
// FieldType -> ConditionValue[]
const fieldConditions: FieldConditions = {
SINGLE_LINE_TEXT: textCondition,
MULTI_LINE_TEXT: containsCondition,
RICH_TEXT: containsCondition,
NUMBER: numberCondition,
CHECK_BOX: containsCondition,
RADIO_BUTTON: containsCondition,
DROP_DOWN: containsCondition,
MULTI_SELECT: containsCondition,
USER_SELECT: containsCondition,
ORGANIZATION_SELECT: containsCondition,
GROUP_SELECT: containsCondition,
LINK: textCondition,
CALC: numberCondition,
TIME: timeCondition,
DATE: timeCondition,
DATETIME: timeCondition,
CREATED_TIME: timeCondition,
CREATOR: containsCondition,
UPDATED_TIME: timeCondition,
MODIFIER: containsCondition,
RECORD_NUMBER: numberCondition,
} as const;
// fieldCode -> conditionList: ConditionItem[]
export const getAvailableCondition = (fieldCode: string, fieldsInfo: FieldsInfo, subTableCode: string | '') => {
if (!fieldCode || !fieldsInfo.fields) return;
const fieldObj = getFieldObj(fieldCode, fieldsInfo, '');
if (!fieldObj) return;
const conditions = fieldConditions[fieldObj.type] || textCondition; // TODO a fallback here
return conditions.map((condition) => {
const res = { ...conditionMap[condition] };
res.label = typeof res.label === 'function' ? res.label(fieldObj) : res.label;
return res;
});
};
const dateTimeComponent = {
time: 'kuc-time',
date: 'date',
datetime: 'datetime',
} as const;
export const multiValueComponent = {
multiChoice: 'kuc-multichoice',
multiInput: 'multi-input',
} as const;
const component = {
input: 'kuc-text',
select: 'kuc-combobox',
...dateTimeComponent,
...multiValueComponent,
} as const;
export type ComponentType = keyof typeof component;
const isDateTimeType = (field: OneOf) => {
return field.type in dateTimeComponentMap;
};
const isMultiInputType = (field: OneOf) => {
return multiValueComponentMap[field.type] === 'multiInput';
};
const dateTimeComponentMap: Partial<Record<FieldType, keyof typeof dateTimeComponent>> = {
TIME: 'time',
DATE: 'date',
DATETIME: 'datetime',
CREATED_TIME: 'datetime',
UPDATED_TIME: 'datetime',
};
const multiValueComponentMap: Partial<Record<FieldType, keyof typeof multiValueComponent>> = {
CHECK_BOX: 'multiChoice',
DROP_DOWN: 'multiChoice',
RADIO_BUTTON: 'multiChoice',
MULTI_SELECT: 'multiChoice',
SINGLE_LINE_TEXT: 'multiInput',
LINK: 'multiInput',
};
export const getComponent = (value: ConditionValue, fieldObj: OneOf) => {
if (!value || !fieldObj) return;
const condition = conditionMap[value].type;
return component[typeof condition === 'function' ? condition(fieldObj) : condition];
};
type DateFuncItem = {
value: string;
label: string | ((isTime: boolean) => string);
condition?: 'datetime' | 'date';
key: DateFuncKey;
};
export type DateFuncKey =
| ''
| 'FROM_TODAY'
| '---NOW---'
| 'NOW'
| '---DAY---'
| 'YESTERDAY'
| 'TODAY'
| 'TOMORROW'
| '---WEEK---'
| 'LAST_WEEK'
| 'THIS_WEEK'
| 'NEXT_WEEK'
| '---MONTH---'
| 'LAST_MONTH'
| 'THIS_MONTH'
| 'NEXT_MONTH'
| '---YEAR---'
| 'LAST_YEAR'
| 'THIS_YEAR'
| 'NEXT_YEAR';
export const dateFuncList: DateFuncItem[] = [
{ key: '', value: '%s', label: (isTime) => (isTime ? '日時を指定' : '日付を指定') },
{ key: 'FROM_TODAY', value: 'FROM_TODAY(%d, %s)', label: '今日から' },
{ key: '---NOW---', value: '\---NOW---', label: '' },
{ key: 'NOW', value: 'NOW()', label: '当時刻', condition: 'datetime' },
{ key: '---DAY---', value: '\---DAY---', label: '', condition: 'datetime' },
{ key: 'YESTERDAY', value: 'YESTERDAY()', label: '昨日' },
{ key: 'TODAY', value: 'TODAY()', label: '今日' },
{ key: 'TOMORROW', value: 'TOMORROW()', label: '明日' },
{ key: '---WEEK---', value: '\---WEEK---', label: '' },
{ key: 'LAST_WEEK', value: 'LAST_WEEK(%s)', label: '先週' },
{ key: 'THIS_WEEK', value: 'THIS_WEEK(%s)', label: '今週' },
{ key: 'NEXT_WEEK', value: 'NEXT_WEEK(%s)', label: '来週' },
{ key: '---MONTH---', value: '\---MONTH---', label: '' },
{ key: 'LAST_MONTH', value: 'LAST_MONTH(%s)', label: '先月' },
{ key: 'THIS_MONTH', value: 'THIS_MONTH(%s)', label: '今月' },
{ key: 'NEXT_MONTH', value: 'NEXT_MONTH(%s)', label: '来月' },
{ key: '---YEAR---', value: '\---YEAR---', label: '' },
{ key: 'LAST_YEAR', value: 'LAST_YEAR()', label: '昨年' },
{ key: 'THIS_YEAR', value: 'THIS_YEAR()', label: '今年' },
{ key: 'NEXT_YEAR', value: 'NEXT_YEAR()', label: '来年' },
];
// search from dateFuncList
// DateFuncItem = dateFuncMap[DateFuncKey]
export const dateFuncMap: Record<DateFuncKey, DateFuncItem> = dateFuncList.reduce(
(map, item) => {
map[item.key] = item;
return map;
},
{} as Record<DateFuncKey, DateFuncItem>,
);
export const getDateFuncList = (hasTime: boolean) => {
return dateFuncList.filter((item) => {
return item.condition ? item.condition === (hasTime ? 'datetime' : 'date') : true;
});
};

View File

@@ -0,0 +1,34 @@
import type { Field, FieldLayout, SavedData } from "@/types/model";
import { KintoneIndexEventHandler } from "./KintoneIndexEventHandler";
(function (PLUGIN_ID) {
kintone.events.on('app.record.index.show', (event) => {
try{
const setting = kintone.plugin.app.getConfig(PLUGIN_ID);
const config:SavedData<FieldLayout> = getConfig(setting);
const currentApp = kintone.app.getId()?.toString();
if(!currentApp) return;
const handler = new KintoneIndexEventHandler(config,currentApp);
handler.init();
}catch(error){
const detailError =(error instanceof Error) ? "\n詳細:" + error.message : "";
const errorMsg = `データ収集中処理中例外発生しました。${ detailError }`;
event.error = errorMsg;
}
return event;
});
/**
* Config設定値を変換する
* @param setting
* @returns
*/
function getConfig(setting:any):SavedData<FieldLayout>{
const config:SavedData<FieldLayout>={
buttonName:setting.buttonName,
joinTables:JSON.parse(setting.joinTables)
}
return config;
}
})(kintone.$PLUGIN_ID);

View File

@@ -0,0 +1,55 @@
declare var KintoneRestAPIClient: typeof import("@kintone/rest-api-client").KintoneRestAPIClient;
const client = new KintoneRestAPIClient();
export type Properties = Awaited<ReturnType<typeof client.app.getFormFields>>['properties'];
export type Layout = Awaited<ReturnType<typeof client.app.getFormLayout>>['layout'];
export type OneOf = Properties[string];
export type FieldType = OneOf['type'];
const typeNames = [
'RECORD_NUMBER',
'CREATOR',
'CREATED_TIME',
'MODIFIER',
'UPDATED_TIME',
'CATEGORY',
'STATUS',
'STATUS_ASSIGNEE',
'SINGLE_LINE_TEXT',
'NUMBER',
'CALC',
'MULTI_LINE_TEXT',
'RICH_TEXT',
'LINK',
'CHECK_BOX',
'RADIO_BUTTON',
'DROP_DOWN',
'MULTI_SELECT',
'FILE',
'DATE',
'TIME',
'DATETIME',
'USER_SELECT',
'ORGANIZATION_SELECT',
'GROUP_SELECT',
'GROUP',
'REFERENCE_TABLE',
'SUBTABLE',
] as const satisfies readonly FieldType[];
export const types = typeNames.reduce(
(acc, name) => {
acc[name] = name;
return acc;
},
{} as Record<(typeof typeNames)[number], (typeof typeNames)[number]>,
);
type ExtractOneOf<T extends FieldType> = Extract<OneOf, { type: T }>;
function createTypeGuard<T extends FieldType>(type: T) {
return (value: OneOf): value is ExtractOneOf<T> => value?.type === type;
}
export const isType = Object.fromEntries(
typeNames.map((typeName) => [typeName, createTypeGuard(typeName as FieldType)]),
) as { [K in (typeof typeNames)[number]]: (value: OneOf) => value is ExtractOneOf<K> };

View File

@@ -0,0 +1,55 @@
declare var KintoneRestAPIClient: typeof import("@kintone/rest-api-client").KintoneRestAPIClient;
const client = new KintoneRestAPIClient();
export type Properties = Awaited<ReturnType<typeof client.app.getFormFields>>['properties'];
export type Layout = Awaited<ReturnType<typeof client.app.getFormLayout>>['layout'];
export type OneOf = Properties[string];
export type FieldType = OneOf['type'];
const typeNames = [
'RECORD_NUMBER',
'CREATOR',
'CREATED_TIME',
'MODIFIER',
'UPDATED_TIME',
'CATEGORY',
'STATUS',
'STATUS_ASSIGNEE',
'SINGLE_LINE_TEXT',
'NUMBER',
'CALC',
'MULTI_LINE_TEXT',
'RICH_TEXT',
'LINK',
'CHECK_BOX',
'RADIO_BUTTON',
'DROP_DOWN',
'MULTI_SELECT',
'FILE',
'DATE',
'TIME',
'DATETIME',
'USER_SELECT',
'ORGANIZATION_SELECT',
'GROUP_SELECT',
'GROUP',
'REFERENCE_TABLE',
'SUBTABLE',
] as const satisfies readonly FieldType[];
export const types = typeNames.reduce(
(acc, name) => {
acc[name] = name;
return acc;
},
{} as Record<(typeof typeNames)[number], (typeof typeNames)[number]>,
);
type ExtractOneOf<T extends FieldType> = Extract<OneOf, { type: T }>;
function createTypeGuard<T extends FieldType>(type: T) {
return (value: OneOf): value is ExtractOneOf<T> => value?.type === type;
}
export const isType = Object.fromEntries(
typeNames.map((typeName) => [typeName, createTypeGuard(typeName as FieldType)]),
) as { [K in (typeof typeNames)[number]]: (value: OneOf) => value is ExtractOneOf<K> };

View File

@@ -0,0 +1,211 @@
import type { FieldsInfo, FieldsJoinMapping, JoinTable, WhereCondition } from '@/types/model';
import {
client,
isType,
type FieldType,
type App,
type Layout,
type OneOf,
type Properties,
} from './kintone-rest-api-client';
import type { ComboboxItem, DropdownItem } from 'kintone-ui-component';
import { isSpecialType, type SpecialType } from './join';
export const EMPTY_OPTION = {
value: '',
label: '--------',
} as DropdownItem;
export function generateId(): string {
const timestamp = new Date().getTime().toString(36);
const randomNum = Math.random().toString(36).substring(2, 11);
return `${timestamp}-${randomNum}`;
}
export function search(list: Array<WhereCondition | FieldsJoinMapping>, id: string) {
if (!list) return;
return list.find((item) => item.id === id);
}
export const getEmptyWhereCondition = () =>
({ field: '', condition: '', data: '', id: generateId() }) as WhereCondition;
export const getEmptyOnCondition = () => ({ leftField: '', rightField: '', id: generateId() }) as FieldsJoinMapping;
export const getEmptyFieldsMapping = () => ({ leftField: '', rightField: '', id: generateId() }) as FieldsJoinMapping;
export function createEmptyJoinTable(id = generateId()) {
return resetTable({ id, app: '' } as JoinTable);
}
export function resetTable(table: JoinTable) {
table.table = '';
return resetConditions(table);
}
export function resetConditions(table: JoinTable) {
table.onConditions = [getEmptyOnCondition()];
table.fieldsMapping = [getEmptyFieldsMapping()];
table.whereConditions = [getEmptyWhereCondition()];
return table;
}
const LIMIT = 100; // 毎回請求の最大値
export const loadApps = async (offset = 0, _apps: DropdownItem[] = []): Promise<DropdownItem[]> => {
const { apps } = await client.app.getApps({ limit: LIMIT, offset });
const allApps: DropdownItem[] = [
..._apps,
...apps.map((app: App) => ({ value: app.appId, label: app.name + 'ID: ' + app.appId + '' })),
];
if (apps.length === LIMIT) {
return loadApps(offset + LIMIT, allApps);
}
allApps.sort((a, b) => Number(b.value) - Number(a.value));
allApps.unshift(EMPTY_OPTION);
return allApps;
};
export const loadAppFieldsAndLayout = async (appId: string | number = kintone.app.getId() as number) => {
const fields = (await client.app.getFormFields({ app: appId })).properties;
return {
fields: flatFields(fields),
layout: (await client.app.getFormLayout({ app: appId })).layout,
} as FieldsInfo;
};
function flatFields(fields: Properties) {
const subtableFields = {} as Properties;
Object.values(fields).forEach((field) => {
if (isType.SUBTABLE(field)) {
Object.values(field.fields).forEach((subField) => {
const copy = JSON.parse(JSON.stringify(subField)) as typeof subField & {originLabel:string, tableCode:string};
copy.label = '[' + field.label + '].' + subField.label;
copy.originLabel = subField.label;
copy.tableCode = field.code;
subtableFields[subField.code] = copy;
});
}
});
return { ...fields, ...subtableFields };
}
type FilterType = Array<FieldType | SpecialType>;
type Param = {
subTableCode: string | undefined;
filterType?: FilterType;
baseFilter: FieldType[] | undefined;
dependFilterField?: OneOf;
defaultLabel?: string;
defaultDisableCallback?: (field: OneOf) => boolean;
needAllSubTableField?: boolean;
};
export const getFieldsDropdownItems = (
{ fields, layout }: FieldsInfo,
{
subTableCode, // specified subTable
baseFilter, // set not allowed items hidden, undefined means no filter
defaultLabel, // label shown (default '--------')
filterType, // set not allowed items disabled, undefined and [] means no filter
dependFilterField, // used for filterType
defaultDisableCallback, // callback to control disabled items, like filterType
needAllSubTableField = false, // show all subtable fields
}: Param,
) => {
// get used field codes
const fieldOrder = extractFields(layout, baseFilter, !!needAllSubTableField, subTableCode);
const fieldMap = fields;
// create labels
const labels: ComboboxItem[] = [
{
value: EMPTY_OPTION.value,
label: defaultLabel || EMPTY_OPTION.label,
},
];
return fieldOrder.reduce((acc, fieldCode) => {
const field = fieldMap[fieldCode];
if (!fieldCode) return acc;
acc.push({
value: fieldCode,
label: field.label + 'FC: ' + fieldCode + '',
disabled:
(defaultDisableCallback && defaultDisableCallback(field)) ||
(filterType && !checkFilterType(field, dependFilterField, filterType)),
});
return acc;
}, labels);
};
const checkFilterType = (field: OneOf, dependFilterField: OneOf | undefined, filterType: FilterType) => {
if (!filterType.length) return true; // [] means no filter
return !!filterType.find((type) => {
if (isSpecialType(type)) {
return type.check(field, dependFilterField);
}
return isType[type](field);
});
};
export const getTableFieldsDropdownItems = ({ fields }: FieldsInfo, filterType?: FieldType) => {
return Object.keys(fields).reduce(
(acc, fieldCode) => {
const field = fields[fieldCode];
if (filterType && !isType[filterType](field)) return acc;
acc.push({
value: fieldCode,
label: field.label + 'FC: ' + fieldCode + '',
});
return acc;
},
[EMPTY_OPTION],
);
};
const extractFields = (layout: Layout, baseFilter: FieldType[] | undefined, needAllSubTableField: boolean, subTableCode?: string) => {
return layout.reduce((acc, each) => {
if (each.type === 'GROUP') {
acc.push(...extractFields(each.layout, baseFilter, needAllSubTableField, subTableCode));
} else if (each.type === 'ROW' || (!needAllSubTableField && each.code === subTableCode) || (needAllSubTableField && each.type === 'SUBTABLE')) {
acc.push(
...each.fields.map((field) => {
if (!('code' in field)) return '';
if (!baseFilter) return field.code;
return baseFilter.find((t) => t === field.type) ? field?.code || '' : '';
}),
);
}
return acc;
}, [] as string[]);
};
export function getFieldObj(fieldCode: string, { fields }: FieldsInfo, subTableCode?: string) {
const meta = getMeta(fields, subTableCode);
return meta[fieldCode];
}
export function getMeta(fields: Properties, subTableCode?: string, withNoSubTableField = true) {
if (!fields || !subTableCode) {
return fields;
}
let meta = fields;
const table = meta[subTableCode];
if (isType.SUBTABLE(table)) {
const subFields = table.fields;
Object.values(subFields).forEach((field) => {
if (typeof field === 'object' && field !== null) {
(field as Record<string, any>).subField = true;
}
});
if (withNoSubTableField) {
meta = { ...fields, ...subFields };
} else {
meta = subFields;
}
}
return meta;
}
export const isStringArray = (value: any) => {
if (Array.isArray(value) && value.every((x) => typeof x === 'string')) {
return true;
}
return false;
};

View File

@@ -0,0 +1,171 @@
import type { CalcType, LinkProtocolType } from '@/types/my-kintone';
import { isType, type FieldType, type OneOf } from './kintone-rest-api-client';
import { KintoneFormFieldProperty } from '@kintone/rest-api-client';
export function isLeftJoinForceDisable(field: OneOf) {
if (isType.CALC(field)) {
return field.format === 'DAY_HOUR_MINUTE' || field.format === 'HOUR_MINUTE';
}
return false;
}
export function isRightJoinForceDisable(field: OneOf) {
if (isLookup(field)) {
return true;
}
return false;
}
export type SpecialType<T = string, F = any> = {
type: T;
format: F;
check: (checkField: OneOf, selectedField?: OneOf) => boolean;
};
// LEFT
// LEFT - lookup
export type LookupTypeL2R = SpecialType<'LOOKUP_FROM_LEFT', FieldType[]>;
export const forMayLookup = (format: FieldType[]): LookupTypeL2R => {
return {
type: 'LOOKUP_FROM_LEFT',
format,
check: function (checkField: OneOf, selectedLeftField?: OneOf) {
if (isLookup(checkField) && selectedLeftField) {
return isLookup(selectedLeftField) ? checkField.type === selectedLeftField.type : false;
}
return !!this.format?.find((e) => e === checkField.type);
},
};
};
export const mayLookupText = forMayLookup(['SINGLE_LINE_TEXT']);
export const mayLookupNumber = forMayLookup(['NUMBER']);
export const mayLookupTextNumber = forMayLookup(['SINGLE_LINE_TEXT', 'NUMBER']);
// LEFT - calc
export type CalcTypeL2R = SpecialType<'CALC_FROM_LEFT', Record<CalcType, Array<FieldType | LookupTypeL2R>>>;
export const leftCalcType: CalcTypeL2R = {
type: 'CALC_FROM_LEFT',
format: {
NUMBER: [mayLookupTextNumber],
NUMBER_DIGIT: [mayLookupTextNumber],
DATE: ['DATE'],
TIME: ['TIME'],
DATETIME: ['DATETIME'],
HOUR_MINUTE: [],
DAY_HOUR_MINUTE: [],
},
check: function (checkField: OneOf, selectedLeftField?: OneOf) {
let allowed: Array<FieldType | LookupTypeL2R> = [];
if (selectedLeftField && isType.CALC(selectedLeftField)) {
allowed = this.format[selectedLeftField.format];
}
return !!allowed.find((e) => {
if (isSpecialType(e) && isLookupFromLeft(e)) {
return e.check(checkField, selectedLeftField);
}
return e === checkField.type;
});
},
};
// LEFT - link
export type LinkType = SpecialType<'LINK', Record<LinkProtocolType, LinkProtocolType[]>>;
export const linkType: LinkType = {
type: 'LINK',
format: {
// 入力値の種別が同じ場合のみ
WEB: ['WEB'],
CALL: ['CALL'],
MAIL: ['MAIL'],
},
check: function (checkField: OneOf, selectedField?: OneOf) {
let allowed: LinkProtocolType[] = [];
if (selectedField && isType.LINK(selectedField)) {
allowed = this.format[selectedField.protocol];
}
if (checkField && isType.LINK(checkField)) {
return !!allowed.find((e) => e === checkField.protocol);
}
return false;
},
};
// LEFT - rule
export type AvailableRight = FieldType | CalcTypeL2R | LinkType | LookupTypeL2R;
const availableLeftJoinType = {
SINGLE_LINE_TEXT: [mayLookupText],
NUMBER: [mayLookupNumber],
CALC: [leftCalcType],
DATE: ['DATE'],
TIME: ['TIME'],
DATETIME: ['DATETIME'],
LINK: [linkType],
} as Record<FieldType, AvailableRight[]>;
// RIGHT - calc
export type CalcTypeR2L = SpecialType<'CALC_FROM_RIGHT', CalcType[]>;
export const forCalc = (format: CalcType[]): CalcTypeR2L => {
return {
type: 'CALC_FROM_RIGHT',
format,
check: function (checkField: OneOf, selectedRightField?: OneOf) {
return isType.CALC(checkField) && !!this.format?.find((e) => e === checkField.format);
},
};
};
// RIGHT - rule
export type AvailableLeft = FieldType | CalcTypeR2L | LinkType;
const availableRightJoinType = {
SINGLE_LINE_TEXT: ['SINGLE_LINE_TEXT', forCalc(['NUMBER', 'NUMBER_DIGIT'])],
NUMBER: ['NUMBER', forCalc(['NUMBER', 'NUMBER_DIGIT'])],
DATE: ['DATE', forCalc(['DATE'])],
TIME: ['TIME', forCalc(['TIME'])],
DATETIME: ['DATETIME', forCalc(['DATETIME'])],
LINK: [linkType],
} as Record<FieldType, AvailableLeft[]>;
// methods
// undefined means all
export function getRightAvailableJoinType(left?: OneOf | '') {
if (left === undefined) {
return Object.keys(availableRightJoinType) as FieldType[];
}
return left ? availableLeftJoinType[left.type] : [];
}
// undefined means all
export function getLeftAvailableJoinType(right?: OneOf | '') {
if (right === undefined) {
return Object.keys(availableLeftJoinType) as FieldType[];
}
return right ? availableRightJoinType[right.type] : [];
}
export function isSpecialType(obj: FieldType | SpecialType): obj is SpecialType {
return typeof obj === 'object' && !Array.isArray(obj) && 'type' in obj;
}
export function isLookupFromLeft(obj: SpecialType): obj is LookupTypeL2R {
return obj.type === 'LOOKUP_FROM_LEFT';
}
export function isLinkType(obj: SpecialType): obj is LinkType {
return obj.type === 'LINK';
}
export function isCalcFromLeft(obj: SpecialType): obj is CalcTypeL2R {
return obj.type === 'CALC_FROM_LEFT';
}
export function isCalcFromRight(obj: SpecialType): obj is CalcTypeR2L {
return obj.type === 'CALC_FROM_RIGHT';
}
export function isLookup(field: OneOf): field is KintoneFormFieldProperty.Lookup {
return 'lookup' in field;
}

View File

@@ -10,7 +10,7 @@ export type App = {
export type Properties = Awaited<ReturnType<typeof client.app.getFormFields>>['properties'];
export type Layout = Awaited<ReturnType<typeof client.app.getFormLayout>>['layout'];
type OneOf = Properties[string];
export type OneOf = Properties[string];
export type FieldType = OneOf['type'];
const typeNames = [
@@ -54,7 +54,7 @@ export const types = typeNames.reduce(
type ExtractOneOf<T extends FieldType> = Extract<OneOf, { type: T }>;
function createTypeGuard<T extends FieldType>(type: T) {
return (value: OneOf): value is ExtractOneOf<T> => value.type === type;
return (value: OneOf): value is ExtractOneOf<T> => value?.type === type;
}
export const isType = Object.fromEntries(

View File

@@ -0,0 +1,103 @@
import { type FieldType, type OneOf } from './kintone-rest-api-client';
import {
leftCalcType,
linkType,
type SpecialType,
mayLookupTextNumber,
mayLookupText,
type AvailableRight,
type AvailableLeft,
isLookup,
forCalc,
type CalcTypeR2L,
isSpecialType,
} from './join';
import type { SelectType } from '@/types/my-kintone';
// LEFT - rule
const availableLeftMappingType = {
SINGLE_LINE_TEXT: [mayLookupText],
MULTI_LINE_TEXT: ['MULTI_LINE_TEXT'],
RICH_TEXT: ['RICH_TEXT'],
NUMBER: [mayLookupTextNumber],
CALC: [leftCalcType],
RADIO_BUTTON: ['RADIO_BUTTON'],
CHECK_BOX: ['CHECK_BOX'],
MULTI_SELECT: ['MULTI_SELECT'], // TODO 带选项字段报错
DROP_DOWN: ['DROP_DOWN'],
USER_SELECT: ['USER_SELECT'],
ORGANIZATION_SELECT: ['ORGANIZATION_SELECT'],
GROUP_SELECT: ['GROUP_SELECT'],
DATE: ['DATE', 'DATETIME'],
TIME: ['TIME'],
DATETIME: ['DATETIME'],
LINK: [linkType, mayLookupText],
//LOOKUP
RECORD_NUMBER: [mayLookupTextNumber],
CREATOR: ['USER_SELECT'],
CREATED_TIME: ['DATETIME'],
MODIFIER: ['USER_SELECT'],
UPDATED_TIME: ['DATETIME'],
} as Record<FieldType, AvailableRight[]>;
// RIGHT
export type LookupTypeR2L = SpecialType<'LOOKUP_FROM_RIGHT', Array<FieldType | CalcTypeR2L>>;
export const isSameLookupOr = (format: Array<FieldType | CalcTypeR2L>): LookupTypeR2L => {
return {
type: 'LOOKUP_FROM_RIGHT',
format,
check: function (checkField: OneOf, selectedRightField?: OneOf) {
if (selectedRightField && isLookup(selectedRightField)) {
return isLookup(checkField) ? checkField.type === selectedRightField.type : false;
}
return !!this.format?.find((e) => {
if (isSpecialType(e)) {
return e.check(checkField, selectedRightField);
}
return e === checkField.type;
});
},
};
};
const availableRightMappingType = {
SINGLE_LINE_TEXT: [
isSameLookupOr(['SINGLE_LINE_TEXT', 'NUMBER', forCalc(['NUMBER', 'NUMBER_DIGIT']), 'LINK', 'RECORD_NUMBER']),
],
MULTI_LINE_TEXT: ['MULTI_LINE_TEXT'],
RICH_TEXT: ['RICH_TEXT'],
NUMBER: [isSameLookupOr(['NUMBER', forCalc(['NUMBER', 'NUMBER_DIGIT']), 'RECORD_NUMBER'])],
RADIO_BUTTON: ['RADIO_BUTTON'],
CHECK_BOX: ['CHECK_BOX'],
MULTI_SELECT: ['MULTI_SELECT'], // TODO 带选项字段报错
DROP_DOWN: ['DROP_DOWN'],
USER_SELECT: ['USER_SELECT', 'CREATOR', 'MODIFIER'],
ORGANIZATION_SELECT: ['ORGANIZATION_SELECT'],
GROUP_SELECT: ['GROUP_SELECT'],
DATE: ['DATE', forCalc(['DATE'])],
TIME: ['TIME', forCalc(['TIME'])],
DATETIME: ['DATE', 'DATETIME', forCalc(['DATETIME']), 'CREATED_TIME', 'UPDATED_TIME'],
LINK: [linkType],
} as Record<FieldType, Array<AvailableLeft | LookupTypeR2L>>;
// methods
// undefined means all
export function getRightAvailableMappingType(left?: OneOf | '') {
if (left === undefined) {
return Object.keys(availableRightMappingType) as FieldType[];
}
return left ? availableLeftMappingType[left.type] : [];
}
// undefined means all
export function getLeftAvailableMappingType(right?: OneOf | '') {
if (right === undefined) {
return Object.keys(availableLeftMappingType) as FieldType[];
}
return right ? availableRightMappingType[right.type] : [];
}
export function isSelectType(field: OneOf): field is SelectType {
return 'options' in field;
}

View File

@@ -0,0 +1,32 @@
import type { Field, FieldLayout, SavedData } from "@/types/model";
import { KintoneIndexEventHandler } from "./KintoneIndexEventHandler.mobile";
(function (PLUGIN_ID) {
kintone.events.on('mobile.app.record.index.show', (event) => {
try{
const setting = kintone.plugin.app.getConfig(PLUGIN_ID);
const config:SavedData<FieldLayout> = getConfig(setting);
const currentApp = kintone.mobile.app.getId()?.toString();
if(!currentApp) return;
const handler = new KintoneIndexEventHandler(config,currentApp);
handler.init();
}catch(error){
const detailError =(error instanceof Error) ? "\n詳細:" + error.message : "";
const errorMsg = `データ収集中処理中例外発生しました。${ detailError }`;
event.error = errorMsg;
}
return event;
});
/**
* Config設定値を変換する
* @param setting
* @returns
*/
function getConfig(setting:any):SavedData<FieldLayout>{
const config:SavedData<FieldLayout>={
buttonName:setting.buttonName,
joinTables:JSON.parse(setting.joinTables)
}
return config;
}
})(kintone.$PLUGIN_ID);

View File

@@ -1,10 +1,12 @@
{
"$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": 2,
"version": 1,
"type": "APP",
"desktop": {
"js": [
"js/KintoneRestAPIClient.min.js",
"js/kuc.min.js",
"js/desktop.js"
],
"css": [
@@ -16,26 +18,28 @@
"config": {
"html": "html/config.html",
"js": [
"js/config.js"
"js/config.js"
],
"css": [
"css/51-modern-default.css",
"css/config.css"
],
"required_params": [
"message"
"buttonName"
]
},
"name": {
"en": "data fetch pluging",
"en": "data fetch plugin",
"ja": "データ取得プラグイン"
},
"description": {
"en": "create search data pluging",
"en": "create search data plugin",
"ja": "検索結果のデータを生成するプラグインです"
},
"mobile": {
"js": [
"js/KintoneRestAPIClient.min.js",
"js/kuc.min.js",
"js/mobile.js"
],
"css": [

View File

@@ -0,0 +1,71 @@
import type { ConditionValue } from '@/js/conditions';
import type { Layout, Properties } from '@/js/kintone-rest-api-client';
import type { DropdownItem } from 'kintone-ui-component';
export interface FieldsJoinMapping<FieldType = string> {
id: string;
leftField: FieldType;
rightField: FieldType;
}
export interface WhereCondition<FieldType = string> {
id: string;
field: FieldType;
condition: ConditionValue;
data: StringValue;
}
export interface JoinTable<FieldType = string> {
id: string;
app: string; // 取得元アプリ
table: string; // テーブル
onConditions: FieldsJoinMapping<FieldType>[]; // 連結条件
fieldsMapping: FieldsJoinMapping<FieldType>[]; // 取得フィールド
whereConditions: WhereCondition<FieldType>[]; // 絞込条件
meta?: Properties;
}
// 存储的数据格式
export interface SavedData<FieldType = string> {
buttonName: string;
joinTables: JoinTable<FieldType>[];
}
export interface FieldsInfo {
fields: Properties;
layout: Layout;
}
export interface CachedData {
apps: DropdownItem[],
currentAppFields: FieldsInfo,
}
export interface CachedSelectedAppData {
appFields: FieldsInfo,
loading: boolean,
table: JoinTable,
}
export type Record = {
[fieldCode: string]: Field;
};
export type RecordForParameter = {
[fieldCode: string]: {
value: unknown;
};
};
export type Field={
type:string;
value:any;
}
export type FieldLayout={
type:string;
code:string;
}
export type StringValue = string | string[];

View File

@@ -0,0 +1,14 @@
import { KintoneFormFieldProperty } from '@kintone/rest-api-client';
export interface KucEvent<T> {
detail: T;
}
export type CalcType = 'NUMBER' | 'NUMBER_DIGIT' | 'DATETIME' | 'DATE' | 'TIME' | 'HOUR_MINUTE' | 'DAY_HOUR_MINUTE';
export type LinkProtocolType = 'WEB' | 'CALL' | 'MAIL';
export type SelectType =
| KintoneFormFieldProperty.CheckBox
| KintoneFormFieldProperty.RadioButton
| KintoneFormFieldProperty.Dropdown
| KintoneFormFieldProperty.MultiSelect;

View File

@@ -1,8 +1,8 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import copy from "rollup-plugin-copy";
import path from 'path';
import fs from 'fs';
import * as path from 'path';
import * as fs from 'fs';
import Components from 'unplugin-vue-components/vite';
function replaceKucTagsPlugin() {
@@ -26,6 +26,9 @@ function replaceKucTagsPlugin() {
const keyPascal = key.split('-')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join('');
if (key === 'multi-choice') {
key = 'multichoice';
}
importScript += `import * as Kuc${keyPascal} from "kintone-ui-component/lib/${key}";`
});
importScript += '</script>';
@@ -80,6 +83,6 @@ export default defineConfig({
assetFileNames: 'src/[ext]/[name].[ext]',
},
},
sourcemap:'inline',
sourcemap: 'inline',
}
})

View File

@@ -1,13 +0,0 @@
module.exports = {
extends: [
'@cybozu/eslint-config/globals/kintone.js',
'@cybozu/eslint-config/lib/base.js',
'@cybozu/eslint-config/lib/kintone.js',
'@cybozu/eslint-config/lib/prettier.js',
],
rules: {
'prettier/prettier': ['error', { singleQuote: true }],
'space-before-function-paren': 0,
'object-curly-spacing': 0,
},
};

View File

@@ -1,15 +0,0 @@
# My-kintone-plugin
1. 命令:
- `npm run build` 会将代码按照 kintone plugin 格式打包到 `dist`,并生成 `plugin.zip`
- `npm run upload` 会将 `plugin.zip` 上传到 kintone
2. 使用 Vue3.0 开发:
- 配置页面在 `components/Config.vue` 中开发
- Desktop 页面在 `js/desktop.ts` 中开发
3. 关于组件:
- 使用了 https://ui-component.kintone.dev/ 组件库,但是它没有支持 Vue所以需要作为 Web Component 来使用
- 又由于 Web Component 的格式是类似 `<kuc-button-1-18-0>`,为了开发方便能写成 `<kuc-button>`,手动进行了全局替换和引入包(见 `vite.config.js`
- 同时也定死了 kintone-ui-component 版本号,更新版本号需要修改 `vite.config.js` 的插件
- 目前尚未实现 `<template>` 中组件的 ts 提示

View File

@@ -1,15 +0,0 @@
-----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQCF7z/zsYmoe+L1AYTeCYvy9yBXlsXOniTzox6svsXunibVP3y+
f1jEwu2cnTdp/GABOzsVHNSrYGedRDlwG93Y8qxe7qNKLZAFL6ujmJ0FJixuYrh4
xvaWR6SlKIbws+803qAyE6dUN893xeZeJdWGelZNBsCZu8Nwmi28k1flzQIDAQAB
AoGAbWJchJZ2qtejIB5BeWWqmqAiFebZXkniO+j44HReCue3J2pWYu52fRwGG2Z7
H2AyuE67jh6hweVWOibCEkFwCM+MwkSpKNRyFqJwdzZGoMm/oT67dDGYELrmNCx/
9G5DdLgLXsA2dAANxTybaK8wg123Hhrh7NwJDETn9OC+uzECQQDeJTq4OSK9qUw9
RCpgijpVdnzc4hC0CNjKe/+z8bQOPVcX7zLcggwX/7i2UmNxBxfYFrCN8XIGJNGN
VXMpUdCjAkEAmliRAdgAJvoMvaS+gCcJt9tU18F2aunnGudpdwMWDFYdsnztLSJQ
uLPsPQM0TJJYwXWZ+akQuReqXeKg4WgmzwJBAMZAg38VvqN1C81BoHA37IeJDzYx
qqaBnrhWoYV+GCr9I1UA7GtOxGxGlBpivMyKgAUher+y0wgYo8t2jyg5E/ECQCRH
JO42AvMmWtBIZK5ifppEZ1C/HEJM8BEWy2c5xnjn1NsbGfQ92JNRVvmQQz6sN0hh
h+tynYej1Ft05TOV82kCQQCDd0/JtINW3Myj2nWIe8c9IjsBUtNOkaCa13tGOzwJ
3G8Bg0GzdVSC73OnEaguC72kBvyGO4enUFkOq6p6kmFQ
-----END RSA PRIVATE KEY-----

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 B

View File

@@ -1,83 +0,0 @@
<template>
<h2 class="settings-heading">{{ $t('config.title') }}</h2>
<p class="kintoneplugin-desc">{{ $t('config.desc') }}</p>
<plugin-row class="header-row border">
<plugin-input v-model="data.buttonName" placeholder="集約" label="集約ボタン名" />
</plugin-row>
<div id="main-area" ref="mainArea">
<plugin-table-area v-for="joinTable in data.joinTables" :table="joinTable" :key="joinTable.id" />
</div>
<plugin-row class="footer-row border">
<kuc-button text="キャンセル" type="normal" @click="cancel" />
<kuc-button text="保存する" class="save-btn" type="submit" @click="save" />
</plugin-row>
<kuc-spinner :container="mainArea" ref="spinner"></kuc-spinner>
</template>
<script setup lang="ts">
import { createEmptyJoinTable, loadApps, loadAppFieldsAndLayout, EMPTY_OPTION, getEmptyOnCondition } from '@/js/helper';
import type { CachedData, FieldsInfo, JoinTable, SavedData } from '@/types/model';
import type { Spinner } from 'kintone-ui-component';
import { onMounted, watch, provide, reactive, ref, shallowRef, nextTick } from 'vue';
const props = defineProps<{ pluginId: string }>();
const loading = ref(false);
const data: SavedData = reactive({
buttonName: '',
joinTables: [createEmptyJoinTable()],
});
const cachedData: CachedData = reactive({
apps: [EMPTY_OPTION],
currentAppFields: { fields: {}, layout: [] } as FieldsInfo,
});
provide('savedData', data);
provide('cachedData', cachedData);
const mainArea = shallowRef<HTMLElement | null>(null);
const spinner = shallowRef<Spinner | null>(null);
onMounted(async () => {
nextTick(async () => {
spinner.value?.close(); // fix bug: kuc-spinner will not auto amount to target HTML element when init loading
const savedData = kintone.plugin.app.getConfig(props.pluginId);
loading.value = true;
cachedData.apps = await loadApps();
cachedData.currentAppFields = await loadAppFieldsAndLayout();
if (savedData?.joinTables) {
data.joinTables = JSON.parse(savedData.joinTables); //TODO JSON;
}
data.buttonName = savedData?.buttonName || '集約';
loading.value = false;
});
});
watch(loading, (load) => {
load ? spinner.value?.open() : spinner.value?.close();
});
watch(
() => data.joinTables.length,
(newLength) => {
if (newLength === 1) {
data.joinTables[0].onConditions = [getEmptyOnCondition()];
}
},
);
function save() {
kintone.plugin.app.setConfig({
buttonName: data.buttonName,
joinTables: JSON.stringify(data.joinTables || []),
});
}
function cancel() {
window.location.href = `../../${kintone.app.getId()}/plugin/`;
}
</script>

View File

@@ -1,79 +0,0 @@
<template>
<kuc-table :class-name.camel="['plugin-kuc-table']" :columns="columns" :data="modelValue" />
</template>
<script setup lang="ts">
import type { CachedData, CachedSelectedAppData, FieldsJoinMapping, WhereCondition } from '@/types/model';
import { defineProps, inject, computed, reactive, render, h, watch } from 'vue';
import { generateId, getFieldsDropdownItems, search } from '@/js/helper';
import TableCombobox from './TableCombobox.vue';
const props = defineProps<{
connector: string;
modelValue: FieldsJoinMapping[];
}>();
const cachedData = inject<CachedData>('cachedData') as CachedData;
const selectedAppData = inject<CachedSelectedAppData>('selectedAppData') as CachedSelectedAppData;
const table = computed(() => selectedAppData.table.table);
const columns = reactive([
{
title: '取得元アプリのフィールド',
field: 'leftField',
render: (cellData: string, rowData: WhereCondition) => {
if (!rowData.id) {
rowData.id = generateId();
}
const container = document.createElement('div');
const vnode = h(TableCombobox, {
items: computed(() =>
getFieldsDropdownItems(selectedAppData.appFields, { subTableCode: table.value, filterType: undefined }),
),
modelValue: (search(props.modelValue, rowData.id) as FieldsJoinMapping)?.leftField || '',
selectedAppData,
dataList: props.modelValue,
id: rowData.id,
'onUpdate:modelValue': (data) => {
if (data.obj) {
(data.obj as FieldsJoinMapping).leftField = data.value;
}
},
});
render(vnode, container);
return container;
},
},
{
title: '',
field: 'connector',
render: () => {
return props.connector;
},
},
{
title: 'このアプリのフィールド',
field: 'rightField',
render: (cellData: string, rowData: WhereCondition) => {
if (!rowData.id) {
rowData.id = generateId();
}
const container = document.createElement('div');
const vnode = h(TableCombobox, {
items: computed(() => getFieldsDropdownItems(cachedData.currentAppFields, { filterType: undefined })),
modelValue: (search(props.modelValue, rowData.id) as FieldsJoinMapping)?.rightField || '',
selectedAppData,
dataList: props.modelValue,
id: rowData.id,
'onUpdate:modelValue': (data) => {
if (data.obj) {
(data.obj as FieldsJoinMapping).rightField = data.value;
}
},
});
render(vnode, container);
return container;
},
},
]);
</script>

Some files were not shown because too many files have changed in this diff Show More