条件エディタ実装

This commit is contained in:
2024-01-31 05:22:09 +09:00
parent 5cd6d02f6e
commit 6de60c82ba
18 changed files with 867 additions and 66 deletions

View File

@@ -25,6 +25,7 @@ api.interceptors.response.use(
(error)=>{
const orgReq=error.config;
if(error.response && error.response.status===401){
console.error("401エラー");
localStorage.removeItem('token');
router.replace({
path:"/login",

View File

@@ -1,13 +1,30 @@
<template>
<show-dialog v-model:visible="showflg" name="条件エディタ" @close="closeDg" width="60vw" height="60vh">
<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="copyCondition()">
<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="pasteCondition()">
<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>
<NodeCondition v-model:conditionTree="tree"></NodeCondition>
</show-dialog>
</template>
<script lang="ts">
import { defineComponent, ref ,watchEffect} from 'vue';
import ShowDialog from '../../components/ShowDialog.vue';
import NodeCondition from './NodeCondition.vue';
import { ConditionTree } from '../../types/Conditions';
import ShowDialog from '../../components/ShowDialog.vue';
import NodeCondition from './NodeCondition.vue';
import { ConditionTree } from '../../types/Conditions';
import { useQuasar } from 'quasar';
export default defineComponent({
name: 'ConditionObject',
components: {
@@ -24,18 +41,55 @@ import { defineComponent, ref ,watchEffect} from 'vue';
default:false
}
},
emits:[
"closed",
"update:conditionTree",
"update:show"
],
setup(props,context) {
const appDg = ref();
const $q=useQuasar();
const tree = ref(props.conditionTree);
const closeDg = (val:string) => {
if (val == 'OK') {
if(tree.value.root.children.length===0){
$q.notify({
type: 'negative',
message: `条件式を設定してください。`
});
}
context.emit("update:conditionTree",tree.value);
}
showflg.value=false;
context.emit("update:show",false);
context.emit("closed",val);
};
const showflg =ref(props.show);
//条件式をコピーする
const copyCondition=()=>{
if (navigator.clipboard) {
const jsonData=tree.value.toJson();
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 pasteCondition=async ()=>{
try {
const text = await navigator.clipboard.readText();
console.log('Text from clipboard:', text);
tree.value.fromJson(text);
} catch (err) {
console.error('Failed to read text from clipboard: ', err);
throw err;
}
}
watchEffect(() => {
showflg.value=props.show;
});
@@ -44,7 +98,9 @@ import { defineComponent, ref ,watchEffect} from 'vue';
tree,
appDg,
closeDg,
showflg
showflg,
copyCondition,
pasteCondition
};
}
});

View File

@@ -11,20 +11,20 @@
</template>
</q-field>
<show-dialog v-model:visible="show" name="フィールド一覧" @close="closeDg" widht="400px">
<field-select ref="appDg" name="フィールド" type="single" :appId="store.appInfo?.appId"></field-select>
<condition-objects ref="appDg" name="フィールド" type="single" :appId="store.appInfo?.appId"></condition-objects>
</show-dialog>
</template>
<script lang="ts">
import { defineComponent, ref ,watchEffect,computed} from 'vue';
import ShowDialog from '../ShowDialog.vue';
import FieldSelect from '../FieldSelect.vue';
import ConditionObjects from '../ConditionObjects.vue';
import { useFlowEditorStore } from '../../stores/flowEditor';
export default defineComponent({
name: 'ConditionObject',
components: {
ShowDialog,
FieldSelect,
ConditionObjects,
},
props: {
modelValue: {

View File

@@ -34,15 +34,6 @@
<q-btn flat round dense icon="more_horiz" size="sm" >
<q-menu auto-close anchor="top right">
<q-list>
<q-item clickable @click="addGroup(prop.node, LogicalOperator.AND)">
<q-item-section avatar><q-icon name="playlist_add" ></q-icon></q-item-section>
<q-item-section >グループ追加</q-item-section>
</q-item>
<q-item clickable @click="addCondition(prop.node)">
<q-item-section avatar><q-icon name="add_circle_outline" ></q-icon></q-item-section>
<q-item-section >条件式追加</q-item-section>
</q-item>
<q-separator inset/>
<q-item clickable @click="moveUp(prop.node)">
<q-item-section avatar><q-icon name="arrow_upward" ></q-icon></q-item-section>
<q-item-section >一つ上に移動</q-item-section>
@@ -52,6 +43,19 @@
<q-item-section >一つ下に移動</q-item-section>
</q-item>
<q-separator inset/>
<q-item clickable @click="addGroup(prop.node, LogicalOperator.AND)">
<q-item-section avatar><q-icon name="playlist_add" ></q-icon></q-item-section>
<q-item-section >グループ追加</q-item-section>
</q-item>
<q-item clickable @click="addCondition(prop.node)">
<q-item-section avatar><q-icon name="add_circle_outline" ></q-icon></q-item-section>
<q-item-section >条件式追加</q-item-section>
</q-item>
<q-separator inset/>
<q-item clickable @click="splitGroup(prop.node)">
<q-item-section avatar><q-icon name="playlist_remove" color="negative"></q-icon></q-item-section>
<q-item-section >グループ化解除</q-item-section>
</q-item>
<q-item clickable @click="removeNode(prop.node)">
<q-item-section avatar><q-icon name="delete" color="negative"></q-icon></q-item-section>
<q-item-section >削除</q-item-section>
@@ -86,6 +90,11 @@
<q-item-section >一つ下に移動</q-item-section>
</q-item>
<q-separator inset/>
<q-item clickable @click="groupMerge(prop.node)" v-if="canMerge(prop.node)">
<q-item-section avatar><q-icon name="playlist_add"></q-icon></q-item-section>
<q-item-section >グループ化</q-item-section>
</q-item>
<q-separator inset/>
<q-item clickable @click="removeNode(prop.node)">
<q-item-section avatar><q-icon name="delete" color="negative"></q-icon></q-item-section>
<q-item-section>削除</q-item-section>
@@ -102,6 +111,7 @@
<q-btn @click="getConditionJson()" class="q-mt-md" color="primary" icon="mdi-plus">Show Condtion data</q-btn>
<q-btn @click="LoadCondition()" class="q-mt-md" color="primary" icon="mdi-plus">Load Condition</q-btn> -->
<q-tooltip anchor="center middle" v-model="showingCondition" no-parent-event>
import { finished } from 'stream';
{{ conditionString }}
</q-tooltip>
</div>
@@ -160,7 +170,6 @@ export default defineComponent( {
return opts;
};
const addGroup = (parent:GroupNode, logicOp:LogicalOperator) => {
if(!parent){
parent=tree.root;
@@ -169,7 +178,7 @@ export default defineComponent( {
};
const addCondition = (parent:GroupNode) => {
const newNode = new ConditionNode(LogicalOperator.AND,{},Operator.Equal,'',parent);
const newNode = new ConditionNode({},Operator.Equal,'',parent);
tree.addNode(parent,newNode);
};
@@ -188,10 +197,36 @@ export default defineComponent( {
const getConditionJson=()=>{
return tree.toJson();
}
//JsonからConditionTreeのインスタンスを作成
const LoadCondition=()=>{
tree.fromJson(conditionString.value);
}
//グループ化
const groupMerge=(node:INode)=>{
const checkedNodes:INode[]=[];
const checkedIndexs:number[] = ticked.value;
checkedIndexs.forEach(index => {
const node = tree.findByIndex(index);
if(node){
checkedNodes.push(node);
}
});
tree.createGroupNode(node,checkedNodes,LogicalOperator.AND);
ticked.value=[];
}
//グループ化可能かをチェックする
const canMerge =(node:INode)=>{
const checkedIndexs:number[] = ticked.value;
const findNode = checkedIndexs.find(index=>node.index===index);
console.log("findNode=>",findNode!==undefined,findNode);
return findNode!==undefined;
}
//グループ化解散
const splitGroup=(node:INode)=>{
tree.dissolveGroupNode(node as GroupNode);
ticked.value=[];
}
const expanded=computed(()=>tree.getGroups(tree.root));
// addCondition(tree.root);
@@ -214,7 +249,10 @@ export default defineComponent( {
getConditionJson,
LoadCondition,
objectValueOptions,
expanded
expanded,
canMerge,
groupMerge,
splitGroup
};
},
});

View File

@@ -0,0 +1,54 @@
<template>
<div class="q-pa-md">
<div v-if="!isLoaded" class="spinner flex flex-center">
<q-spinner color="primary" size="3em" />
</div>
<q-table v-else row-key="name" :selection="type" v-model:selected="selected" :columns="columns" :rows="rows" />
</div>
</template>
<script>
import { ref,onMounted,reactive } from 'vue'
import { api } from 'boot/axios';
export default {
name: 'ConditionObjects',
props: {
name: String,
type: String,
appId:Number
},
setup(props) {
const isLoaded=ref(false);
const columns = [
{ name: 'name', required: true,label: 'フィールド名',align: 'left',field: row=>row.name,sortable: true},
{ name: 'code', label: 'フィールドコード', align: 'left',field: 'code', sortable: true },
{ name: 'type', label: 'フィールドタイプ', align: 'left',field: 'type', sortable: true }
]
const rows = reactive([])
onMounted( async () => {
const res = await api.get('api/v1/appfields', {
params:{
app: props.appId
}
});
let fields = res.data.properties;
console.log(fields);
Object.keys(fields).forEach((key) =>
{
const fld=fields[key];
// rows.push({name:fields[key].label,code:fields[key].code,type:fields[key].type});
rows.push({name:fld.label,objectType:'field',...fld});
});
isLoaded.value=true;
});
return {
columns,
rows,
selected: ref([]),
isLoaded
}
},
}
</script>

View File

@@ -2,10 +2,15 @@
<!-- <div class="q-pa-md q-gutter-sm" > -->
<q-dialog :model-value="visible" persistent bordered>
<q-card :style="{minWidth : width}" >
<q-toolbar class="bg-grey-4">
<q-toolbar-title>{{ name }}</q-toolbar-title>
<q-space></q-space>
<slot name="toolbar"></slot>
<q-btn flat round dense icon="close" @click="CloseDialogue('Cancel')" />
</q-toolbar>
<q-card-section>
<div class="text-h6">{{ name }}</div>
<!-- <div class="text-h6">{{ name }}</div> -->
</q-card-section>
<q-card-section class="q-pt-none" :style="{...(height? {minHeight:height}:{}) }">
<slot></slot>
</q-card-section>

View File

@@ -26,7 +26,11 @@
</q-toolbar>
<q-separator />
<q-card-section>
<div class="text-h7">{{ node.title }}</div>
<div class="row">
<span class="text-h7">{{ node.title }}</span>
<q-space></q-space>
<q-chip color="info" text-color="white" size="0.70rem" v-if="varName(node)" clickable>{{ varName(node) }}</q-chip>
</div>
</q-card-section>
<template v-if="hasBranch">
<q-separator />
@@ -134,7 +138,14 @@ export default defineComponent({
*/
const onDeleteAllNode=()=>{
context.emit('deleteAllNextNodes', props.actionNode);
}
};
/**
* 変数名取得
*/
const varName =(node:IActionNode)=>{
const prop = node.actionProps.find((prop) => prop.props.name === "verName");
return prop?.props.modelValue;
};
return {
node: props.actionNode,
isRoot: props.actionNode.isRoot,
@@ -145,7 +156,8 @@ export default defineComponent({
onNodeClick,
onEditNode,
onDeleteNode,
onDeleteAllNode
onDeleteAllNode,
varName
}
}
});

View File

@@ -12,7 +12,7 @@
</q-card>
</template>
</q-field>
<condition-editor v-model:show="show" v-model:conditionTree="tree"></condition-editor>
<condition-editor v-model:show="show" v-model:conditionTree="tree" @closed="onClosed"></condition-editor>
</template>
<script lang="ts">
@@ -54,7 +54,7 @@
if(props.modelValue && props.modelValue!==''){
tree.fromJson(props.modelValue);
}else{
const newNode = new ConditionNode(LogicalOperator.AND,{},Operator.Equal,'',tree.root);
const newNode = new ConditionNode({},Operator.Equal,'',tree.root);
tree.addNode(tree.root,newNode);
}
@@ -68,7 +68,7 @@
show.value = true;
};
const closeDg = (val:string) => {
const onClosed = (val:string) => {
if (val == 'OK') {
const conditionJson = tree.toJson();
isSetted.value=true;
@@ -86,7 +86,7 @@
isSetted,
show,
showDg,
closeDg,
onClosed,
tree,
conditionString
};

View File

@@ -13,7 +13,7 @@
>
<q-card class="column full-height" style="width: 300px">
<q-card-section>
<div class="text-h6">プロパティ</div>
<div class="text-h6">{{ actionNode.subTitle }}設定</div>
</q-card-section>
<q-card-section class="col q-pt-none">
<property-list :node-props="actionProps" v-if="showPanel" ></property-list>

View File

@@ -4,7 +4,7 @@
<q-btn @click="showCondition()" class="q-mt-md" color="primary" icon="mdi-plus">条件エディタ表示</q-btn>
</div>
<condition-editor v-model:show="show" v-model:conditionTree="tree"></condition-editor>
<p>{{conditionString}}</p>
<q-code>{{conditionString}}</q-code>
</q-page>
</template>
@@ -16,7 +16,7 @@ import { ConditionTree,GroupNode,ConditionNode,LogicalOperator,Operator } from '
const store = useFlowEditorStore();
const tree = reactive(new ConditionTree());
const newNode = new ConditionNode(LogicalOperator.AND,{},Operator.Equal,'',tree.root);
const newNode = new ConditionNode({},Operator.Equal,'',tree.root);
tree.addNode(tree.root,newNode);
const show =ref(false);

View File

@@ -89,7 +89,7 @@ export class ConditionNode implements INode {
return 'generic';
}
constructor(logicOp: LogicalOperator, object: any, operator: Operator, value: any, parent: GroupNode) {
constructor(object: any, operator: Operator, value: any, parent: GroupNode) {
this.index=0;
this.type = NodeType.Condition;
this.object = object;
@@ -100,7 +100,6 @@ export class ConditionNode implements INode {
static fromJSON(json: any, parent: GroupNode): ConditionNode {
const node= new ConditionNode(
json.logicalOperator,
json.object,
json.operator,
json.value,
@@ -144,20 +143,38 @@ export class ConditionTree {
return this.findChildren(this.root,index);
}
findChildren(parent:GroupNode,index:number):INode|undefined{
if(parent.index===index){
findChildren(parent: GroupNode, index: number): INode | undefined {
if (parent.index === index) {
return parent;
}
return parent.children.findLast((node:INode)=>{
if(node.index===index){
for (const node of parent.children) {
if (node.index === index) {
return node;
}
if(node.type!==NodeType.Condition){
return this.findChildren(node as GroupNode,index);
if (node.type !== NodeType.Condition) {
const foundNode = this.findChildren(node as GroupNode, index);
if (foundNode) {
return foundNode;
}
}
});
}
return undefined;
}
getMaxIndex(node:INode):number{
let maxIndex:number=node.index;
if(node.type!==NodeType.Condition){
const groupNode = node as GroupNode;
groupNode.children.forEach((child)=>{
const childMax = this.getMaxIndex(child);
if(childMax>maxIndex){
maxIndex=childMax;
}
});
}
return maxIndex;
}
//条件式を表示する
buildConditionString(node:INode){
if (node.type !== NodeType.Condition) {
@@ -176,9 +193,9 @@ export class ConditionTree {
return conditionString;
} else {
const condNode=node as ConditionNode;
if (condNode.object && condNode.operator && condNode.value!==null) {
if (condNode.object && condNode.operator ) {
let value=condNode.value;
if(typeof value ==='object' && ('label' in condNode.value)){
if(value && typeof value ==='object' && ('label' in value)){
value =condNode.value.label;
}
return `${condNode.object.name} ${condNode.operator} '${value}'`;
@@ -230,11 +247,80 @@ export class ConditionTree {
return groups;
}
/**
* 条件ノードをグループ化
* @param nodes 結合ノードを選択する
* @param logicOp
* @returns
*/
createGroupNode(firstNode:INode,nodes: INode[], logicOp: LogicalOperator): GroupNode | null {
if (nodes.length === 0) {
return null;
}
// 最初のノードの親ノードを取得
const parent = firstNode.parent as GroupNode;
if (!parent) {
throw new Error('ルートノードをグループ化できません');
}
// 親ノードを取得
const filteredNodes = nodes.filter(node => node.parent === parent);
// 新しいグループノードを作成
const newGroup = new GroupNode(logicOp, parent);
this.maxIndex++;
newGroup.index = this.maxIndex;
// 新しいグループノードの挿入位置を取得
let firstNodeIndex = parent.children.length;
if (filteredNodes.length > 0) {
firstNodeIndex = parent.children.indexOf(filteredNodes[0]);
}
filteredNodes.forEach(node => {
// 元の親ノードから削除する
const nodeIndex = parent.children.indexOf(node);
parent.children.splice(nodeIndex, 1);
// 新しいグループに追加
node.parent = newGroup;
newGroup.children.push(node);
});
// 新しいGroupNodeを挿入する
parent.children.splice(firstNodeIndex, 0, newGroup);
return newGroup;
}
/**
* GroupNodeを解散する
* @param groupNode 対象グループノード
*/
dissolveGroupNode(groupNode: GroupNode): void {
if (groupNode.parent === null || groupNode.type !== NodeType.LogicGroup) {
throw new Error('ルートノードと非グループノードを解散することができません');
}
// 親ノードを取得
const parent = groupNode.parent as GroupNode;
const groupIndex = parent.children.indexOf(groupNode);
// 子ノードをリセットする
groupNode.children.forEach(child => {
child.parent = parent;
parent.children.splice(groupIndex, 0, child);
});
//グループノードを削除する
parent.children.splice(groupIndex + groupNode.children.length, 1);
}
// Jsonから復元
fromJson(jsonString: string): INode {
const json = JSON.parse(jsonString);
this.root = GroupNode.fromJSON(json) as GroupNode;
this.maxIndex=this.getMaxIndex(this.root);
return this.root;
}
@@ -246,4 +332,5 @@ export class ConditionTree {
return value;
});
}
}