This commit is contained in:
13
.eslintrc.js
Normal file
13
.eslintrc.js
Normal 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,
|
||||
},
|
||||
};
|
||||
25
.gitignore
vendored
Normal file
25
.gitignore
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
data
|
||||
7
.prettierrc
Normal file
7
.prettierrc
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"semi": true,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 120,
|
||||
"bracketSpacing": true
|
||||
}
|
||||
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
||||
41
README.md
Normal file
41
README.md
Normal file
@@ -0,0 +1,41 @@
|
||||
|
||||
# data-fetch-plugin
|
||||
|
||||
1. コマンド:
|
||||
- `package.json`ファイルを開き、`scripts`内の`upload`コマンドのパラメータを接続する必要があるkintoneドメインに変更してください。
|
||||
package.json:
|
||||
```json
|
||||
"upload": "kintone-plugin-uploader --base-url https://{YOUR-KINTONE-DOMAIN}.cybozu.com --username {YOUR-USERID} --password {YOUR-PASSWORD} dist/plugin.zip ",
|
||||
```
|
||||
- `npm run build` を実行すると、`dist` ディレクトリにパッケージファイルを生成し、`plugin.zip` が作成されます。
|
||||
- `npm run upload` を実行すると、`plugin.zip` がkintoneにアップロードされます。
|
||||
- `npm run build-upload` を実行すると、上記両方同時実行されます。
|
||||
|
||||
2. Vue3.0を使用した開発:
|
||||
- 設定ページは `components/Config.vue` にて開発します。
|
||||
- Desktopページは `js/desktop.ts` にて開発します。
|
||||
- Mobileページは `js/desktop.ts` にて開発します。
|
||||
|
||||
3. 依存環境作成:
|
||||
- 最新のNode.js と npm のインストール
|
||||
- Yarnのインストール
|
||||
```bash
|
||||
npm install -g yarn
|
||||
```
|
||||
- 依存環境をインストール
|
||||
```bash
|
||||
yarn
|
||||
```
|
||||
- ビルド
|
||||
```bash
|
||||
yarn build
|
||||
```
|
||||
- pluginアップロード
|
||||
```bash
|
||||
yarn upload
|
||||
```
|
||||
- ビルド&アップロード
|
||||
```
|
||||
yarn build-upload
|
||||
```
|
||||
---
|
||||
28
components.d.ts
vendored
Normal file
28
components.d.ts
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
export {}
|
||||
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
CellInput: typeof import('./src/components/basic/conditions/CellInput.vue')['default']
|
||||
Config: typeof import('./src/components/Config.vue')['default']
|
||||
ErrorDialog: typeof import('./src/components/basic/ErrorDialog.vue')['default']
|
||||
PluginDropdown: typeof import('./src/components/basic/PluginDropdown.vue')['default']
|
||||
PluginInput: typeof import('./src/components/basic/PluginInput.vue')['default']
|
||||
PluginLabel: typeof import('./src/components/basic/PluginLabel.vue')['default']
|
||||
PluginRow: typeof import('./src/components/basic/PluginRow.vue')['default']
|
||||
PluginTableActionIcon: typeof import('./src/components/basic/PluginTableActionIcon.vue')['default']
|
||||
PluginTableActionIconGroup: typeof import('./src/components/basic/PluginTableActionIconGroup.vue')['default']
|
||||
PluginTableArea: typeof import('./src/components/basic/PluginTableArea.vue')['default']
|
||||
PluginTableConditionRow: typeof import('./src/components/basic/PluginTableConditionRow.vue')['default']
|
||||
PluginTableConnectRow: typeof import('./src/components/basic/PluginTableConnectRow.vue')['default']
|
||||
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']
|
||||
}
|
||||
}
|
||||
8
env.d.ts
vendored
Normal file
8
env.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
/// <reference types="vite/client" />
|
||||
/// <reference types="kintone" />
|
||||
|
||||
declare module '*.vue' {
|
||||
import { DefineComponent } from 'vue';
|
||||
const component: DefineComponent<{}, {}, any>;
|
||||
export default component;
|
||||
}
|
||||
4
index.html
Normal file
4
index.html
Normal file
@@ -0,0 +1,4 @@
|
||||
<section class="settings">
|
||||
<div id="app"></div>
|
||||
</section>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
9347
package-lock.json
generated
Normal file
9347
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
38
package.json
Normal file
38
package.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "data-fetch-plugin",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"preview": "vite preview",
|
||||
"build": "vite build && npm run pkg",
|
||||
"vite:build": "vite build",
|
||||
"build-upload": "npm run build && npm run upload",
|
||||
"build-up2":"npm run build && npm run upload2",
|
||||
"pkg": "kintone-plugin-packer --ppk private.ppk --out dist/plugin.zip dist/src",
|
||||
"upload": "kintone-plugin-uploader --base-url https://mfu07rkgnb7c.cybozu.com --username MXZ --password maxz1205 dist/plugin.zip ",
|
||||
"upload2": "kintone-plugin-uploader --base-url https://alicorn.cybozu.com --username maxz --password 7ld7i8vd dist/plugin.zip "
|
||||
},
|
||||
"dependencies": {
|
||||
"@kintone/rest-api-client": "^5.7.0",
|
||||
"kintone-ui-component": "1.18.0",
|
||||
"rollup-plugin-css-only": "^4.5.2",
|
||||
"vue": "^3.5.13",
|
||||
"vue-i18n": "^11.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cybozu/eslint-config": "^23.0.0",
|
||||
"@kintone/dts-gen": "^8.1.1",
|
||||
"@kintone/plugin-packer": "^8.1.3",
|
||||
"@kintone/plugin-uploader": "^9.1.2",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"eslint": "^8.57.0",
|
||||
"rollup-plugin-copy": "^3.5.0",
|
||||
"typescript": "^5.7.3",
|
||||
"unplugin-vue-components": "^28.0.0",
|
||||
"vite": "^6.0.1",
|
||||
"vue-tsc": "^2.1.10"
|
||||
}
|
||||
}
|
||||
15
private.ppk
Normal file
15
private.ppk
Normal 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-----
|
||||
1
src/assets/KintoneRestAPIClient.min.js
vendored
Normal file
1
src/assets/KintoneRestAPIClient.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
src/assets/icon.png
Normal file
BIN
src/assets/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 110 B |
2377
src/assets/kuc.min.js
vendored
Normal file
2377
src/assets/kuc.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
166
src/components/Config.vue
Normal file
166
src/components/Config.vue
Normal file
@@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<h2 class="settings-heading">{{ $t('config.title') }}</h2>
|
||||
<p class="kintoneplugin-desc">{{ $t('config.desc') }}</p>
|
||||
|
||||
<plugin-row class="header-row border">
|
||||
<plugin-input v-model="data.buttonName" placeholder="ボタン名を入力してください" label="集約ボタン名" />
|
||||
</plugin-row>
|
||||
|
||||
<div id="main-area" ref="mainArea">
|
||||
<plugin-table-area v-for="joinTable in data.joinTables" :table="joinTable" :key="joinTable.id" />
|
||||
</div>
|
||||
|
||||
<plugin-row class="footer-row border">
|
||||
<kuc-button text="キャンセル" type="normal" @click="cancel" />
|
||||
<kuc-button :disabled="!canSave" text="保存する" class="save-btn" type="submit" @click="save" />
|
||||
</plugin-row>
|
||||
|
||||
<kuc-spinner :container="mainArea" ref="spinner"></kuc-spinner>
|
||||
<error-dialog :message="errMessage" :show="showError" @update:show="(value) => showError = value"></error-dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
createEmptyJoinTable,
|
||||
loadApps,
|
||||
loadAppFieldsAndLayout,
|
||||
EMPTY_OPTION,
|
||||
getEmptyOnCondition,
|
||||
getMeta,
|
||||
} from '@/js/helper';
|
||||
import { isType, type OneOf, type Properties } from '@/js/kintone-rest-api-client';
|
||||
import type { CachedData, FieldsInfo, JoinTable, SavedData } from '@/types/model';
|
||||
import type { Spinner } from 'kintone-ui-component';
|
||||
|
||||
import { onMounted, watch, provide, reactive, ref, shallowRef, nextTick } from 'vue';
|
||||
|
||||
const props = defineProps<{ pluginId: string }>();
|
||||
const loading = ref(false);
|
||||
const canSave = ref(true);
|
||||
const data: SavedData = reactive({
|
||||
buttonName: '',
|
||||
joinTables: [createEmptyJoinTable()],
|
||||
});
|
||||
const showError = ref(false);
|
||||
const errMessage = ref("");
|
||||
|
||||
const cachedData: CachedData = reactive({
|
||||
apps: [EMPTY_OPTION],
|
||||
currentAppFields: { fields: {}, layout: [] } as FieldsInfo,
|
||||
});
|
||||
|
||||
provide('savedData', data);
|
||||
provide('canSave', (data: boolean) => {
|
||||
canSave.value = data;
|
||||
});
|
||||
provide('cachedData', cachedData);
|
||||
|
||||
const mainArea = shallowRef<HTMLElement | null>(null);
|
||||
const spinner = shallowRef<Spinner | null>(null);
|
||||
|
||||
onMounted(async () => {
|
||||
nextTick(async () => {
|
||||
spinner.value?.close(); // fix bug: kuc-spinner will not auto amount to target HTML element when init loading
|
||||
const savedData = kintone.plugin.app.getConfig(props.pluginId);
|
||||
loading.value = true;
|
||||
cachedData.apps = await loadApps();
|
||||
cachedData.currentAppFields = await loadAppFieldsAndLayout();
|
||||
if (savedData?.joinTablesForConfig) {
|
||||
const newJoinTables = JSON.parse(savedData.joinTablesForConfig);
|
||||
data.joinTables.length = 0;
|
||||
data.joinTables.push(...newJoinTables);
|
||||
}
|
||||
data.buttonName = savedData?.buttonName || '集約';
|
||||
loading.value = false;
|
||||
});
|
||||
});
|
||||
|
||||
watch(loading, (load) => {
|
||||
load ? spinner.value?.open() : spinner.value?.close();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => data.joinTables.length,
|
||||
(newLength) => {
|
||||
console.log(data.joinTables);
|
||||
if (newLength === 1) {
|
||||
data.joinTables[0].onConditions = [getEmptyOnCondition()];
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* 保存データのバリデーション関数
|
||||
*/
|
||||
function validate(data: SavedData<string>): boolean {
|
||||
// ボタン名が空の場合、エラーを表示
|
||||
if (!data.buttonName.trim()) {
|
||||
errMessage.value = 'ボタン名を入力してください。';
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const joinTable of data.joinTables) {
|
||||
// 取得元アプリが空の場合、エラーを表示
|
||||
if (!joinTable.app.trim()) {
|
||||
errMessage.value = '取得元アプリを指定してください。';
|
||||
return false;
|
||||
}
|
||||
|
||||
// 取得フィールドのマッピングが1つ未満の場合、エラーを表示
|
||||
if (
|
||||
joinTable.fieldsMapping.length < 1 ||
|
||||
!joinTable.fieldsMapping[0].leftField?.trim() ||
|
||||
!joinTable.fieldsMapping[0].rightField?.trim()
|
||||
) {
|
||||
errMessage.value = '取得フィールドを1つ以上設定してください。';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function save() {
|
||||
if(!validate(data)){
|
||||
showError.value=true;
|
||||
return;
|
||||
}
|
||||
const currentAppMeta = cachedData.currentAppFields.fields;
|
||||
const convertJoinTables = JSON.parse(JSON.stringify(data.joinTables)) as JoinTable<OneOf | string>[];
|
||||
convertJoinTables.forEach((item) => {
|
||||
const meta = getMeta(item.meta as Properties, item.table);
|
||||
if (!meta) return;
|
||||
|
||||
// Process onConditions
|
||||
item.onConditions.forEach((condition) => {
|
||||
condition.leftField = meta[condition.leftField as string] || condition.leftField;
|
||||
condition.rightField = currentAppMeta[condition.rightField as string] || condition.rightField;
|
||||
});
|
||||
|
||||
// Process fieldsMapping
|
||||
item.fieldsMapping.forEach((mapping) => {
|
||||
mapping.leftField = meta[mapping.leftField as string] || mapping.leftField;
|
||||
mapping.rightField = currentAppMeta[mapping.rightField as string] || mapping.rightField;
|
||||
});
|
||||
|
||||
// Process whereConditions
|
||||
item.whereConditions.forEach((condition) => {
|
||||
condition.field = meta[condition.field as string] || condition.field;
|
||||
});
|
||||
delete item.meta;
|
||||
});
|
||||
|
||||
data.joinTables.forEach((item) => {
|
||||
delete item.meta;
|
||||
});
|
||||
|
||||
kintone.plugin.app.setConfig({
|
||||
buttonName: data.buttonName,
|
||||
joinTables: JSON.stringify(convertJoinTables),
|
||||
joinTablesForConfig: JSON.stringify(data.joinTables || []),
|
||||
});
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
window.location.href = `../../${kintone.app.getId()}/plugin/`;
|
||||
}
|
||||
</script>
|
||||
79
src/components/basic/ErrorDialog.vue
Normal file
79
src/components/basic/ErrorDialog.vue
Normal file
@@ -0,0 +1,79 @@
|
||||
|
||||
<template>
|
||||
<div ref="dialogContainer">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Button, Dialog } from "kintone-ui-component";
|
||||
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
message: string;
|
||||
show: boolean;
|
||||
}>();
|
||||
|
||||
const isDialogVisible = ref(props.show);
|
||||
|
||||
watch(
|
||||
() => props.message,
|
||||
(newMessage) => {
|
||||
if (dialog.value) {
|
||||
dialog.value.content=newMessage;
|
||||
}
|
||||
}
|
||||
);
|
||||
watch(
|
||||
() => props.show,
|
||||
(newValue) => {
|
||||
if (dialog.value) {
|
||||
if (newValue) {
|
||||
dialog.value.open();
|
||||
} else {
|
||||
dialog.value.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits(["update:show"]);
|
||||
|
||||
const dialog = ref<Dialog | null>(null);
|
||||
const dialogContainer = ref<HTMLDivElement | null>(null);
|
||||
|
||||
const closeDialog = () => {
|
||||
if (dialog.value) {
|
||||
dialog.value.close();
|
||||
}
|
||||
emit("update:show", false);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!dialogContainer.value) return;
|
||||
|
||||
const okButton = new Button({ text: "OK", type: "normal" });
|
||||
okButton.addEventListener("click", closeDialog);
|
||||
|
||||
const footerDiv = document.createElement("div");
|
||||
footerDiv.className="dialog-action-bar";
|
||||
footerDiv.appendChild(okButton);
|
||||
|
||||
// 创建 Dialog 实例
|
||||
dialog.value = new Dialog({
|
||||
header: "エラー情報",
|
||||
content: props.message,
|
||||
icon: "error",
|
||||
container: dialogContainer.value,
|
||||
footer: footerDiv,
|
||||
footerVisible:true
|
||||
});
|
||||
|
||||
if (props.show) {
|
||||
dialog.value.open();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
27
src/components/basic/PluginDropdown.vue
Normal file
27
src/components/basic/PluginDropdown.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<div class="kintoneplugin-input-container flex-row">
|
||||
<plugin-label v-if="label" :label="label" />
|
||||
<kuc-combobox className="kuc-text-input" :items="items" :value="modelValue" @change="updateValue" :disabled="disabled"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { KucEvent } from '@/types/my-kintone';
|
||||
import type { ComboboxChangeEventDetail, DropdownItem } from 'kintone-ui-component';
|
||||
import { defineProps, defineEmits } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
label: string;
|
||||
disabled: boolean;
|
||||
items: DropdownItem[];
|
||||
modelValue: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: string): void;
|
||||
}>();
|
||||
|
||||
const updateValue = ({ detail }: KucEvent<ComboboxChangeEventDetail>) => {
|
||||
emit('update:modelValue', detail.value || '');
|
||||
};
|
||||
</script>
|
||||
26
src/components/basic/PluginInput.vue
Normal file
26
src/components/basic/PluginInput.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<div class="kintoneplugin-input-container flex-row">
|
||||
<plugin-label v-if="label" :label="label" />
|
||||
<kuc-text :placeholder="placeholder" className="kuc-text-input" :value="modelValue" @change="updateValue" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { KucEvent } from '@/types/my-kintone';
|
||||
import type { TextInputEventDetail } from 'kintone-ui-component';
|
||||
import { defineProps, defineEmits } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
label: string;
|
||||
modelValue: string;
|
||||
placeholder: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: string): void;
|
||||
}>();
|
||||
|
||||
const updateValue = ({ detail }: KucEvent<TextInputEventDetail>) => {
|
||||
emit('update:modelValue', detail.value || '');
|
||||
};
|
||||
</script>
|
||||
11
src/components/basic/PluginLabel.vue
Normal file
11
src/components/basic/PluginLabel.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div class="kintoneplugin-label">{{ label }}</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineProps } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
label: string;
|
||||
}>();
|
||||
</script>
|
||||
7
src/components/basic/PluginRow.vue
Normal file
7
src/components/basic/PluginRow.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<div class="kintoneplugin-row">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
23
src/components/basic/PluginTableActionIcon.vue
Normal file
23
src/components/basic/PluginTableActionIcon.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<button :class="['kuc-action-button', isAdd ? 'add' : 'remove']" :title="title">
|
||||
<svg fill="none" width="18" height="18" viewBox="0 0 16 16" aria-hidden="true">
|
||||
<path :d="dPath" fill-rule="evenodd" clip-rule="evenodd" :fill="fillPath"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps<{ isAdd: boolean; title: string }>();
|
||||
|
||||
const dAdd =
|
||||
'M8 16C12.4183 16 16 12.4183 16 8C16 3.58172 12.4183 0 8 0C3.58172 0 0 3.58172 0 8C0 12.4183 3.58172 16 8 16ZM12.0355 8.49997V7.49997H8.50008V3.96454H7.50008V7.49997H3.96443V8.49997H7.50008V12.0356H8.50008V8.49997H12.0355Z';
|
||||
const fillAdd = '#3498db';
|
||||
const dRemove =
|
||||
'M16 8C16 12.4183 12.4183 16 8 16C3.58172 16 0 12.4183 0 8C0 3.58172 3.58172 0 8 0C12.4183 0 16 3.58172 16 8ZM12.0355 7.49997V8.49997L3.96443 8.49997V7.49997H12.0355Z';
|
||||
const fillRemove = '#b5b5b5';
|
||||
|
||||
const dPath = computed(() => (props.isAdd ? dAdd : dRemove));
|
||||
const fillPath = computed(() => (props.isAdd ? fillAdd : fillRemove));
|
||||
</script>
|
||||
38
src/components/basic/PluginTableActionIconGroup.vue
Normal file
38
src/components/basic/PluginTableActionIconGroup.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<div>
|
||||
<plugin-table-action-icon v-if="canAdd" :is-add="true" title="Add Row" @click="onClick('add')" />
|
||||
<plugin-table-action-icon v-if="canDelete" :is-add="false" title="Delete this row" @click="onClick('remove')" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { createEmptyJoinTable } from '@/js/helper';
|
||||
import type { SavedData } from '@/types/model';
|
||||
import { defineProps, withDefaults, inject } from 'vue';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
canAdd?: boolean;
|
||||
canDelete?: boolean;
|
||||
tableId: string;
|
||||
}>(),
|
||||
{
|
||||
canAdd: true,
|
||||
canDelete: true,
|
||||
},
|
||||
);
|
||||
|
||||
const savedData = inject<SavedData>('savedData') as SavedData;
|
||||
const onClick = (type: 'add' | 'remove') => {
|
||||
if (type === 'add') {
|
||||
const currentIndex = savedData.joinTables.findIndex((t) => t.id === props.tableId);
|
||||
if (currentIndex !== -1) {
|
||||
savedData.joinTables.splice(currentIndex + 1, 0, createEmptyJoinTable());
|
||||
} else {
|
||||
savedData.joinTables.push(createEmptyJoinTable());
|
||||
}
|
||||
} else if (savedData.joinTables.length > 1) {
|
||||
savedData.joinTables = savedData.joinTables.filter((t) => t.id !== props.tableId);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
110
src/components/basic/PluginTableArea.vue
Normal file
110
src/components/basic/PluginTableArea.vue
Normal file
@@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<plugin-row class="table-area flex-row border">
|
||||
<div class="table-main-area">
|
||||
<plugin-row>
|
||||
<plugin-dropdown :disabled="false" label="取得元アプリ" :items="apps" v-model="table.app" />
|
||||
</plugin-row>
|
||||
<plugin-row>
|
||||
<plugin-dropdown
|
||||
:disabled="selectedAppData.loading"
|
||||
label="テーブル"
|
||||
:items="tableOptions"
|
||||
v-model="table.table"
|
||||
@change="selectTable"
|
||||
:key="componentKey"
|
||||
/>
|
||||
</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-row>
|
||||
<plugin-row class="flex-row">
|
||||
<plugin-label label="取得フィールド" />
|
||||
<plugin-table-connect-row connector="→" type="mapping" :modelValue="table.fieldsMapping" />
|
||||
</plugin-row>
|
||||
<plugin-row class="flex-row">
|
||||
<plugin-label label="絞込条件" />
|
||||
<plugin-table-condition-row :modelValue="table.whereConditions" :table="table"
|
||||
@update:modelValue="(newData:any) => table.whereConditions = newData"/>
|
||||
</plugin-row>
|
||||
</div>
|
||||
<div class="table-action-area">
|
||||
<plugin-table-action-icon-group :can-delete="savedData.joinTables.length > 1" :table-id="table.id" />
|
||||
</div>
|
||||
</plugin-row>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
EMPTY_OPTION,
|
||||
getTableFieldsDropdownItems,
|
||||
loadAppFieldsAndLayout,
|
||||
resetConditions,
|
||||
resetTable,
|
||||
} from '@/js/helper';
|
||||
import { types } from '@/js/kintone-rest-api-client';
|
||||
import type { CachedData, CachedSelectedAppData, FieldsInfo, JoinTable, SavedData } from '@/types/model';
|
||||
import { computed, inject, provide, reactive, ref, watch } from 'vue';
|
||||
|
||||
const savedData = inject<SavedData>('savedData') as SavedData;
|
||||
const cachedData = inject<CachedData>('cachedData') as CachedData;
|
||||
|
||||
const props = defineProps<{
|
||||
table: JoinTable;
|
||||
}>();
|
||||
const apps = computed(() => cachedData.apps);
|
||||
const tableOptions = ref([EMPTY_OPTION]);
|
||||
const loading = ref(false);
|
||||
|
||||
const selectedAppData: CachedSelectedAppData = reactive({
|
||||
appFields: { fields: {}, layout: [] } as FieldsInfo,
|
||||
table: props.table,
|
||||
loading: false,
|
||||
});
|
||||
provide('selectedAppData', selectedAppData);
|
||||
|
||||
const componentKey = ref(0);
|
||||
// fix-bug: force select saved data when load config
|
||||
watch(
|
||||
() => tableOptions.value,
|
||||
() => {
|
||||
if (!props.table.table) return;
|
||||
componentKey.value += 1;
|
||||
},
|
||||
{
|
||||
once: true,
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.table.app,
|
||||
async (newVal, oldVal) => {
|
||||
if (!newVal) {
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
const fields = await loadAppFieldsAndLayout(newVal);
|
||||
tableOptions.value = getTableFieldsDropdownItems(fields, types.SUBTABLE);
|
||||
selectedAppData.appFields = fields;
|
||||
props.table.meta = fields.fields;
|
||||
!!oldVal && resetTable(props.table);
|
||||
loading.value = false;
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const selectTable = () => {
|
||||
resetConditions(props.table);
|
||||
};
|
||||
|
||||
watch(
|
||||
() => !props.table.app || loading.value,
|
||||
(val) => {
|
||||
selectedAppData.loading = val;
|
||||
},
|
||||
);
|
||||
|
||||
const isJoinConditionShown = (table: JoinTable) => {
|
||||
return savedData.joinTables[0].id !== table.id;
|
||||
};
|
||||
</script>
|
||||
123
src/components/basic/PluginTableConditionRow.vue
Normal file
123
src/components/basic/PluginTableConditionRow.vue
Normal file
@@ -0,0 +1,123 @@
|
||||
<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[];
|
||||
table:JoinTable;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:modelValue', value: WhereCondition[]): void;
|
||||
}>();
|
||||
|
||||
const savedData = inject<SavedData>('savedData') as SavedData;
|
||||
const cachedData = inject<CachedData>('cachedData') as CachedData;
|
||||
const selectedAppData = inject<CachedSelectedAppData>('selectedAppData') as CachedSelectedAppData;
|
||||
// const table = computed(() => selectedAppData.table.table);
|
||||
|
||||
const 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}) => {
|
||||
if(obj){
|
||||
obj.data = value;
|
||||
const newData = props.modelValue.map((item) =>
|
||||
item.id === obj.id ? { ...item, data: value } : item
|
||||
);
|
||||
emit('update:modelValue', newData);
|
||||
}
|
||||
},
|
||||
});
|
||||
render(vnode, container);
|
||||
return container;
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
</script>
|
||||
118
src/components/basic/PluginTableConnectRow.vue
Normal file
118
src/components/basic/PluginTableConnectRow.vue
Normal file
@@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<kuc-table className='plugin-kuc-table' :columns="columns" :data="modelValue"/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { CachedData, CachedSelectedAppData, FieldsJoinMapping, WhereCondition } from '@/types/model';
|
||||
import { defineProps, inject, computed, reactive, render, h } from 'vue';
|
||||
import { generateId, getFieldObj, getFieldsDropdownItems, search } from '@/js/helper';
|
||||
import { getLeftAvailableJoinType, getRightAvailableJoinType, isLeftJoinForceDisable, isRightJoinForceDisable, } from '@/js/join';
|
||||
import { getLeftAvailableMappingType, getRightAvailableMappingType } from '@/js/mapping';
|
||||
import TableCombobox from './TableCombobox.vue';
|
||||
import { type FieldType, type OneOf } from '@/js/kintone-rest-api-client';
|
||||
|
||||
const props = defineProps<{
|
||||
connector: string;
|
||||
modelValue: FieldsJoinMapping[];
|
||||
type: 'connect' | 'mapping';
|
||||
}>();
|
||||
|
||||
const cachedData = inject<CachedData>('cachedData') as CachedData;
|
||||
const selectedAppData = inject<CachedSelectedAppData>('selectedAppData') as CachedSelectedAppData;
|
||||
const table = computed(() => selectedAppData.table.table);
|
||||
|
||||
const filterFunc = {
|
||||
connect: {
|
||||
left: (right?: OneOf | '') => getLeftAvailableJoinType(right),
|
||||
right: (left?: OneOf | '') => getRightAvailableJoinType(left),
|
||||
},
|
||||
mapping: {
|
||||
left: (right?: OneOf | '') => getLeftAvailableMappingType(right),
|
||||
right: (left?: OneOf | '') => getRightAvailableMappingType(left),
|
||||
},
|
||||
};
|
||||
|
||||
const columns = reactive([
|
||||
{
|
||||
title: '取得元アプリのフィールド',
|
||||
field: 'leftField',
|
||||
render: (cellData: string, rowData: WhereCondition) => {
|
||||
if (!rowData.id) {
|
||||
rowData.id = generateId();
|
||||
}
|
||||
const container = document.createElement('div');
|
||||
const vnode = h(TableCombobox, {
|
||||
items: computed(() => {
|
||||
const dependFilterField = getField('rightField', rowData.id);
|
||||
return getFieldsDropdownItems(selectedAppData.appFields, {
|
||||
subTableCode: table.value,
|
||||
baseFilter: filterFunc[props.type].left() as FieldType[],
|
||||
filterType: filterFunc[props.type].left(dependFilterField),
|
||||
dependFilterField,
|
||||
defaultDisableCallback: isLeftJoinForceDisable,
|
||||
});
|
||||
}),
|
||||
modelValue: computed(() => (search(props.modelValue, rowData.id) as FieldsJoinMapping)?.leftField || ''),
|
||||
selectedAppData,
|
||||
dataList: props.modelValue,
|
||||
id: rowData.id,
|
||||
'onUpdate:modelValue': (data) => {
|
||||
if (data.obj) {
|
||||
(data.obj as FieldsJoinMapping).leftField = data.value;
|
||||
}
|
||||
},
|
||||
});
|
||||
render(vnode, container);
|
||||
return container;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
field: 'connector',
|
||||
render: () => {
|
||||
return props.connector;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'このアプリのフィールド',
|
||||
field: 'rightField',
|
||||
render: (cellData: string, rowData: WhereCondition) => {
|
||||
if (!rowData.id) {
|
||||
rowData.id = generateId();
|
||||
}
|
||||
const container = document.createElement('div');
|
||||
const vnode = h(TableCombobox, {
|
||||
items: computed(() => {
|
||||
const dependFilterField = getField('leftField', rowData.id);
|
||||
return getFieldsDropdownItems(cachedData.currentAppFields, {
|
||||
subTableCode: '', // subtable not allowed for current app
|
||||
baseFilter: filterFunc[props.type].right() as FieldType[],
|
||||
filterType: filterFunc[props.type].right(dependFilterField),
|
||||
dependFilterField,
|
||||
defaultDisableCallback: props.type === 'connect' ? isRightJoinForceDisable : undefined,
|
||||
});
|
||||
}),
|
||||
modelValue: computed(() => (search(props.modelValue, rowData.id) as FieldsJoinMapping)?.rightField || ''),
|
||||
selectedAppData,
|
||||
dataList: props.modelValue,
|
||||
id: rowData.id,
|
||||
'onUpdate:modelValue': (data) => {
|
||||
if (data.obj) {
|
||||
(data.obj as FieldsJoinMapping).rightField = data.value;
|
||||
}
|
||||
},
|
||||
});
|
||||
render(vnode, container);
|
||||
return container;
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
function getField(key: 'leftField' | 'rightField', id: string) {
|
||||
const dataRow = search(props.modelValue, id) as FieldsJoinMapping | undefined;
|
||||
const fieldCode = dataRow ? dataRow[key] || '' : '';
|
||||
const targetFieldMap = key === 'leftField' ? selectedAppData.appFields : cachedData.currentAppFields;
|
||||
const targetTable = key === 'leftField' ? table.value : '';
|
||||
return getFieldObj(fieldCode, targetFieldMap, targetTable);
|
||||
}
|
||||
</script>
|
||||
52
src/components/basic/TableCombobox.vue
Normal file
52
src/components/basic/TableCombobox.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<kuc-combobox
|
||||
className="kuc-text-input"
|
||||
:items="items.value"
|
||||
:value="modelValue.value"
|
||||
@change="updateValue"
|
||||
:key="componentKey"
|
||||
:disabled="selectedAppData.loading == undefined ? false : selectedAppData.loading"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { search } from '@/js/helper';
|
||||
import type { CachedSelectedAppData } 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';
|
||||
|
||||
const props = defineProps<{
|
||||
items: Ref<DropdownItem[]>;
|
||||
modelValue: Ref<string>;
|
||||
dataList: any[];
|
||||
id: string;
|
||||
selectedAppData: CachedSelectedAppData;
|
||||
}>();
|
||||
|
||||
const componentKey = ref(0);
|
||||
// fix-bug: force select saved data when load config
|
||||
watch(
|
||||
() => props.items.value,
|
||||
() => {
|
||||
if (!props.modelValue.value) return;
|
||||
componentKey.value += 1;
|
||||
},
|
||||
{
|
||||
once: true,
|
||||
},
|
||||
);
|
||||
|
||||
type EmitData = {
|
||||
obj?: any;
|
||||
value: string;
|
||||
};
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', data: EmitData): void;
|
||||
}>();
|
||||
|
||||
const updateValue = ({ detail }: KucEvent<ComboboxChangeEventDetail>) => {
|
||||
emit('update:modelValue', { obj: search(props.dataList, props.id), value: detail.value || '' });
|
||||
};
|
||||
</script>
|
||||
22
src/components/basic/conditions/CellInput.vue
Normal file
22
src/components/basic/conditions/CellInput.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<kuc-text className="kuc-text-input" :value="modelValue" @change="updateValue" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { KucEvent } from '@/types/my-kintone';
|
||||
import type { TextInputEventDetail } from 'kintone-ui-component';
|
||||
import { defineProps, defineEmits, type Ref } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: string): void;
|
||||
}>();
|
||||
|
||||
const updateValue = ({ detail }: KucEvent<TextInputEventDetail>) => {
|
||||
emit('update:modelValue', detail.value || '');
|
||||
};
|
||||
</script>
|
||||
|
||||
66
src/components/basic/conditions/TableCondition.vue
Normal file
66
src/components/basic/conditions/TableCondition.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<kuc-combobox
|
||||
v-if="items?.length"
|
||||
:items="items"
|
||||
:value="value"
|
||||
@change.stop="updateValue"
|
||||
:disabled="selectedAppData.loading == undefined ? false : selectedAppData.loading"
|
||||
className="condition-combobox-short"
|
||||
:data-val="value"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getAvailableCondition, type ConditionValue } from '@/js/conditions';
|
||||
import { search } from '@/js/helper';
|
||||
import type { CachedSelectedAppData, WhereCondition } from '@/types/model';
|
||||
import type { KucEvent } from '@/types/my-kintone';
|
||||
import type { ComboboxChangeEventDetail } from 'kintone-ui-component';
|
||||
import { defineProps, defineEmits, computed, watch, ref, type Ref } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: Ref<string>;
|
||||
selectedAppData: CachedSelectedAppData;
|
||||
whereConditions: WhereCondition[];
|
||||
id: string;
|
||||
}>();
|
||||
|
||||
const whereCondition = computed(() => search(props.whereConditions, props.id) as WhereCondition | undefined);
|
||||
|
||||
const items = computed(() =>
|
||||
getAvailableCondition(
|
||||
whereCondition.value?.field || '',
|
||||
props.selectedAppData.appFields,
|
||||
props.selectedAppData.table.table,
|
||||
),
|
||||
);
|
||||
|
||||
const value = ref(props.modelValue.value);
|
||||
|
||||
watch(
|
||||
() => items,
|
||||
() => {
|
||||
if (whereCondition.value?.condition === '') {
|
||||
// select first option
|
||||
const option = items.value?.[0] || { value: '' };
|
||||
value.value = option.value;
|
||||
updateValue({ detail: option });
|
||||
}
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
type EmitData = {
|
||||
obj?: WhereCondition;
|
||||
value: ConditionValue;
|
||||
};
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', data: EmitData): void;
|
||||
}>();
|
||||
|
||||
const updateValue = ({ detail }: KucEvent<ComboboxChangeEventDetail>) => {
|
||||
value.value = detail.value || '';
|
||||
emit('update:modelValue', { obj: whereCondition.value, value: detail.value as ConditionValue });
|
||||
};
|
||||
</script>
|
||||
150
src/components/basic/conditions/TableConditionValue.vue
Normal file
150
src/components/basic/conditions/TableConditionValue.vue
Normal file
@@ -0,0 +1,150 @@
|
||||
<template>
|
||||
<kuc-text
|
||||
v-if="valueType === 'kuc-text'"
|
||||
:value="modelValue.value"
|
||||
@change="updateValue"
|
||||
:className="needPlaceholderWidthClass"
|
||||
:placeholder="placeholder"
|
||||
:disabled="selectedAppData.loading == undefined ? false : selectedAppData.loading"
|
||||
/>
|
||||
<kuc-combobox
|
||||
v-else-if="valueType === 'kuc-combobox'"
|
||||
:value="modelValue.value"
|
||||
@change="updateValue"
|
||||
:disabled="selectedAppData.loading == undefined ? false : selectedAppData.loading"
|
||||
/>
|
||||
<kuc-time-picker
|
||||
v-else-if="valueType === 'kuc-time'"
|
||||
:value="modelValue.value"
|
||||
@change="updateValue"
|
||||
:disabled="selectedAppData.loading == undefined ? false : selectedAppData.loading"
|
||||
/>
|
||||
<table-condition-value-date-time
|
||||
v-else-if="valueType === 'datetime' || valueType === 'date'"
|
||||
:time="valueType === 'datetime'"
|
||||
:value="modelValue.value as string"
|
||||
@change="updateValue"
|
||||
:disabled="selectedAppData.loading == undefined ? false : selectedAppData.loading"
|
||||
/>
|
||||
<kuc-multi-choice
|
||||
v-else-if="valueType === 'kuc-multichoice'"
|
||||
:value="multiValue"
|
||||
:items="multiChoiceItems"
|
||||
:requiredIcon="true"
|
||||
@change="updateMultiValue"
|
||||
:disabled="selectedAppData.loading == undefined ? false : selectedAppData.loading"
|
||||
/>
|
||||
<table-condition-value-multi-input
|
||||
v-else-if="valueType === 'multi-input'"
|
||||
:value="multiInput"
|
||||
@change="updateTableValue"
|
||||
:disabled="selectedAppData.loading == undefined ? false : selectedAppData.loading"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getComponent } from '@/js/conditions';
|
||||
import { getFieldObj, isStringArray, search } from '@/js/helper';
|
||||
import { isType } from '@/js/kintone-rest-api-client';
|
||||
import { isSelectType } from '@/js/mapping';
|
||||
import type { CachedSelectedAppData, StringValue, WhereCondition } from '@/types/model';
|
||||
import type { KucEvent } from '@/types/my-kintone';
|
||||
import type { ComboboxChangeEventDetail, TextInputEventDetail, MultiChoiceChangeEventDetail } from 'kintone-ui-component';
|
||||
import { defineProps, defineEmits, computed, type Ref, inject, provide, ref, watch, watchEffect } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: Ref<StringValue>;
|
||||
selectedAppData: CachedSelectedAppData;
|
||||
whereConditions: WhereCondition[];
|
||||
id: string;
|
||||
canSave: (canSave: boolean) => void;
|
||||
}>();
|
||||
|
||||
provide('canSave', props.canSave);
|
||||
|
||||
const whereCondition = computed(() => search(props.whereConditions, props.id) as WhereCondition | undefined);
|
||||
|
||||
const needPlaceholderWidthClass = computed(() => (placeholder.value ? 'kuc-text-input-placeholder-width' : ''));
|
||||
|
||||
const placeholder = computed(() => {
|
||||
const field = getFieldObj(whereCondition.value?.field || '', props.selectedAppData.appFields, '');
|
||||
if (isType.GROUP_SELECT(field)) {
|
||||
return 'グループコードをカンマ区切りで指定';
|
||||
} else if (isType.ORGANIZATION_SELECT(field)) {
|
||||
return '組織コードをカンマ区切りで指定';
|
||||
} else if (isType.USER_SELECT(field) || isType.CREATOR(field) || isType.MODIFIER(field)) {
|
||||
return 'ログイン名をカンマ区切りで指定';
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
const multiChoiceItems = computed(() => {
|
||||
const field = getFieldObj(whereCondition.value?.field || '', props.selectedAppData.appFields, '');
|
||||
const items = [{
|
||||
label: '--',
|
||||
value: '',
|
||||
}];
|
||||
if (field && isSelectType(field)) {
|
||||
const opts = field.options;
|
||||
const multiOpts = Object.values(opts).map((opt) => {
|
||||
return {
|
||||
label: opt.label,
|
||||
value: opt.label,
|
||||
};
|
||||
});
|
||||
items.push(...multiOpts);
|
||||
}
|
||||
return items;
|
||||
});
|
||||
|
||||
const valueType = computed(() => {
|
||||
const field = getFieldObj(whereCondition.value?.field || '', props.selectedAppData.appFields, '');
|
||||
return getComponent(whereCondition.value?.condition || '', field);
|
||||
});
|
||||
|
||||
type EmitData = {
|
||||
obj?: WhereCondition;
|
||||
value: StringValue;
|
||||
};
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', data: EmitData): void;
|
||||
}>();
|
||||
|
||||
const updateValue = (event: KucEvent<ComboboxChangeEventDetail | TextInputEventDetail>) => {
|
||||
emit('update:modelValue', { obj: whereCondition.value, value: event.detail.value || '' });
|
||||
};
|
||||
|
||||
const multiValue = ref(isStringArray(props.modelValue.value) ? props.modelValue.value : []);
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
() => {
|
||||
const field = getFieldObj(whereCondition.value?.field || '', props.selectedAppData.appFields, '');
|
||||
const vType = valueType.value;
|
||||
const moduleValue = props.modelValue.value;
|
||||
if (field && isSelectType(field) && vType === 'kuc-multichoice') {
|
||||
multiValue.value = isStringArray(moduleValue) ? moduleValue : [];
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const multiInput = ref(isStringArray(props.modelValue.value) ? (props.modelValue.value as string[]) : ['', '']);
|
||||
watchEffect(() => {
|
||||
const vType = valueType.value;
|
||||
const moduleValue = props.modelValue.value;
|
||||
if (vType === 'multi-input') {
|
||||
multiInput.value = isStringArray(moduleValue) ? (moduleValue as string[]) : ['', ''];
|
||||
}
|
||||
});
|
||||
|
||||
const updateMultiValue = (event: KucEvent<MultiChoiceChangeEventDetail>) => {
|
||||
emit('update:modelValue', { obj: whereCondition.value, value: event.detail.value || [] });
|
||||
};
|
||||
|
||||
const updateTableValue = (event: KucEvent<string[]>) => {
|
||||
let value = event.detail || ['', ''];
|
||||
multiInput.value = value;
|
||||
emit('update:modelValue', { obj: whereCondition.value, value: value });
|
||||
};
|
||||
</script>
|
||||
267
src/components/basic/conditions/TableConditionValueDateTime.vue
Normal file
267
src/components/basic/conditions/TableConditionValueDateTime.vue
Normal file
@@ -0,0 +1,267 @@
|
||||
<template>
|
||||
<div style="display: flex; position: relative">
|
||||
<kuc-combobox
|
||||
:value="funcValue"
|
||||
:items="options"
|
||||
@change.stop="updateFuncValue"
|
||||
:disabled="disabled"
|
||||
:className="shortConditionClass"
|
||||
:key="time"
|
||||
/>
|
||||
|
||||
<template v-if="isInput()">
|
||||
<kuc-datetime-picker v-if="time" :value="inputValue" @change.stop="updateValue" :disabled="disabled" />
|
||||
<kuc-date-picker v-else :value="inputValue" @change.stop="updateValue" :disabled="disabled" />
|
||||
</template>
|
||||
<kuc-combobox
|
||||
v-else-if="isWeek()"
|
||||
:items="weekOptions"
|
||||
:value="selectValue"
|
||||
@change.stop="updateValue"
|
||||
:disabled="disabled"
|
||||
:className="weekClassName"
|
||||
/>
|
||||
<kuc-combobox
|
||||
v-else-if="isMonth()"
|
||||
:items="monthOptions"
|
||||
:value="selectValue"
|
||||
@change.stop="updateValue"
|
||||
:disabled="disabled"
|
||||
:className="monthClassName"
|
||||
/>
|
||||
|
||||
<template v-if="isFromToday()">
|
||||
<kuc-text
|
||||
:error="fromTodayError"
|
||||
:value="inputValue"
|
||||
@change.stop="updateFromTodayValue"
|
||||
:disabled="disabled"
|
||||
:className="fromTodayError ? 'from-today-input input error' : 'from-today-input input'"
|
||||
/>
|
||||
<kuc-combobox
|
||||
:items="fromOptions"
|
||||
:value="selectValue"
|
||||
@change.stop="updateFromTodaySelectValue"
|
||||
:disabled="disabled"
|
||||
className="from-today-input"
|
||||
/>
|
||||
<kuc-combobox
|
||||
:items="additionOptions"
|
||||
:value="additionSelectValue"
|
||||
@change.stop="updateFromTodayAdditionValue"
|
||||
:disabled="disabled"
|
||||
className="from-today-input addition"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { dateFuncMap, getDateFuncList, type DateFuncKey } from '@/js/conditions';
|
||||
import type { KucEvent } from '@/types/my-kintone';
|
||||
import type { ComboboxChangeEventDetail } from 'kintone-ui-component';
|
||||
import { defineProps, defineEmits, computed, ref, watch, inject, type Ref } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
value: string;
|
||||
time: boolean;
|
||||
disabled: boolean;
|
||||
}>();
|
||||
|
||||
const canSave = inject<(canSave: boolean) => void>('canSave');
|
||||
|
||||
const inputValue = ref('');
|
||||
const selectValue = ref('');
|
||||
const additionSelectValue = ref('');
|
||||
const funcValue = ref('');
|
||||
|
||||
const fromTodayError = ref('');
|
||||
|
||||
watch(
|
||||
() => fromTodayError.value,
|
||||
() => {
|
||||
canSave?.(!fromTodayError.value);
|
||||
},
|
||||
);
|
||||
|
||||
const isInput = (func = funcValue.value) => func === dateFuncMap[''].value;
|
||||
const isWeek = (func = funcValue.value) => func.includes('WEEK');
|
||||
const isMonth = (func = funcValue.value) => func.includes('MONTH');
|
||||
const isFromToday = (func = funcValue.value) => func.includes('FROM_TODAY');
|
||||
|
||||
const weekClassName = computed(() => {
|
||||
if (isWeek()) {
|
||||
return selectValue.value === DEFAULT_WEEK_MONTH ? 'week-all-combobox' : 'week-combobox';
|
||||
}
|
||||
return '';
|
||||
});
|
||||
const monthClassName = computed(() => {
|
||||
if (isMonth()) {
|
||||
return selectValue.value === DEFAULT_WEEK_MONTH ? 'month-all-combobox' : 'month-combobox';
|
||||
}
|
||||
return '';
|
||||
});
|
||||
const shortConditionClass = computed(() => {
|
||||
const className = 'datetime-condition-combobox';
|
||||
if (isInput()) { return className }
|
||||
if (isFromToday() || funcValue.value === dateFuncMap['NOW'].value) {
|
||||
return className + ' mid';
|
||||
} else {
|
||||
return className + ' short';
|
||||
}
|
||||
});
|
||||
|
||||
const regex = /^(?<func>\w+)\((?<val>.*)\)$/;
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
(current, before) => {
|
||||
if (props.value === '') {
|
||||
// select default one when empty
|
||||
funcValue.value = dateFuncMap[''].value;
|
||||
selectValue.value = '';
|
||||
inputValue.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const match = props.value.match(regex);
|
||||
|
||||
funcValue.value = dateFuncMap[(match?.groups?.func || '') as DateFuncKey].value;
|
||||
|
||||
const value = match?.groups?.val || (props.value.includes('%') ? '' : props.value);
|
||||
|
||||
// TODO set values is this method but isFromToday
|
||||
if (isInput()) {
|
||||
inputValue.value = value;
|
||||
selectValue.value = '';
|
||||
} else if (isWeek() || isMonth()) {
|
||||
inputValue.value = '';
|
||||
selectValue.value = value || DEFAULT_WEEK_MONTH;
|
||||
} else if (isFromToday() && !(before && isFromToday(before))) {
|
||||
// only called when first open page
|
||||
const split = value.split(', ');
|
||||
inputValue.value = split[0] === '0' ? '' : split[0].replace('-', '');
|
||||
selectValue.value = split[1] || fromOptions[0].value;
|
||||
additionSelectValue.value = split[0] ? (split[0].startsWith('-') ? '-' : '+') : '+';
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const options = computed(() => {
|
||||
return getDateFuncList(props.time).map((item) => {
|
||||
return { label: typeof item.label === 'function' ? item.label(props.time) : item.label, value: item.value };
|
||||
});
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'change', data: KucEvent<{ value: string }>): void;
|
||||
}>();
|
||||
|
||||
const updateValue = (event: KucEvent<ComboboxChangeEventDetail>) => {
|
||||
emit('change', { detail: { value: buildResult({ value: event.detail.value }) } });
|
||||
};
|
||||
|
||||
const updateFuncValue = (event: KucEvent<ComboboxChangeEventDetail>) => {
|
||||
selectValue.value = '';
|
||||
inputValue.value = '';
|
||||
emit('change', { detail: { value: buildResult({ func: event.detail.value }) } });
|
||||
};
|
||||
|
||||
const updateFromTodayValue = (event: KucEvent<ComboboxChangeEventDetail>) => {
|
||||
const value = buildFromTodayResult({ value: event.detail.value });
|
||||
if (!value) return;
|
||||
inputValue.value = event.detail.value as string;
|
||||
emit('change', { detail: { value } });
|
||||
};
|
||||
|
||||
const updateFromTodaySelectValue = (event: KucEvent<ComboboxChangeEventDetail>) => {
|
||||
const value = buildFromTodayResult({ select: event.detail.value });
|
||||
if (!value) return;
|
||||
selectValue.value = event.detail.value as string;
|
||||
emit('change', { detail: { value } });
|
||||
};
|
||||
|
||||
const updateFromTodayAdditionValue = (event: KucEvent<ComboboxChangeEventDetail>) => {
|
||||
const value = buildFromTodayResult({ addition: event.detail.value });
|
||||
if (!value) return;
|
||||
additionSelectValue.value = event.detail.value as string;
|
||||
emit('change', { detail: { value } });
|
||||
};
|
||||
|
||||
type FromTodayParam = {
|
||||
value?: string;
|
||||
select?: string;
|
||||
addition?: string;
|
||||
};
|
||||
|
||||
function buildFromTodayResult({
|
||||
value = inputValue.value || '0',
|
||||
select = selectValue.value || 'DAYS',
|
||||
addition = additionSelectValue.value || '-',
|
||||
}: FromTodayParam) {
|
||||
if (value?.match(/^-?[0-9]+$/)) {
|
||||
fromTodayError.value = '';
|
||||
let res = value;
|
||||
if (value && value !== '0' && addition === '-') {
|
||||
if (value.startsWith('-')) {
|
||||
res = res.replace('-', '');
|
||||
} else {
|
||||
res = '-' + res;
|
||||
}
|
||||
}
|
||||
return funcValue.value.replace('%s', select).replace('%d', res);
|
||||
} else if (!fromTodayError.value) {
|
||||
inputValue.value = value || '';
|
||||
fromTodayError.value = '整数値を指定してください。';
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
type Param = {
|
||||
func?: string;
|
||||
value?: string;
|
||||
};
|
||||
|
||||
function buildResult({ func = funcValue.value, value }: Param) {
|
||||
let val = value || inputValue.value;
|
||||
if (isWeek(func) || isMonth(func)) {
|
||||
val = value || selectValue.value;
|
||||
val = val === DEFAULT_WEEK_MONTH ? '' : val;
|
||||
} else if (isFromToday(func)) {
|
||||
return func.replace('%d', val || '0').replace('%s', val || 'DAYS');
|
||||
}
|
||||
return func.replace('%s', val || '');
|
||||
}
|
||||
|
||||
const DEFAULT_WEEK_MONTH = '_';
|
||||
|
||||
const fromOptions = [
|
||||
{ label: '日', value: 'DAYS' },
|
||||
{ label: '周', value: 'WEEKS' },
|
||||
{ label: '月', value: 'MONTHS' },
|
||||
{ label: '年', value: 'YEARS' },
|
||||
];
|
||||
|
||||
const additionOptions = [
|
||||
{ label: '前', value: '-' },
|
||||
{ label: '後', value: '+' },
|
||||
];
|
||||
|
||||
const weekOptions = [
|
||||
{ label: 'すべての曜日', value: '_' },
|
||||
{ label: '日', value: 'SUNDAY' },
|
||||
{ label: '月', value: 'MONDAY' },
|
||||
{ label: '火', value: 'TUESDAY' },
|
||||
{ label: '水', value: 'WEDNESDAY' },
|
||||
{ label: '木', value: 'THURSDAY' },
|
||||
{ label: '金', value: 'FRIDAY' },
|
||||
{ label: '土', value: 'SATURDAY' },
|
||||
];
|
||||
|
||||
const monthOptions = [{ label: 'すべて', value: '_' }];
|
||||
for (let i = 1; i <= 31; i++) {
|
||||
monthOptions.push({ label: i.toString() + '日', value: i.toString() });
|
||||
}
|
||||
monthOptions.push({ label: '末日', value: 'LAST' });
|
||||
</script>
|
||||
@@ -0,0 +1,82 @@
|
||||
|
||||
<template>
|
||||
<!-- <kuc-table className='table-option'
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
@change="updateValue"
|
||||
:headerVisible="false"
|
||||
ref="table"/> -->
|
||||
<div ref="tableContainer"></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { KucEvent } from '@/types/my-kintone';
|
||||
import { Table, Text, type TableChangeEventDetail } from 'kintone-ui-component';
|
||||
import { defineProps, defineEmits, computed, ref, watch, inject, type Ref, onMounted, onUnmounted } from 'vue';
|
||||
interface MuiltItem{
|
||||
value:string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
value: string[];
|
||||
}>();
|
||||
|
||||
const tableContainer = ref<HTMLDivElement>();
|
||||
const table = ref<Table | null>(null);
|
||||
const data = ref<MuiltItem[]>((props.value || ['', '']).map(x => ({ value: x })));
|
||||
watch(
|
||||
() => props.value,
|
||||
(newValue)=>{
|
||||
data.value =(newValue || ['', '']).map((x) => ({ value: x }));
|
||||
if (table.value) {
|
||||
table.value.data = data.value; // 更新 Table 数据
|
||||
}
|
||||
},
|
||||
{
|
||||
deep:true,immediate:true
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'change', data: KucEvent<string[]>): void;
|
||||
(e: 'update:modelValue', value: string[]): void;
|
||||
}>();
|
||||
|
||||
|
||||
const updateValue=(event:KucEvent<TableChangeEventDetail<MuiltItem>>)=>{
|
||||
data.value = event.detail.data||[{value:''},{value:''}];
|
||||
if (table.value) {
|
||||
table.value.data = data.value;
|
||||
}
|
||||
const muiltData = event.detail.data ? event.detail.data.map(x=>x.value) :[];
|
||||
emit('change', { detail: [...muiltData] });
|
||||
// emit('update:modelValue', [...muiltData] );
|
||||
// emit('change', muiltData);
|
||||
}
|
||||
|
||||
onMounted(()=>{
|
||||
table.value = new Table({
|
||||
className:'table-option',
|
||||
headerVisible:false,
|
||||
actionButton:true,
|
||||
columns:[
|
||||
{
|
||||
field:"value",
|
||||
render:(cellData:any)=>{
|
||||
const text = new Text({value:cellData});
|
||||
return text;
|
||||
}
|
||||
},
|
||||
],
|
||||
data:data.value
|
||||
});
|
||||
table.value.addEventListener('change', updateValue);
|
||||
tableContainer.value?.appendChild(table.value);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (table.value) {
|
||||
table.value.removeEventListener('change', updateValue);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
665
src/css/51-modern-default.css
Normal file
665
src/css/51-modern-default.css
Normal file
File diff suppressed because one or more lines are too long
253
src/css/config.css
Normal file
253
src/css/config.css
Normal file
@@ -0,0 +1,253 @@
|
||||
/* 辅助类 */
|
||||
.flex-row {
|
||||
display: flex;
|
||||
}
|
||||
.hidden {
|
||||
visibility: hidden;
|
||||
}
|
||||
.border {
|
||||
border: 1px solid #e3e7e8;
|
||||
}
|
||||
/* config 页面 */
|
||||
#app {
|
||||
width: 60vw;
|
||||
min-width: 1030px;
|
||||
}
|
||||
|
||||
/* 最上面的说明 */
|
||||
.settings-heading {
|
||||
padding: 1em 0;
|
||||
}
|
||||
|
||||
/* label 样式 */
|
||||
.kintoneplugin-label {
|
||||
padding-left: 20px;
|
||||
line-height: 40px;
|
||||
}
|
||||
/* laebl input 单行的情况 */
|
||||
.flex-row .kintoneplugin-label {
|
||||
margin: 0;
|
||||
width: 8.5em;
|
||||
}
|
||||
|
||||
/* 遮罩 */
|
||||
#main-area {
|
||||
position: relative;
|
||||
}
|
||||
#main-area .kuc-spinner-1-18-0__mask {
|
||||
position: absolute;
|
||||
background-color: white;
|
||||
}
|
||||
#main-area .kuc-spinner-1-18-0__spinner {
|
||||
position: absolute;
|
||||
}
|
||||
#main-area .kuc-spinner-1-18-0__spinner__loader {
|
||||
fill: #3498db;
|
||||
}
|
||||
|
||||
/* 表格内容垂直居中 */
|
||||
.table-area {
|
||||
margin: 0;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 整体边框相关样式 */
|
||||
.header-row {
|
||||
padding: 24px 0;
|
||||
margin: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
.table-main-area {
|
||||
flex: 1;
|
||||
border-right: 1px solid #e3e7e8;
|
||||
padding-top: 24px;
|
||||
}
|
||||
.footer-row {
|
||||
padding: 24px 0;
|
||||
margin-bottom: 32px;
|
||||
text-align: right;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
/* 底部按钮空间 */
|
||||
.save-btn {
|
||||
margin-left: 16px;
|
||||
margin-right: 24px;
|
||||
}
|
||||
|
||||
/* 输入框宽度 */
|
||||
.kuc-text-input {
|
||||
--kuc-text-input-width: max(16vw, 200px);
|
||||
--kuc-dropdown-toggle-width: max(16vw, 200px);
|
||||
--kuc-combobox-toggle-width: max(16vw, 200px);
|
||||
}
|
||||
|
||||
.plugin-kuc-table .kuc-text-input {
|
||||
--kuc-text-input-width: max(15vw, 200px);
|
||||
--kuc-dropdown-toggle-width: max(15vw, 200px);
|
||||
--kuc-combobox-toggle-width: max(15vw, 200px);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1840px) {
|
||||
.plugin-kuc-table .kuc-text-input {
|
||||
--kuc-text-input-width: max(13vw, 200px);
|
||||
--kuc-dropdown-toggle-width: max(13vw, 200px);
|
||||
--kuc-combobox-toggle-width: max(13vw, 200px);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1760px) {
|
||||
.plugin-kuc-table .kuc-text-input {
|
||||
--kuc-text-input-width: max(12vw, 200px);
|
||||
--kuc-dropdown-toggle-width: max(12vw, 200px);
|
||||
--kuc-combobox-toggle-width: max(12vw, 200px);
|
||||
}
|
||||
}
|
||||
|
||||
/* 统一 kintone +/- 按钮样式 */
|
||||
.kuc-action-button {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
padding: 2px;
|
||||
cursor: pointer;
|
||||
margin: 0 4px;
|
||||
}
|
||||
.kuc-action-button.remove {
|
||||
margin-right: 8px;
|
||||
}
|
||||
.kuc-action-button.add {
|
||||
margin-left: 8px;
|
||||
}
|
||||
.kuc-action-button:focus {
|
||||
border: 1px solid #3498db;
|
||||
outline: none;
|
||||
}
|
||||
.kuc-action-button.remove:hover path {
|
||||
fill: #e74c3c;
|
||||
}
|
||||
|
||||
/* 覆盖表格样式 */
|
||||
.plugin-kuc-table > table > tbody > tr > td {
|
||||
border-left-color: rgba(0, 0, 0, 0);
|
||||
border-right-color: rgba(0, 0, 0, 0);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.plugin-kuc-table > table > tbody > tr > td:nth-last-child(2) {
|
||||
border-right-color: #e3e7e8;
|
||||
}
|
||||
.plugin-kuc-table > table > tbody > tr > td:first-child {
|
||||
border-left-color: #e3e7e8;
|
||||
}
|
||||
.plugin-kuc-table > table > tbody > tr > .kuc-table-1-18-0__table__body__row__action {
|
||||
height: 55px;
|
||||
align-items: center;
|
||||
}
|
||||
.plugin-kuc-table:not(.condition-table) > table > tbody > tr > td:nth-child(2) {
|
||||
--kuc-table-header-1-width: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.condition-table > table > tbody > tr > td[style]:not(:first-child),
|
||||
.condition-table > table > thead > tr > th[style]:not(:first-child) {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
/* 絞り込み条件选择相关样式 */
|
||||
.row-connector-area {
|
||||
margin: 0 1em;
|
||||
}
|
||||
|
||||
.condition-combobox-short {
|
||||
--kuc-combobox-toggle-width: 168px;
|
||||
}
|
||||
/* .condition-combobox-short {
|
||||
--kuc-combobox-toggle-width: 140px
|
||||
}
|
||||
.condition-combobox-short[data-val='!='] {
|
||||
--kuc-combobox-toggle-width: 168px
|
||||
}
|
||||
.condition-combobox-short[data-val='like'] {
|
||||
--kuc-combobox-toggle-width: 200px
|
||||
}
|
||||
.condition-combobox-short[data-val='in'] {
|
||||
--kuc-combobox-toggle-width: 185px
|
||||
}
|
||||
.condition-combobox-short[data-val='not like'] {
|
||||
--kuc-combobox-toggle-width: 225px
|
||||
}
|
||||
.condition-combobox-short[data-val='not in'] {
|
||||
--kuc-combobox-toggle-width: 200px
|
||||
} */
|
||||
|
||||
.kuc-text-input-placeholder-width {
|
||||
--kuc-text-input-width: 258px;
|
||||
}
|
||||
|
||||
.datetime-condition-combobox {
|
||||
--kuc-combobox-toggle-width: 130px;
|
||||
}
|
||||
.datetime-condition-combobox.mid {
|
||||
--kuc-combobox-toggle-width: 112px;
|
||||
}
|
||||
.datetime-condition-combobox.short {
|
||||
--kuc-combobox-toggle-width: 92px;
|
||||
}
|
||||
.datetime-condition-combobox + * {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.datetime-condition-combobox li[value^='\-'] {
|
||||
user-select: none;
|
||||
margin: 8px 0;
|
||||
cursor: default;
|
||||
padding: 0;
|
||||
height: 1px;
|
||||
background-color: #eee;
|
||||
list-style: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.week-all-combobox {
|
||||
--kuc-combobox-toggle-width: 140px;
|
||||
}
|
||||
.week-combobox {
|
||||
--kuc-combobox-toggle-width: 72px;
|
||||
}
|
||||
.month-all-combobox {
|
||||
--kuc-combobox-toggle-width: 100px;
|
||||
}
|
||||
.month-combobox {
|
||||
--kuc-combobox-toggle-width: 86px;
|
||||
}
|
||||
.from-today-input {
|
||||
--kuc-text-input-width: 75px;
|
||||
--kuc-combobox-toggle-width: 75px;
|
||||
}
|
||||
.from-today-input.input {
|
||||
--kuc-text-input-width: 50px;
|
||||
--kuc-combobox-toggle-width: 50px;
|
||||
}
|
||||
|
||||
/* .from-today-input error */
|
||||
.condition-table.plugin-kuc-table > table > tbody > tr > td {
|
||||
vertical-align: top;
|
||||
}
|
||||
.from-today-input.input div[class^="kuc-base-error"] {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
.from-today-input.input.error {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.table-option td {
|
||||
padding: 1px;
|
||||
margin: 0;
|
||||
border: none;
|
||||
}
|
||||
.dialog-action-bar{
|
||||
text-align: right;
|
||||
}
|
||||
10
src/css/desktop.css
Normal file
10
src/css/desktop.css
Normal 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;
|
||||
}
|
||||
11
src/css/mobile.css
Normal file
11
src/css/mobile.css
Normal file
@@ -0,0 +1,11 @@
|
||||
.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;
|
||||
}
|
||||
13
src/i18n/index.ts
Normal file
13
src/i18n/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { createI18n } from 'vue-i18n';
|
||||
import ja from './lang/ja';
|
||||
import en from './lang/en';
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: kintone.getLoginUser().language || 'ja',
|
||||
messages: {
|
||||
ja,
|
||||
en,
|
||||
},
|
||||
});
|
||||
export default i18n;
|
||||
6
src/i18n/lang/en.ts
Normal file
6
src/i18n/lang/en.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
config: {
|
||||
title: 'Data Fetch Plugin Settings',
|
||||
desc: 'Set the aggregation button name, data source app, fetch fields, filter conditions, and linking conditions, then save.',
|
||||
},
|
||||
};
|
||||
6
src/i18n/lang/ja.ts
Normal file
6
src/i18n/lang/ja.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
config: {
|
||||
title: 'データ取得プラグインの設定',
|
||||
desc: '集約ボタン名とデータ取得元アプリ、取得フィールド、絞込条件や連結条件を設定後、保存してください。',
|
||||
},
|
||||
};
|
||||
349
src/js/KintoneIndexEventHandler.mobile.ts
Normal file
349
src/js/KintoneIndexEventHandler.mobile.ts
Normal file
@@ -0,0 +1,349 @@
|
||||
import type { FieldLayout, FieldsJoinMapping, JoinTable, Record, RecordForParameter, SavedData,StringValue, WhereCondition } from "@/types/model";
|
||||
import { type OneOf, isType } from "./field-types-mobile";
|
||||
import type { ConditionValue } from "./conditions";
|
||||
declare var KintoneRestAPIClient: typeof import("@kintone/rest-api-client").KintoneRestAPIClient;
|
||||
export class KintoneIndexEventHandler {
|
||||
private config: SavedData<FieldLayout>;
|
||||
private currentApp: string;
|
||||
constructor(config: SavedData<FieldLayout>, currentApp: string) {
|
||||
this.config = config;
|
||||
this.currentApp = currentApp;
|
||||
}
|
||||
|
||||
public init(): void {
|
||||
this.addButtonToView();
|
||||
}
|
||||
|
||||
// ボタン追加
|
||||
private addButtonToView(): void {
|
||||
const headerSpace =kintone.mobile.app.getHeaderSpaceElement();
|
||||
if (!headerSpace) {
|
||||
throw new Error('このページではヘッダー要素が利用できません。');
|
||||
};
|
||||
|
||||
// ボタン追加
|
||||
if (document.getElementById('btn-data-fetch')) return;
|
||||
const kuc = Kucs['1.18.0'];
|
||||
const button = new kuc.MobileButton({
|
||||
text: this.config.buttonName,
|
||||
type: "submit",
|
||||
id: 'btn-data-fetch',
|
||||
});
|
||||
button.addEventListener('click', () => this.handleButtonClick());
|
||||
headerSpace.appendChild(button);
|
||||
}
|
||||
|
||||
// ボタンクリック
|
||||
private handleButtonClick = async (): Promise<void> => {
|
||||
const spinner = this.showSpinner();
|
||||
try {
|
||||
console.log('データ収集開始...');
|
||||
await this.execDataFectch();
|
||||
spinner.close();
|
||||
location.reload();
|
||||
} catch (error) {
|
||||
spinner.close();
|
||||
const detailError = (error instanceof Error) ? "\n詳細:" + error.message : "";
|
||||
const errorMsg = `データ収集中処理中例外発生しました。${detailError}`;
|
||||
console.error(errorMsg, error);
|
||||
window.alert(errorMsg);
|
||||
}
|
||||
}
|
||||
|
||||
private showSpinner() {
|
||||
const kuc = Kucs['1.18.0'];
|
||||
const spinner = new kuc.Spinner({
|
||||
text: 'データ収集中',
|
||||
container: document.body
|
||||
});
|
||||
spinner.open();
|
||||
return spinner;
|
||||
}
|
||||
|
||||
/**
|
||||
* 検索データ取得&作成処理
|
||||
*/
|
||||
private execDataFectch = async (): Promise<void> => {
|
||||
const mainTable = this.config.joinTables[0];
|
||||
const mainRecords = await this.fetchDataFromApp(mainTable);
|
||||
// フィールド結合
|
||||
let mainData = mainRecords.map((record) => {
|
||||
const rightRecord: Record = {};
|
||||
mainTable.fieldsMapping.forEach((mapping => {
|
||||
rightRecord[this.fieldCode(mapping.rightField)] = record[this.fieldCode(mapping.leftField)]
|
||||
}));
|
||||
return rightRecord;
|
||||
});
|
||||
const joinTables = this.config.joinTables.filter((table, index) => index > 0);
|
||||
for (const table of joinTables) {
|
||||
const subDatas = await this.fetchDataFromApp(table);
|
||||
mainData = this.leftJoin(mainData, subDatas, table);
|
||||
// console.log("LeftJoin", mainData);
|
||||
};
|
||||
//現在のデータをクリアする
|
||||
await this.deleteCurrentRecords();
|
||||
//データを更新する
|
||||
await this.saveDataToCurrentApp(mainData);
|
||||
}
|
||||
/**
|
||||
* Appからデータを取得する
|
||||
* @param joinTable 対象アプリかられレコードを取得する
|
||||
* @returns
|
||||
*/
|
||||
private fetchDataFromApp = async (joinTable: JoinTable<FieldLayout>): Promise<Record[]> => {
|
||||
// Filter 条件作成
|
||||
const filter = this.getWhereCondition(joinTable.whereConditions);
|
||||
//取得列を設定する
|
||||
const fetchFields = joinTable.fieldsMapping.map(map => this.fieldCode(map.leftField));
|
||||
if (joinTable.table) {
|
||||
fetchFields.push(joinTable.table);
|
||||
}
|
||||
const onFields =joinTable.onConditions.map(cond=>this.fieldCode(cond.leftField));
|
||||
onFields.forEach(fld=>{
|
||||
if(!fetchFields.includes(fld)){
|
||||
fetchFields.push(fld);
|
||||
}
|
||||
});
|
||||
// KintoneRESTAPI
|
||||
const client = new KintoneRestAPIClient();
|
||||
const records = await client.record.getAllRecords({
|
||||
app: joinTable.app,
|
||||
fields: fetchFields,
|
||||
condition: filter
|
||||
});
|
||||
//console.log("Data Fetch", records);
|
||||
//SubTableが含まれる場合、フラットなデータに変換する
|
||||
return this.convertToFlatDatas(records, joinTable.table);
|
||||
|
||||
}
|
||||
/**
|
||||
* 絞り込み条件式作成
|
||||
* @param whereCondifions
|
||||
* @returns
|
||||
*/
|
||||
private getWhereCondition(whereCondifions: WhereCondition<FieldLayout>[]): string {
|
||||
const conds = whereCondifions
|
||||
.filter((cond) => this.fieldCode(cond.field) !== '');
|
||||
const condition = conds.map((cond) => {
|
||||
let condition = cond.condition;
|
||||
if ("subField" in cond.field && cond.field.subField) {
|
||||
condition = this.mapConditionForSubField(cond.condition);
|
||||
}
|
||||
const condValue = this.getConditionValue(cond.field as OneOf, condition, cond.data);
|
||||
return `${this.fieldCode(cond.field)} ${condition} ${condValue}`;
|
||||
}).join(' and ');
|
||||
return condition;
|
||||
}
|
||||
/**
|
||||
* サブフィールドの演算子対応
|
||||
* @param condition
|
||||
* @returns
|
||||
*/
|
||||
private mapConditionForSubField(condition: ConditionValue): ConditionValue {
|
||||
switch (condition) {
|
||||
case "=":
|
||||
return "in";
|
||||
case "!=":
|
||||
return "not in";
|
||||
default:
|
||||
return condition; // 既存の条件をそのまま使用
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 条件比較値を変換する
|
||||
* @param field
|
||||
* @param condition
|
||||
* @param value
|
||||
* @returns
|
||||
*/
|
||||
private getConditionValue(field: OneOf, condition: ConditionValue, value: StringValue): string {
|
||||
if (!value) return "\"\"";
|
||||
if(this.isStringArray(value)){
|
||||
//マルチデータの場合
|
||||
const items = (value as string[]).map(item => `"${item.trim()}"`);
|
||||
return `(${items.join(",")})`;
|
||||
}
|
||||
const data = value as string;
|
||||
if (isType.NUMBER(field) || isType.RECORD_NUMBER(field)) {
|
||||
// For numbers, return as is
|
||||
return data;
|
||||
} else if (isType.DATE(field)) {
|
||||
// If field is DATE, format as "yyyy-MM-dd" unless it's a reserved function
|
||||
if (/^\d{4}-\d{2}-\d{2}$/.test(data)){
|
||||
return `"${data}"`;
|
||||
}else if(data.match(/^\w+\(.*\)$/)){
|
||||
return data;
|
||||
}
|
||||
const date = new Date(data);
|
||||
return `"${date.toISOString().split('T')[0]}"`;
|
||||
} else if (isType.DATETIME(field) || isType.CREATED_TIME(field) || isType.UPDATED_TIME(field)) {
|
||||
// 関数を使用する場合
|
||||
if (data.match(/^\w+\(.*\)$/)) {
|
||||
return data;
|
||||
}
|
||||
const dateTime = new Date(data);
|
||||
return `"${dateTime.toISOString()}"`;
|
||||
} else if ((condition === "in" || condition === "not in")) {
|
||||
if (data.includes(",")) {
|
||||
// Handle "in" and "not in" with comma-separated strings
|
||||
const items = data.split(",").map(item => `"${item.trim()}"`);
|
||||
return `(${items.join(",")})`;
|
||||
} else {
|
||||
return `("${data}")`;
|
||||
}
|
||||
} else {
|
||||
// Default case for other types (treat as text)
|
||||
return `"${data}"`;
|
||||
}
|
||||
}
|
||||
|
||||
private isStringArray=(value:any)=>{
|
||||
if(Array.isArray(value) && value.every(x=>typeof x ==='string')){
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* fieldからコードを取得する
|
||||
* @param field
|
||||
* @returns
|
||||
*/
|
||||
private fieldCode(field: any): string {
|
||||
if (!field) {
|
||||
return "";
|
||||
}
|
||||
if (typeof field === 'string' && field) {
|
||||
return field;
|
||||
} else if (typeof field === 'object' && 'code' in field) {
|
||||
return field.code;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* ネストされたサブテーブルデータをフラットなデータに変換し、親レコードを複製します。
|
||||
* @param records レコードの配列
|
||||
* @returns 変換後のフラットなレコード配列
|
||||
*/
|
||||
private convertToFlatDatas(records: Record[], subTable: string): Record[] {
|
||||
if (!subTable) {
|
||||
return records;
|
||||
}
|
||||
const flattenedData: Record[] = [];
|
||||
records.forEach((record) => {
|
||||
// テーブルフィールドが存在するかを確認
|
||||
if (record[subTable]?.type === "SUBTABLE" && record[subTable].value.length > 0) {
|
||||
// サブテーブル内の各レコードを処理
|
||||
record[subTable].value.forEach((nested: Record) => {
|
||||
// 親レコードのコピーを作成
|
||||
const flatRecord = { ...record };
|
||||
|
||||
// サブテーブルフィールドを抽出してフラットな構造に追加
|
||||
Object.entries(nested.value).forEach(([key, field]) => {
|
||||
flatRecord[key] = { value: field.value, type: field.type };
|
||||
});
|
||||
|
||||
// テーブルフィールドを削除
|
||||
delete flatRecord[subTable];
|
||||
|
||||
// 結果の配列に追加
|
||||
flattenedData.push(flatRecord);
|
||||
});
|
||||
} else {
|
||||
// サブテーブルが空の場合、親レコードをそのまま追加
|
||||
const flatRecord = { ...record };
|
||||
delete flatRecord[subTable];
|
||||
flattenedData.push(flatRecord);
|
||||
}
|
||||
});
|
||||
// console.log("FlatDatas=>", flattenedData);
|
||||
return flattenedData;
|
||||
}
|
||||
|
||||
/**
|
||||
* データLeftJoin処理
|
||||
* @param mainData
|
||||
* @param subData
|
||||
* @param onConditions
|
||||
* @param fieldsMapping
|
||||
* @returns
|
||||
*/
|
||||
private leftJoin(
|
||||
mainData: Record[],
|
||||
subData: Record[],
|
||||
joinTable: JoinTable<FieldLayout>
|
||||
): Record[] {
|
||||
const joinedRecords: Record[] = [];
|
||||
mainData.forEach((mainRecord) => {
|
||||
const matchedRecords = subData.filter((subRecord) =>
|
||||
joinTable.onConditions.every(
|
||||
(cond) => mainRecord[this.fieldCode(cond.rightField)]?.value === subRecord[this.fieldCode(cond.leftField)]?.value
|
||||
)
|
||||
);
|
||||
|
||||
// マッチ出来ない場合、LEFTの列のみ返す
|
||||
if (!matchedRecords || matchedRecords.length==0) {
|
||||
joinedRecords.push(mainRecord);
|
||||
} else {
|
||||
matchedRecords.forEach((matchedRecord) => {
|
||||
// フィールド結合
|
||||
const combinedRecord: Record = { ...mainRecord };
|
||||
joinTable.fieldsMapping.forEach((mapping) => {
|
||||
combinedRecord[this.fieldCode(mapping.rightField)] = matchedRecord[this.fieldCode(mapping.leftField)];
|
||||
});
|
||||
joinedRecords.push(combinedRecord);
|
||||
});
|
||||
}
|
||||
});
|
||||
return joinedRecords;
|
||||
}
|
||||
|
||||
/**
|
||||
* 現在アプリのすべてレコードを削除する
|
||||
*/
|
||||
private async deleteCurrentRecords(): Promise<void> {
|
||||
const client = new KintoneRestAPIClient();
|
||||
const currentRecords = await client.record.getAllRecords({
|
||||
app: this.currentApp,
|
||||
fields: ["$id"],
|
||||
});
|
||||
const deleteRecords = currentRecords.map(record => {
|
||||
return { id: record.$id.value as string }
|
||||
});
|
||||
await client.record.deleteAllRecords({
|
||||
app: this.currentApp,
|
||||
records: deleteRecords
|
||||
});
|
||||
client.record.addAllRecords
|
||||
}
|
||||
/**
|
||||
* 結合後のデータを現在のアプリに挿入する
|
||||
* @param records
|
||||
*/
|
||||
private async saveDataToCurrentApp(records: Record[]): Promise<void> {
|
||||
try {
|
||||
const client = new KintoneRestAPIClient();
|
||||
const result = await client.record.addAllRecords({
|
||||
app: this.currentApp,
|
||||
records: this.convertForUpdate(records)
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('データ作成時エラーが発生しました:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recordを更新時の形式を変換する
|
||||
* @param resords
|
||||
* @returns
|
||||
*/
|
||||
private convertForUpdate(resords: Record[]): RecordForParameter[] {
|
||||
return resords.map((record) =>
|
||||
Object.fromEntries(
|
||||
Object.entries(record).map(([fieldCode, { value }]) => [fieldCode, { value }])
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
342
src/js/KintoneIndexEventHandler.ts
Normal file
342
src/js/KintoneIndexEventHandler.ts
Normal file
@@ -0,0 +1,342 @@
|
||||
import type { FieldLayout, FieldsJoinMapping, JoinTable, Record, RecordForParameter, SavedData, StringValue,WhereCondition } from "@/types/model";
|
||||
import { type OneOf, isType } from "./field-types";
|
||||
import type { ConditionValue } from "./conditions";
|
||||
declare var KintoneRestAPIClient: typeof import("@kintone/rest-api-client").KintoneRestAPIClient;
|
||||
export class KintoneIndexEventHandler {
|
||||
private config: SavedData<FieldLayout>;
|
||||
private currentApp: string;
|
||||
constructor(config: SavedData<FieldLayout>, currentApp: string) {
|
||||
this.config = config;
|
||||
this.currentApp = currentApp;
|
||||
}
|
||||
|
||||
public init(): void {
|
||||
this.addButtonToView();
|
||||
}
|
||||
|
||||
// ボタン追加
|
||||
private addButtonToView(): void {
|
||||
const headerSpace = kintone.app.getHeaderMenuSpaceElement();
|
||||
if (!headerSpace) {
|
||||
throw new Error('このページではヘッダー要素が利用できません。');
|
||||
};
|
||||
|
||||
// ボタン追加
|
||||
if (document.getElementById('btn-data-fetch')) return;
|
||||
const kuc = Kucs['1.18.0'];
|
||||
const button = new kuc.Button({
|
||||
text: this.config.buttonName,
|
||||
type: "submit",
|
||||
id: 'btn-data-fetch',
|
||||
});
|
||||
button.addEventListener('click', () => this.handleButtonClick());
|
||||
headerSpace.appendChild(button);
|
||||
}
|
||||
|
||||
// ボタンクリック
|
||||
private handleButtonClick = async (): Promise<void> => {
|
||||
const spinner = this.showSpinner();
|
||||
try {
|
||||
console.log('データ収集開始...');
|
||||
await this.execDataFectch();
|
||||
spinner.close();
|
||||
location.reload();
|
||||
} catch (error) {
|
||||
spinner.close();
|
||||
const detailError = (error instanceof Error) ? "\n詳細:" + error.message : "";
|
||||
const errorMsg = `データ収集中処理中例外発生しました。${detailError}`;
|
||||
console.error(errorMsg, error);
|
||||
window.alert(errorMsg);
|
||||
}
|
||||
}
|
||||
|
||||
private showSpinner() {
|
||||
const kuc = Kucs['1.18.0'];
|
||||
const spinner = new kuc.Spinner({
|
||||
text: 'データ収集中',
|
||||
container: document.body
|
||||
});
|
||||
spinner.open();
|
||||
return spinner;
|
||||
}
|
||||
|
||||
/**
|
||||
* 検索データ取得&作成処理
|
||||
*/
|
||||
private execDataFectch = async (): Promise<void> => {
|
||||
const mainTable = this.config.joinTables[0];
|
||||
const mainRecords = await this.fetchDataFromApp(mainTable);
|
||||
// フィールド結合
|
||||
let mainData = mainRecords.map((record) => {
|
||||
const rightRecord: Record = {};
|
||||
mainTable.fieldsMapping.forEach((mapping => {
|
||||
rightRecord[this.fieldCode(mapping.rightField)] = record[this.fieldCode(mapping.leftField)]
|
||||
}));
|
||||
return rightRecord;
|
||||
});
|
||||
const joinTables = this.config.joinTables.filter((table, index) => index > 0);
|
||||
for (const table of joinTables) {
|
||||
const subDatas = await this.fetchDataFromApp(table);
|
||||
mainData = this.leftJoin(mainData, subDatas, table);
|
||||
// console.log("LeftJoin", mainData);
|
||||
};
|
||||
//現在のデータをクリアする
|
||||
await this.deleteCurrentRecords();
|
||||
//データを更新する
|
||||
await this.saveDataToCurrentApp(mainData);
|
||||
}
|
||||
/**
|
||||
* Appからデータを取得する
|
||||
* @param joinTable 対象アプリかられレコードを取得する
|
||||
* @returns
|
||||
*/
|
||||
private fetchDataFromApp = async (joinTable: JoinTable<FieldLayout>): Promise<Record[]> => {
|
||||
// Filter 条件作成
|
||||
const filter = this.getWhereCondition(joinTable.whereConditions);
|
||||
//取得列を設定する
|
||||
const fetchFields = joinTable.fieldsMapping.map(map => this.fieldCode(map.leftField));
|
||||
if (joinTable.table) {
|
||||
fetchFields.push(joinTable.table);
|
||||
}
|
||||
const onFields =joinTable.onConditions.map(cond=>this.fieldCode(cond.leftField));
|
||||
onFields.forEach(fld=>{
|
||||
if(!fetchFields.includes(fld)){
|
||||
fetchFields.push(fld);
|
||||
}
|
||||
});
|
||||
// KintoneRESTAPI
|
||||
const client = new KintoneRestAPIClient();
|
||||
const records = await client.record.getAllRecords({
|
||||
app: joinTable.app,
|
||||
fields: fetchFields,
|
||||
condition: filter
|
||||
});
|
||||
//console.log("Data Fetch", records);
|
||||
//SubTableが含まれる場合、フラットなデータに変換する
|
||||
return this.convertToFlatDatas(records, joinTable.table);
|
||||
|
||||
}
|
||||
/**
|
||||
* 絞り込み条件式作成
|
||||
* @param whereCondifions
|
||||
* @returns
|
||||
*/
|
||||
private getWhereCondition(whereCondifions: WhereCondition<FieldLayout>[]): string {
|
||||
const conds = whereCondifions
|
||||
.filter((cond) => this.fieldCode(cond.field) !== '');
|
||||
const condition = conds.map((cond) => {
|
||||
let condition = cond.condition;
|
||||
if ("subField" in cond.field && cond.field.subField) {
|
||||
condition = this.mapConditionForSubField(cond.condition);
|
||||
}
|
||||
const condValue = this.getConditionValue(cond.field as OneOf, condition, cond.data);
|
||||
return `${this.fieldCode(cond.field)} ${condition} ${condValue}`;
|
||||
}).join(' and ');
|
||||
return condition;
|
||||
}
|
||||
/**
|
||||
* サブフィールドの演算子対応
|
||||
* @param condition
|
||||
* @returns
|
||||
*/
|
||||
private mapConditionForSubField(condition: ConditionValue): ConditionValue {
|
||||
switch (condition) {
|
||||
case "=":
|
||||
return "in";
|
||||
case "!=":
|
||||
return "not in";
|
||||
default:
|
||||
return condition; // 既存の条件をそのまま使用
|
||||
}
|
||||
}
|
||||
private getConditionValue(field: OneOf, condition: ConditionValue, value: StringValue): string {
|
||||
if (!value) return "\"\"";
|
||||
if(this.isStringArray(value)){
|
||||
//マルチデータの場合
|
||||
const items = (value as string[]).map(item => `"${item.trim()}"`);
|
||||
return `(${items.join(",")})`;
|
||||
}
|
||||
const data = value as string;
|
||||
if (isType.NUMBER(field) || isType.RECORD_NUMBER(field)) {
|
||||
// For numbers, return as is
|
||||
return data;
|
||||
} else if (isType.DATE(field)) {
|
||||
// If field is DATE, format as "yyyy-MM-dd" unless it's a reserved function
|
||||
if (/^\d{4}-\d{2}-\d{2}$/.test(data)){
|
||||
return `"${data}"`;
|
||||
}else if(data.match(/^\w+\(.*\)$/)){
|
||||
return data;
|
||||
}
|
||||
const date = new Date(data);
|
||||
return `"${date.toISOString().split('T')[0]}"`;
|
||||
} else if (isType.DATETIME(field) || isType.CREATED_TIME(field) || isType.UPDATED_TIME(field)) {
|
||||
// If field is DATETIME, format as "yyyy-MM-ddTHH:mm:ssZ"
|
||||
if (data.match(/^\w+\(.*\)$/)) {
|
||||
return data;
|
||||
}
|
||||
const dateTime = new Date(data);
|
||||
return `"${dateTime.toISOString()}"`;
|
||||
} else if ((condition === "in" || condition === "not in")) {
|
||||
if (data.includes(",")) {
|
||||
// Handle "in" and "not in" with comma-separated strings
|
||||
const items = data.split(",").map(item => `"${item.trim()}"`);
|
||||
return `(${items.join(",")})`;
|
||||
} else {
|
||||
return `("${data}")`;
|
||||
}
|
||||
} else {
|
||||
// Default case for other types (treat as text)
|
||||
return `"${data}"`;
|
||||
}
|
||||
}
|
||||
|
||||
private isStringArray=(value:any)=>{
|
||||
if(Array.isArray(value) && value.every(x=>typeof x ==='string')){
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* fieldからコードを取得する
|
||||
* @param field
|
||||
* @returns
|
||||
*/
|
||||
private fieldCode(field: any): string {
|
||||
if (!field) {
|
||||
return "";
|
||||
}
|
||||
if (typeof field === 'string' && field) {
|
||||
return field;
|
||||
} else if (typeof field === 'object' && 'code' in field) {
|
||||
return field.code;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* ネストされたサブテーブルデータをフラットなデータに変換し、親レコードを複製します。
|
||||
* @param records レコードの配列
|
||||
* @returns 変換後のフラットなレコード配列
|
||||
*/
|
||||
private convertToFlatDatas(records: Record[], subTable: string): Record[] {
|
||||
if (!subTable) {
|
||||
return records;
|
||||
}
|
||||
const flattenedData: Record[] = [];
|
||||
records.forEach((record) => {
|
||||
// テーブルフィールドが存在するかを確認
|
||||
if (record[subTable]?.type === "SUBTABLE" && record[subTable].value.length > 0) {
|
||||
// サブテーブル内の各レコードを処理
|
||||
record[subTable].value.forEach((nested: Record) => {
|
||||
// 親レコードのコピーを作成
|
||||
const flatRecord = { ...record };
|
||||
|
||||
// サブテーブルフィールドを抽出してフラットな構造に追加
|
||||
Object.entries(nested.value).forEach(([key, field]) => {
|
||||
flatRecord[key] = { value: field.value, type: field.type };
|
||||
});
|
||||
|
||||
// テーブルフィールドを削除
|
||||
delete flatRecord[subTable];
|
||||
|
||||
// 結果の配列に追加
|
||||
flattenedData.push(flatRecord);
|
||||
});
|
||||
} else {
|
||||
// サブテーブルが空の場合、親レコードをそのまま追加
|
||||
const flatRecord = { ...record };
|
||||
delete flatRecord[subTable];
|
||||
flattenedData.push(flatRecord);
|
||||
}
|
||||
});
|
||||
// console.log("FlatDatas=>", flattenedData);
|
||||
return flattenedData;
|
||||
}
|
||||
|
||||
/**
|
||||
* データLeftJoin処理
|
||||
* @param mainData
|
||||
* @param subData
|
||||
* @param onConditions
|
||||
* @param fieldsMapping
|
||||
* @returns
|
||||
*/
|
||||
private leftJoin(
|
||||
mainData: Record[],
|
||||
subData: Record[],
|
||||
joinTable: JoinTable<FieldLayout>
|
||||
): Record[] {
|
||||
const joinedRecords: Record[] = [];
|
||||
mainData.forEach((mainRecord) => {
|
||||
const matchedRecords = subData.filter((subRecord) =>
|
||||
joinTable.onConditions.every(
|
||||
(cond) => mainRecord[this.fieldCode(cond.rightField)]?.value === subRecord[this.fieldCode(cond.leftField)]?.value
|
||||
)
|
||||
);
|
||||
|
||||
// マッチ出来ない場合、LEFTの列のみ返す
|
||||
if (!matchedRecords || matchedRecords.length==0) {
|
||||
joinedRecords.push(mainRecord);
|
||||
} else {
|
||||
matchedRecords.forEach((matchedRecord) => {
|
||||
// フィールド結合
|
||||
const combinedRecord: Record = { ...mainRecord };
|
||||
joinTable.fieldsMapping.forEach((mapping) => {
|
||||
combinedRecord[this.fieldCode(mapping.rightField)] = matchedRecord[this.fieldCode(mapping.leftField)];
|
||||
});
|
||||
joinedRecords.push(combinedRecord);
|
||||
});
|
||||
}
|
||||
});
|
||||
return joinedRecords;
|
||||
}
|
||||
|
||||
/**
|
||||
* 現在アプリのすべてレコードを削除する
|
||||
*/
|
||||
private async deleteCurrentRecords(): Promise<void> {
|
||||
const client = new KintoneRestAPIClient();
|
||||
const currentRecords = await client.record.getAllRecords({
|
||||
app: this.currentApp,
|
||||
fields: ["$id"],
|
||||
});
|
||||
const deleteRecords = currentRecords.map(record => {
|
||||
return { id: record.$id.value as string }
|
||||
});
|
||||
await client.record.deleteAllRecords({
|
||||
app: this.currentApp,
|
||||
records: deleteRecords
|
||||
});
|
||||
client.record.addAllRecords
|
||||
}
|
||||
/**
|
||||
* 結合後のデータを現在のアプリに挿入する
|
||||
* @param records
|
||||
*/
|
||||
private async saveDataToCurrentApp(records: Record[]): Promise<void> {
|
||||
try {
|
||||
const client = new KintoneRestAPIClient();
|
||||
const result = await client.record.addAllRecords({
|
||||
app: this.currentApp,
|
||||
records: this.convertForUpdate(records)
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('データ作成時エラーが発生しました:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recordを更新時の形式を変換する
|
||||
* @param resords
|
||||
* @returns
|
||||
*/
|
||||
private convertForUpdate(resords: Record[]): RecordForParameter[] {
|
||||
return resords.map((record) =>
|
||||
Object.fromEntries(
|
||||
Object.entries(record).map(([fieldCode, { value }]) => [fieldCode, { value }])
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
196
src/js/conditions.ts
Normal file
196
src/js/conditions.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
import type { FieldsInfo } from '@/types/model';
|
||||
import { isType, type FieldType, type OneOf } from './kintone-rest-api-client';
|
||||
import { getFieldObj } from './helper';
|
||||
|
||||
// conditionValue = '' | 'eq' | 'ne'
|
||||
// conditionItem = { value: 'eq', label: '=(等しい)', type: 'input', func: (a: string, b: string) => a === b }
|
||||
// = conditionMap[conditionValue]
|
||||
export type ConditionValue = '' | '=' | '!=' | '>=' | '<=' | '<' | '>' | 'like' | 'not like' | 'in' | 'not in';
|
||||
|
||||
type ConditionItem = {
|
||||
value: ConditionValue;
|
||||
label: string | ((field: OneOf) => string);
|
||||
type: ComponentType | ((field: OneOf) => ComponentType);
|
||||
};
|
||||
|
||||
export const conditionList: ConditionItem[] = [
|
||||
{ value: '=', label: '=(等しい)', type: (field) => dateTimeComponent[field.type] || 'input' },
|
||||
{ value: '!=', label: '≠ (等しくない)', type: (field) => dateTimeComponent[field.type] || 'input' },
|
||||
{
|
||||
value: '<=',
|
||||
label: (field) => (isDateTimeType(field) ? '≦ (以前)' : '≦ (以下)'),
|
||||
type: (field) => dateTimeComponent[field.type] || 'input',
|
||||
},
|
||||
{ value: '<', label: '< (より前)', type: (field) => dateTimeComponent[field.type] || 'input' },
|
||||
{
|
||||
value: '>=',
|
||||
label: (field) => (isDateTimeType(field) ? '≧ (以降)' : '≧ (以上)'),
|
||||
type: (field) => dateTimeComponent[field.type] || 'input',
|
||||
},
|
||||
{ value: '>', label: '> (より後)', type: (field) => dateTimeComponent[field.type] || 'input' },
|
||||
{ value: 'like', label: '次のキーワードを含む', type: 'input' },
|
||||
{ value: 'not like', label: '次のキーワードを含まない', type: 'input' },
|
||||
{ value: 'in', label: '次のいずれかを含む', type: (field) => MultiChoiceComponent[field.type] || 'input' },
|
||||
{ value: 'not in', label: '次のいずれも含まない', type: (field) => MultiChoiceComponent[field.type] || 'input' },
|
||||
];
|
||||
|
||||
// search from conditionList
|
||||
// conditionItem = conditionMap[conditionValue]
|
||||
export const conditionMap: Record<ConditionValue, ConditionItem> = conditionList.reduce(
|
||||
(map, item) => {
|
||||
map[item.value] = item;
|
||||
return map;
|
||||
},
|
||||
{} as Record<ConditionValue, ConditionItem>,
|
||||
);
|
||||
|
||||
type FieldConditions = Partial<Record<FieldType, ConditionValue[]>>;
|
||||
const textCondition: ConditionValue[] = ['=', '!=', 'in', 'like', 'not like'];
|
||||
const numberCondition: ConditionValue[] = ['=', '!=', '<=', '>='];
|
||||
const timeCondition: ConditionValue[] = ['=', '!=', '<=', '>=', '<', '>'];
|
||||
const containsCondition: ConditionValue[] = ['in', 'not in'];
|
||||
|
||||
// FieldType -> ConditionValue[]
|
||||
const fieldConditions: FieldConditions = {
|
||||
SINGLE_LINE_TEXT: textCondition,
|
||||
MULTI_LINE_TEXT: containsCondition,
|
||||
RICH_TEXT: containsCondition,
|
||||
NUMBER: numberCondition,
|
||||
CHECK_BOX: containsCondition,
|
||||
RADIO_BUTTON: containsCondition,
|
||||
DROP_DOWN: containsCondition,
|
||||
MULTI_SELECT: containsCondition,
|
||||
USER_SELECT: containsCondition,
|
||||
ORGANIZATION_SELECT: containsCondition,
|
||||
GROUP_SELECT: containsCondition,
|
||||
LINK: textCondition,
|
||||
CALC: numberCondition,
|
||||
|
||||
TIME: timeCondition,
|
||||
DATE: timeCondition,
|
||||
DATETIME: timeCondition,
|
||||
CREATED_TIME: timeCondition,
|
||||
CREATOR: containsCondition,
|
||||
UPDATED_TIME: timeCondition,
|
||||
MODIFIER: containsCondition,
|
||||
RECORD_NUMBER: numberCondition,
|
||||
} as const;
|
||||
|
||||
// fieldCode -> conditionList: ConditionItem[]
|
||||
export const getAvailableCondition = (fieldCode: string, fieldsInfo: FieldsInfo, subTableCode: string | '') => {
|
||||
if (!fieldCode || !fieldsInfo.fields) return;
|
||||
const fieldObj = getFieldObj(fieldCode, fieldsInfo, '');
|
||||
if (!fieldObj) return;
|
||||
const conditions = fieldConditions[fieldObj.type] || textCondition; // TODO a fallback here
|
||||
return conditions.map((condition) => {
|
||||
const res = { ...conditionMap[condition] };
|
||||
res.label = typeof res.label === 'function' ? res.label(fieldObj) : res.label;
|
||||
return res;
|
||||
});
|
||||
};
|
||||
|
||||
const component = {
|
||||
input: 'kuc-text',
|
||||
select: 'kuc-combobox',
|
||||
time: 'kuc-time',
|
||||
date: 'date',
|
||||
datetime: 'datetime',
|
||||
multiChoice: 'kuc-multichoice',
|
||||
multiInput: 'multi-input',
|
||||
};
|
||||
|
||||
export const isDateTimeType = (field: OneOf) => {
|
||||
return field.type in dateTimeComponent;
|
||||
};
|
||||
|
||||
const dateTimeComponent: Partial<Record<FieldType, ComponentType>> = {
|
||||
TIME: 'time',
|
||||
DATE: 'date',
|
||||
DATETIME: 'datetime',
|
||||
CREATED_TIME: 'datetime',
|
||||
UPDATED_TIME: 'datetime',
|
||||
};
|
||||
|
||||
const MultiChoiceComponent: Partial<Record<FieldType, ComponentType>> = {
|
||||
CHECK_BOX: 'multiChoice',
|
||||
DROP_DOWN: 'multiChoice',
|
||||
RADIO_BUTTON: 'multiChoice',
|
||||
MULTI_SELECT: 'multiChoice',
|
||||
SINGLE_LINE_TEXT: 'multiInput',
|
||||
LINK: 'multiInput',
|
||||
};
|
||||
|
||||
export type ComponentType = keyof typeof component;
|
||||
export const getComponent = (value: ConditionValue, fieldObj: OneOf) => {
|
||||
if (!value || !fieldObj) return;
|
||||
const condition = conditionMap[value].type;
|
||||
return component[typeof condition === 'function' ? condition(fieldObj) : condition];
|
||||
};
|
||||
|
||||
type DateFuncItem = {
|
||||
value: string;
|
||||
label: string | ((isTime: boolean) => string);
|
||||
condition?: 'datetime' | 'date';
|
||||
key: DateFuncKey;
|
||||
};
|
||||
|
||||
export type DateFuncKey =
|
||||
| ''
|
||||
| 'FROM_TODAY'
|
||||
| '---NOW---'
|
||||
| 'NOW'
|
||||
| '---DAY---'
|
||||
| 'YESTERDAY'
|
||||
| 'TODAY'
|
||||
| 'TOMORROW'
|
||||
| '---WEEK---'
|
||||
| 'LAST_WEEK'
|
||||
| 'THIS_WEEK'
|
||||
| 'NEXT_WEEK'
|
||||
| '---MONTH---'
|
||||
| 'LAST_MONTH'
|
||||
| 'THIS_MONTH'
|
||||
| 'NEXT_MONTH'
|
||||
| '---YEAR---'
|
||||
| 'LAST_YEAR'
|
||||
| 'THIS_YEAR'
|
||||
| 'NEXT_YEAR';
|
||||
|
||||
export const dateFuncList: DateFuncItem[] = [
|
||||
{ key: '', value: '%s', label: (isTime) => (isTime ? '日時を指定' : '日付を指定') },
|
||||
{ key: 'FROM_TODAY', value: 'FROM_TODAY(%d, %s)', label: '今日から' },
|
||||
{ key: '---NOW---', value: '\---NOW---', label: '' },
|
||||
{ key: 'NOW', value: 'NOW()', label: '当時刻', condition: 'datetime' },
|
||||
{ key: '---DAY---', value: '\---DAY---', label: '', condition: 'datetime' },
|
||||
{ key: 'YESTERDAY', value: 'YESTERDAY()', label: '昨日' },
|
||||
{ key: 'TODAY', value: 'TODAY()', label: '今日' },
|
||||
{ key: 'TOMORROW', value: 'TOMORROW()', label: '明日' },
|
||||
{ key: '---WEEK---', value: '\---WEEK---', label: '' },
|
||||
{ key: 'LAST_WEEK', value: 'LAST_WEEK(%s)', label: '先週' },
|
||||
{ key: 'THIS_WEEK', value: 'THIS_WEEK(%s)', label: '今週' },
|
||||
{ key: 'NEXT_WEEK', value: 'NEXT_WEEK(%s)', label: '来週' },
|
||||
{ key: '---MONTH---', value: '\---MONTH---', label: '' },
|
||||
{ key: 'LAST_MONTH', value: 'LAST_MONTH(%s)', label: '先月' },
|
||||
{ key: 'THIS_MONTH', value: 'THIS_MONTH(%s)', label: '今月' },
|
||||
{ key: 'NEXT_MONTH', value: 'NEXT_MONTH(%s)', label: '来月' },
|
||||
{ key: '---YEAR---', value: '\---YEAR---', label: '' },
|
||||
{ key: 'LAST_YEAR', value: 'LAST_YEAR()', label: '昨年' },
|
||||
{ key: 'THIS_YEAR', value: 'THIS_YEAR()', label: '今年' },
|
||||
{ key: 'NEXT_YEAR', value: 'NEXT_YEAR()', label: '来年' },
|
||||
];
|
||||
|
||||
// search from dateFuncList
|
||||
// DateFuncItem = dateFuncMap[DateFuncKey]
|
||||
export const dateFuncMap: Record<DateFuncKey, DateFuncItem> = dateFuncList.reduce(
|
||||
(map, item) => {
|
||||
map[item.key] = item;
|
||||
return map;
|
||||
},
|
||||
{} as Record<DateFuncKey, DateFuncItem>,
|
||||
);
|
||||
|
||||
export const getDateFuncList = (hasTime: boolean) => {
|
||||
return dateFuncList.filter((item) => {
|
||||
return item.condition ? item.condition === (hasTime ? 'datetime' : 'date') : true;
|
||||
});
|
||||
};
|
||||
34
src/js/desktop.ts
Normal file
34
src/js/desktop.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { Field, FieldLayout, SavedData } from "@/types/model";
|
||||
import { KintoneIndexEventHandler } from "./KintoneIndexEventHandler";
|
||||
|
||||
(function (PLUGIN_ID) {
|
||||
kintone.events.on('app.record.index.show', (event) => {
|
||||
try{
|
||||
const setting = kintone.plugin.app.getConfig(PLUGIN_ID);
|
||||
const config:SavedData<FieldLayout> = getConfig(setting);
|
||||
const currentApp = kintone.app.getId()?.toString();
|
||||
if(!currentApp) return;
|
||||
const handler = new KintoneIndexEventHandler(config,currentApp);
|
||||
handler.init();
|
||||
}catch(error){
|
||||
const detailError =(error instanceof Error) ? "\n詳細:" + error.message : "";
|
||||
const errorMsg = `データ収集中処理中例外発生しました。${ detailError }`;
|
||||
event.error = errorMsg;
|
||||
}
|
||||
return event;
|
||||
});
|
||||
/**
|
||||
* Config設定値を変換する
|
||||
* @param setting
|
||||
* @returns
|
||||
*/
|
||||
function getConfig(setting:any):SavedData<FieldLayout>{
|
||||
const config:SavedData<FieldLayout>={
|
||||
buttonName:setting.buttonName,
|
||||
joinTables:JSON.parse(setting.joinTables)
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
})(kintone.$PLUGIN_ID);
|
||||
|
||||
55
src/js/field-types-mobile.ts
Normal file
55
src/js/field-types-mobile.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
declare var KintoneRestAPIClient: typeof import("@kintone/rest-api-client").KintoneRestAPIClient;
|
||||
const client = new KintoneRestAPIClient();
|
||||
export type Properties = Awaited<ReturnType<typeof client.app.getFormFields>>['properties'];
|
||||
export type Layout = Awaited<ReturnType<typeof client.app.getFormLayout>>['layout'];
|
||||
|
||||
export type OneOf = Properties[string];
|
||||
export type FieldType = OneOf['type'];
|
||||
|
||||
const typeNames = [
|
||||
'RECORD_NUMBER',
|
||||
'CREATOR',
|
||||
'CREATED_TIME',
|
||||
'MODIFIER',
|
||||
'UPDATED_TIME',
|
||||
'CATEGORY',
|
||||
'STATUS',
|
||||
'STATUS_ASSIGNEE',
|
||||
'SINGLE_LINE_TEXT',
|
||||
'NUMBER',
|
||||
'CALC',
|
||||
'MULTI_LINE_TEXT',
|
||||
'RICH_TEXT',
|
||||
'LINK',
|
||||
'CHECK_BOX',
|
||||
'RADIO_BUTTON',
|
||||
'DROP_DOWN',
|
||||
'MULTI_SELECT',
|
||||
'FILE',
|
||||
'DATE',
|
||||
'TIME',
|
||||
'DATETIME',
|
||||
'USER_SELECT',
|
||||
'ORGANIZATION_SELECT',
|
||||
'GROUP_SELECT',
|
||||
'GROUP',
|
||||
'REFERENCE_TABLE',
|
||||
'SUBTABLE',
|
||||
] as const satisfies readonly FieldType[];
|
||||
|
||||
export const types = typeNames.reduce(
|
||||
(acc, name) => {
|
||||
acc[name] = name;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<(typeof typeNames)[number], (typeof typeNames)[number]>,
|
||||
);
|
||||
|
||||
type ExtractOneOf<T extends FieldType> = Extract<OneOf, { type: T }>;
|
||||
function createTypeGuard<T extends FieldType>(type: T) {
|
||||
return (value: OneOf): value is ExtractOneOf<T> => value?.type === type;
|
||||
}
|
||||
|
||||
export const isType = Object.fromEntries(
|
||||
typeNames.map((typeName) => [typeName, createTypeGuard(typeName as FieldType)]),
|
||||
) as { [K in (typeof typeNames)[number]]: (value: OneOf) => value is ExtractOneOf<K> };
|
||||
55
src/js/field-types.ts
Normal file
55
src/js/field-types.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
declare var KintoneRestAPIClient: typeof import("@kintone/rest-api-client").KintoneRestAPIClient;
|
||||
const client = new KintoneRestAPIClient();
|
||||
export type Properties = Awaited<ReturnType<typeof client.app.getFormFields>>['properties'];
|
||||
export type Layout = Awaited<ReturnType<typeof client.app.getFormLayout>>['layout'];
|
||||
|
||||
export type OneOf = Properties[string];
|
||||
export type FieldType = OneOf['type'];
|
||||
|
||||
const typeNames = [
|
||||
'RECORD_NUMBER',
|
||||
'CREATOR',
|
||||
'CREATED_TIME',
|
||||
'MODIFIER',
|
||||
'UPDATED_TIME',
|
||||
'CATEGORY',
|
||||
'STATUS',
|
||||
'STATUS_ASSIGNEE',
|
||||
'SINGLE_LINE_TEXT',
|
||||
'NUMBER',
|
||||
'CALC',
|
||||
'MULTI_LINE_TEXT',
|
||||
'RICH_TEXT',
|
||||
'LINK',
|
||||
'CHECK_BOX',
|
||||
'RADIO_BUTTON',
|
||||
'DROP_DOWN',
|
||||
'MULTI_SELECT',
|
||||
'FILE',
|
||||
'DATE',
|
||||
'TIME',
|
||||
'DATETIME',
|
||||
'USER_SELECT',
|
||||
'ORGANIZATION_SELECT',
|
||||
'GROUP_SELECT',
|
||||
'GROUP',
|
||||
'REFERENCE_TABLE',
|
||||
'SUBTABLE',
|
||||
] as const satisfies readonly FieldType[];
|
||||
|
||||
export const types = typeNames.reduce(
|
||||
(acc, name) => {
|
||||
acc[name] = name;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<(typeof typeNames)[number], (typeof typeNames)[number]>,
|
||||
);
|
||||
|
||||
type ExtractOneOf<T extends FieldType> = Extract<OneOf, { type: T }>;
|
||||
function createTypeGuard<T extends FieldType>(type: T) {
|
||||
return (value: OneOf): value is ExtractOneOf<T> => value?.type === type;
|
||||
}
|
||||
|
||||
export const isType = Object.fromEntries(
|
||||
typeNames.map((typeName) => [typeName, createTypeGuard(typeName as FieldType)]),
|
||||
) as { [K in (typeof typeNames)[number]]: (value: OneOf) => value is ExtractOneOf<K> };
|
||||
211
src/js/helper.ts
Normal file
211
src/js/helper.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
import type { FieldsInfo, FieldsJoinMapping, JoinTable, WhereCondition } from '@/types/model';
|
||||
import {
|
||||
client,
|
||||
isType,
|
||||
type FieldType,
|
||||
type App,
|
||||
type Layout,
|
||||
type OneOf,
|
||||
type Properties,
|
||||
} from './kintone-rest-api-client';
|
||||
import type { ComboboxItem, DropdownItem } from 'kintone-ui-component';
|
||||
import { isSpecialType, type SpecialType } from './join';
|
||||
|
||||
export const EMPTY_OPTION = {
|
||||
value: '',
|
||||
label: '--------',
|
||||
} as DropdownItem;
|
||||
|
||||
export function generateId(): string {
|
||||
const timestamp = new Date().getTime().toString(36);
|
||||
const randomNum = Math.random().toString(36).substring(2, 11);
|
||||
return `${timestamp}-${randomNum}`;
|
||||
}
|
||||
|
||||
export function search(list: Array<WhereCondition | FieldsJoinMapping>, id: string) {
|
||||
if (!list) return;
|
||||
return list.find((item) => item.id === id);
|
||||
}
|
||||
|
||||
export const getEmptyWhereCondition = () =>
|
||||
({ field: '', condition: '', data: '', id: generateId() }) as WhereCondition;
|
||||
export const getEmptyOnCondition = () => ({ leftField: '', rightField: '', id: generateId() }) as FieldsJoinMapping;
|
||||
export const getEmptyFieldsMapping = () => ({ leftField: '', rightField: '', id: generateId() }) as FieldsJoinMapping;
|
||||
|
||||
export function createEmptyJoinTable(id = generateId()) {
|
||||
return resetTable({ id, app: '' } as JoinTable);
|
||||
}
|
||||
|
||||
export function resetTable(table: JoinTable) {
|
||||
table.table = '';
|
||||
return resetConditions(table);
|
||||
}
|
||||
|
||||
export function resetConditions(table: JoinTable) {
|
||||
table.onConditions = [getEmptyOnCondition()];
|
||||
table.fieldsMapping = [getEmptyFieldsMapping()];
|
||||
table.whereConditions = [getEmptyWhereCondition()];
|
||||
return table;
|
||||
}
|
||||
|
||||
const LIMIT = 100; // 毎回請求の最大値
|
||||
export const loadApps = async (offset = 0, _apps: DropdownItem[] = []): Promise<DropdownItem[]> => {
|
||||
const { apps } = await client.app.getApps({ limit: LIMIT, offset });
|
||||
const allApps: DropdownItem[] = [
|
||||
..._apps,
|
||||
...apps.map((app: App) => ({ value: app.appId, label: app.name + '(ID: ' + app.appId + ')' })),
|
||||
];
|
||||
if (apps.length === LIMIT) {
|
||||
return loadApps(offset + LIMIT, allApps);
|
||||
}
|
||||
allApps.sort((a, b) => Number(b.value) - Number(a.value));
|
||||
allApps.unshift(EMPTY_OPTION);
|
||||
return allApps;
|
||||
};
|
||||
|
||||
export const loadAppFieldsAndLayout = async (appId: string | number = kintone.app.getId() as number) => {
|
||||
const fields = (await client.app.getFormFields({ app: appId })).properties;
|
||||
return {
|
||||
fields: flatFields(fields),
|
||||
layout: (await client.app.getFormLayout({ app: appId })).layout,
|
||||
} as FieldsInfo;
|
||||
};
|
||||
|
||||
function flatFields(fields: Properties) {
|
||||
const subtableFields = {} as Properties;
|
||||
Object.values(fields).forEach((field) => {
|
||||
if (isType.SUBTABLE(field)) {
|
||||
Object.values(field.fields).forEach((subField) => {
|
||||
const copy = JSON.parse(JSON.stringify(subField)) as typeof subField & {originLabel:string, tableCode:string};
|
||||
copy.label = '[' + field.label + '].' + subField.label;
|
||||
copy.originLabel = subField.label;
|
||||
copy.tableCode = field.code;
|
||||
subtableFields[subField.code] = copy;
|
||||
});
|
||||
}
|
||||
});
|
||||
return { ...fields, ...subtableFields };
|
||||
}
|
||||
|
||||
type FilterType = Array<FieldType | SpecialType>;
|
||||
type Param = {
|
||||
subTableCode: string | undefined;
|
||||
filterType?: FilterType;
|
||||
baseFilter: FieldType[] | undefined;
|
||||
dependFilterField?: OneOf;
|
||||
defaultLabel?: string;
|
||||
defaultDisableCallback?: (field: OneOf) => boolean;
|
||||
needAllSubTableField?: boolean;
|
||||
};
|
||||
export const getFieldsDropdownItems = (
|
||||
{ fields, layout }: FieldsInfo,
|
||||
{
|
||||
subTableCode, // specified subTable
|
||||
baseFilter, // set not allowed items hidden, undefined means no filter
|
||||
defaultLabel, // label shown (default '--------')
|
||||
filterType, // set not allowed items disabled, undefined and [] means no filter
|
||||
dependFilterField, // used for filterType
|
||||
defaultDisableCallback, // callback to control disabled items, like filterType
|
||||
needAllSubTableField = false, // show all subtable fields
|
||||
}: Param,
|
||||
) => {
|
||||
// get used field codes
|
||||
const fieldOrder = extractFields(layout, baseFilter, !!needAllSubTableField, subTableCode);
|
||||
const fieldMap = fields;
|
||||
|
||||
// create labels
|
||||
const labels: ComboboxItem[] = [
|
||||
{
|
||||
value: EMPTY_OPTION.value,
|
||||
label: defaultLabel || EMPTY_OPTION.label,
|
||||
},
|
||||
];
|
||||
return fieldOrder.reduce((acc, fieldCode) => {
|
||||
const field = fieldMap[fieldCode];
|
||||
if (!fieldCode) return acc;
|
||||
acc.push({
|
||||
value: fieldCode,
|
||||
label: field.label + '(FC: ' + fieldCode + ')',
|
||||
disabled:
|
||||
(defaultDisableCallback && defaultDisableCallback(field)) ||
|
||||
(filterType && !checkFilterType(field, dependFilterField, filterType)),
|
||||
});
|
||||
return acc;
|
||||
}, labels);
|
||||
};
|
||||
|
||||
const checkFilterType = (field: OneOf, dependFilterField: OneOf | undefined, filterType: FilterType) => {
|
||||
if (!filterType.length) return true; // [] means no filter
|
||||
return !!filterType.find((type) => {
|
||||
if (isSpecialType(type)) {
|
||||
return type.check(field, dependFilterField);
|
||||
}
|
||||
return isType[type](field);
|
||||
});
|
||||
};
|
||||
|
||||
export const getTableFieldsDropdownItems = ({ fields }: FieldsInfo, filterType?: FieldType) => {
|
||||
return Object.keys(fields).reduce(
|
||||
(acc, fieldCode) => {
|
||||
const field = fields[fieldCode];
|
||||
if (filterType && !isType[filterType](field)) return acc;
|
||||
acc.push({
|
||||
value: fieldCode,
|
||||
label: field.label + '(FC: ' + fieldCode + ')',
|
||||
});
|
||||
return acc;
|
||||
},
|
||||
[EMPTY_OPTION],
|
||||
);
|
||||
};
|
||||
|
||||
const extractFields = (layout: Layout, baseFilter: FieldType[] | undefined, needAllSubTableField: boolean, subTableCode?: string) => {
|
||||
return layout.reduce((acc, each) => {
|
||||
if (each.type === 'GROUP') {
|
||||
acc.push(...extractFields(each.layout, baseFilter, needAllSubTableField, subTableCode));
|
||||
} else if (each.type === 'ROW' || (!needAllSubTableField && each.code === subTableCode) || (needAllSubTableField && each.type === 'SUBTABLE')) {
|
||||
acc.push(
|
||||
...each.fields.map((field) => {
|
||||
if (!('code' in field)) return '';
|
||||
if (!baseFilter) return field.code;
|
||||
return baseFilter.find((t) => t === field.type) ? field?.code || '' : '';
|
||||
}),
|
||||
);
|
||||
}
|
||||
return acc;
|
||||
}, [] as string[]);
|
||||
};
|
||||
|
||||
export function getFieldObj(fieldCode: string, { fields }: FieldsInfo, subTableCode?: string) {
|
||||
const meta = getMeta(fields, subTableCode);
|
||||
return meta[fieldCode];
|
||||
}
|
||||
|
||||
export function getMeta(fields: Properties, subTableCode?: string, withNoSubTableField = true) {
|
||||
if (!fields || !subTableCode) {
|
||||
return fields;
|
||||
}
|
||||
let meta = fields;
|
||||
const table = meta[subTableCode];
|
||||
if (isType.SUBTABLE(table)) {
|
||||
const subFields = table.fields;
|
||||
Object.values(subFields).forEach((field) => {
|
||||
if (typeof field === 'object' && field !== null) {
|
||||
(field as Record<string, any>).subField = true;
|
||||
}
|
||||
});
|
||||
if (withNoSubTableField) {
|
||||
meta = { ...fields, ...subFields };
|
||||
} else {
|
||||
meta = subFields;
|
||||
}
|
||||
}
|
||||
return meta;
|
||||
}
|
||||
|
||||
export const isStringArray = (value: any) => {
|
||||
if (Array.isArray(value) && value.every((x) => typeof x === 'string')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
171
src/js/join.ts
Normal file
171
src/js/join.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import type { CalcType, LinkProtocolType } from '@/types/my-kintone';
|
||||
import { isType, type FieldType, type OneOf } from './kintone-rest-api-client';
|
||||
import { KintoneFormFieldProperty } from '@kintone/rest-api-client';
|
||||
|
||||
export function isLeftJoinForceDisable(field: OneOf) {
|
||||
if (isType.CALC(field)) {
|
||||
return field.format === 'DAY_HOUR_MINUTE' || field.format === 'HOUR_MINUTE';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isRightJoinForceDisable(field: OneOf) {
|
||||
if (isLookup(field)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export type SpecialType<T = string, F = any> = {
|
||||
type: T;
|
||||
format: F;
|
||||
check: (checkField: OneOf, selectedField?: OneOf) => boolean;
|
||||
};
|
||||
|
||||
// LEFT
|
||||
// LEFT - lookup
|
||||
export type LookupTypeL2R = SpecialType<'LOOKUP_FROM_LEFT', FieldType[]>;
|
||||
export const forMayLookup = (format: FieldType[]): LookupTypeL2R => {
|
||||
return {
|
||||
type: 'LOOKUP_FROM_LEFT',
|
||||
format,
|
||||
check: function (checkField: OneOf, selectedLeftField?: OneOf) {
|
||||
if (isLookup(checkField) && selectedLeftField) {
|
||||
return isLookup(selectedLeftField) ? checkField.type === selectedLeftField.type : false;
|
||||
}
|
||||
return !!this.format?.find((e) => e === checkField.type);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const mayLookupText = forMayLookup(['SINGLE_LINE_TEXT']);
|
||||
export const mayLookupNumber = forMayLookup(['NUMBER']);
|
||||
export const mayLookupTextNumber = forMayLookup(['SINGLE_LINE_TEXT', 'NUMBER']);
|
||||
|
||||
// LEFT - calc
|
||||
export type CalcTypeL2R = SpecialType<'CALC_FROM_LEFT', Record<CalcType, Array<FieldType | LookupTypeL2R>>>;
|
||||
|
||||
export const leftCalcType: CalcTypeL2R = {
|
||||
type: 'CALC_FROM_LEFT',
|
||||
format: {
|
||||
NUMBER: [mayLookupTextNumber],
|
||||
NUMBER_DIGIT: [mayLookupTextNumber],
|
||||
DATE: ['DATE'],
|
||||
TIME: ['TIME'],
|
||||
DATETIME: ['DATETIME'],
|
||||
HOUR_MINUTE: [],
|
||||
DAY_HOUR_MINUTE: [],
|
||||
},
|
||||
check: function (checkField: OneOf, selectedLeftField?: OneOf) {
|
||||
let allowed: Array<FieldType | LookupTypeL2R> = [];
|
||||
if (selectedLeftField && isType.CALC(selectedLeftField)) {
|
||||
allowed = this.format[selectedLeftField.format];
|
||||
}
|
||||
return !!allowed.find((e) => {
|
||||
if (isSpecialType(e) && isLookupFromLeft(e)) {
|
||||
return e.check(checkField, selectedLeftField);
|
||||
}
|
||||
return e === checkField.type;
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
// LEFT - link
|
||||
export type LinkType = SpecialType<'LINK', Record<LinkProtocolType, LinkProtocolType[]>>;
|
||||
|
||||
export const linkType: LinkType = {
|
||||
type: 'LINK',
|
||||
format: {
|
||||
// 入力値の種別が同じ場合のみ
|
||||
WEB: ['WEB'],
|
||||
CALL: ['CALL'],
|
||||
MAIL: ['MAIL'],
|
||||
},
|
||||
check: function (checkField: OneOf, selectedField?: OneOf) {
|
||||
let allowed: LinkProtocolType[] = [];
|
||||
if (selectedField && isType.LINK(selectedField)) {
|
||||
allowed = this.format[selectedField.protocol];
|
||||
}
|
||||
if (checkField && isType.LINK(checkField)) {
|
||||
return !!allowed.find((e) => e === checkField.protocol);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
};
|
||||
|
||||
// LEFT - rule
|
||||
export type AvailableRight = FieldType | CalcTypeL2R | LinkType | LookupTypeL2R;
|
||||
const availableLeftJoinType = {
|
||||
SINGLE_LINE_TEXT: [mayLookupText],
|
||||
NUMBER: [mayLookupNumber],
|
||||
CALC: [leftCalcType],
|
||||
DATE: ['DATE'],
|
||||
TIME: ['TIME'],
|
||||
DATETIME: ['DATETIME'],
|
||||
LINK: [linkType],
|
||||
} as Record<FieldType, AvailableRight[]>;
|
||||
|
||||
// RIGHT - calc
|
||||
export type CalcTypeR2L = SpecialType<'CALC_FROM_RIGHT', CalcType[]>;
|
||||
|
||||
export const forCalc = (format: CalcType[]): CalcTypeR2L => {
|
||||
return {
|
||||
type: 'CALC_FROM_RIGHT',
|
||||
format,
|
||||
check: function (checkField: OneOf, selectedRightField?: OneOf) {
|
||||
return isType.CALC(checkField) && !!this.format?.find((e) => e === checkField.format);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// RIGHT - rule
|
||||
export type AvailableLeft = FieldType | CalcTypeR2L | LinkType;
|
||||
const availableRightJoinType = {
|
||||
SINGLE_LINE_TEXT: ['SINGLE_LINE_TEXT', forCalc(['NUMBER', 'NUMBER_DIGIT'])],
|
||||
NUMBER: ['NUMBER', forCalc(['NUMBER', 'NUMBER_DIGIT'])],
|
||||
DATE: ['DATE', forCalc(['DATE'])],
|
||||
TIME: ['TIME', forCalc(['TIME'])],
|
||||
DATETIME: ['DATETIME', forCalc(['DATETIME'])],
|
||||
LINK: [linkType],
|
||||
} as Record<FieldType, AvailableLeft[]>;
|
||||
|
||||
// methods
|
||||
// undefined means all
|
||||
export function getRightAvailableJoinType(left?: OneOf | '') {
|
||||
if (left === undefined) {
|
||||
return Object.keys(availableRightJoinType) as FieldType[];
|
||||
}
|
||||
return left ? availableLeftJoinType[left.type] : [];
|
||||
}
|
||||
|
||||
// undefined means all
|
||||
export function getLeftAvailableJoinType(right?: OneOf | '') {
|
||||
if (right === undefined) {
|
||||
return Object.keys(availableLeftJoinType) as FieldType[];
|
||||
}
|
||||
return right ? availableRightJoinType[right.type] : [];
|
||||
}
|
||||
|
||||
export function isSpecialType(obj: FieldType | SpecialType): obj is SpecialType {
|
||||
return typeof obj === 'object' && !Array.isArray(obj) && 'type' in obj;
|
||||
}
|
||||
|
||||
export function isLookupFromLeft(obj: SpecialType): obj is LookupTypeL2R {
|
||||
return obj.type === 'LOOKUP_FROM_LEFT';
|
||||
}
|
||||
|
||||
export function isLinkType(obj: SpecialType): obj is LinkType {
|
||||
return obj.type === 'LINK';
|
||||
}
|
||||
|
||||
export function isCalcFromLeft(obj: SpecialType): obj is CalcTypeL2R {
|
||||
return obj.type === 'CALC_FROM_LEFT';
|
||||
}
|
||||
|
||||
export function isCalcFromRight(obj: SpecialType): obj is CalcTypeR2L {
|
||||
return obj.type === 'CALC_FROM_RIGHT';
|
||||
}
|
||||
|
||||
export function isLookup(field: OneOf): field is KintoneFormFieldProperty.Lookup {
|
||||
return 'lookup' in field;
|
||||
}
|
||||
62
src/js/kintone-rest-api-client.ts
Normal file
62
src/js/kintone-rest-api-client.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { KintoneRestAPIClient } from '@kintone/rest-api-client';
|
||||
|
||||
export const client = new KintoneRestAPIClient();
|
||||
|
||||
export type App = {
|
||||
appId: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
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> };
|
||||
103
src/js/mapping.ts
Normal file
103
src/js/mapping.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { type FieldType, type OneOf } from './kintone-rest-api-client';
|
||||
import {
|
||||
leftCalcType,
|
||||
linkType,
|
||||
type SpecialType,
|
||||
mayLookupTextNumber,
|
||||
mayLookupText,
|
||||
type AvailableRight,
|
||||
type AvailableLeft,
|
||||
isLookup,
|
||||
forCalc,
|
||||
type CalcTypeR2L,
|
||||
isSpecialType,
|
||||
} from './join';
|
||||
import type { SelectType } from '@/types/my-kintone';
|
||||
|
||||
// LEFT - rule
|
||||
const availableLeftMappingType = {
|
||||
SINGLE_LINE_TEXT: [mayLookupText],
|
||||
MULTI_LINE_TEXT: ['MULTI_LINE_TEXT'],
|
||||
RICH_TEXT: ['RICH_TEXT'],
|
||||
NUMBER: [mayLookupTextNumber],
|
||||
CALC: [leftCalcType],
|
||||
RADIO_BUTTON: ['RADIO_BUTTON'],
|
||||
CHECK_BOX: ['CHECK_BOX'],
|
||||
MULTI_SELECT: ['MULTI_SELECT'], // TODO 带选项字段报错
|
||||
DROP_DOWN: ['DROP_DOWN'],
|
||||
USER_SELECT: ['USER_SELECT'],
|
||||
ORGANIZATION_SELECT: ['ORGANIZATION_SELECT'],
|
||||
GROUP_SELECT: ['GROUP_SELECT'],
|
||||
DATE: ['DATE', 'DATETIME'],
|
||||
TIME: ['TIME'],
|
||||
DATETIME: ['DATETIME'],
|
||||
LINK: [linkType, mayLookupText],
|
||||
//LOOKUP
|
||||
RECORD_NUMBER: [mayLookupTextNumber],
|
||||
CREATOR: ['USER_SELECT'],
|
||||
CREATED_TIME: ['DATETIME'],
|
||||
MODIFIER: ['USER_SELECT'],
|
||||
UPDATED_TIME: ['DATETIME'],
|
||||
} as Record<FieldType, AvailableRight[]>;
|
||||
|
||||
// RIGHT
|
||||
export type LookupTypeR2L = SpecialType<'LOOKUP_FROM_RIGHT', Array<FieldType | CalcTypeR2L>>;
|
||||
|
||||
export const isSameLookupOr = (format: Array<FieldType | CalcTypeR2L>): LookupTypeR2L => {
|
||||
return {
|
||||
type: 'LOOKUP_FROM_RIGHT',
|
||||
format,
|
||||
check: function (checkField: OneOf, selectedRightField?: OneOf) {
|
||||
if (selectedRightField && isLookup(selectedRightField)) {
|
||||
return isLookup(checkField) ? checkField.type === selectedRightField.type : false;
|
||||
}
|
||||
return !!this.format?.find((e) => {
|
||||
if (isSpecialType(e)) {
|
||||
return e.check(checkField, selectedRightField);
|
||||
}
|
||||
return e === checkField.type;
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const availableRightMappingType = {
|
||||
SINGLE_LINE_TEXT: [
|
||||
isSameLookupOr(['SINGLE_LINE_TEXT', 'NUMBER', forCalc(['NUMBER', 'NUMBER_DIGIT']), 'LINK', 'RECORD_NUMBER']),
|
||||
],
|
||||
MULTI_LINE_TEXT: ['MULTI_LINE_TEXT'],
|
||||
RICH_TEXT: ['RICH_TEXT'],
|
||||
NUMBER: [isSameLookupOr(['NUMBER', forCalc(['NUMBER', 'NUMBER_DIGIT']), 'RECORD_NUMBER'])],
|
||||
RADIO_BUTTON: ['RADIO_BUTTON'],
|
||||
CHECK_BOX: ['CHECK_BOX'],
|
||||
MULTI_SELECT: ['MULTI_SELECT'], // TODO 带选项字段报错
|
||||
DROP_DOWN: ['DROP_DOWN'],
|
||||
USER_SELECT: ['USER_SELECT', 'CREATOR', 'MODIFIER'],
|
||||
ORGANIZATION_SELECT: ['ORGANIZATION_SELECT'],
|
||||
GROUP_SELECT: ['GROUP_SELECT'],
|
||||
DATE: ['DATE', forCalc(['DATE'])],
|
||||
TIME: ['TIME', forCalc(['TIME'])],
|
||||
DATETIME: ['DATE', 'DATETIME', forCalc(['DATETIME']), 'CREATED_TIME', 'UPDATED_TIME'],
|
||||
LINK: [linkType],
|
||||
} as Record<FieldType, Array<AvailableLeft | LookupTypeR2L>>;
|
||||
|
||||
// methods
|
||||
// undefined means all
|
||||
export function getRightAvailableMappingType(left?: OneOf | '') {
|
||||
if (left === undefined) {
|
||||
return Object.keys(availableRightMappingType) as FieldType[];
|
||||
}
|
||||
return left ? availableLeftMappingType[left.type] : [];
|
||||
}
|
||||
|
||||
// undefined means all
|
||||
export function getLeftAvailableMappingType(right?: OneOf | '') {
|
||||
if (right === undefined) {
|
||||
return Object.keys(availableLeftMappingType) as FieldType[];
|
||||
}
|
||||
return right ? availableRightMappingType[right.type] : [];
|
||||
}
|
||||
|
||||
export function isSelectType(field: OneOf): field is SelectType {
|
||||
return 'options' in field;
|
||||
}
|
||||
32
src/js/mobile.ts
Normal file
32
src/js/mobile.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { Field, FieldLayout, SavedData } from "@/types/model";
|
||||
import { KintoneIndexEventHandler } from "./KintoneIndexEventHandler.mobile";
|
||||
|
||||
(function (PLUGIN_ID) {
|
||||
kintone.events.on('mobile.app.record.index.show', (event) => {
|
||||
try{
|
||||
const setting = kintone.plugin.app.getConfig(PLUGIN_ID);
|
||||
const config:SavedData<FieldLayout> = getConfig(setting);
|
||||
const currentApp = kintone.mobile.app.getId()?.toString();
|
||||
if(!currentApp) return;
|
||||
const handler = new KintoneIndexEventHandler(config,currentApp);
|
||||
handler.init();
|
||||
}catch(error){
|
||||
const detailError =(error instanceof Error) ? "\n詳細:" + error.message : "";
|
||||
const errorMsg = `データ収集中処理中例外発生しました。${ detailError }`;
|
||||
event.error = errorMsg;
|
||||
}
|
||||
return event;
|
||||
});
|
||||
/**
|
||||
* Config設定値を変換する
|
||||
* @param setting
|
||||
* @returns
|
||||
*/
|
||||
function getConfig(setting:any):SavedData<FieldLayout>{
|
||||
const config:SavedData<FieldLayout>={
|
||||
buttonName:setting.buttonName,
|
||||
joinTables:JSON.parse(setting.joinTables)
|
||||
}
|
||||
return config;
|
||||
}
|
||||
})(kintone.$PLUGIN_ID);
|
||||
5
src/main.ts
Normal file
5
src/main.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { createApp } from 'vue';
|
||||
import Config from './components/Config.vue';
|
||||
import i18n from './i18n/index';
|
||||
|
||||
createApp(Config, { pluginId: kintone.$PLUGIN_ID }).use(i18n).mount('#app');
|
||||
49
src/manifest.json
Normal file
49
src/manifest.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"$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,
|
||||
"type": "APP",
|
||||
"desktop": {
|
||||
"js": [
|
||||
"js/KintoneRestAPIClient.min.js",
|
||||
"js/kuc.min.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": [
|
||||
"buttonName"
|
||||
]
|
||||
},
|
||||
"name": {
|
||||
"en": "data fetch plugin",
|
||||
"ja": "データ取得プラグイン"
|
||||
},
|
||||
"description": {
|
||||
"en": "create search data plugin",
|
||||
"ja": "検索結果のデータを生成するプラグインです"
|
||||
},
|
||||
"mobile": {
|
||||
"js": [
|
||||
"js/KintoneRestAPIClient.min.js",
|
||||
"js/kuc.min.js",
|
||||
"js/mobile.js"
|
||||
],
|
||||
"css": [
|
||||
"css/mobile.css"
|
||||
]
|
||||
}
|
||||
}
|
||||
6
src/types/index.d.ts
vendored
Normal file
6
src/types/index.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
declare global {
|
||||
const Kucs: {
|
||||
[version: string]: any;
|
||||
};
|
||||
}
|
||||
export {};
|
||||
71
src/types/model.d.ts
vendored
Normal file
71
src/types/model.d.ts
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
import type { ConditionValue } from '@/js/conditions';
|
||||
import type { Layout, Properties } from '@/js/kintone-rest-api-client';
|
||||
import type { DropdownItem } from 'kintone-ui-component';
|
||||
|
||||
|
||||
|
||||
export interface FieldsJoinMapping<FieldType = string> {
|
||||
id: string;
|
||||
leftField: FieldType;
|
||||
rightField: FieldType;
|
||||
}
|
||||
|
||||
export interface WhereCondition<FieldType = string> {
|
||||
id: string;
|
||||
field: FieldType;
|
||||
condition: ConditionValue;
|
||||
data: StringValue;
|
||||
}
|
||||
|
||||
export interface JoinTable<FieldType = string> {
|
||||
id: string;
|
||||
app: string; // 取得元アプリ
|
||||
table: string; // テーブル
|
||||
onConditions: FieldsJoinMapping<FieldType>[]; // 連結条件
|
||||
fieldsMapping: FieldsJoinMapping<FieldType>[]; // 取得フィールド
|
||||
whereConditions: WhereCondition<FieldType>[]; // 絞込条件
|
||||
meta?: Properties;
|
||||
}
|
||||
|
||||
// 存储的数据格式
|
||||
export interface SavedData<FieldType = string> {
|
||||
buttonName: string;
|
||||
joinTables: JoinTable<FieldType>[];
|
||||
}
|
||||
|
||||
export interface FieldsInfo {
|
||||
fields: Properties;
|
||||
layout: Layout;
|
||||
}
|
||||
|
||||
export interface CachedData {
|
||||
apps: DropdownItem[],
|
||||
currentAppFields: FieldsInfo,
|
||||
}
|
||||
|
||||
export interface CachedSelectedAppData {
|
||||
appFields: FieldsInfo,
|
||||
loading: boolean,
|
||||
table: JoinTable,
|
||||
}
|
||||
export type Record = {
|
||||
[fieldCode: string]: Field;
|
||||
};
|
||||
|
||||
export type RecordForParameter = {
|
||||
[fieldCode: string]: {
|
||||
value: unknown;
|
||||
};
|
||||
};
|
||||
|
||||
export type Field={
|
||||
type:string;
|
||||
value:any;
|
||||
}
|
||||
|
||||
export type FieldLayout={
|
||||
type:string;
|
||||
code:string;
|
||||
}
|
||||
|
||||
export type StringValue = string | string[];
|
||||
14
src/types/my-kintone.d.ts
vendored
Normal file
14
src/types/my-kintone.d.ts
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
import { KintoneFormFieldProperty } from '@kintone/rest-api-client';
|
||||
|
||||
export interface KucEvent<T> {
|
||||
detail: T;
|
||||
}
|
||||
|
||||
export type CalcType = 'NUMBER' | 'NUMBER_DIGIT' | 'DATETIME' | 'DATE' | 'TIME' | 'HOUR_MINUTE' | 'DAY_HOUR_MINUTE';
|
||||
export type LinkProtocolType = 'WEB' | 'CALL' | 'MAIL';
|
||||
|
||||
export type SelectType =
|
||||
| KintoneFormFieldProperty.CheckBox
|
||||
| KintoneFormFieldProperty.RadioButton
|
||||
| KintoneFormFieldProperty.Dropdown
|
||||
| KintoneFormFieldProperty.MultiSelect;
|
||||
14
tsconfig.json
Normal file
14
tsconfig.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue", "components.d.ts"],
|
||||
"exclude": ["src/**/__tests__/*"],
|
||||
"files" : [
|
||||
"./node_modules/@kintone/dts-gen/kintone.d.ts",
|
||||
],
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
88
vite.config.ts
Normal file
88
vite.config.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import copy from "rollup-plugin-copy";
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import Components from 'unplugin-vue-components/vite';
|
||||
|
||||
function replaceKucTagsPlugin() {
|
||||
return {
|
||||
name: 'vite-plugin-replace-tags',
|
||||
async load(id) {
|
||||
if (id.endsWith('.vue')) {
|
||||
const content = await fs.promises.readFile(id, 'utf-8');
|
||||
|
||||
const usedComponent = {}
|
||||
|
||||
let res = content
|
||||
.replace(/<\/kuc-([a-zA-Z0-9-]+)(?![0-9-])>/g, (match, p1) => `</kuc-${p1}-1-18-0>`)
|
||||
.replace(/<kuc-([a-zA-Z0-9-]+)(?![0-9-])([^>]*)>/g, (match, p1, p2) => {
|
||||
usedComponent[p1] = true;
|
||||
return `<kuc-${p1}-1-18-0${p2}>`
|
||||
});
|
||||
if (Object.keys(usedComponent).length) {
|
||||
let importScript = '<script lang="ts">'
|
||||
Object.keys(usedComponent).forEach((key) => {
|
||||
const keyPascal = key.split('-')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join('');
|
||||
if (key === 'multi-choice') {
|
||||
key = 'multichoice';
|
||||
}
|
||||
importScript += `import * as Kuc${keyPascal} from "kintone-ui-component/lib/${key}";`
|
||||
});
|
||||
importScript += '</script>';
|
||||
res = importScript + res;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue({
|
||||
template: {
|
||||
compilerOptions: {
|
||||
isCustomElement: (tag) => tag.startsWith("kuc-"),
|
||||
}
|
||||
}
|
||||
}),
|
||||
Components(),
|
||||
copy({
|
||||
targets: [
|
||||
{ src: 'dist/index.html', dest: 'dist/src/html', rename: 'config.html' },
|
||||
{ src: 'src/manifest.json', dest: 'dist/src' },
|
||||
{ src: 'src/assets/*.js', dest: 'dist/src/js' },
|
||||
{ src: 'src/assets/*.png', dest: 'dist/src/image' },
|
||||
{ src: 'src/css/*', dest: 'dist/src/css' },
|
||||
],
|
||||
hook: 'writeBundle' // 指定在何时复制文件
|
||||
}),
|
||||
replaceKucTagsPlugin()
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, 'src'),
|
||||
},
|
||||
},
|
||||
build: {
|
||||
rollupOptions: {
|
||||
input: {
|
||||
config: path.resolve(__dirname, 'index.html'),
|
||||
desktop: path.resolve(__dirname, 'src/js/desktop.ts'),
|
||||
mobile: path.resolve(__dirname, 'src/js/mobile.ts'),
|
||||
},
|
||||
output: {
|
||||
entryFileNames: (chunkInfo) => {
|
||||
return 'src/js/[name].js'; // 默认处理为 JS 文件
|
||||
},
|
||||
assetFileNames: 'src/[ext]/[name].[ext]',
|
||||
},
|
||||
},
|
||||
sourcemap: 'inline',
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user