2 Commits

Author SHA1 Message Date
11ca450ee7 try fix table 2025-01-24 01:46:57 +08:00
27ee106297 fix ts 2025-01-24 01:46:20 +08:00
115 changed files with 6685 additions and 12617 deletions

15
data-fetch-pluging/.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,15 @@
{
// 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}"
}
]
}

BIN
data-fetch-pluging/dist/plugin.zip vendored Normal file

Binary file not shown.

BIN
data-fetch-pluging/dist/plugin/PUBKEY vendored Normal file

Binary file not shown.

View File

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

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

View File

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

Before

Width:  |  Height:  |  Size: 110 B

After

Width:  |  Height:  |  Size: 110 B

View File

@@ -0,0 +1,25 @@
(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

@@ -0,0 +1,23 @@
(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

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

4803
data-fetch-pluging/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

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

@@ -0,0 +1,15 @@
"use strict";
const runAll = require("npm-run-all");
runAll(["develop", "upload"], {
parallel: true,
stdout: process.stdout,
stdin: process.stdin
}).catch(({results}) => {
results
.filter(({code}) => code)
.forEach(({name}) => {
console.log(`"npm run ${name}" was failed`);
})
;
});

View File

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

View File

@@ -0,0 +1,10 @@
.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,11 +1,10 @@
.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,16 @@
<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

Before

Width:  |  Height:  |  Size: 110 B

After

Width:  |  Height:  |  Size: 110 B

View File

@@ -0,0 +1,25 @@
(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

@@ -0,0 +1,22 @@
(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

@@ -0,0 +1,22 @@
(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

@@ -0,0 +1,45 @@
{
"$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"
]
}
}

Binary file not shown.

View File

@@ -1,41 +0,0 @@
# 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

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

Binary file not shown.

View File

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

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,166 +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 :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

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

@@ -1,108 +0,0 @@
<template>
<kuc-table className="plugin-kuc-table condition-table" :columns="columns" :data="modelValue" />
</template>
<script setup lang="ts">
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 TableCondition from './conditions/TableCondition.vue';
import TableConditionValue from './conditions/TableConditionValue.vue';
const props = defineProps<{
modelValue: WhereCondition[];
}>();
const savedData = inject<SavedData>('savedData') as SavedData;
const cachedData = inject<CachedData>('cachedData') as CachedData;
const selectedAppData = inject<CachedSelectedAppData>('selectedAppData') as CachedSelectedAppData;
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([
{
title: '取得元アプリのフィールド',
field: 'field',
render: (cellData: string, rowData: WhereCondition) => {
if (!rowData.id) {
rowData.id = generateId();
}
const container = document.createElement('div');
const vnode = h(TableCombobox, {
items: computed(() =>
getFieldsDropdownItems(selectedAppData.appFields, {
subTableCode: '', //table.value,
baseFilter: undefined,
defaultLabel: 'すべてのレコード',
needAllSubTableField: true,
}),
),
modelValue: computed(() => (search(props.modelValue, rowData.id) as WhereCondition)?.field || ''),
selectedAppData,
dataList: props.modelValue,
id: rowData.id,
'onUpdate:modelValue': (data) => {
const obj = data.obj as WhereCondition;
if (obj) {
obj.field = data.value;
obj.condition = '';
obj.data = '';
}
},
});
render(vnode, container);
return container;
},
},
{
title: '',
field: 'condition',
render: (cellData: string, rowData: WhereCondition) => {
const container = document.createElement('div');
const vnode = h(TableCondition, {
modelValue: computed(() => (search(props.modelValue, rowData.id) as WhereCondition)?.condition || ''),
selectedAppData,
id: rowData.id,
whereConditions: props.modelValue,
'onUpdate:modelValue': ({ obj, value }) => {
obj && (obj.condition = value);
},
});
render(vnode, container);
return container;
},
},
{
title: '',
field: 'data',
render: (cellData: string, rowData: WhereCondition) => {
const container = document.createElement('div');
const vnode = h(TableConditionValue, {
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);
},
});
render(vnode, container);
return container;
},
},
]);
</script>

View File

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

View File

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

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

View File

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

View File

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

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

View File

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

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

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

View File

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

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

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

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

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

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

@@ -1,32 +0,0 @@
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,71 +0,0 @@
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

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

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

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

View File

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

@@ -8,7 +8,6 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
Config: typeof import('./src/components/Config.vue')['default']
ErrorDialog: typeof import('./src/components/basic/ErrorDialog.vue')['default']
PluginDropdown: typeof import('./src/components/basic/PluginDropdown.vue')['default']
PluginInput: typeof import('./src/components/basic/PluginInput.vue')['default']
PluginLabel: typeof import('./src/components/basic/PluginLabel.vue')['default']
@@ -21,7 +20,5 @@ 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,15 @@
-----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.

After

Width:  |  Height:  |  Size: 110 B

View File

@@ -0,0 +1,83 @@
<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, SavedData } from '@/types/model';
import type { Spinner } from 'kintone-ui-component';
import { onMounted, watch, provide, reactive, ref, shallowRef, nextTick } from 'vue';
const props = defineProps<{ pluginId: string }>();
const loading = ref(false);
const data: SavedData = reactive({
buttonName: '',
joinTables: [createEmptyJoinTable()],
});
const cachedData: CachedData = reactive({
apps: [EMPTY_OPTION],
currentAppFields: { fields: {}, layout: [] } as FieldsInfo,
});
provide('savedData', data);
provide('cachedData', cachedData);
const mainArea = shallowRef<HTMLElement | null>(null);
const spinner = shallowRef<Spinner | null>(null);
onMounted(async () => {
nextTick(async () => {
spinner.value?.close(); // fix bug: kuc-spinner will not auto amount to target HTML element when init loading
const savedData = kintone.plugin.app.getConfig(props.pluginId);
loading.value = true;
cachedData.apps = await loadApps();
cachedData.currentAppFields = await loadAppFieldsAndLayout();
if (savedData?.joinTables) {
data.joinTables = JSON.parse(savedData.joinTables); //TODO JSON;
}
data.buttonName = savedData?.buttonName || '集約';
loading.value = false;
});
});
watch(loading, (load) => {
load ? spinner.value?.open() : spinner.value?.close();
});
watch(
() => data.joinTables.length,
(newLength) => {
if (newLength === 1) {
data.joinTables[0].onConditions = [getEmptyOnCondition()];
}
},
);
function save() {
kintone.plugin.app.setConfig({
buttonName: data.buttonName,
joinTables: JSON.stringify(data.joinTables || []),
});
}
function cancel() {
window.location.href = `../../${kintone.app.getId()}/plugin/`;
}
</script>

View File

@@ -16,15 +16,15 @@
</plugin-row>
<plugin-row class="flex-row" v-if="isJoinConditionShown(table)">
<plugin-label label="連結条件" />
<plugin-table-connect-row connector="=" type="connect" :modelValue="table.onConditions" />
<plugin-table-connect-row connector="=" :modelValue="table.onConditions" />
</plugin-row>
<plugin-row class="flex-row">
<plugin-label label="取得フィールド" />
<plugin-table-connect-row connector="→" type="mapping" :modelValue="table.fieldsMapping" />
<plugin-table-connect-row connector="→" :modelValue="table.fieldsMapping" />
</plugin-row>
<plugin-row class="flex-row">
<plugin-label label="絞込条件" />
<plugin-table-condition-row :modelValue="table.whereConditions" />
<plugin-table-condition-row :modelValue="table.whereConditions"/>
</plugin-row>
</div>
<div class="table-action-area">
@@ -85,7 +85,6 @@ 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;
},

View File

@@ -0,0 +1,134 @@
<template>
<kuc-table
:class-name.camel="['plugin-kuc-table']"
:columns="columns"
:data="whereConditionsCopy"
@change="updateTable"
/>
</template>
<script setup lang="ts">
import type { CachedData, CachedSelectedAppData, EmptyOneOf, SavedData, WhereCondition } from '@/types/model';
import { defineProps, inject, computed, render, h, reactive, ref } from 'vue';
import TableCombobox from './TableCombobox.vue';
import { generateId, getEmptyOneOf, getEmptyWhereCondition, getFieldsDropdownItems, search } from '@/js/helper';
import TableCondition from './conditions/TableCondition.vue';
import TableConditionValue from './conditions/TableConditionValue.vue';
import type { KucEvent } from '@/types/my-kintone';
import type { TableChangeEventDetail } from 'kintone-ui-component';
import type { OneOf } from '@/js/kintone-rest-api-client';
const props = defineProps<{
modelValue: WhereCondition[];
}>();
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 whereConditionsCopy = computed(() => JSON.parse(JSON.stringify(props.modelValue)) as WhereCondition<string>[]);
const columns = reactive([
{
title: '取得元アプリのフィールド',
field: 'field',
render: (cellData: string, rowData: WhereCondition<string>) => {
if (!rowData.id) {
rowData.id = generateId();
}
const container = document.createElement('div');
const vnode = h(TableCombobox, {
items: computed(() =>
getFieldsDropdownItems(selectedAppData.appFields, {
subTableCode: table.value,
defaultLabel: 'すべてのレコード',
}),
),
modelValue: (search(props.modelValue, rowData.id) as WhereCondition)?.field || getEmptyOneOf(),
selectedAppData,
dataList: props.modelValue,
id: rowData.id,
'onUpdate:modelValue': (data) => {
const obj = data.obj as WhereCondition;
if (obj) {
obj.field = data.value;
(obj.condition = ''), (obj.data = '');
}
},
});
render(vnode, container);
return container;
},
},
{
title: '',
field: 'condition',
render: (cellData: string, rowData: WhereCondition<string>) => {
const container = document.createElement('div');
const vnode = h(TableCondition, {
modelValue: (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 = '';
}
},
});
render(vnode, container);
return container;
},
},
{
title: '',
field: 'data',
render: (cellData: string, rowData: WhereCondition<string>) => {
const container = document.createElement('div');
const vnode = h(TableConditionValue, {
modelValue: (search(props.modelValue, rowData.id) as WhereCondition)?.data || '',
selectedAppData,
id: rowData.id,
whereConditions: props.modelValue,
'onUpdate:modelValue': ({ obj, value }) => {
obj && (obj.data = value);
},
});
render(vnode, container);
return container;
},
},
]);
function updateTable({ detail }: KucEvent<TableChangeEventDetail<WhereCondition<string>>>) {
if (detail.type === 'remove-row') {
props.modelValue.splice(detail.rowIndex, 1);
return;
}
const rowData = detail.data ? detail.data[detail.rowIndex] : { ...getEmptyWhereCondition(), field: '' } as WhereCondition<string|EmptyOneOf|OneOf>;
if (detail.type === 'add-row') {
rowData.field = getEmptyOneOf();
props.modelValue.splice(detail.rowIndex + 1, 0, getEmptyWhereCondition(rowData.id));
return;
}
if (detail.type === 'change-cell') {
const updateField = detail.field as keyof WhereCondition;
if (!updateField) return;
if (updateField === 'field') {
rowData.field = selectedAppData.appFields.fields[rowData.field as string];
}
if (isWhereCondition(rowData)) {
props.modelValue[detail.rowIndex][updateField] = rowData[updateField] as any;
}
}
}
function isWhereCondition(rowData: WhereCondition<string|EmptyOneOf|OneOf>): rowData is WhereCondition {
return (typeof rowData.field !== 'string');
}
</script>

View File

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

View File

@@ -1,8 +1,7 @@
<template>
<kuc-combobox
className="kuc-text-input"
:items="items.value"
:value="modelValue.value"
:value="comboboxValue"
@change="updateValue"
:key="componentKey"
:disabled="selectedAppData.loading == undefined ? false : selectedAppData.loading"
@@ -10,26 +9,29 @@
</template>
<script setup lang="ts">
import { search } from '@/js/helper';
import type { CachedSelectedAppData } from '@/types/model';
import { getEmptyOneOf, search } from '@/js/helper';
import type { OneOf } from '@/js/kintone-rest-api-client';
import type { CachedSelectedAppData, EmptyOneOf } from '@/types/model';
import type { KucEvent } from '@/types/my-kintone';
import type { ComboboxChangeEventDetail, DropdownItem } from 'kintone-ui-component';
import { defineProps, defineEmits, type Ref, watch, ref } from 'vue';
import { defineProps, defineEmits, type Ref, watch, ref, computed } from 'vue';
const props = defineProps<{
items: Ref<DropdownItem[]>;
modelValue: Ref<string>;
modelValue: OneOf | EmptyOneOf;
dataList: any[];
id: string;
selectedAppData: CachedSelectedAppData;
}>();
const comboboxValue = computed(() => (props.modelValue?.code || ''));
const componentKey = ref(0);
// fix-bug: force select saved data when load config
watch(
() => props.items.value,
() => {
if (!props.modelValue.value) return;
if (!props.modelValue) return;
componentKey.value += 1;
},
{
@@ -39,7 +41,7 @@ watch(
type EmitData = {
obj?: any;
value: string;
value: OneOf | EmptyOneOf;
};
const emit = defineEmits<{
@@ -47,6 +49,10 @@ const emit = defineEmits<{
}>();
const updateValue = ({ detail }: KucEvent<ComboboxChangeEventDetail>) => {
emit('update:modelValue', { obj: search(props.dataList, props.id), value: detail.value || '' });
console.log(props.selectedAppData.appFields.fields, detail.value)
emit('update:modelValue', {
obj: search(props.dataList, props.id),
value: props.selectedAppData.appFields.fields[detail.value || ''] || getEmptyOneOf(),
});
};
</script>

View File

@@ -2,53 +2,33 @@
<kuc-combobox
v-if="items?.length"
:items="items"
:value="value"
@change.stop="updateValue"
:value="modelValue"
@change="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 { getEmptyOneOf, 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';
import { defineProps, defineEmits, computed } from 'vue';
const props = defineProps<{
modelValue: Ref<string>;
modelValue: 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 },
);
const items = computed(() => {
console.log(props.whereConditions);
const field = whereCondition.value?.field || getEmptyOneOf();
return getAvailableCondition(field.code, props.selectedAppData.appFields);
});
type EmitData = {
obj?: WhereCondition;
@@ -60,7 +40,6 @@ const emit = defineEmits<{
}>();
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,47 @@
<template>
<kuc-text
v-if="type == 'kuc-text'"
:value="modelValue"
@change="updateValue"
:disabled="selectedAppData.loading == undefined ? false : selectedAppData.loading"
/>
<kuc-combobox
v-if="type == 'kuc-combobox'"
:value="modelValue"
@change="updateValue"
:disabled="selectedAppData.loading == undefined ? false : selectedAppData.loading"
/>
</template>
<script setup lang="ts">
import { getComponent } 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, TextInputEventDetail } from 'kintone-ui-component';
import { defineProps, defineEmits, computed } from 'vue';
const props = defineProps<{
modelValue: string;
selectedAppData: CachedSelectedAppData;
whereConditions: WhereCondition[];
id: string;
}>();
const whereCondition = computed(() => search(props.whereConditions, props.id) as WhereCondition | undefined);
const type = computed(() => getComponent(whereCondition.value?.condition || ''));
type EmitData = {
obj?: WhereCondition;
value: string | '';
};
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 || '' });
};
</script>

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,130 @@
/* 辅助类 */
.flex-row {
display: flex;
}
.hidden {
visibility: hidden;
}
.border {
border: 1px solid #e3e7e8;
}
/* config 页面 */
#app {
width: 60vw;
min-width: 920px;
}
/* 最上面的说明 */
.settings-heading {
padding: 1em 0;
}
/* label 样式 */
.kintoneplugin-label {
padding-left: 20px;
line-height: 40px;
}
/* laebl input 单行的情况 */
.flex-row .kintoneplugin-label {
margin: 0;
width: 10em;
}
/* 遮罩 */
#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(15vw, 200px);
--kuc-dropdown-toggle-width: max(15vw, 200px);
--kuc-combobox-toggle-width: max(15vw, 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 td {
border-left-color: rgba(0, 0, 0, 0);
border-right-color: rgba(0, 0, 0, 0);
vertical-align: middle;
}
.plugin-kuc-table tr td:nth-last-child(2) {
border-right-color: #e3e7e8;
}
.plugin-kuc-table tr td:first-child {
border-left-color: #e3e7e8;
}
.plugin-kuc-table .kuc-table-1-18-0__table__body__row__action {
height: 55px;
align-items: center;
}
/* 絞り込み条件选择相关样式 */
.row-connector-area {
margin: 0 1em;
}

View File

@@ -0,0 +1,10 @@
.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

@@ -0,0 +1,6 @@
export default {
config: {
title: 'Settings for data fetch plugin',
desc: 'This message is displayed on the app page after the app has been updated.',
},
};

View File

@@ -0,0 +1,6 @@
export default {
config: {
title: 'Settings for data fetch plugin',
desc: 'This message is displayed on the app page after the app has been updated.',
},
};

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