Merged PR 98: 階層化ドロップダウンの実装完了
階層化ドロップダウンの実装完了しました。 Related work items: #227
This commit is contained in:
271
frontend/src/components/CascadingDropDownBox.vue
Normal file
271
frontend/src/components/CascadingDropDownBox.vue
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<q-stepper v-model="step" ref="stepper" color="primary" animated flat>
|
||||||
|
<q-step :name="1" title="データソースの設定" icon="app_registration" :done="step > 1">
|
||||||
|
<div class="row justify-between items-center">
|
||||||
|
<div>アプリの選択 :</div>
|
||||||
|
<div>
|
||||||
|
<a v-if="data.sourceApp?.name" class="q-mr-xs"
|
||||||
|
:href="data.sourceApp ? `${authStore.currentDomain.kintoneUrl}/k/${data.sourceApp.id}` : ''"
|
||||||
|
target="_blank" title="Kiontoneへ">
|
||||||
|
{{ data.sourceApp?.name }}
|
||||||
|
</a>
|
||||||
|
<div v-else class="text-red">APPを選択してください</div>
|
||||||
|
<q-btn v-if="data.sourceApp?.name" flat color="grey" icon="clear" size="sm" padding="none"
|
||||||
|
@click="clearSelectedApp" />
|
||||||
|
</div>
|
||||||
|
<q-btn outline dense label="変更" padding="xs sm" color="primary" @click="showAppDialog" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- フィールド設定部分 -->
|
||||||
|
<template v-if="data.sourceApp?.name">
|
||||||
|
<q-separator class="q-mt-md" />
|
||||||
|
<div class="q-my-md row justify-between items-center">
|
||||||
|
データ階層を設定する :
|
||||||
|
<q-btn icon="add" size="sm" padding="xs" outline color="primary" @click="addRow" />
|
||||||
|
</div>
|
||||||
|
<q-virtual-scroll style="max-height: 13.5rem;" :items="data.fieldList" separator v-slot="{ item, index }">
|
||||||
|
<div class="row justify-between items-center q-my-md">
|
||||||
|
<div>{{ index + 1 }}階層 :</div>
|
||||||
|
<div>{{ item.source?.name }}</div>
|
||||||
|
<q-btn-group outline>
|
||||||
|
<q-btn outline dense label="変更" padding="xs sm" color="primary"
|
||||||
|
@click="() => showFieldDialog(item, 'source')" />
|
||||||
|
<q-btn outline dense label="削除" padding="xs sm" color="primary" @click="() => delRow(index)" />
|
||||||
|
</q-btn-group>
|
||||||
|
</div>
|
||||||
|
</q-virtual-scroll>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- アプリ選択ダイアログ -->
|
||||||
|
<ShowDialog v-model:visible="data.sourceApp.showSelectApp" name="アプリ選択" @close="closeAppDialog" min-width="50vw"
|
||||||
|
min-height="50vh">
|
||||||
|
<template v-slot:toolbar>
|
||||||
|
<q-input dense debounce="300" v-model="data.sourceApp.appFilter" placeholder="検索" clearable>
|
||||||
|
<template v-slot:before>
|
||||||
|
<q-icon name="search" />
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</template>
|
||||||
|
<AppSelectBox ref="appDg" name="アプリ" type="single" :filter="data.sourceApp.appFilter" />
|
||||||
|
</ShowDialog>
|
||||||
|
</q-step>
|
||||||
|
|
||||||
|
<q-step :name="2" title="ドロップダウンフィールドの設定" icon="multiple_stop" :done="step > 2">
|
||||||
|
<div class="row q-pa-sm q-col-gutter-x-sm flex-center">
|
||||||
|
<div class="col-grow row q-col-gutter-x-sm">
|
||||||
|
<div class="col-6">データソース</div>
|
||||||
|
<div class="col-6">ドロップダウンフィールド</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<div style="width: 88px; height: 1px;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-for="(item) in data.fieldList" :key="item.id" class="row q-pa-sm q-col-gutter-x-sm flex-center">
|
||||||
|
<div class="col-grow row q-col-gutter-x-sm">
|
||||||
|
|
||||||
|
<div class="col-6">{{ item.source.name }}</div>
|
||||||
|
<div class="col-6">{{ item.dropDown?.name }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<div class="row justify-end">
|
||||||
|
<q-btn-group outline>
|
||||||
|
<q-btn outline dense label="設定" padding="xs sm" color="primary"
|
||||||
|
@click="() => showFieldDialog(item, 'dropDown')" />
|
||||||
|
<q-btn outline dense label="クリア" padding="xs sm" color="primary"
|
||||||
|
@click="() => item.dropDown = undefined" />
|
||||||
|
</q-btn-group>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-step>
|
||||||
|
|
||||||
|
<!-- ステップナビゲーション -->
|
||||||
|
<template v-slot:navigation>
|
||||||
|
<q-stepper-navigation>
|
||||||
|
<div class="row justify-end q-mt-md">
|
||||||
|
<q-btn v-if="step > 1" flat color="primary" @click="$refs.stepper.previous()" label="戻る" class="q-ml-sm" />
|
||||||
|
<q-btn @click="stepperNext" color="primary" :label="step === 2 ? '保存' : '次へ'"
|
||||||
|
:disable="nextBtnCheck()" />
|
||||||
|
</div>
|
||||||
|
</q-stepper-navigation>
|
||||||
|
</template>
|
||||||
|
</q-stepper>
|
||||||
|
|
||||||
|
<!-- フィールド選択ダイアログ -->
|
||||||
|
<template v-for="(item, index) in data.fieldList" :key="`dg${item.id}`">
|
||||||
|
<show-dialog v-model:visible="item.sourceDg.show" name="フィールド一覧" min-width="400px">
|
||||||
|
<template v-slot:toolbar>
|
||||||
|
<q-input dense debounce="300" v-model="item.sourceDg.filter" placeholder="検索" clearable>
|
||||||
|
<template v-slot:before>
|
||||||
|
<q-icon name="search" />
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</template>
|
||||||
|
<FieldSelect name="フィールド" :appId="data.sourceApp.id" :selectedFields="item.source"
|
||||||
|
:filter="item.sourceDg.filter" :updateSelectFields="(f) => updateSelectField(f, item, index, 'source')"
|
||||||
|
:blackListLabel="blackListLabel" />
|
||||||
|
</show-dialog>
|
||||||
|
|
||||||
|
<show-dialog v-model:visible="item.dropDownDg.show" name="フィールド一覧" min-width="400px">
|
||||||
|
<template v-slot:toolbar>
|
||||||
|
<q-input dense debounce="300" v-model="item.dropDownDg.filter" placeholder="検索" clearable>
|
||||||
|
<template v-slot:before>
|
||||||
|
<q-icon name="search" />
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</template>
|
||||||
|
<FieldSelect name="フィールド" :appId="data.dropDownApp.id" :selectedFields="item.source"
|
||||||
|
:filter="item.dropDownDg.filter" :updateSelectFields="(f) => updateSelectField(f, item, index, 'dropDown')"
|
||||||
|
:blackListLabel="blackListLabel" />
|
||||||
|
</show-dialog>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, reactive, ref, watchEffect,watch } from 'vue';
|
||||||
|
import ShowDialog from './ShowDialog.vue';
|
||||||
|
import AppSelectBox from './AppSelectBox.vue';
|
||||||
|
import FieldSelect from './FieldSelect.vue';
|
||||||
|
import { useAuthStore } from 'src/stores/useAuthStore';
|
||||||
|
import { useFlowEditorStore } from 'src/stores/flowEditor';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import { useQuasar } from 'quasar';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'CascadingDropDownBox',
|
||||||
|
inheritAttrs: false,
|
||||||
|
components: { ShowDialog, AppSelectBox, FieldSelect },
|
||||||
|
props: {
|
||||||
|
modelValue: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
finishDialogHandler: Function,
|
||||||
|
},
|
||||||
|
emits: ['update:modelValue'],
|
||||||
|
setup(props, { emit }) {
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const flowStore = useFlowEditorStore();
|
||||||
|
const $q = useQuasar();
|
||||||
|
const appDg = ref();
|
||||||
|
const stepper = ref();
|
||||||
|
const step = ref(1);
|
||||||
|
|
||||||
|
const data =ref(props.modelValue);
|
||||||
|
// const data = ref({
|
||||||
|
// sourceApp: props.modelValue.sourceApp ?? { appFilter: '', showSelectApp: false },
|
||||||
|
// dropDownApp: props.modelValue.dropDownApp,
|
||||||
|
// fieldList: props.modelValue.fieldList ?? [],
|
||||||
|
// });
|
||||||
|
|
||||||
|
// アプリ関連の関数
|
||||||
|
const showAppDialog = () => data.value.sourceApp.showSelectApp = true;
|
||||||
|
|
||||||
|
const clearSelectedApp = () => {
|
||||||
|
data.value.sourceApp = { appFilter: '', showSelectApp: false };
|
||||||
|
data.value.fieldList = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeAppDialog = (val: 'OK' | 'Cancel') => {
|
||||||
|
data.value.sourceApp.showSelectApp = false;
|
||||||
|
const selected = appDg.value?.selected[0];
|
||||||
|
if (val === 'OK' && selected) {
|
||||||
|
if (flowStore.appInfo?.appId === selected.id) {
|
||||||
|
$q.notify({
|
||||||
|
type: 'negative',
|
||||||
|
caption: "エラー",
|
||||||
|
message: 'データソースを現在のアプリにすることはできません。'
|
||||||
|
});
|
||||||
|
} else if (selected.id !== data.value.sourceApp.id) {
|
||||||
|
clearSelectedApp();
|
||||||
|
Object.assign(data.value.sourceApp, { id: selected.id, name: selected.name });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// フィールド関連の関数
|
||||||
|
const defaultRow = () => ({
|
||||||
|
id: uuidv4(),
|
||||||
|
source: undefined,
|
||||||
|
dropDown: undefined,
|
||||||
|
sourceDg: { show: false, filter: '' },
|
||||||
|
dropDownDg: { show: false, filter: '' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const addRow = () => data.value.fieldList.push(defaultRow());
|
||||||
|
const delRow = (index: number) => data.value.fieldList.splice(index, 1);
|
||||||
|
|
||||||
|
const showFieldDialog = (item: any, keyName: string) => item[`${keyName}Dg`].show = true;
|
||||||
|
|
||||||
|
const updateSelectField = (f: any, item: any, index: number, keyName: 'source' | 'dropDown') => {
|
||||||
|
const [selected] = f.value;
|
||||||
|
const isDuplicate = data.value.fieldList.some((field, idx) =>
|
||||||
|
idx !== index && (field[keyName]?.code === selected.code || field[keyName]?.label === selected.label)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isDuplicate) {
|
||||||
|
$q.notify({
|
||||||
|
type: 'negative',
|
||||||
|
caption: "エラー",
|
||||||
|
message: '重複したフィールドは選択できません'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
item[keyName] = selected;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ステッパー関連の関数
|
||||||
|
const nextBtnCheck = () => {
|
||||||
|
const s = step.value
|
||||||
|
if (s === 1) {
|
||||||
|
return !(data.value.sourceApp?.id && data.value.fieldList?.length > 0 && data.value.fieldList?.every(f => f.source?.name));
|
||||||
|
} else if (s === 2) {
|
||||||
|
return !data.value.fieldList?.every(f => f.dropDown?.name);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const stepperNext = () => {
|
||||||
|
if (step.value === 2) {
|
||||||
|
props.finishDialogHandler?.(data.value);
|
||||||
|
} else {
|
||||||
|
data.value.dropDownApp = { name: flowStore.appInfo?.name, id: flowStore.appInfo?.appId };
|
||||||
|
stepper.value?.next();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// // データ変更の監視
|
||||||
|
// watchEffect(() =>{
|
||||||
|
// emit('update:modelValue', data.value);
|
||||||
|
// });
|
||||||
|
|
||||||
|
return {
|
||||||
|
// 状態と参照
|
||||||
|
authStore,
|
||||||
|
step,
|
||||||
|
stepper,
|
||||||
|
appDg,
|
||||||
|
data,
|
||||||
|
// アプリ関連の関数
|
||||||
|
showAppDialog,
|
||||||
|
closeAppDialog,
|
||||||
|
clearSelectedApp,
|
||||||
|
|
||||||
|
// フィールド関連の関数
|
||||||
|
addRow,
|
||||||
|
delRow,
|
||||||
|
showFieldDialog,
|
||||||
|
updateSelectField,
|
||||||
|
|
||||||
|
// ステッパー関連の関数
|
||||||
|
nextBtnCheck,
|
||||||
|
stepperNext,
|
||||||
|
|
||||||
|
// 定数
|
||||||
|
blackListLabel: ['レコード番号', '作業者', '更新者', '更新日時', '作成日時', '作成者', 'カテゴリー', 'ステータス'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -37,6 +37,10 @@ export default {
|
|||||||
updateSelectFields: {
|
updateSelectFields: {
|
||||||
type: Function
|
type: Function
|
||||||
},
|
},
|
||||||
|
blackListLabel: {
|
||||||
|
type:Array,
|
||||||
|
default:()=>[]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
const isLoaded = ref(false);
|
const isLoaded = ref(false);
|
||||||
@@ -62,16 +66,25 @@ export default {
|
|||||||
app: props.appId
|
app: props.appId
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
let fields = res.data.properties;
|
let fields = Object.values(res.data.properties);
|
||||||
Object.keys(fields).forEach((key,index) => {
|
for (const index in fields) {
|
||||||
const fld = fields[key];
|
const fld = fields[index]
|
||||||
if(props.fieldTypes.length===0 || props.fieldTypes.includes(fld.type)){
|
if(props.blackListLabel.length > 0){
|
||||||
rows.push({id:index, name: fld.label || fld.code, ...fld });
|
if(!props.blackListLabel.find(blackListItem => blackListItem === fld.label)){
|
||||||
}else if(props.fieldTypes.includes("lookup") && ("lookup" in fld)){
|
if(props.fieldTypes.length===0 || props.fieldTypes.includes(fld.type)){
|
||||||
rows.push({id:index, name: fld.label || fld.code, ...fld });
|
rows.push({id:index, name: fld.label || fld.code, ...fld });
|
||||||
|
}else if(props.fieldTypes.includes("lookup") && ("lookup" in fld)){
|
||||||
|
rows.push({id:index, name: fld.label || fld.code, ...fld });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if(props.fieldTypes.length===0 || props.fieldTypes.includes(fld.type)){
|
||||||
|
rows.push({id:index, name: fld.label || fld.code, ...fld });
|
||||||
|
}else if(props.fieldTypes.includes("lookup") && ("lookup" in fld)){
|
||||||
|
rows.push({id:index, name: fld.label || fld.code, ...fld });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
|
||||||
isLoaded.value = true;
|
isLoaded.value = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<q-card-section class="q-mt-md" :style="sectionStyle">
|
<q-card-section class="q-mt-md" :style="sectionStyle">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-card-actions align="right" class="text-primary">
|
<q-card-actions v-if="!disableBtn" align="right" class="text-primary">
|
||||||
<q-btn flat label="確定" v-close-popup @click="CloseDialogue('OK')" />
|
<q-btn flat label="確定" v-close-popup @click="CloseDialogue('OK')" />
|
||||||
<q-btn flat label="キャンセル" v-close-popup @click="CloseDialogue('Cancel')" />
|
<q-btn flat label="キャンセル" v-close-popup @click="CloseDialogue('Cancel')" />
|
||||||
</q-card-actions>
|
</q-card-actions>
|
||||||
@@ -29,7 +29,11 @@ export default {
|
|||||||
width:String,
|
width:String,
|
||||||
height:String,
|
height:String,
|
||||||
minWidth:String,
|
minWidth:String,
|
||||||
minHeight:String
|
minHeight:String,
|
||||||
|
disableBtn:{
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
emits: [
|
emits: [
|
||||||
'close'
|
'close'
|
||||||
|
|||||||
133
frontend/src/components/right/CascadingDropDown.vue
Normal file
133
frontend/src/components/right/CascadingDropDown.vue
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
<template>
|
||||||
|
<div v-bind="$attrs">
|
||||||
|
<q-field :label="displayName" labelColor="primary" stack-label lazy-rules="ondemand" ref="fieldRef">
|
||||||
|
<template v-slot:control>
|
||||||
|
<q-card flat class="full-width">
|
||||||
|
<q-card-actions vertical>
|
||||||
|
<q-btn color="grey-3" text-color="black" @click="() => { dgIsShow = true }">クリックで設定</q-btn>
|
||||||
|
</q-card-actions>
|
||||||
|
<q-card-section class="text-caption">
|
||||||
|
<div v-if="data.dropDownApp?.name">
|
||||||
|
{{ `${data.sourceApp?.name} -> ${data.dropDownApp?.name}` }}
|
||||||
|
</div>
|
||||||
|
<div v-else>{{ placeholder }}</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</template>
|
||||||
|
</q-field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ShowDialog v-model:visible="dgIsShow" name="ドロップダウン階層化設定" @close="closeDg" min-width="50vw" min-height="20vh" disableBtn>
|
||||||
|
<template v-slot:toolbar>
|
||||||
|
<q-btn flat round dense icon="more_vert" >
|
||||||
|
<q-menu auto-close anchor="bottom start">
|
||||||
|
<q-list>
|
||||||
|
<q-item clickable @click="copySetting()">
|
||||||
|
<q-item-section avatar><q-icon name="content_copy" ></q-icon></q-item-section>
|
||||||
|
<q-item-section >コピー</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
<q-item clickable @click="pasteSetting()">
|
||||||
|
<q-item-section avatar><q-icon name="content_paste" ></q-icon></q-item-section>
|
||||||
|
<q-item-section >貼り付け</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</q-menu>
|
||||||
|
</q-btn>
|
||||||
|
</template>
|
||||||
|
<div class="q-mb-md q-ml-md q-mr-md">
|
||||||
|
<CascadingDropDownBox v-model:model-value="data" :finishDialogHandler="finishDialogHandler" />
|
||||||
|
</div>
|
||||||
|
</ShowDialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, ref, watchEffect } from 'vue';
|
||||||
|
import ShowDialog from '../ShowDialog.vue';
|
||||||
|
import CascadingDropDownBox from '../CascadingDropDownBox.vue';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
inheritAttrs: false,
|
||||||
|
name: 'CascadingDropDown',
|
||||||
|
components: {
|
||||||
|
ShowDialog,
|
||||||
|
CascadingDropDownBox
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
displayName: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
modelValue: {
|
||||||
|
type: Object,
|
||||||
|
default: () => { ({}) }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props, { emit }) {
|
||||||
|
const dgIsShow = ref(false);
|
||||||
|
// const data = ref(props.modelValue);
|
||||||
|
const data = ref({
|
||||||
|
sourceApp: props.modelValue.sourceApp ?? { appFilter: '', showSelectApp: false },
|
||||||
|
dropDownApp: props.modelValue.dropDownApp,
|
||||||
|
fieldList: props.modelValue.fieldList ?? [],
|
||||||
|
});
|
||||||
|
const closeDg = (state: string) => {
|
||||||
|
dgIsShow.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const finishDialogHandler = (boxData) => {
|
||||||
|
data.value = boxData
|
||||||
|
dgIsShow.value = false
|
||||||
|
emit('update:modelValue', data.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
//設定をコピーする
|
||||||
|
const copySetting=()=>{
|
||||||
|
if (navigator.clipboard) {
|
||||||
|
const jsonData= JSON.stringify(data.value);
|
||||||
|
navigator.clipboard.writeText(jsonData).then(() => {
|
||||||
|
console.log('Text successfully copied to clipboard');
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
console.error('Error in copying text: ', err);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log('Clipboard API not available');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
//設定を貼り付ける
|
||||||
|
const pasteSetting=async ()=>{
|
||||||
|
try {
|
||||||
|
const text = await navigator.clipboard.readText();
|
||||||
|
console.log('Text from clipboard:', text);
|
||||||
|
const jsonData=JSON.parse(text);
|
||||||
|
if('sourceApp' in jsonData && 'dropDownApp' in jsonData && 'fieldList' in jsonData){
|
||||||
|
const {sourceApp,dropDownApp, fieldList}=jsonData;
|
||||||
|
data.value.sourceApp=sourceApp;
|
||||||
|
data.value.dropDownApp=dropDownApp;
|
||||||
|
data.value.fieldList=fieldList;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to read text from clipboard: ', err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// watchEffect(() => {
|
||||||
|
// emit('update:modelValue', data.value);
|
||||||
|
// });
|
||||||
|
|
||||||
|
return {
|
||||||
|
dgIsShow,
|
||||||
|
closeDg,
|
||||||
|
data,
|
||||||
|
finishDialogHandler,
|
||||||
|
copySetting,
|
||||||
|
pasteSetting
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -24,6 +24,7 @@ import NumInput from './NumInput.vue';
|
|||||||
import DataProcessing from './DataProcessing.vue';
|
import DataProcessing from './DataProcessing.vue';
|
||||||
import DataMapping from './DataMapping.vue';
|
import DataMapping from './DataMapping.vue';
|
||||||
import AppSelect from './AppSelect.vue';
|
import AppSelect from './AppSelect.vue';
|
||||||
|
import CascadingDropDown from './CascadingDropDown.vue';
|
||||||
import { IActionNode,IActionProperty,IProp } from 'src/types/ActionTypes';
|
import { IActionNode,IActionProperty,IProp } from 'src/types/ActionTypes';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
@@ -41,7 +42,8 @@ export default defineComponent({
|
|||||||
NumInput,
|
NumInput,
|
||||||
DataProcessing,
|
DataProcessing,
|
||||||
DataMapping,
|
DataMapping,
|
||||||
AppSelect
|
AppSelect,
|
||||||
|
CascadingDropDown
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
nodeProps: {
|
nodeProps: {
|
||||||
|
|||||||
@@ -1,51 +1,3 @@
|
|||||||
// @import 'bootstrap/scss/bootstrap';
|
|
||||||
@import "bootstrap/scss/functions";
|
|
||||||
@import "bootstrap/scss/variables";
|
|
||||||
@import "bootstrap/scss/variables-dark";
|
|
||||||
@import "bootstrap/scss/maps";
|
|
||||||
@import "bootstrap/scss/mixins";
|
|
||||||
@import "bootstrap/scss/root";
|
|
||||||
.bs-scope{
|
|
||||||
// Required
|
|
||||||
@import "bootstrap/scss/utilities";
|
|
||||||
@import "bootstrap/scss/reboot";
|
|
||||||
@import "bootstrap/scss/type";
|
|
||||||
@import "bootstrap/scss/images";
|
|
||||||
@import "bootstrap/scss/containers";
|
|
||||||
@import "bootstrap/scss/grid";
|
|
||||||
// @import "bootstrap/scss/tables";
|
|
||||||
// @import "bootstrap/scss/forms";
|
|
||||||
@import "bootstrap/scss/buttons";
|
|
||||||
@import "bootstrap/scss/transitions";
|
|
||||||
@import "bootstrap/scss/dropdown";
|
|
||||||
// @import "bootstrap/scss/button-group";
|
|
||||||
// @import "bootstrap/scss/nav";
|
|
||||||
// @import "bootstrap/scss/navbar"; // Requires nav
|
|
||||||
@import "bootstrap/scss/card";
|
|
||||||
// @import "bootstrap/scss/breadcrumb";
|
|
||||||
// @import "bootstrap/scss/accordion";
|
|
||||||
// @import "bootstrap/scss/pagination";
|
|
||||||
// @import "bootstrap/scss/badge";
|
|
||||||
// @import "bootstrap/scss/alert";
|
|
||||||
// @import "bootstrap/scss/progress";
|
|
||||||
// @import "bootstrap/scss/list-group";
|
|
||||||
@import "bootstrap/scss/close";
|
|
||||||
// @import "bootstrap/scss/toasts";
|
|
||||||
@import "bootstrap/scss/modal"; // Requires transitions
|
|
||||||
// @import "bootstrap/scss/tooltip";
|
|
||||||
@import "bootstrap/scss/popover";
|
|
||||||
// @import "bootstrap/scss/carousel";
|
|
||||||
@import "bootstrap/scss/spinners";
|
|
||||||
@import "bootstrap/scss/offcanvas"; // Requires transitions
|
|
||||||
// @import "bootstrap/scss/placeholders";
|
|
||||||
|
|
||||||
// Helpers
|
|
||||||
// @import "bootstrap/scss/helpers";
|
|
||||||
|
|
||||||
// Utilities
|
|
||||||
@import "bootstrap/scss/utilities/api";
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-backdrop {
|
.modal-backdrop {
|
||||||
--bs-backdrop-zindex: 1050;
|
--bs-backdrop-zindex: 1050;
|
||||||
--bs-backdrop-bg: #000;
|
--bs-backdrop-bg: #000;
|
||||||
|
|||||||
47
plugin/kintone-addins/src/actions/bootstrap.scss
vendored
Normal file
47
plugin/kintone-addins/src/actions/bootstrap.scss
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
// @import 'bootstrap/scss/bootstrap';
|
||||||
|
@import "bootstrap/scss/functions";
|
||||||
|
@import "bootstrap/scss/variables";
|
||||||
|
@import "bootstrap/scss/variables-dark";
|
||||||
|
@import "bootstrap/scss/maps";
|
||||||
|
@import "bootstrap/scss/mixins";
|
||||||
|
@import "bootstrap/scss/root";
|
||||||
|
.bs-scope{
|
||||||
|
// Required
|
||||||
|
@import "bootstrap/scss/utilities";
|
||||||
|
@import "bootstrap/scss/reboot";
|
||||||
|
@import "bootstrap/scss/type";
|
||||||
|
@import "bootstrap/scss/images";
|
||||||
|
@import "bootstrap/scss/containers";
|
||||||
|
@import "bootstrap/scss/grid";
|
||||||
|
// @import "bootstrap/scss/tables";
|
||||||
|
@import "bootstrap/scss/forms";
|
||||||
|
@import "bootstrap/scss/buttons";
|
||||||
|
@import "bootstrap/scss/transitions";
|
||||||
|
@import "bootstrap/scss/dropdown";
|
||||||
|
// @import "bootstrap/scss/button-group";
|
||||||
|
// @import "bootstrap/scss/nav";
|
||||||
|
// @import "bootstrap/scss/navbar"; // Requires nav
|
||||||
|
@import "bootstrap/scss/card";
|
||||||
|
// @import "bootstrap/scss/breadcrumb";
|
||||||
|
// @import "bootstrap/scss/accordion";
|
||||||
|
// @import "bootstrap/scss/pagination";
|
||||||
|
// @import "bootstrap/scss/badge";
|
||||||
|
// @import "bootstrap/scss/alert";
|
||||||
|
// @import "bootstrap/scss/progress";
|
||||||
|
// @import "bootstrap/scss/list-group";
|
||||||
|
@import "bootstrap/scss/close";
|
||||||
|
// @import "bootstrap/scss/toasts";
|
||||||
|
@import "bootstrap/scss/modal"; // Requires transitions
|
||||||
|
// @import "bootstrap/scss/tooltip";
|
||||||
|
@import "bootstrap/scss/popover";
|
||||||
|
// @import "bootstrap/scss/carousel";
|
||||||
|
@import "bootstrap/scss/spinners";
|
||||||
|
@import "bootstrap/scss/offcanvas"; // Requires transitions
|
||||||
|
// @import "bootstrap/scss/placeholders";
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
// @import "bootstrap/scss/helpers";
|
||||||
|
|
||||||
|
// Utilities
|
||||||
|
@import "bootstrap/scss/utilities/api";
|
||||||
|
}
|
||||||
18
plugin/kintone-addins/src/actions/cascading-dropdown.scss
Normal file
18
plugin/kintone-addins/src/actions/cascading-dropdown.scss
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
.alc-loading {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, .5);
|
||||||
|
display: flex;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alc-loading > div {
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alc-dnone{
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
80
plugin/kintone-addins/src/actions/cascading-dropdown.ts
Normal file
80
plugin/kintone-addins/src/actions/cascading-dropdown.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import {
|
||||||
|
IAction,
|
||||||
|
IActionResult,
|
||||||
|
IActionNode,
|
||||||
|
IActionProperty,
|
||||||
|
IContext,
|
||||||
|
} from "../types/ActionTypes";
|
||||||
|
import { actionAddins } from ".";
|
||||||
|
// import { KintoneRestAPIClient } from "@kintone/rest-api-client";
|
||||||
|
// import { getPageState } from "../util/url";
|
||||||
|
import { Snipper } from '../util/ui-helper';
|
||||||
|
import { DropDownManager,ICascadingDropDown, IFieldList} from '../types/CascadingDropDownManager'
|
||||||
|
import "./cascading-dropdown.scss";
|
||||||
|
// import { Record as KTRecord } from "@kintone/rest-api-client/lib/src/client/types";
|
||||||
|
|
||||||
|
// 階層化ドロップダウンメニューのプロパティインターフェース
|
||||||
|
interface ICascadingDropDownProps {
|
||||||
|
displayName: string;
|
||||||
|
cascadingDropDown: ICascadingDropDown;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 階層化ドロップダウンのクラス実装
|
||||||
|
*/
|
||||||
|
export class CascadingDropDownAction implements IAction {
|
||||||
|
name: string;
|
||||||
|
actionProps: IActionProperty[];
|
||||||
|
props: ICascadingDropDownProps;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.name = "階層化ドロップダウン";
|
||||||
|
this.actionProps = [];
|
||||||
|
this.props = {} as ICascadingDropDownProps;
|
||||||
|
this.register();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* アクションのプロセス実行
|
||||||
|
* @param actionNode
|
||||||
|
* @param event
|
||||||
|
* @param context
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async process(
|
||||||
|
actionNode: IActionNode,
|
||||||
|
event: any,
|
||||||
|
context: IContext
|
||||||
|
): Promise<IActionResult> {
|
||||||
|
this.actionProps = actionNode.actionProps;
|
||||||
|
this.props = actionNode.ActionValue as ICascadingDropDownProps;
|
||||||
|
|
||||||
|
const result: IActionResult = { canNext: true, result: "" };
|
||||||
|
const snipper = new Snipper("body");
|
||||||
|
const dropDownManager= new DropDownManager(this.props.cascadingDropDown,event);
|
||||||
|
try {
|
||||||
|
if (!this.props) return result;
|
||||||
|
const appId = this.props.cascadingDropDown.dropDownApp.id;
|
||||||
|
//snipper表示
|
||||||
|
snipper.showSpinner();
|
||||||
|
await dropDownManager.handlePageState(appId);
|
||||||
|
snipper.hideSpinner();
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"CascadingDropDownAction プロセス中にエラーが発生しました:",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
context.errors.handleError(error, actionNode);
|
||||||
|
return { canNext: false, result: "" };
|
||||||
|
}finally{
|
||||||
|
snipper.removeSpinner();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
register(): void {
|
||||||
|
actionAddins[this.name] = this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new CascadingDropDownAction();
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
import { IAction } from "../types/ActionTypes";
|
import { IAction } from "../types/ActionTypes";
|
||||||
|
import './bootstrap.scss'
|
||||||
export const actionAddins :Record<string,IAction>={};
|
export const actionAddins :Record<string,IAction>={};
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ export interface IField{
|
|||||||
type?:string;
|
type?:string;
|
||||||
required?:boolean;
|
required?:boolean;
|
||||||
options?:string;
|
options?:string;
|
||||||
|
label?: string;
|
||||||
}
|
}
|
||||||
//変数のインターフェース
|
//変数のインターフェース
|
||||||
export interface IVarName{
|
export interface IVarName{
|
||||||
|
|||||||
353
plugin/kintone-addins/src/types/CascadingDropDownManager.ts
Normal file
353
plugin/kintone-addins/src/types/CascadingDropDownManager.ts
Normal file
@@ -0,0 +1,353 @@
|
|||||||
|
import {IField,IActionNode} from "../types/ActionTypes";
|
||||||
|
import { KintoneRestAPIClient } from "@kintone/rest-api-client";
|
||||||
|
import { getPageState } from "../util/url";
|
||||||
|
import $ from 'jquery';
|
||||||
|
import { Record as KTRecord } from "@kintone/rest-api-client/lib/src/client/types";
|
||||||
|
|
||||||
|
|
||||||
|
// 階層化ドロップダウンメニューの設定インターフェース
|
||||||
|
export interface ICascadingDropDown {
|
||||||
|
sourceApp: IApp;
|
||||||
|
dropDownApp: IApp;
|
||||||
|
fieldList: IFieldList[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// アプリケーションインターフェース
|
||||||
|
export interface IApp {
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
// フィールドリストインターフェース
|
||||||
|
export interface IFieldList {
|
||||||
|
source: IField;
|
||||||
|
dropDown: IField;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ドロップダウンメニューの辞書タイプ、ドロップダウンオプションの検索を高速化するために使用
|
||||||
|
type DropdownDictionary = Record<string, string[]>;
|
||||||
|
|
||||||
|
// ドロップダウンメニューの処理クラス
|
||||||
|
export class DropDownManager {
|
||||||
|
private dictionary: DropdownDictionary = {};
|
||||||
|
private state: Record<string, string> = {};
|
||||||
|
private selects: Map<string, HTMLElement> = new Map();
|
||||||
|
private columnMap: Map<HTMLElement, IFieldList> = new Map();
|
||||||
|
private columMapValueArray: IFieldList[] = [];
|
||||||
|
private columMapArray: [HTMLElement, IFieldList][] = [];
|
||||||
|
private props :ICascadingDropDown;
|
||||||
|
private event:Event;
|
||||||
|
|
||||||
|
constructor(props: ICascadingDropDown,event:any){
|
||||||
|
this.props=props;
|
||||||
|
this.event=event;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初期化メソッド
|
||||||
|
async init(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const client = new KintoneRestAPIClient();
|
||||||
|
const sourceAppId = this.props.sourceApp.id;
|
||||||
|
const fields = this.props.fieldList.map((f) => f.source.code);
|
||||||
|
const records = await client.record.getAllRecords({
|
||||||
|
app: sourceAppId,
|
||||||
|
fields,
|
||||||
|
});
|
||||||
|
this.dictionary =this.buildDropdownDictionary(records, this.props.fieldList);
|
||||||
|
// const hash = await this.calculateHash(records, this.actionNode);
|
||||||
|
// const storageKey = `dropdown_dictionary::${this.props.dropDownApp.id}_${hash}`;
|
||||||
|
|
||||||
|
// const lsDictionary = this.getFromLocalStorage(storageKey);
|
||||||
|
// this.dictionary =
|
||||||
|
// lsDictionary || this.buildDropdownDictionary(records, this.props.fieldList);
|
||||||
|
// if (!lsDictionary) {
|
||||||
|
// this.saveToLocalStorage(storageKey, this.dictionary);
|
||||||
|
// }
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"階層化ドロップダウンの初期化中にエラーが発生しました:",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Web Crypto APIを使用してハッシュ値を計算
|
||||||
|
private async calculateHash(
|
||||||
|
records: KTRecord[],
|
||||||
|
actionNode: IActionNode
|
||||||
|
): Promise<string> {
|
||||||
|
const str = JSON.stringify(records) + JSON.stringify(actionNode);
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const data = encoder.encode(str);
|
||||||
|
const hashBuffer = await crypto.subtle.digest("SHA-1", data);
|
||||||
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||||
|
const hashHex = hashArray
|
||||||
|
.map((b) => b.toString(16).padStart(2, "0"))
|
||||||
|
.join("");
|
||||||
|
return hashHex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ドロップダウンメニューの辞書を構築
|
||||||
|
* @param records データソースのレコード
|
||||||
|
* @param fieldList データソースの階層フィールド
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
private buildDropdownDictionary(records: KTRecord[],fieldList: IFieldList[]): DropdownDictionary {
|
||||||
|
const tempDictionary: Record<string, Set<string>> = { "0_TOP": new Set() };
|
||||||
|
const fieldCodeToIndexMap = new Map(
|
||||||
|
fieldList.map((field, index) => [field.source.code, index])
|
||||||
|
);
|
||||||
|
|
||||||
|
records
|
||||||
|
.map((record) =>
|
||||||
|
Object.entries(record)
|
||||||
|
.map(([fieldCode, fieldData]) => ({
|
||||||
|
fieldCode,
|
||||||
|
value: fieldData.value,
|
||||||
|
index: fieldCodeToIndexMap.get(fieldCode),
|
||||||
|
}))
|
||||||
|
.filter((item) => item.index !== undefined)
|
||||||
|
.sort((a, b) => a.index! - b.index!)
|
||||||
|
)
|
||||||
|
.forEach((recordArray) => {
|
||||||
|
recordArray.forEach((item, index, array) => {
|
||||||
|
const { value, fieldCode } = item;
|
||||||
|
if (!value) return;
|
||||||
|
const v = value as string;
|
||||||
|
|
||||||
|
if (index === 0) {
|
||||||
|
tempDictionary["0_TOP"].add(v);
|
||||||
|
} else {
|
||||||
|
const previousItem = array[index - 1];
|
||||||
|
const previousKey = `${previousItem.index}_${previousItem.value}`;
|
||||||
|
tempDictionary[previousKey] =
|
||||||
|
tempDictionary[previousKey] || new Set();
|
||||||
|
tempDictionary[previousKey].add(v);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const dictionary: DropdownDictionary = {};
|
||||||
|
for (const [key, set] of Object.entries(tempDictionary)) {
|
||||||
|
dictionary[key] = Array.from(set).sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
return dictionary;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ローカルストレージから辞書を取得
|
||||||
|
private getFromLocalStorage(key: string): DropdownDictionary | null {
|
||||||
|
const data = localStorage.getItem(key);
|
||||||
|
this.clearUpDictionary(key);
|
||||||
|
return data ? JSON.parse(data) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 古い辞書をクリア
|
||||||
|
private clearUpDictionary(key: string): void {
|
||||||
|
Object.keys(localStorage)
|
||||||
|
.filter((k) => k.startsWith("dropdown_dictionary::") && !k.endsWith(key))
|
||||||
|
.forEach((k) => localStorage.removeItem(k));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ローカルストレージに辞書を保存
|
||||||
|
private saveToLocalStorage(key: string, data: DropdownDictionary): void {
|
||||||
|
localStorage.setItem(key, JSON.stringify(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ページの状態を処理
|
||||||
|
async handlePageState(appId: string): Promise<void> {
|
||||||
|
const currentState = getPageState(
|
||||||
|
window.location.href.replace("#", "?"),
|
||||||
|
appId
|
||||||
|
);
|
||||||
|
|
||||||
|
switch (currentState.type) {
|
||||||
|
case "app":
|
||||||
|
case "edit":
|
||||||
|
case "show":
|
||||||
|
if (currentState.type === "show" && currentState.mode !== "edit") break;
|
||||||
|
await this.init();
|
||||||
|
this.setupCascadingDropDown(currentState.type);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 階層化ドロップダウンを設定する
|
||||||
|
setupCascadingDropDown(pageType: string): void {
|
||||||
|
const tableElement = document.getElementById(
|
||||||
|
pageType === "app" ? "view-list-data-gaia" : "record-gaia"
|
||||||
|
);
|
||||||
|
if (!tableElement) {
|
||||||
|
console.error("ルート要素が見つかりません");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(pageType==="app"){
|
||||||
|
this.initDropdownContainerForList();
|
||||||
|
}else{
|
||||||
|
this.initDropdownContainerforDetail(tableElement);
|
||||||
|
}
|
||||||
|
this.renderDropdownContainer();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ドロップダウンメニューコンテナを初期化(一覧編集画面)
|
||||||
|
* @param tableElement
|
||||||
|
*/
|
||||||
|
private initDropdownContainerForList(){
|
||||||
|
const fieldLists = this.props.fieldList;
|
||||||
|
fieldLists.forEach((fld,index,array)=>{
|
||||||
|
const elems = kintone.app.getFieldElements(fld.dropDown.code);
|
||||||
|
if(elems){
|
||||||
|
const editElem = $(elems).filter(".recordlist-editcell-gaia").get(0);
|
||||||
|
if(editElem!==undefined){
|
||||||
|
this.columnMap.set(editElem, fld);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.columMapArray = Array.from(this.columnMap.entries());
|
||||||
|
this.columMapValueArray = Array.from(this.columnMap.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ドロップダウンメニューコンテナを初期化(明細編集画面)
|
||||||
|
private initDropdownContainerforDetail(tableElement: HTMLElement): void {
|
||||||
|
const fieldList = this.props.fieldList;
|
||||||
|
let headerCells = $(tableElement).find(".control-gaia");
|
||||||
|
for (const field of fieldList) {
|
||||||
|
const cell = headerCells.has(`div.control-label-gaia span:contains("${field.dropDown.label}")`).get(0);
|
||||||
|
if(cell!==undefined){
|
||||||
|
const valueElem = $(cell).find(".control-value-gaia").get(0);
|
||||||
|
if(valueElem!==undefined){
|
||||||
|
this.columnMap.set(cell, field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.columMapArray = Array.from(this.columnMap.entries());
|
||||||
|
this.columMapValueArray = Array.from(this.columnMap.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ドロップダウンメニューをレンダリング
|
||||||
|
private renderDropdownContainer(): void {
|
||||||
|
this.columnMap.forEach((field, cell) => {
|
||||||
|
// const cell = cells[columnIndex];
|
||||||
|
if (!cell) return;
|
||||||
|
|
||||||
|
const input = cell.querySelector<HTMLInputElement>("input");
|
||||||
|
if (!input) return;
|
||||||
|
|
||||||
|
this.createSelect(input, field.dropDown.code);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ドロップダウンメニューを作成
|
||||||
|
private createSelect(input: HTMLInputElement, fieldCode: string): void {
|
||||||
|
const div = document.createElement("div");
|
||||||
|
div.className = "bs-scope";
|
||||||
|
div.style.margin = "0.12rem";
|
||||||
|
const select = document.createElement("select");
|
||||||
|
select.className = "custom-dropdown form-select";
|
||||||
|
select.dataset.field = fieldCode;
|
||||||
|
select.addEventListener("change", (event) =>{
|
||||||
|
this.selectorChangeHandle(fieldCode, event);
|
||||||
|
input.value=this.state[fieldCode];
|
||||||
|
});
|
||||||
|
div.appendChild(select);
|
||||||
|
this.updateOptions(fieldCode, select, input.value);
|
||||||
|
input.parentNode?.insertBefore(div, input.nextSibling);
|
||||||
|
input.style.display = "none";
|
||||||
|
this.selects.set(fieldCode, div);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// ドロップダウンメニューのオプションを更新
|
||||||
|
private updateOptions(
|
||||||
|
fieldCode: string,
|
||||||
|
initSelect?: HTMLSelectElement | undefined | null,
|
||||||
|
value?: string
|
||||||
|
): void {
|
||||||
|
let select = initSelect;
|
||||||
|
if (!initSelect) {
|
||||||
|
select = this.selects
|
||||||
|
.get(fieldCode)
|
||||||
|
?.querySelector<HTMLSelectElement>("select");
|
||||||
|
if (!select) {
|
||||||
|
console.error(
|
||||||
|
`フィールド ${fieldCode} のドロップダウンメニュー要素が見つかりません`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.state[fieldCode] = value!;
|
||||||
|
}
|
||||||
|
|
||||||
|
const field = this.props.fieldList.find((f) => f.dropDown.code === fieldCode);
|
||||||
|
if (!field) {
|
||||||
|
console.error(`フィールド ${fieldCode} の設定が見つかりません`);
|
||||||
|
throw new Error(`フィールド ${fieldCode} の設定が見つかりません`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const level = this.getLevel(fieldCode);
|
||||||
|
const previousValue = this.getPreviousValueFromState(fieldCode);
|
||||||
|
const options = this.getOptions(level, previousValue);
|
||||||
|
if (!select) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
select.innerHTML = '<option value="">選択してください</option>';
|
||||||
|
options.forEach((option) => {
|
||||||
|
const optionElement = document.createElement("option");
|
||||||
|
optionElement.value = option;
|
||||||
|
optionElement.textContent = option;
|
||||||
|
select?.appendChild(optionElement);
|
||||||
|
});
|
||||||
|
|
||||||
|
select.value = value ?? this.state[fieldCode] ?? "";
|
||||||
|
select.disabled = level > 0 && !previousValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ドロップダウンメニューが変更された後にトリガーされる関数
|
||||||
|
private selectorChangeHandle=(fieldCode: string, event: Event)=> {
|
||||||
|
const select = event.target as HTMLSelectElement;
|
||||||
|
this.state[fieldCode] = select.value;
|
||||||
|
const currentLevel = this.getLevel(fieldCode);
|
||||||
|
this.columMapArray
|
||||||
|
.filter((_, arrayIndex) => arrayIndex > currentLevel)
|
||||||
|
.forEach(([_, field]) => {
|
||||||
|
const fieldCode = field.dropDown.code;
|
||||||
|
this.updateOptions(fieldCode);
|
||||||
|
const inputElem = this.selects.get(fieldCode)?.previousElementSibling as HTMLInputElement;
|
||||||
|
if(inputElem){
|
||||||
|
inputElem.value="";
|
||||||
|
}
|
||||||
|
delete this.state[fieldCode];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// フィールドのレベルを取得
|
||||||
|
private getLevel(fieldCode: string): number {
|
||||||
|
return this.columMapValueArray.findIndex(
|
||||||
|
(field) => field.dropDown.code === fieldCode
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 前のレベルのフィールドの値を取得
|
||||||
|
private getPreviousValueFromState(fieldCode: string): string {
|
||||||
|
const currentIndex = this.getLevel(fieldCode);
|
||||||
|
if (currentIndex <= 0) return "";
|
||||||
|
const previousField = this.columMapValueArray[currentIndex - 1];
|
||||||
|
return this.state[previousField.dropDown.code] ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 指定された階層と値のドロップダウンメニューオプションを取得
|
||||||
|
private getOptions(level: number, value: string): string[] {
|
||||||
|
const key = level === 0 ? "0_TOP" : this.buildKey(level - 1, value);
|
||||||
|
return this.dictionary[key] || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ドロップダウンメニューのキーを構築
|
||||||
|
private buildKey(level: number, value: string): string {
|
||||||
|
return `${level}_${value}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,6 +26,7 @@ import '../actions/style-field';
|
|||||||
import '../actions/datetime-calc';
|
import '../actions/datetime-calc';
|
||||||
import '../actions/end-of-month';
|
import '../actions/end-of-month';
|
||||||
import '../actions/date-specified';
|
import '../actions/date-specified';
|
||||||
|
import '../actions/cascading-dropdown';
|
||||||
import { ActionFlow,ErrorManager,IActionFlow, IActionResult,IContext } from "./ActionTypes";
|
import { ActionFlow,ErrorManager,IActionFlow, IActionResult,IContext } from "./ActionTypes";
|
||||||
const ShowErrorEvents:string[] = [
|
const ShowErrorEvents:string[] = [
|
||||||
"app.record.create.submit.success",
|
"app.record.create.submit.success",
|
||||||
|
|||||||
37
plugin/kintone-addins/src/util/ui-helper.ts
Normal file
37
plugin/kintone-addins/src/util/ui-helper.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import $ from 'jquery';
|
||||||
|
/**
|
||||||
|
* 画面処理中のLoadding表示
|
||||||
|
*/
|
||||||
|
export class Snipper {
|
||||||
|
private spinnerElement: JQuery<HTMLElement>;;
|
||||||
|
private container :JQuery<HTMLElement>;
|
||||||
|
constructor(selector: string) {
|
||||||
|
this.container = $(selector??'body');
|
||||||
|
this.spinnerElement=this.createSpinner();
|
||||||
|
}
|
||||||
|
|
||||||
|
createSpinner() {
|
||||||
|
const html =[
|
||||||
|
'<div class="bs-scope alc-loading alc-dnone">',
|
||||||
|
'<div class="spinner-border" style="width: 3rem; height: 3rem;" role="status">',
|
||||||
|
'<span class="visually-hidden"></span>',
|
||||||
|
'</div></div>'
|
||||||
|
].join("");
|
||||||
|
const spinner = $(html);
|
||||||
|
this.container.append(spinner);
|
||||||
|
return spinner;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeSpinner() {
|
||||||
|
this.spinnerElement.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
showSpinner() {
|
||||||
|
this.spinnerElement.removeClass('alc-dnone');
|
||||||
|
}
|
||||||
|
|
||||||
|
hideSpinner() {
|
||||||
|
this.spinnerElement.addClass('alc-dnone');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
86
plugin/kintone-addins/src/util/url.ts
Normal file
86
plugin/kintone-addins/src/util/url.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
/**
|
||||||
|
* ページの種類を表す型
|
||||||
|
*/
|
||||||
|
export type PageType = 'app' | 'show' | 'edit' | 'other';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* showページのモードを表す型
|
||||||
|
*/
|
||||||
|
export type ShowPageMode = 'show' | 'edit' | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ページの状態を表すインターフェース
|
||||||
|
*/
|
||||||
|
export interface PageState {
|
||||||
|
type: PageType;
|
||||||
|
mode: ShowPageMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URLの特定のセグメントを取得する
|
||||||
|
* @param url 分析対象のURL
|
||||||
|
* @param index 取得したいセグメントのインデックス(負の数は末尾からのインデックス)
|
||||||
|
*/
|
||||||
|
const getSegment = (url: URL, index: number): string => {
|
||||||
|
const segments = url.pathname.split('/').filter(Boolean);
|
||||||
|
return segments.at(index) ?? '';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URLとアプリIDに基づいて現在のページの状態を判断する
|
||||||
|
* @param url 分析対象のURL
|
||||||
|
* @param appId 確認するアプリID
|
||||||
|
*/
|
||||||
|
export const getPageState = (url: string, appId: string): PageState => {
|
||||||
|
const parsedUrl = new URL(url);
|
||||||
|
const lastSegment = getSegment(parsedUrl, -1);
|
||||||
|
const secondLastSegment = getSegment(parsedUrl, -2);
|
||||||
|
|
||||||
|
if (lastSegment === appId && /^\d+$/.test(appId)) {
|
||||||
|
return { type: 'app', mode: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastSegment.toLowerCase() === 'show' && secondLastSegment === appId && /^\d+$/.test(appId)) {
|
||||||
|
const mode = parsedUrl.searchParams.get('mode');
|
||||||
|
return {
|
||||||
|
type: 'show',
|
||||||
|
mode: (mode === 'show' || mode === 'edit') ? mode : null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastSegment.toLowerCase() === 'edit' && secondLastSegment === appId && /^\d+$/.test(appId)) {
|
||||||
|
return { type: 'edit', mode: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { type: 'other', mode: null };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 現在のページの状態を判断する(現在のURLを使用)
|
||||||
|
* @param appId 確認するアプリID
|
||||||
|
*/
|
||||||
|
export const getCurrentPageState = (appId: string): PageState =>
|
||||||
|
getPageState(window.location.href, appId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URLを正規化する(末尾のスラッシュを削除)
|
||||||
|
* @param url 正規化対象のURL
|
||||||
|
*/
|
||||||
|
export const normalizeUrl = (url: string): string => {
|
||||||
|
const parsedUrl = new URL(url);
|
||||||
|
parsedUrl.pathname = parsedUrl.pathname.replace(/\/$/, '');
|
||||||
|
return parsedUrl.toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URLを安全に解析する
|
||||||
|
* @param url 解析対象のURL文字列
|
||||||
|
*/
|
||||||
|
export const safeParseUrl = (url: string): URL | null => {
|
||||||
|
try {
|
||||||
|
return new URL(url);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
Reference in New Issue
Block a user