Files
KintoneAppBuilder/frontend/src/pages/FlowChart.vue
2024-12-09 16:04:25 +08:00

422 lines
13 KiB
Vue

<template>
<q-page>
<q-layout container class="absolute-full shadow-2 rounded-borders">
<div class="q-pa-sm q-gutter-sm ">
<q-drawer side="left" :overlay="true" bordered v-model="drawerLeft" :show-if-above="false" elevated>
<div class="flex-center absolute-full" style="padding:15px">
<q-scroll-area class="fit" :horizontal-thumb-style="{ opacity: '0' }">
<EventTree />
</q-scroll-area>
</div>
<div class="flex-center fixed-bottom bg-grey-3 q-pa-md row ">
<q-btn color="secondary" glossy label="デプロイ" @click="onDeploy" icon="sync" :loading="deployLoading" />
<q-space></q-space>
<q-btn-dropdown color="primary" label="保存" icon="save" :loading="saveLoading" >
<q-list>
<q-item clickable v-close-popup @click="onSaveVersion">
<q-item-section avatar >
<q-icon name="bookmark_border"></q-icon>
</q-item-section>
<q-item-section>
<q-item-label>新バージョン保存</q-item-label>
</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="onSaveFlow">
<q-item-section avatar >
<q-icon name="save" color="primary"></q-icon>
</q-item-section>
<q-item-section>
<q-item-label>選択中フローの保存</q-item-label>
</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="onSaveAllFlow">
<q-item-section avatar>
<q-icon name="collections_bookmark" color="accent"></q-icon>
</q-item-section>
<q-item-section>
<q-item-label>一括保存</q-item-label>
</q-item-section>
</q-item>
</q-list>
</q-btn-dropdown>
</div>
</q-drawer>
</div>
<q-btn flat dense round
:icon="drawerLeft?'keyboard_double_arrow_left':'keyboard_double_arrow_right'"
:style="{'left': fixedLeftPosition}"
@click="drawerLeft=!drawerLeft" class="expand" />
<q-breadcrumbs v-if="store.appInfo" class="fixed q-pl-md"
:style="{'left': fixedLeftPosition}">
<q-breadcrumbs-el icon="widgets" label="アプリ管理" to="/app" />
<q-breadcrumbs-el>
<template v-slot>
<a class="full-width" :href="!store.appInfo?'':`${authStore.currentDomain.kintoneUrl}/k/${store.appInfo?.appId}`" target="_blank" title="Kiontoneへ">
{{ store.appInfo?.name }}
<q-icon
class="q-ma-xs"
name="open_in_new"
color="grey-9"
/>
</a>
</template>
</q-breadcrumbs-el>
</q-breadcrumbs>
<div class="q-pa-md q-gutter-sm" :style="{minWidth: minPanelWidth}">
<div class="flowchart" v-if="store.currentFlow" :style="[drawerLeft?{paddingLeft:'300px'}:{}]">
<node-item v-if="rootNode!==undefined" :key="rootNode.id" :isSelected="rootNode === store.activeNode"
:actionNode="rootNode" @addNode="addNode" @nodeSelected="onNodeSelected" @nodeEdit="onNodeEdit"
@deleteNode="onDeleteNode" @deleteAllNextNodes="onDeleteAllNextNodes" @copyFlow="onCopyFlow"></node-item>
</div>
</div>
<PropertyPanel :actionNode="store.activeNode" v-model:drawerRight="drawerRight" @save-action-props="onSaveActionProps"></PropertyPanel>
</q-layout>
<ShowDialog v-model:visible="showAddAction" name="アクション" @close="closeDg" min-width="500px" min-height="500px">
<template v-slot:toolbar>
<q-input dense debounce="200" v-model="filter" placeholder="検索" clearable>
<template v-slot:before>
<q-icon name="search" />
</template>
</q-input>
</template>
<action-select ref="appDg" name="model" :filter="filter" type="single" @clearFilter="onClearFilter" ></action-select>
</ShowDialog>
<!-- save version dialog -->
<ShowDialog v-model:visible="saveVersionAction" name="新バージョン保存" @close="closeSaveVersionDg" min-width="500px">
<version-input v-model="versionInfo" />
</ShowDialog>
<q-inner-loading
:showing="initLoading"
color="primary"
label="読み込み中..."
/>
</q-page>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue';
import { useRoute } from 'vue-router';
import { IActionNode, ActionNode, IActionFlow, ActionFlow, RootAction, IActionProperty } from 'src/types/ActionTypes';
import { IAppDisplay, IManagedApp, IVersionInfo } from 'src/types/AppTypes';
import { storeToRefs } from 'pinia';
import { useFlowEditorStore } from 'stores/flowEditor';
import { useAuthStore } from 'stores/useAuthStore';
import { api } from 'boot/axios';
import NodeItem from 'src/components/main/NodeItem.vue';
import ShowDialog from 'components/ShowDialog.vue';
import ActionSelect from 'components/ActionSelect.vue';
import PropertyPanel from 'components/right/PropertyPanel.vue';
import EventTree from 'components/left/EventTree.vue';
import VersionInput from 'components/dialog/VersionInput.vue';
import { FlowCtrl } from '../control/flowctrl';
import { useQuasar } from 'quasar';
const deployLoading = ref(false);
const saveLoading = ref(false);
const initLoading = ref(true);
const drawerLeft = ref(false);
const versionInfo = ref<IVersionInfo>();
const $q = useQuasar();
const store = useFlowEditorStore();
const authStore = useAuthStore();
const route = useRoute()
const appDg = ref();
const prevNodeIfo = ref({
prevNode: {} as IActionNode,
inputPoint: ""
});
// const refFlow = ref<ActionFlow|null>(null);
const showAddAction = ref(false);
const saveVersionAction = ref(false);
const drawerRight = ref(false);
const filter=ref("");
const model = ref("");
const rootNode = computed(()=>{
return store.currentFlow?.getRoot();
});
const minPanelWidth=computed(()=>{
const root = store.currentFlow?.getRoot();
if(store.currentFlow && root){
return store.currentFlow?.getColumns(root) * 300 + 'px';
}else{
return "300px";
}
});
const fixedLeftPosition = computed(()=>{
return drawerLeft.value?"300px":"0px";
});
const addNode = (node: IActionNode, inputPoint: string) => {
if (drawerRight.value) {
drawerRight.value = false;
}
showAddAction.value = true;
prevNodeIfo.value.prevNode = node;
prevNodeIfo.value.inputPoint = inputPoint;
}
const onNodeSelected = (node: IActionNode) => {
//右パネルが開いている場合、自動閉じる
if (drawerRight.value && store.activeNode?.id !== node.id) {
drawerRight.value = false;
}
store.setActiveNode(node);
}
const onNodeEdit = (node: IActionNode) => {
store.setActiveNode(node);
drawerRight.value = true;
}
const onDeleteNode = (node: IActionNode) => {
if (!store.currentFlow) return;
//右パネルが開いている場合、自動閉じる
if (drawerRight.value && store.activeNode?.id === node.id) {
drawerRight.value = false;
}
store.currentFlow?.removeNode(node);
}
const onDeleteAllNextNodes = (node: IActionNode) => {
if (!store.currentFlow) return;
//右パネルが開いている場合、自動閉じる
if (drawerRight.value) {
drawerRight.value = false;
}
store.currentFlow?.removeAllNext(node.id);
}
const closeDg = (val: any) => {
console.log("Dialog closed->", val);
if (val == 'OK' && appDg?.value?.selected?.length > 0) {
const data = appDg.value.selected[0];
const actionProps = JSON.parse(data.property);
const outputPoint = JSON.parse(data.outputPoints);
const action = new ActionNode(data.name, data.desc, "", outputPoint, actionProps);
store.currentFlow?.addNode(action, prevNodeIfo.value.prevNode, prevNodeIfo.value.inputPoint);
}
}
/*
*フローのデータをコピーする
*/
const onCopyFlow = () => {
if (navigator.clipboard) {
const jsonData =JSON.stringify(store.currentFlow) ;
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 onDeploy = async () => {
if (store.appInfo === undefined || store.flows?.length === 0) {
$q.notify({
type: 'negative',
caption: "エラー",
message: `設定されたフローがありません。`
});
return;
}
try {
deployLoading.value = true;
await store.deploy();
deployLoading.value = false;
$q.notify({
type: 'positive',
caption: "通知",
message: `デプロイを成功しました。`
});
} catch (error) {
console.error(error);
deployLoading.value = false;
$q.notify({
type: 'negative',
caption: "エラー",
message: `デプロイが失敗しました。`
})
}
return;
}
const onSaveActionProps=(props:IActionProperty[])=>{
if(store.activeNode){
store.activeNode.actionProps=props;
$q.notify({
type: 'positive',
caption: "通知",
message: `${store.activeNode?.subTitle}の属性を設定しました。(保存はされていません)`
});
}
};
const onSaveVersion = async () => {
versionInfo.value = {
id: '1' // TODO
}
saveVersionAction.value = true;
// await onSaveAllFlow();
}
const closeSaveVersionDg = (val: 'OK'|'CANCEL') => {
if (val == 'OK') {
console.log(versionInfo.value);
}
}
const onSaveFlow = async () => {
const targetFlow = store.selectedFlow;
if (targetFlow === undefined) {
$q.notify({
type: 'negative',
caption: 'エラー',
message: `選択中のフローがありません。`
});
return;
}
try {
saveLoading.value = true;
await store.saveFlow(targetFlow);
saveLoading.value = false;
$q.notify({
type: 'positive',
caption: "通知",
message: `${targetFlow.getRoot()?.subTitle}のフロー設定を保存しました。`
});
} catch (error) {
console.error(error);
saveLoading.value = false;
$q.notify({
type: 'negative',
caption: "エラー",
message: `${targetFlow.getRoot()?.subTitle}のフローの設定の保存が失敗しました。`
})
}
}
/**
* すべてフローの設定を保存する
*/
const onSaveAllFlow= async ()=>{
try{
const targetFlows = store.eventTree.findAllFlows();
if (!targetFlows || targetFlows.length === 0 ) {
$q.notify({
type: 'negative',
caption: 'エラー',
message: `設定されたフローがありません。`
});
return;
}
saveLoading.value = true;
for(const flow of targetFlows ){
const isNew = flow.id === '';
if(isNew && flow.actionNodes.length===1){
continue;
}
await store.saveFlow(flow);
}
$q.notify({
type: 'positive',
caption: "通知",
message: `すべてのフロー設定を保存しました。`
});
saveLoading.value = false;
}catch (error) {
console.error(error);
saveLoading.value = false;
$q.notify({
type: 'negative',
caption: "エラー",
message: `フローの設定の保存が失敗しました。`
});
}
}
const fetchData = async () => {
initLoading.value = true;
if (store.appInfo === undefined && route?.params?.id !== undefined) {
// only for page refreshed
const app = await fetchAppById(route.params.id as string);
store.setApp(app);
};
await store.loadFlow();
initLoading.value = false
drawerLeft.value = true;
}
const fetchAppById = async(id: string) => {
let result = await api.get('api/apps');
const app = result.data?.data?.find((item: IManagedApp) => item.appid === id ) as IManagedApp;
if (app) {
return convertManagedAppToAppInfo(app);
}
result = await api.get(`api/v1/app?app=${id}`);
const kApp = result?.data as IAppDisplay | KErrorMsg;
if (isErrorMsg(kApp)) {
$q.notify({
type: 'negative',
caption: 'エラー',
message: kApp.message,
});
}
return kApp;
}
type KErrorMsg = {
message: string;
}
const isErrorMsg = (e: IAppDisplay | KErrorMsg): e is KErrorMsg => {
return 'message' in e;
};
const convertManagedAppToAppInfo = (app: IManagedApp): AppInfo => {
return {
appId: app.appid,
name: app.appname
}
};
const onClearFilter=()=>{
filter.value='';
}
onMounted(() => {
authStore.setLeftMenu(false);
fetchData();
});
</script>
<style lang="scss">
.flowchart {
padding-top: 10px;
}
.flow-toolbar {
opacity: 50%;
}
.event-tree .q-drawer {
top: 50px;
z-index: 999;
}
.expand{
position: fixed;
left: 0px;
top: 50%;
z-index: 9999;
}
</style>