323 lines
13 KiB
Vue
323 lines
13 KiB
Vue
<template>
|
|
<div class="q-my-md" v-bind="$attrs">
|
|
<q-field :label="displayName" labelColor="primary" stack-label
|
|
v-model="mappingProps"
|
|
:rules="rulesExp"
|
|
ref="fieldRef"
|
|
>
|
|
<template v-slot:control>
|
|
<q-card flat class="full-width">
|
|
<q-card-actions vertical>
|
|
<q-btn color="grey-3" text-color="black" :disable="btnDisable"
|
|
@click="() => { dgIsShow = true }">クリックで設定</q-btn>
|
|
</q-card-actions>
|
|
<q-card-section class="text-caption">
|
|
<div v-if="mappingObjectsInputDisplay && mappingObjectsInputDisplay.length > 0">
|
|
<div v-for="(item) in mappingObjectsInputDisplay" :key="item">{{ item }}</div>
|
|
</div>
|
|
<div v-else>{{ placeholder }}</div>
|
|
</q-card-section>
|
|
</q-card>
|
|
</template>
|
|
</q-field>
|
|
<show-dialog v-model:visible="dgIsShow" name="データマッピング" @close="closeDg" min-width="55vw" min-height="60vh">
|
|
<div class="">
|
|
<div class="row q-col-gutter-x-xs flex-center">
|
|
<div class="col-5">
|
|
<div class="q-mx-xs">ソース</div>
|
|
</div>
|
|
<!-- <div class="col-1">
|
|
</div> -->
|
|
<div class="col-5">
|
|
<div class="row justify-between q-mr-md">
|
|
<div class="">{{ sourceApp?.name }}</div>
|
|
<q-btn outline color="primary" size="xs" label="最新のフィールドを取得する"
|
|
@click="() => updateFields(sourceAppId!)" />
|
|
</div>
|
|
</div>
|
|
<div class="col-1 q-pl-sm">
|
|
キー
|
|
</div>
|
|
</div>
|
|
<q-virtual-scroll style="max-height: 60vh;" :items="mappingProps.data" separator v-slot="{ item, index }">
|
|
<!-- <div class="q-my-sm" v-for="(item, index) in mappingProps" :key="item.id"> -->
|
|
<div class="row q-pa-sm q-col-gutter-x-md flex-center">
|
|
<div class="col-5">
|
|
<ConditionObject :config="config" v-model="item.from" :disabled="item.disabled"
|
|
:label="item.disabled ? '「Lookup」によってロックされる' : undefined" />
|
|
</div>
|
|
<!-- <div class="col-1">
|
|
</div> -->
|
|
<div class="col-5">
|
|
<q-field v-model="item.vName" type="text" outlined dense :disable="item.disabled" >
|
|
<!-- <template v-slot:append>
|
|
<q-icon name="search" class="cursor-pointer"
|
|
@click="() => { mappingProps[index].to.isDialogVisible = true }" />
|
|
</template> -->
|
|
<template v-slot:control>
|
|
<div class="self-center full-width no-outline" tabindex="0"
|
|
v-if="item.to.app?.name && item.to.fields?.length > 0 && item.to.fields[0].label">
|
|
{{ `${item.to.fields[0].label}` }}
|
|
<span class="text-red" v-if="item.to.fields[0].required">*</span>
|
|
<q-tooltip class="bg-yellow-2 text-black shadow-4" >
|
|
<div>アプリ : {{ item.to.app.name }}</div>
|
|
<div>フィールドのコード : {{ item.to.fields[0].code }}</div>
|
|
<div>フィールドのタイプ : {{ item.to.fields[0].type }}</div>
|
|
<div v-if="item.to.fields[0].required">必須項目</div>
|
|
<!-- <div>フィールド : {{ item.to.fields[0] }}</div>
|
|
<div>フィールド : {{ item.isKey }}</div> -->
|
|
</q-tooltip>
|
|
</div>
|
|
</template>
|
|
</q-field>
|
|
</div>
|
|
<div class="col-1">
|
|
<q-checkbox size="sm" v-model="item.isKey" :disable="item.disabled" />
|
|
<!-- <q-btn flat round dense icon="delete" size="sm" @click="() => deleteMappingObject(index)" /> -->
|
|
</div>
|
|
</div>
|
|
|
|
<show-dialog v-model:visible="mappingProps.data[index].to.isDialogVisible" name="フィールド一覧"
|
|
@close="closeToDg" ref="fieldDlg">
|
|
<FieldSelect v-if="onlySourceSelect" ref="fieldDlg" name="フィールド" :appId="sourceAppId" not_page
|
|
:selectedFields="mappingProps.data[index].to.fields"
|
|
:updateSelects="(fields) => { mappingProps.data[index].to.fields = fields; mappingProps.data[index].to.app = sourceApp }">
|
|
</FieldSelect>
|
|
<AppFieldSelectBox v-else v-model:selectedField="mappingProps.data[index].to" />
|
|
</show-dialog>
|
|
<!-- </div> -->
|
|
</q-virtual-scroll>
|
|
|
|
<div class="q-mt-lg q-ml-md row ">
|
|
<q-checkbox size="sm" v-model="mappingProps.createWithNull" label="キーが存在しない場合は新規に作成され、存在する場合はデータが更新されます。" />
|
|
</div>
|
|
</div>
|
|
</show-dialog>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import { v4 as uuidv4 } from 'uuid';
|
|
import { computed, defineComponent, watch, isRef, reactive, ref, watchEffect } from 'vue';
|
|
import ConditionObject from '../ConditionEditor/ConditionObject.vue';
|
|
import ShowDialog from '../ShowDialog.vue';
|
|
import AppFieldSelectBox from '../AppFieldSelectBox.vue';
|
|
import FieldSelect from '../FieldSelect.vue';
|
|
import { IApp, IField } from './AppFieldSelect.vue';
|
|
import { api } from 'boot/axios';
|
|
|
|
type ContextProps = {
|
|
props?: {
|
|
name: string;
|
|
modelValue?: {
|
|
app: {
|
|
id: string;
|
|
name: string;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
interface IMappingSetting {
|
|
data: IMappingValueType[];
|
|
createWithNull: boolean;
|
|
}
|
|
|
|
interface IMappingValueType {
|
|
id: string;
|
|
from: { sharedText?: string };
|
|
to: {
|
|
app?: IApp,
|
|
fields: IField[],
|
|
isDialogVisible: boolean;
|
|
};
|
|
isKey: boolean;
|
|
disabled: boolean;
|
|
}
|
|
|
|
const blackListLabelName = ['レコード番号', '作業者', '更新者', '更新日時', '作成日時', '作成者']
|
|
|
|
export default defineComponent({
|
|
name: 'DataMapping',
|
|
inheritAttrs: false,
|
|
components: {
|
|
ShowDialog,
|
|
ConditionObject,
|
|
AppFieldSelectBox,
|
|
FieldSelect
|
|
},
|
|
props: {
|
|
context: {
|
|
type: Array<ContextProps>,
|
|
default: '',
|
|
},
|
|
displayName: {
|
|
type: String,
|
|
default: '',
|
|
},
|
|
name: {
|
|
type: String,
|
|
default: '',
|
|
},
|
|
modelValue: {
|
|
type: Object as () => IMappingSetting,
|
|
},
|
|
placeholder: {
|
|
type: String,
|
|
default: '',
|
|
},
|
|
onlySourceSelect: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
fieldTypes:{
|
|
type:Array,
|
|
default:()=>[]
|
|
},
|
|
//例:[val=>!!val ||'入力してください']
|
|
rules: {
|
|
type: String,
|
|
default: undefined
|
|
},
|
|
required: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
requiredMessage: {
|
|
type: String,
|
|
default: ''
|
|
}
|
|
},
|
|
setup(props, { emit }) {
|
|
const fieldRef=ref();
|
|
const source = props.context.find(element => element?.props?.name === 'sources')
|
|
|
|
const sourceApp = computed(() => source?.props?.modelValue?.app);
|
|
|
|
const sourceAppId = computed(() => sourceApp.value?.id);
|
|
|
|
//ルール設定
|
|
const checkMapping = (val:IMappingSetting)=>{
|
|
if(!val || !val.data){
|
|
return false;
|
|
}
|
|
console.log(val);
|
|
const mappingDatas = val.data.filter(item=>item.from?.sharedText && item.to.fields?.length > 0);
|
|
return mappingDatas.length>0;
|
|
}
|
|
const customExp = props.rules === undefined ? [] : eval(props.rules);
|
|
const errmsg = props.requiredMessage?props.requiredMessage:`${props.displayName}を選択してください。`;
|
|
const requiredExp = props.required ? [((val: any) => checkMapping(val) || errmsg)] : [];
|
|
const rulesExp = [...requiredExp, ...customExp];
|
|
|
|
// const mappingProps = ref(props.modelValue?.data ?? []);
|
|
|
|
// const createWithNull = ref(props.modelValue?.createWithNull ?? false);
|
|
|
|
const mappingProps = reactive<IMappingSetting>({
|
|
data:props.modelValue?.data ?? [],
|
|
createWithNull:props.modelValue?.createWithNull ?? false
|
|
});
|
|
|
|
const closeDg = () => {
|
|
fieldRef.value.validate();
|
|
emit('update:modelValue',mappingProps);
|
|
}
|
|
|
|
const closeToDg = () => {
|
|
emit('update:modelValue',mappingProps);
|
|
}
|
|
|
|
// 外部ソースコンポーネントの appid をリッスンし、変更されたときに現在のコンポーネントを更新します
|
|
watch(() => sourceAppId.value, async (newId,) => {
|
|
if (!newId) return;
|
|
updateFields(newId)
|
|
})
|
|
|
|
const updateFields = async (sourceAppId: string) => {
|
|
const ktAppFields = await api.get('api/v1/appfields', {
|
|
params: {
|
|
app: sourceAppId
|
|
}
|
|
}).then(res => {
|
|
return Object.values(res.data.properties)
|
|
// kintoneのデフォルトの非表示フィールドフィルタリング
|
|
.filter(f => !blackListLabelName.find(label => f.label === label))
|
|
.map(f => ({ name: f.label, objectType: 'field', ...f }))
|
|
.map(f => {
|
|
// 更新前の値を求める
|
|
const beforeData = mappingProps.data.find(m => m.to.fields[0].code === f.code)
|
|
return {
|
|
id: uuidv4(),
|
|
from: beforeData?.from ?? {}, // 以前のデータを入力します
|
|
to: {
|
|
app: sourceApp.value,
|
|
fields: [f],
|
|
isDialogVisible: false
|
|
},
|
|
isKey: beforeData?.isKey ?? false, // 以前のデータを入力します
|
|
disabled: false
|
|
}
|
|
})
|
|
})
|
|
|
|
// 「ルックアップ」によってロックされているフィールドを検索する
|
|
const lookupFixedField = ktAppFields
|
|
.filter(field => field.to.fields[0].lookup !== undefined)
|
|
.flatMap(field => field.to.fields[0].lookup.fieldMappings.map((m) => m.field))
|
|
|
|
// 「ルックアップ」でロックされたビューコンポーネントを非対話型に設定します
|
|
if (lookupFixedField.length > 0) {
|
|
ktAppFields.filter(f => lookupFixedField.includes(f.to.fields[0].code)).forEach(f => f.disabled = true)
|
|
}
|
|
|
|
mappingProps.data = ktAppFields
|
|
}
|
|
|
|
const mappingObjectsInputDisplay = computed(() =>
|
|
(mappingProps.data && Array.isArray(mappingProps.data)) ?
|
|
mappingProps.data
|
|
.filter(item => item.from?.sharedText && item.to.fields?.length > 0)
|
|
.map(item => {
|
|
return `field(${item.to.app?.id},${item.to.fields[0].label}) = ${item.from.sharedText} `;
|
|
})
|
|
: []
|
|
);
|
|
|
|
const btnDisable = computed(() => props.onlySourceSelect ? !(source?.props?.modelValue?.app?.id) : false);
|
|
|
|
watchEffect(() => {
|
|
emit('update:modelValue', mappingProps);
|
|
});
|
|
|
|
return {
|
|
uuidv4,
|
|
dgIsShow: ref(false),
|
|
fieldRef,
|
|
closeDg,
|
|
toDgIsShow: ref(false),
|
|
closeToDg,
|
|
mappingProps,
|
|
updateFields,
|
|
// addMappingObject: () => mappingProps.push(defaultMappingProp()),
|
|
// deleteMappingObject,
|
|
mappingObjectsInputDisplay,
|
|
sourceApp,
|
|
sourceAppId,
|
|
btnDisable,
|
|
rulesExp,
|
|
checkMapping,
|
|
config: {
|
|
canInput: false,
|
|
buttonsConfig: [
|
|
{ label: 'フィールド', color: 'primary', type: 'FieldAdd' },
|
|
{ label: '変数', color: 'green', type: 'VariableAdd', editable: false },
|
|
]
|
|
}
|
|
};
|
|
},
|
|
});
|
|
|
|
</script>
|
|
<style lang="scss"></style>
|