46 Commits

Author SHA1 Message Date
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 7602 additions and 6467 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

@@ -7,7 +7,9 @@ export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
CellInput: typeof import('./src/components/basic/conditions/CellInput.vue')['default']
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 +22,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

@@ -11,7 +11,7 @@
/* config 页面 */
#app {
width: 60vw;
min-width: 920px;
min-width: 940px;
}
/* 最上面的说明 */
@@ -27,7 +27,7 @@
/* laebl input 单行的情况 */
.flex-row .kintoneplugin-label {
margin: 0;
width: 10em;
width: 8.5em;
}
/* 遮罩 */

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

View File

@@ -0,0 +1,3 @@
var f=Object.defineProperty;var E=(c,e,t)=>e in c?f(c,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):c[e]=t;var l=(c,e,t)=>E(c,typeof e!="symbol"?e+"":e,t);new KintoneRestAPIClient;const h=["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"];h.reduce((c,e)=>(c[e]=e,c),{});function C(c){return e=>(e==null?void 0:e.type)===c}const u=Object.fromEntries(h.map(c=>[c,C(c)]));class T{constructor(e,t){l(this,"config");l(this,"currentApp");l(this,"handleButtonClick",async()=>{const e=this.showSpinner();try{console.log("データ収集開始..."),await this.execDataFectch(),e.close(),location.reload()}catch(t){e.close();const r=`データ収集中処理中例外発生しました。${t instanceof Error?`
詳細:`+t.message:""}`;console.error(r,t),window.alert(r)}});l(this,"execDataFectch",async()=>{const e=this.config.joinTables[0];let n=(await this.fetchDataFromApp(e)).map(i=>{const o={};return e.fieldsMapping.forEach(s=>{o[this.fieldCode(s.rightField)]=i[this.fieldCode(s.leftField)]}),o});const r=this.config.joinTables.filter((i,o)=>o>0);for(const i of r){const o=await this.fetchDataFromApp(i);n=this.leftJoin(n,o,i)}await this.deleteCurrentRecords(),await this.saveDataToCurrentApp(n)});l(this,"fetchDataFromApp",async e=>{const t=this.getWhereCondition(e.whereConditions),n=e.fieldsMapping.map(s=>this.fieldCode(s.leftField));e.table&&n.push(e.table),e.onConditions.map(s=>this.fieldCode(s.leftField)).forEach(s=>{n.includes(s)||n.push(s)});const o=await new KintoneRestAPIClient().record.getAllRecords({app:e.app,fields:n,condition:t});return this.convertToFlatDatas(o,e.table)});this.config=e,this.currentApp=t}init(){this.addButtonToView()}addButtonToView(){const e=kintone.app.getHeaderMenuSpaceElement();if(!e)throw new Error("このページではヘッダー要素が利用できません。");if(document.getElementById("btn-data-fetch"))return;const t=Kucs["1.18.0"],n=new t.Button({text:this.config.buttonName,type:"submit",id:"btn-data-fetch"});n.addEventListener("click",()=>this.handleButtonClick()),e.appendChild(n)}showSpinner(){const e=Kucs["1.18.0"],t=new e.Spinner({text:"データ収集中",container:document.body});return t.open(),t}getWhereCondition(e){return e.filter(r=>this.fieldCode(r.field)!=="").map(r=>{let i=r.condition;"subField"in r.field&&r.field.subField&&(i=this.mapConditionForSubField(r.condition));const o=this.getConditionValue(r.field,i,r.data);return`${this.fieldCode(r.field)} ${i} ${o}`}).join(" and ")}mapConditionForSubField(e){switch(e){case"=":return"in";case"!=":return"not in";default:return e}}getConditionValue(e,t,n){return n?u.NUMBER(e)||u.RECORD_NUMBER(e)?n:u.DATE(e)?/^\d{4}-\d{2}-\d{2}$/.test(n)||n.match(/^\w+\(.*\)$/)?`"${n}"`:`"${new Date(n).toISOString().split("T")[0]}"`:u.DATETIME(e)||u.CREATED_TIME(e)||u.UPDATED_TIME(e)?n.match(/^\w+\(.*\)$/)?`"${n}"`:`"${new Date(n).toISOString()}"`:t==="in"||t==="not in"?n.includes(",")?`(${n.split(",").map(i=>`"${i.trim()}"`).join(",")})`:`("${n}")`:`"${n}"`:""}fieldCode(e){return e?typeof e=="string"&&e?e:typeof e=="object"&&"code"in e?e.code:"":""}convertToFlatDatas(e,t){if(!t)return e;const n=[];return e.forEach(r=>{var i;if(((i=r[t])==null?void 0:i.type)==="SUBTABLE"&&r[t].value.length>0)r[t].value.forEach(o=>{const s={...r};Object.entries(o.value).forEach(([d,a])=>{s[d]={value:a.value,type:a.type}}),delete s[t],n.push(s)});else{const o={...r};delete o[t],n.push(o)}}),n}leftJoin(e,t,n){const r=[];return e.forEach(i=>{const o=t.filter(s=>n.onConditions.every(d=>{var a,p;return((a=i[this.fieldCode(d.rightField)])==null?void 0:a.value)===((p=s[this.fieldCode(d.leftField)])==null?void 0:p.value)}));o?o.forEach(s=>{const d={...i};n.fieldsMapping.forEach(a=>{d[this.fieldCode(a.rightField)]=s[this.fieldCode(a.leftField)]}),r.push(d)}):r.push(i)}),r}async deleteCurrentRecords(){const e=new KintoneRestAPIClient,n=(await e.record.getAllRecords({app:this.currentApp,fields:["$id"]})).map(r=>({id:r.$id.value}));await e.record.deleteAllRecords({app:this.currentApp,records:n}),e.record.addAllRecords}async saveDataToCurrentApp(e){try{const n=await new KintoneRestAPIClient().record.addAllRecords({app:this.currentApp,records:this.convertForUpdate(e)})}catch(t){throw console.error("データ作成時エラーが発生しました:",t),t}}convertForUpdate(e){return e.map(t=>Object.fromEntries(Object.entries(t).map(([n,{value:r}])=>[n,{value:r}])))}}(function(c){kintone.events.on("app.record.index.show",t=>{var n;try{const r=kintone.plugin.app.getConfig(c),i=e(r),o=(n=kintone.app.getId())==null?void 0:n.toString();if(!o)return;new T(i,o).init()}catch(r){const o=`データ収集中処理中例外発生しました。${r instanceof Error?`
詳細:`+r.message:""}`;t.error=o}return t});function e(t){return{buttonName:t.buttonName,joinTables:JSON.parse(t.joinTables)}}})(kintone.$PLUGIN_ID);

View File

@@ -0,0 +1,3 @@
var f=Object.defineProperty;var E=(c,e,t)=>e in c?f(c,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):c[e]=t;var l=(c,e,t)=>E(c,typeof e!="symbol"?e+"":e,t);new KintoneRestAPIClient;const h=["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"];h.reduce((c,e)=>(c[e]=e,c),{});function C(c){return e=>(e==null?void 0:e.type)===c}const p=Object.fromEntries(h.map(c=>[c,C(c)]));class T{constructor(e,t){l(this,"config");l(this,"currentApp");l(this,"handleButtonClick",async()=>{const e=this.showSpinner();try{console.log("データ収集開始..."),await this.execDataFectch(),e.close(),location.reload()}catch(t){e.close();const r=`データ収集中処理中例外発生しました。${t instanceof Error?`
詳細:`+t.message:""}`;console.error(r,t),window.alert(r)}});l(this,"execDataFectch",async()=>{const e=this.config.joinTables[0];let n=(await this.fetchDataFromApp(e)).map(i=>{const o={};return e.fieldsMapping.forEach(s=>{o[this.fieldCode(s.rightField)]=i[this.fieldCode(s.leftField)]}),o});const r=this.config.joinTables.filter((i,o)=>o>0);for(const i of r){const o=await this.fetchDataFromApp(i);n=this.leftJoin(n,o,i)}await this.deleteCurrentRecords(),await this.saveDataToCurrentApp(n)});l(this,"fetchDataFromApp",async e=>{const t=this.getWhereCondition(e.whereConditions),n=e.fieldsMapping.map(s=>this.fieldCode(s.leftField));e.table&&n.push(e.table),e.onConditions.map(s=>this.fieldCode(s.leftField)).forEach(s=>{n.includes(s)||n.push(s)});const o=await new KintoneRestAPIClient().record.getAllRecords({app:e.app,fields:n,condition:t});return this.convertToFlatDatas(o,e.table)});this.config=e,this.currentApp=t}init(){this.addButtonToView()}addButtonToView(){const e=kintone.mobile.app.getHeaderSpaceElement();if(!e)throw new Error("このページではヘッダー要素が利用できません。");if(document.getElementById("btn-data-fetch"))return;const t=Kucs["1.18.0"],n=new t.Button({text:this.config.buttonName,type:"submit",id:"btn-data-fetch"});n.addEventListener("click",()=>this.handleButtonClick()),e.appendChild(n)}showSpinner(){const e=Kucs["1.18.0"],t=new e.Spinner({text:"データ収集中",container:document.body});return t.open(),t}getWhereCondition(e){return e.filter(r=>this.fieldCode(r.field)!=="").map(r=>{let i=r.condition;"subField"in r.field&&r.field.subField&&(i=this.mapConditionForSubField(r.condition));const o=this.getConditionValue(r.field,i,r.data);return`${this.fieldCode(r.field)} ${i} ${o}`}).join(" and ")}mapConditionForSubField(e){switch(e){case"=":return"in";case"!=":return"not in";default:return e}}getConditionValue(e,t,n){return n?p.NUMBER(e)||p.RECORD_NUMBER(e)?n:p.DATE(e)?/^\d{4}-\d{2}-\d{2}$/.test(n)||n.match(/^\w+\(.*\)$/)?`"${n}"`:`"${new Date(n).toISOString().split("T")[0]}"`:p.DATETIME(e)||p.CREATED_TIME(e)||p.UPDATED_TIME(e)?n.match(/^\w+\(.*\)$/)?`"${n}"`:`"${new Date(n).toISOString()}"`:t==="in"||t==="not in"?n.includes(",")?`(${n.split(",").map(i=>`"${i.trim()}"`).join(",")})`:`("${n}")`:`"${n}"`:""}fieldCode(e){return e?typeof e=="string"&&e?e:typeof e=="object"&&"code"in e?e.code:"":""}convertToFlatDatas(e,t){if(!t)return e;const n=[];return e.forEach(r=>{var i;if(((i=r[t])==null?void 0:i.type)==="SUBTABLE"&&r[t].value.length>0)r[t].value.forEach(o=>{const s={...r};Object.entries(o.value).forEach(([d,a])=>{s[d]={value:a.value,type:a.type}}),delete s[t],n.push(s)});else{const o={...r};delete o[t],n.push(o)}}),n}leftJoin(e,t,n){const r=[];return e.forEach(i=>{const o=t.filter(s=>n.onConditions.every(d=>{var a,u;return((a=i[this.fieldCode(d.rightField)])==null?void 0:a.value)===((u=s[this.fieldCode(d.leftField)])==null?void 0:u.value)}));o?o.forEach(s=>{const d={...i};n.fieldsMapping.forEach(a=>{d[this.fieldCode(a.rightField)]=s[this.fieldCode(a.leftField)]}),r.push(d)}):r.push(i)}),r}async deleteCurrentRecords(){const e=new KintoneRestAPIClient,n=(await e.record.getAllRecords({app:this.currentApp,fields:["$id"]})).map(r=>({id:r.$id.value}));await e.record.deleteAllRecords({app:this.currentApp,records:n}),e.record.addAllRecords}async saveDataToCurrentApp(e){try{const n=await new KintoneRestAPIClient().record.addAllRecords({app:this.currentApp,records:this.convertForUpdate(e)})}catch(t){throw console.error("データ作成時エラーが発生しました:",t),t}}convertForUpdate(e){return e.map(t=>Object.fromEntries(Object.entries(t).map(([n,{value:r}])=>[n,{value:r}])))}}(function(c){kintone.events.on("mobile.app.record.index.show",t=>{var n;try{const r=kintone.plugin.app.getConfig(c),i=e(r),o=(n=kintone.mobile.app.getId())==null?void 0:n.toString();if(!o)return;new T(i,o).init()}catch(r){const o=`データ収集中処理中例外発生しました。${r instanceof Error?`
詳細:`+r.message:""}`;t.error=o}return t});function e(t){return{buttonName:t.buttonName,joinTables:JSON.parse(t.joinTables)}}})(kintone.$PLUGIN_ID);

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,16 @@
</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" :table="table"
@update:modelValue="(newData:any) => table.whereConditions = newData"/>
</plugin-row>
</div>
<div class="table-action-area">
@@ -43,7 +44,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 +86,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,25 +1,44 @@
<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[];
table:JoinTable;
}>();
const emit = defineEmits<{
(event: 'update:modelValue', value: WhereCondition[]): void;
}>();
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 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,11 +52,13 @@ 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,
@@ -60,15 +81,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 = '';
}
obj && (obj.condition = value);
},
});
render(vnode, container);
@@ -81,12 +99,19 @@ 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);
if(obj){
obj.data = value;
const newData = props.modelValue.map((item) =>
item.id === obj.id ? { ...item, data: value } : item
);
emit('update:modelValue', newData);
}
},
});
render(vnode, container);

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,22 @@
<template>
<kuc-text className="kuc-text-input" :value="modelValue" @change="updateValue" />
</template>
<script setup lang="ts">
import type { KucEvent } from '@/types/my-kintone';
import type { TextInputEventDetail } from 'kintone-ui-component';
import { defineProps, defineEmits, type Ref } from 'vue';
const props = defineProps<{
modelValue: string;
}>();
const emit = defineEmits<{
(e: 'update:modelValue', value: string): void;
}>();
const updateValue = ({ detail }: KucEvent<TextInputEventDetail>) => {
emit('update:modelValue', 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,150 @@
<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="valueType === 'kuc-multichoice'"
:value="multiValue"
:items="multiChoiceItems"
:requiredIcon="true"
@change="updateMultiValue"
:disabled="selectedAppData.loading == undefined ? false : selectedAppData.loading"
/>
<table-condition-value-multi-input
v-else-if="valueType === 'multi-input'"
:value="multiInput"
@change="updateTableValue"
:disabled="selectedAppData.loading == undefined ? false : selectedAppData.loading"
/>
</template>
<script setup lang="ts">
import { getComponent } 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 multiChoiceItems = computed(() => {
const field = getFieldObj(whereCondition.value?.field || '', props.selectedAppData.appFields, '');
const items = [{
label: '--',
value: '',
}];
if (field && isSelectType(field)) {
const opts = field.options;
const multiOpts = Object.values(opts).map((opt) => {
return {
label: opt.label,
value: opt.label,
};
});
items.push(...multiOpts);
}
return items;
});
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 || '' });
};
const multiValue = ref(isStringArray(props.modelValue.value) ? props.modelValue.value : []);
watch(
() => props.modelValue,
() => {
const field = getFieldObj(whereCondition.value?.field || '', props.selectedAppData.appFields, '');
const vType = valueType.value;
const moduleValue = props.modelValue.value;
if (field && isSelectType(field) && vType === 'kuc-multichoice') {
multiValue.value = isStringArray(moduleValue) ? moduleValue : [];
}
},
);
const multiInput = ref(isStringArray(props.modelValue.value) ? (props.modelValue.value as string[]) : ['', '']);
watchEffect(() => {
const vType = valueType.value;
const moduleValue = props.modelValue.value;
if (vType === 'multi-input') {
multiInput.value = isStringArray(moduleValue) ? (moduleValue as string[]) : ['', ''];
}
});
const updateMultiValue = (event: KucEvent<MultiChoiceChangeEventDetail>) => {
emit('update:modelValue', { obj: whereCondition.value, value: event.detail.value || [] });
};
const updateTableValue = (event: KucEvent<string[]>) => {
let value = event.detail || ['', ''];
multiInput.value = value;
emit('update:modelValue', { obj: whereCondition.value, value: value });
};
</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,82 @@
<template>
<!-- <kuc-table className='table-option'
:columns="columns"
:data="data"
@change="updateValue"
:headerVisible="false"
ref="table"/> -->
<div ref="tableContainer"></div>
</template>
<script setup lang="ts">
import type { KucEvent } from '@/types/my-kintone';
import { Table, Text, type TableChangeEventDetail } from 'kintone-ui-component';
import { defineProps, defineEmits, computed, ref, watch, inject, type Ref, onMounted, onUnmounted } from 'vue';
interface MuiltItem{
value:string
}
const props = defineProps<{
value: string[];
}>();
const tableContainer = ref<HTMLDivElement>();
const table = ref<Table | null>(null);
const data = ref<MuiltItem[]>((props.value || ['', '']).map(x => ({ value: x })));
watch(
() => props.value,
(newValue)=>{
data.value =(newValue || ['', '']).map((x) => ({ value: x }));
if (table.value) {
table.value.data = data.value; // 更新 Table 数据
}
},
{
deep:true,immediate:true
});
const emit = defineEmits<{
(e: 'change', data: KucEvent<string[]>): void;
(e: 'update:modelValue', value: string[]): void;
}>();
const updateValue=(event:KucEvent<TableChangeEventDetail<MuiltItem>>)=>{
data.value = event.detail.data||[{value:''},{value:''}];
if (table.value) {
table.value.data = data.value;
}
const muiltData = event.detail.data ? event.detail.data.map(x=>x.value) :[];
emit('change', { detail: [...muiltData] });
// emit('update:modelValue', [...muiltData] );
// emit('change', muiltData);
}
onMounted(()=>{
table.value = new Table({
className:'table-option',
headerVisible:false,
actionButton:true,
columns:[
{
field:"value",
render:(cellData:any)=>{
const text = new Text({value:cellData});
return text;
}
},
],
data:data.value
});
table.value.addEventListener('change', updateValue);
tableContainer.value?.appendChild(table.value);
});
onUnmounted(() => {
if (table.value) {
table.value.removeEventListener('change', updateValue);
}
});
</script>

View File

@@ -0,0 +1,253 @@
/* 辅助类 */
.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;
}
.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 > .kuc-table-1-18-0__table__body__row__action {
height: 55px;
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,196 @@
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) => dateTimeComponent[field.type] || 'input' },
{ value: '!=', label: '≠ (等しくない)', type: (field) => dateTimeComponent[field.type] || 'input' },
{
value: '<=',
label: (field) => (isDateTimeType(field) ? '≦ (以前)' : '≦ (以下)'),
type: (field) => dateTimeComponent[field.type] || 'input',
},
{ value: '<', label: '< (より前)', type: (field) => dateTimeComponent[field.type] || 'input' },
{
value: '>=',
label: (field) => (isDateTimeType(field) ? '≧ (以降)' : '≧ (以上)'),
type: (field) => dateTimeComponent[field.type] || 'input',
},
{ value: '>', label: '> (より後)', type: (field) => dateTimeComponent[field.type] || 'input' },
{ value: 'like', label: '次のキーワードを含む', type: 'input' },
{ value: 'not like', label: '次のキーワードを含まない', type: 'input' },
{ value: 'in', label: '次のいずれかを含む', type: (field) => MultiChoiceComponent[field.type] || 'input' },
{ value: 'not in', label: '次のいずれも含まない', type: (field) => MultiChoiceComponent[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 component = {
input: 'kuc-text',
select: 'kuc-combobox',
time: 'kuc-time',
date: 'date',
datetime: 'datetime',
multiChoice: 'kuc-multichoice',
multiInput: 'multi-input',
};
export const isDateTimeType = (field: OneOf) => {
return field.type in dateTimeComponent;
};
const dateTimeComponent: Partial<Record<FieldType, ComponentType>> = {
TIME: 'time',
DATE: 'date',
DATETIME: 'datetime',
CREATED_TIME: 'datetime',
UPDATED_TIME: 'datetime',
};
const MultiChoiceComponent: Partial<Record<FieldType, ComponentType>> = {
CHECK_BOX: 'multiChoice',
DROP_DOWN: 'multiChoice',
RADIO_BUTTON: 'multiChoice',
MULTI_SELECT: 'multiChoice',
SINGLE_LINE_TEXT: 'multiInput',
LINK: 'multiInput',
};
export type ComponentType = keyof typeof component;
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>

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