フロー保存の実装

This commit is contained in:
2023-10-16 13:38:51 +09:00
parent cdfb1d4310
commit 0b414fbfbe
12 changed files with 357 additions and 385 deletions

Binary file not shown.

View File

@@ -1,11 +1,5 @@
<template> <template>
<div <div class="row app-box">
class="row"
style="
border-radius: 2px;
box-shadow: rgba(255, 255, 255, 0.1) 0px 0px 0px 1px inset,
rgba(0, 0, 0, 0.3) 0px 0px 0px 1px;
">
<q-icon <q-icon
class="self-center q-ma-sm" class="self-center q-ma-sm"
name="widgets" name="widgets"
@@ -13,7 +7,7 @@
style="font-size: 2em" style="font-size: 2em"
/> />
<div class="col-7 self-center ellipsis"> <div class="col-7 self-center ellipsis">
{{ selectedApp.name }} {{ store.appInfo?.name }}
</div> </div>
<div class="self-center"> <div class="self-center">
<q-btn <q-btn
@@ -51,22 +45,19 @@ export default defineComponent({
const store = useFlowEditorStore(); const store = useFlowEditorStore();
const appDg = ref(); const appDg = ref();
const showSelectApp=ref(false); const showSelectApp=ref(false);
const selectedApp =ref<AppInfo>({
appId:"",
name:"",
});
const closeDg=(val :any)=>{ const closeDg=(val :any)=>{
showSelectApp.value=false; showSelectApp.value=false;
console.log("Dialog closed->",val); console.log("Dialog closed->",val);
if (val == 'OK') { if (val == 'OK') {
const data = appDg.value.selected[0]; const data = appDg.value.selected[0];
console.log(data); console.log(data);
selectedApp.value={ const appInfo={
appId:data.id , appId:data.id ,
name:data.name name:data.name
}; };
store.setApp(selectedApp.value); store.setApp(appInfo);
store.setFlow(); store.loadFlow();
} }
} }
const showAppDialog=()=>{ const showAppDialog=()=>{
@@ -74,7 +65,6 @@ export default defineComponent({
} }
return { return {
store, store,
selectedApp,
showSelectApp, showSelectApp,
showAppDialog, showAppDialog,
closeDg, closeDg,
@@ -83,3 +73,9 @@ export default defineComponent({
} }
}); });
</script> </script>
<style lang="scss">
.app-box{
border-radius: 2px;
box-shadow: rgba(255, 255, 255, 0.1) 0px 0px 0px 1px inset,rgba(0, 0, 0, 0.3) 0px 0px 0px 1px;
}
</style>

View File

@@ -1,11 +1,11 @@
<template> <template>
<div class="q-pa-md q-gutter-sm"> <!-- <div class="q-pa-md q-gutter-sm"> -->
<q-tree <q-tree
:nodes="store.eventTree.screens" :nodes="store.eventTree.screens"
node-key="label" node-key="label"
children-key="events" children-key="events"
no-connectors no-connectors
v-model:expanded="expanded" v-model:expanded="store.expandedScreen"
:dense="true" :dense="true"
> >
<template v-slot:default-header="prop"> <template v-slot:default-header="prop">
@@ -19,7 +19,7 @@
</div> </div>
</template> </template>
</q-tree> </q-tree>
</div> <!-- </div> -->
</template> </template>
<script lang="ts"> <script lang="ts">
@@ -36,9 +36,7 @@ export default defineComponent({
// const eventTree=ref(kintoneEvents); // const eventTree=ref(kintoneEvents);
// const selectedFlow = store.currentFlow; // const selectedFlow = store.currentFlow;
const expanded=ref([ // const expanded=ref();
store.currentFlow?.getRoot()?.title
]);
const selectedEvent = ref<IKintoneEvent|null>(null); const selectedEvent = ref<IKintoneEvent|null>(null);
const onSelected=(node:IKintoneEvent)=>{ const onSelected=(node:IKintoneEvent)=>{
if(!node.eventId){ if(!node.eventId){
@@ -63,7 +61,7 @@ export default defineComponent({
} }
return { return {
// eventTree, // eventTree,
expanded, // expanded,
onSelected, onSelected,
selectedEvent, selectedEvent,
store store

View File

@@ -1,6 +1,6 @@
<template> <template>
<q-input v-model="selectedDate" :label="placeholder" mask="date" :rules="['date']"> <q-input v-model="selectedDate" :label="displayName" :placeholder="placeholder" mask="date" :rules="['date']">
<template v-slot:append> <template v-slot:append>
<q-icon name="event" class="cursor-pointer"> <q-icon name="event" class="cursor-pointer">
<q-popup-proxy cover transition-show="scale" transition-hide="scale"> <q-popup-proxy cover transition-show="scale" transition-hide="scale">
@@ -21,6 +21,10 @@ import { defineComponent, ref ,watchEffect} from 'vue';
export default defineComponent({ export default defineComponent({
name: 'DatePicker', name: 'DatePicker',
props: { props: {
displayName:{
type: String,
default: '',
},
placeholder: { placeholder: {
type: String, type: String,
default: '', default: '',

View File

@@ -1,5 +1,5 @@
<template> <template>
<q-input v-model="selectedField" :label="placeholder"> <q-input v-model="selectedField" :label="displayName" :placeholder="placeholder" >
<template v-slot:append> <template v-slot:append>
<q-icon name="search" class="cursor-pointer" @click="showDg"/> <q-icon name="search" class="cursor-pointer" @click="showDg"/>
</template> </template>
@@ -21,6 +21,10 @@ export default defineComponent({
FieldSelect, FieldSelect,
}, },
props: { props: {
displayName:{
type: String,
default: '',
},
placeholder: { placeholder: {
type: String, type: String,
default: '', default: '',

View File

@@ -1,5 +1,5 @@
<template> <template>
<q-input :label="placeholder" v-model="inputValue"/> <q-input :label="displayName" :placeholder="placeholder" v-model="inputValue"/>
</template> </template>
<script lang="ts"> <script lang="ts">
@@ -8,6 +8,10 @@ import { defineComponent,ref,watchEffect } from 'vue';
export default defineComponent({ export default defineComponent({
name: 'InputText', name: 'InputText',
props: { props: {
displayName:{
type: String,
default: '',
},
placeholder: { placeholder: {
type: String, type: String,
default: '', default: '',

View File

@@ -1,5 +1,5 @@
<template> <template>
<q-select v-model="selectedValue" :label="placeholder" :options="options"/> <q-select v-model="selectedValue" :label="displayName" :options="options"/>
</template> </template>
<script lang="ts"> <script lang="ts">
@@ -8,6 +8,10 @@ import { defineComponent,ref,watchEffect } from 'vue';
export default defineComponent({ export default defineComponent({
name: 'SelectBox', name: 'SelectBox',
props: { props: {
displayName:{
type: String,
default: '',
},
placeholder: { placeholder: {
type: String, type: String,
default: '', default: '',

View File

@@ -6,16 +6,22 @@ export class FlowCtrl
async getFlows(appId:string):Promise<ActionFlow[]> async getFlows(appId:string):Promise<ActionFlow[]>
{ {
const flows:ActionFlow[]=[];
try{
const result = await api.get(`http://127.0.0.1:8000/api/flows/${appId}`); const result = await api.get(`http://127.0.0.1:8000/api/flows/${appId}`);
//console.info(result.data); //console.info(result.data);
if(!result.data || !Array.isArray(result.data)){ if(!result.data || !Array.isArray(result.data)){
return []; return [];
} }
const flows:ActionFlow[]=[];
for(const flow of result.data){ for(const flow of result.data){
flows.push(ActionFlow.fromJSON(flow.content)); flows.push(ActionFlow.fromJSON(flow.content));
} }
return flows; return flows;
}catch(error){
console.error(error);
return flows;
}
} }
async SaveFlow(jsonData:any):Promise<boolean> async SaveFlow(jsonData:any):Promise<boolean>

View File

@@ -8,21 +8,23 @@
:show-if-above="false" :show-if-above="false"
elevated elevated
> >
<!-- <q-card class="column full-height" style="width: 300px"> <q-card class="column full-height" >
<q-card-section> --> <q-card-section>
<div class="flex-center " >
<div class="flex-center fixd-top" >
<AppSelector /> <AppSelector />
</div> </div>
<!-- </q-card-section>
<!-- </q-card-section> --> <q-card-section> -->
<q-separator />
<!-- <q-card-section> -->
<div class="flex-center"> <div class="flex-center">
<EventTree /> <EventTree />
</div> </div>
<!-- </q-card-section> --> </q-card-section>
<!-- </q-card> --> </q-card>
<div class="flex-center fixed-bottom bg-grey-3 q-pa-md row ">
<q-btn color="deep-orange" glossy label="デプロイ" @click="onDeploy" icon="sync"/>
<q-space></q-space>
<q-btn color="primary" label="保存" @click="onSaveFlow" icon="save" />
</div>
</q-drawer> </q-drawer>
</div> </div>
@@ -59,8 +61,9 @@ import PropertyPanel from 'components/right/PropertyPanel.vue';
import AppSelector from 'components/left/AppSelector.vue'; import AppSelector from 'components/left/AppSelector.vue';
import EventTree from 'components/left/EventTree.vue'; import EventTree from 'components/left/EventTree.vue';
import {FlowCtrl } from '../control/flowctrl'; import {FlowCtrl } from '../control/flowctrl';
import { useQuasar } from 'quasar';
const drawerLeft = ref(true); const drawerLeft = ref(true);
const $q=useQuasar();
const store = useFlowEditorStore(); const store = useFlowEditorStore();
// ref関数を使ってtemplateとバインド // ref関数を使ってtemplateとバインド
const state=reactive({ const state=reactive({
@@ -73,7 +76,7 @@ const prevNodeIfo=ref({
prevNode:{} as IActionNode, prevNode:{} as IActionNode,
inputPoint:"" inputPoint:""
}); });
const refFlow = ref<ActionFlow|null>(null); // const refFlow = ref<ActionFlow|null>(null);
const showAddAction=ref(false); const showAddAction=ref(false);
const drawerRight=ref(false); const drawerRight=ref(false);
const model=ref(""); const model=ref("");
@@ -120,6 +123,37 @@ const closeDg=(val :any)=>{
} }
} }
const onDeploy=()=>{
return;
}
const onSaveFlow = async ()=>{
const targetFlow = store.selectedFlow;
if(targetFlow===undefined){
$q.notify({
type: 'negative',
caption:"エラー",
message: `編集中のフローがありません。`
});
return;
}
try{
await store.saveFlow(targetFlow);
$q.notify({
type: 'positive',
caption:"通知",
message: `${targetFlow.getRoot()?.subTitle}のフロー設定を保存しました。`
});
}catch(error){
$q.notify({
type: 'negative',
caption:"エラー",
message: `${targetFlow.getRoot()?.subTitle}のフローの設定の保存が失敗しました。`
})
}
}
const fetchData = async ()=>{ const fetchData = async ()=>{
const flowCtrl = new FlowCtrl(); const flowCtrl = new FlowCtrl();
if(store.appInfo===undefined) return; if(store.appInfo===undefined) return;
@@ -130,8 +164,7 @@ const fetchData= async ()=>{
if(actionFlows && actionFlows.length==1){ if(actionFlows && actionFlows.length==1){
store.selectFlow(actionFlows[0]); store.selectFlow(actionFlows[0]);
} }
refFlow.value=actionFlows[0]; const root =actionFlows[0].getRoot();
const root =refFlow.value.getRoot();
if(root){ if(root){
state.activeNode=root; state.activeNode=root;
} }

View File

@@ -10,6 +10,7 @@ export interface FlowEditorState{
selectedFlow?:IActionFlow|undefined; selectedFlow?:IActionFlow|undefined;
eventTree:KintoneEventManager; eventTree:KintoneEventManager;
selectedEvent:IKintoneEvent|undefined; selectedEvent:IKintoneEvent|undefined;
expandedScreen:any[];
} }
const flowCtrl=new FlowCtrl(); const flowCtrl=new FlowCtrl();
export const useFlowEditorStore = defineStore("flowEditor",{ export const useFlowEditorStore = defineStore("flowEditor",{
@@ -19,7 +20,8 @@ export const useFlowEditorStore = defineStore("flowEditor",{
flows:[], flows:[],
selectedFlow:undefined, selectedFlow:undefined,
eventTree:kintoneEvents, eventTree:kintoneEvents,
selectedEvent:undefined selectedEvent:undefined,
expandedScreen:[]
}), }),
getters: { getters: {
/** /**
@@ -54,17 +56,46 @@ export const useFlowEditorStore = defineStore("flowEditor",{
setApp(app:AppInfo){ setApp(app:AppInfo){
this.appInfo=app; this.appInfo=app;
}, },
async setFlow(){ /**
* DBからフルーを保存する
* @returns
*/
async loadFlow(){
if(this.appInfo===undefined) return; if(this.appInfo===undefined) return;
const actionFlows = await flowCtrl.getFlows(this.appInfo?.appId); const actionFlows = await flowCtrl.getFlows(this.appInfo?.appId);
//eventTreeにバンドする //eventTreeにバンドする
this.eventTree.bindFlows(actionFlows); this.eventTree.bindFlows(actionFlows);
if(actionFlows && actionFlows.length>0){ if(actionFlows===undefined || actionFlows.length===0){
this.setFlows(actionFlows); this.flows=[];
this.selectedFlow=undefined;
return;
} }
if(actionFlows && actionFlows.length==1){ this.setFlows(actionFlows);
if(actionFlows && actionFlows.length>0){
this.selectFlow(actionFlows[0]); this.selectFlow(actionFlows[0]);
} }
const expandName =actionFlows[0].getRoot()?.title;
this.expandedScreen=[expandName];
},
/**
* フローをDBに保存及び更新する
*/
async saveFlow(flow:IActionFlow){
const root=flow.getRoot();
const isNew = flow.id==='';
const jsonData={
flowid: isNew ? flow.createNewId():flow.id,
appid: this.appInfo?.appId,
eventid: root?.name,
name: root?.subTitle,
content: JSON.stringify(flow)
}
if(isNew){
return await flowCtrl.SaveFlow(jsonData);
}else{
return await flowCtrl.UpdateFlow(jsonData);
}
} }
} }

View File

@@ -52,6 +52,7 @@ export interface IActionFlow {
id: string; id: string;
actionNodes: Array<IActionNode>; actionNodes: Array<IActionNode>;
getRoot(): IActionNode | undefined; getRoot(): IActionNode | undefined;
createNewId():string;
} }
/** /**
@@ -97,7 +98,10 @@ class ActionProperty implements IActionProperty {
export class ActionNode implements IActionNode { export class ActionNode implements IActionNode {
id: string; id: string;
name: string; name: string;
title:string; get title(): string {
const prop = this.actionProps.find((prop) => prop.props.name === "displayName");
return prop?.props.modelValue;
};
get subTitle(): string { get subTitle(): string {
return this.name; return this.name;
}; };
@@ -126,11 +130,10 @@ export class ActionNode implements IActionNode {
) { ) {
this.id = uuidv4(); this.id = uuidv4();
this.name = name; this.name = name;
this.title=title;
this.inputPoint = inputPoint; this.inputPoint = inputPoint;
this.outputPoints = outputPoint; this.outputPoints = outputPoint;
const defProp = ActionProperty.defaultProperty(); const defProp = ActionProperty.defaultProperty();
defProp.props.displayName=title; defProp.props.modelValue = title;
this.actionProps = actionProps; this.actionProps = actionProps;
const prop = this.actionProps.find((prop) => prop.props.name === defProp.props.name); const prop = this.actionProps.find((prop) => prop.props.name === defProp.props.name);
if (prop === undefined) { if (prop === undefined) {
@@ -186,7 +189,8 @@ export class ActionFlow implements IActionFlow {
} else { } else {
this.actionNodes = [actionNodes]; this.actionNodes = [actionNodes];
} }
this.id=uuidv4(); this.id = '';
//this.id = uuidv4();
} }
/** /**
* ノードを追加する * ノードを追加する
@@ -200,8 +204,7 @@ export class ActionFlow implements IActionFlow {
addNode( addNode(
newNode: IActionNode, newNode: IActionNode,
prevNode?: IActionNode, prevNode?: IActionNode,
inputPoint?:string):IActionNode inputPoint?: string): IActionNode {
{
if (inputPoint !== undefined) { if (inputPoint !== undefined) {
newNode.inputPoint = inputPoint; newNode.inputPoint = inputPoint;
} }
@@ -270,7 +273,7 @@ disconnectFromPrevNode(targetNode: IActionNode): void {
} }
} }
// actionNodes 数组中移除节点 // actionNodes からノードを削除する
private removeFromActionNodes(targetNodeId: string): void { private removeFromActionNodes(targetNodeId: string): void {
const index = this.actionNodes.findIndex(node => node.id === targetNodeId); const index = this.actionNodes.findIndex(node => node.id === targetNodeId);
if (index > -1) { if (index > -1) {
@@ -333,14 +336,18 @@ reconnectOrRemoveNextNodes(targetNode: IActionNode): void {
return true; return true;
} }
/**
*
* @param prevNode ノードの接続をリセットする
* @param newNode
* @param inputPoint
*/
resetNodeRelation(prevNode: IActionNode, newNode: IActionNode, inputPoint?: string) { resetNodeRelation(prevNode: IActionNode, newNode: IActionNode, inputPoint?: string) {
// 设置新节点和前节点的关联 //
prevNode.nextNodeIds.set(inputPoint || '', newNode.id); prevNode.nextNodeIds.set(inputPoint || '', newNode.id);
newNode.prevNodeId = prevNode.id; newNode.prevNodeId = prevNode.id;
// 保存前节点原有的后节点ID
const originalNextNodeId = prevNode.nextNodeIds.get(inputPoint || ''); const originalNextNodeId = prevNode.nextNodeIds.get(inputPoint || '');
this.setNewNodeNextId(newNode, originalNextNodeId, inputPoint); this.setNewNodeNextId(newNode, originalNextNodeId, inputPoint);
} }
/** /**
@@ -350,13 +357,13 @@ reconnectOrRemoveNextNodes(targetNode: IActionNode): void {
* @param inputPoint * @param inputPoint
*/ */
private setNewNodeNextId(newNode: IActionNode, originalNextNodeId: string | undefined, inputPoint?: string) { private setNewNodeNextId(newNode: IActionNode, originalNextNodeId: string | undefined, inputPoint?: string) {
// 如果原先的后节点存在 // 元の接続ノードが存在する
if (originalNextNodeId) { if (originalNextNodeId) {
// 检查新节点的 outputPoints 是否包含该 inputPoint // 新しいノードの outputPoints に該当 inputPointが存在するか場合をチェックする
if (newNode.outputPoints.includes(inputPoint || '')) { if (newNode.outputPoints.includes(inputPoint || '')) {
newNode.nextNodeIds.set(inputPoint || '', originalNextNodeId); newNode.nextNodeIds.set(inputPoint || '', originalNextNodeId);
} else { } else {
// 如果不包含,选择新节点的一个 outputPoint // inputPointが存在しない場合、outputPointのポイントの任意ポートを選択する
const alternativeOutputPoint = newNode.outputPoints.length > 0 ? newNode.outputPoints[0] : ''; const alternativeOutputPoint = newNode.outputPoints.length > 0 ? newNode.outputPoints[0] : '';
newNode.nextNodeIds.set(alternativeOutputPoint, originalNextNodeId); newNode.nextNodeIds.set(alternativeOutputPoint, originalNextNodeId);
} }
@@ -387,6 +394,11 @@ reconnectOrRemoveNextNodes(targetNode: IActionNode): void {
return this.actionNodes.find(node => node.isRoot) return this.actionNodes.find(node => node.isRoot)
} }
createNewId():string{
this.id=uuidv4();
return this.id;
}
static fromJSON(json: string): ActionFlow { static fromJSON(json: string): ActionFlow {
const parsedObject = JSON.parse(json); const parsedObject = JSON.parse(json);
@@ -402,7 +414,5 @@ reconnectOrRemoveNextNodes(targetNode: IActionNode): void {
actionFlow.id = parsedObject.id; actionFlow.id = parsedObject.id;
return actionFlow; return actionFlow;
} }
} }

View File

@@ -1,138 +1,20 @@
{
"id": "681ecde3-4439-4210-9fdf-424c6af98f09",
"actionNodes": [
{
"id": "34dfd32e-ba1a-440f-bb46-a8a1999109cd",
"name": "app.record.create.submit",
"title": "レコード追加画面",
"subTitle": "保存するとき",
"inputPoint": "",
"outputPoints": [],
"isRoot": true,
"actionProps": [],
"ActionValue": {},
"nextNodeIds": [
[ [
"",
"ce07775d-9729-4516-a88c-78ee8f2f851e"
]
]
},
{
"id": "ce07775d-9729-4516-a88c-78ee8f2f851e",
"name": "自動採番",
"title": "文書番号を自動採番する",
"inputPoint": "",
"outputPoints": [],
"actionProps": [
{
"component": "InputText",
"props": {
"name": "displayName",
"displayName": "文書番号を自動採番する",
"placeholder": "表示を入力してください",
"modelValue": ""
}
},
{
"component": "InputText",
"props": {
"displayName": "フォーマット",
"modelValue": "",
"name": "format",
"placeholder": "フォーマットを入力してください"
}
},
{ {
"component": "FieldInput", "component": "FieldInput",
"props": { "props": {
"displayName": "採番項目", "displayName": "フィールド",
"modelValue": "", "modelValue": "",
"name": "field", "name": "field",
"placeholder": "採番項目を選択してください" "placeholder": "必須項目を選択してください"
} }
}
],
"prevNodeId": "34dfd32e-ba1a-440f-bb46-a8a1999109cd",
"nextNodeIds": [
[
"",
"0d18c3c9-abee-44e5-83eb-82074316219b"
]
]
}, },
{
"id": "0d18c3c9-abee-44e5-83eb-82074316219b",
"name": "入力データ取得",
"title": "電話番号を取得する",
"inputPoint": "",
"outputPoints": [],
"actionProps": [
{ {
"component": "InputText", "component": "InputText",
"props": { "props": {
"name": "displayName", "displayName": "エラーメッセージ",
"displayName": "表示名", "modelValue": "",
"placeholder": "表示を入力してください", "name": "format",
"modelValue": "" "placeholder": "エラーメッセージを入力してください"
} }
} }
],
"prevNodeId": "ce07775d-9729-4516-a88c-78ee8f2f851e",
"nextNodeIds": [
[
"",
"399d7c04-5345-4bf6-8da3-d745df554524"
]
]
},
{
"id": "399d7c04-5345-4bf6-8da3-d745df554524",
"name": "条件分岐",
"title": "電話番号入力形式チャック",
"inputPoint": "",
"outputPoints": [
"はい",
"いいえ"
],
"actionProps": [
{
"component": "InputText",
"props": {
"name": "displayName",
"displayName": "表示名",
"placeholder": "表示を入力してください",
"modelValue": ""
}
}
],
"prevNodeId": "0d18c3c9-abee-44e5-83eb-82074316219b",
"nextNodeIds": [
[
"いいえ",
"8173e6bc-3fa2-4403-b973-9368884e2dfa"
]
]
},
{
"id": "8173e6bc-3fa2-4403-b973-9368884e2dfa",
"name": "エラー表示",
"title": "エラー表示して保存しない",
"inputPoint": "いいえ",
"outputPoints": [],
"actionProps": [
{
"component": "InputText",
"props": {
"name": "displayName",
"displayName": "表示名",
"placeholder": "表示を入力してください",
"modelValue": ""
}
}
],
"prevNodeId": "399d7c04-5345-4bf6-8da3-d745df554524",
"nextNodeIds": []
}
] ]
}