2025-10-17 14:37:45 +08:00
commit f7a356c157
57 changed files with 20980 additions and 0 deletions

166
src/components/Config.vue Normal file
View File

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

View File

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

View File

@@ -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>

View 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>

View 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>

View File

@@ -0,0 +1,7 @@
<template>
<div class="kintoneplugin-row">
<slot></slot>
</div>
</template>
<script setup lang="ts"></script>

View 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>

View 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>

View 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>

View 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>

View File

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

View File

@@ -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>

View File

@@ -0,0 +1,22 @@
<template>
<kuc-text className="kuc-text-input" :value="modelValue" @change="updateValue" />
</template>
<script setup lang="ts">
import type { KucEvent } from '@/types/my-kintone';
import type { TextInputEventDetail } from 'kintone-ui-component';
import { defineProps, defineEmits, type Ref } from 'vue';
const props = defineProps<{
modelValue: string;
}>();
const emit = defineEmits<{
(e: 'update:modelValue', value: string): void;
}>();
const updateValue = ({ detail }: KucEvent<TextInputEventDetail>) => {
emit('update:modelValue', detail.value || '');
};
</script>

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,82 @@
<template>
<!-- <kuc-table className='table-option'
:columns="columns"
:data="data"
@change="updateValue"
:headerVisible="false"
ref="table"/> -->
<div ref="tableContainer"></div>
</template>
<script setup lang="ts">
import type { KucEvent } from '@/types/my-kintone';
import { Table, Text, type TableChangeEventDetail } from 'kintone-ui-component';
import { defineProps, defineEmits, computed, ref, watch, inject, type Ref, onMounted, onUnmounted } from 'vue';
interface MuiltItem{
value:string
}
const props = defineProps<{
value: string[];
}>();
const tableContainer = ref<HTMLDivElement>();
const table = ref<Table | null>(null);
const data = ref<MuiltItem[]>((props.value || ['', '']).map(x => ({ value: x })));
watch(
() => props.value,
(newValue)=>{
data.value =(newValue || ['', '']).map((x) => ({ value: x }));
if (table.value) {
table.value.data = data.value; // 更新 Table 数据
}
},
{
deep:true,immediate:true
});
const emit = defineEmits<{
(e: 'change', data: KucEvent<string[]>): void;
(e: 'update:modelValue', value: string[]): void;
}>();
const updateValue=(event:KucEvent<TableChangeEventDetail<MuiltItem>>)=>{
data.value = event.detail.data||[{value:''},{value:''}];
if (table.value) {
table.value.data = data.value;
}
const muiltData = event.detail.data ? event.detail.data.map(x=>x.value) :[];
emit('change', { detail: [...muiltData] });
// emit('update:modelValue', [...muiltData] );
// emit('change', muiltData);
}
onMounted(()=>{
table.value = new Table({
className:'table-option',
headerVisible:false,
actionButton:true,
columns:[
{
field:"value",
render:(cellData:any)=>{
const text = new Text({value:cellData});
return text;
}
},
],
data:data.value
});
table.value.addEventListener('change', updateValue);
tableContainer.value?.appendChild(table.value);
});
onUnmounted(() => {
if (table.value) {
table.value.removeEventListener('change', updateValue);
}
});
</script>