条件エディタ実装

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

File diff suppressed because one or more lines are too long

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;
});
}
}

View File

@@ -1,6 +1,6 @@
import { actionAddins } from ".";
import { IField, IAction,IActionResult, IActionNode, IActionProperty } from "../types/ActionTypes";
import { IField, IAction,IActionResult, IActionNode, IActionProperty, IContext } from "../types/ActionTypes";
import { Formatter } from "../util/format";
declare global {
@@ -14,7 +14,7 @@ interface IAutoNumberingProps{
format:string;
prefix:string;
suffix:string;
verName:string;
}
export class AutoNumbering implements IAction{
@@ -29,13 +29,14 @@ export class AutoNumbering implements IAction{
field:{code:''},
format:'',
prefix:'',
suffix:''
suffix:'',
verName:''
}
globalThis.window.$format=this.format;
this.register();
}
async process(actionNode:IActionNode,event:any):Promise<IActionResult> {
async process(actionNode:IActionNode,event:any,context:IContext):Promise<IActionResult> {
let result={
canNext:false,
result:false
@@ -49,6 +50,10 @@ export class AutoNumbering implements IAction{
const record = event.record;
const docNum = await this.createNumber(this.props);
record[this.props.field.code].value=docNum;
//変数設定
if(this.props.verName){
context.variables[this.props.verName]=docNum;
}
result= {
canNext:true,
result:true

View File

@@ -1,12 +1,14 @@
import { actionAddins } from ".";
import { IAction,IActionResult, IActionNode, IActionProperty, IField } from "../types/ActionTypes";
import { IAction,IActionResult, IActionNode, IActionProperty, IField, IContext } from "../types/ActionTypes";
import { ConditionTree } from '../types/Conditions';
/**
* アクションの属性定義
*/
interface IShownProps{
field:IField;
show:string;
condition:string;
}
/**
* 表示/非表示アクション
@@ -18,11 +20,12 @@ export class FieldShownAction implements IAction{
constructor(){
this.name="表示/非表示";
this.actionProps=[];
this.register();
this.props={
this.props={
field:{code:''},
show:''
show:'',
condition:''
}
//アクションを登録する
this.register();
}
/**
@@ -31,21 +34,26 @@ export class FieldShownAction implements IAction{
* @param event
* @returns
*/
async process(actionNode:IActionNode,event:any):Promise<IActionResult> {
async process(actionNode:IActionNode,event:any,context:IContext):Promise<IActionResult> {
let result={
canNext:true,
result:false
};
try{
//属性設定を取得する
this.actionProps=actionNode.actionProps;
if (!('field' in actionNode.ActionValue) && !('show' in actionNode.ActionValue)) {
return result
}
this.props = actionNode.ActionValue as IShownProps;
if(this.props.show==='表示'){
kintone.app.record.setFieldShown(this.props.field.code,true);
}else if (this.props.show==='非表示'){
kintone.app.record.setFieldShown(this.props.field.code,false);
//条件式の計算結果を取得
const conditionResult = this.getConditionResult(context);
if(conditionResult){
if(this.props.show==='表示'){
kintone.app.record.setFieldShown(this.props.field.code,true);
}else if (this.props.show==='非表示'){
kintone.app.record.setFieldShown(this.props.field.code,false);
}
}
result= {
canNext:true,
@@ -60,6 +68,37 @@ export class FieldShownAction implements IAction{
}
}
/**
*
* @param context 条件式を実行する
* @returns
*/
getConditionResult(context:any):boolean{
const tree =this.getCondition(this.props.condition);
if(!tree){
//条件を設定されていません
return true;
}
return tree.evaluate(tree.root,context);
}
/**
* @param condition 条件式ツリーを取得する
* @returns
*/
getCondition(condition:string):ConditionTree|null{
try{
const tree = new ConditionTree();
tree.fromJson(condition);
if(tree.getConditions(tree.root).length>0){
return tree;
}else{
return null;
}
}catch(error){
return null;
}
}
register(): void {
actionAddins[this.name]=this;

View File

@@ -61,13 +61,30 @@ export interface IActionResult{
result?:any;
}
/**
* コンテキスト
* レコードとフローの変数を持つ
*/
export interface IContext{
record:any,
variables:any
}
/**
* アクションのインターフェース
*/
export interface IAction{
//アクションの名前(ユーニック名が必要)
name:string;
//属性設定情報
actionProps: Array<IActionProperty>;
process(prop:IActionNode,event:any):Promise<IActionResult>;
//アクションのプロセス実行関数
process(prop:IActionNode,event:any,context:IContext):Promise<IActionResult>;
//アクションの登録関数
register():void;
}
export interface IField{
name?:string;
code:string;

View File

@@ -0,0 +1,473 @@
import { IContext } from "./ActionTypes";
//ノード種別
export enum NodeType{
Root = 'root',
LogicGroup ='logicgroup',
Condition ='condition'
}
//ロジックオペレーター
export enum LogicalOperator{
AND = 'AND',
OR = 'OR'
}
//条件オペレーター
export enum Operator{
Equal = '=',
NotEqual='!=',
Greater = '>',
GreaterOrEqual = '>=',
Less = '<',
LessOrEqual = '<=',
Contains = 'contains',
NotContains = 'not contains',
StartWith = 'start With',
EndWith = 'end with',
NotStartWith = 'not start with',
NotEndWith = 'not end with'
}
// INode
export interface INode {
index:number;
type: NodeType;
header:string;
parent: INode | null;
logicalOperator:LogicalOperator
}
// ロジックノード
export class GroupNode implements INode {
index:number;
type: NodeType;
children: INode[];
parent: INode | null;
logicalOperator: LogicalOperator;
get label():string{
return this.logicalOperator;
}
get header():string{
return this.type===NodeType.Root?'root':'generic';
}
get expanded():boolean{
return this.children.length>0;
}
constructor(logicOp:LogicalOperator, parent: INode | null) {
this.index=0;
this.type = parent==null?NodeType.Root: NodeType.LogicGroup;
this.logicalOperator = logicOp;
this.parent=parent;
this.children=[];
}
static fromJSON(json: any, parent: INode | null = null): GroupNode {
const node = new GroupNode(json.logicalOperator, parent);
node.index=json.index;
node.children = json.children.map((childJson: any) => {
return childJson.type === NodeType.LogicGroup
? GroupNode.fromJSON(childJson, node)
: ConditionNode.fromJSON(childJson, node);
});
return node;
}
}
// 条件式ノード
export class ConditionNode implements INode {
index: number;
type: NodeType;
parent:INode;
get logicalOperator(): LogicalOperator{
return this.parent.logicalOperator;
};
object: any; // 比較元
operator: Operator; // 比較子
value: any;
get header():string{
return 'generic';
}
constructor(object: any, operator: Operator, value: any, parent: GroupNode) {
this.index=0;
this.type = NodeType.Condition;
this.object = object;
this.operator = operator;
this.value = value;
this.parent=parent;
}
static fromJSON(json: any, parent: GroupNode): ConditionNode {
const node= new ConditionNode(
json.object,
json.operator,
json.value,
parent
);
node.index=json.index;
return node;
}
}
/**
* 条件式の管理クラス
*/
export class ConditionTree {
root: GroupNode;
maxIndex:number;
constructor() {
this.maxIndex=0;
this.root = new GroupNode(LogicalOperator.AND, null);
}
/**
* ノード追加
* @param parent
* @param node
*/
addNode(parent: GroupNode, node: INode): void {
this.maxIndex++;
node.index=this.maxIndex;
parent.children.push(node);
}
/**
* ノード削除
* @param node
*/
removeNode(node: INode): void {
if (node.parent === null) {
throw new Error('ルートノード削除できません');
} else {
const parent = node.parent as GroupNode;
const index = parent.children.indexOf(node);
if (index > -1) {
parent.children.splice(index, 1);
}
}
}
/**
* 条件ツリーからインディクス値で条件ノードを検索する
* @param index
* @returns
*/
findByIndex(index:number):INode|undefined{
return this.findChildren(this.root,index);
}
/**
* 指定のノードグループからインディクス値で条件ノードを検索する
* @param parent
* @param index
* @returns
*/
findChildren(parent: GroupNode, index: number): INode | undefined {
if (parent.index === index) {
return parent;
}
for (const node of parent.children) {
if (node.index === index) {
return node;
}
if (node.type !== NodeType.Condition) {
const foundNode = this.findChildren(node as GroupNode, index);
if (foundNode) {
return foundNode;
}
}
}
return undefined;
}
/**
*
* @param node 最大のインディクス値を取得する
* @returns
*/
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;
}
/**
* 条件式を表示する
* @param node 条件ノード
* @returns
*/
buildConditionString(node:INode){
if (node.type !== NodeType.Condition) {
let conditionString = '(';
const groupNode = node as GroupNode;
for (let i = 0; i < groupNode.children.length; i++) {
const childConditionString = this.buildConditionString(groupNode.children[i]);
if (childConditionString !== '') {
conditionString += childConditionString;
if (i < groupNode.children.length - 1) {
conditionString += ` ${groupNode.logicalOperator} `;
}
}
}
conditionString += ')';
return conditionString;
} else {
const condNode=node as ConditionNode;
if (condNode.object && condNode.operator ) {
let value=condNode.value;
if(value && typeof value ==='object' && ('label' in value)){
value =condNode.value.label;
}
return `${condNode.object.name} ${condNode.operator} '${value}'`;
} else {
return '';
}
}
}
/**
* すべて条件式を取得する
*/
getConditions(parent:GroupNode):ConditionNode[]{
const condiNodes:ConditionNode[]=[];
parent.children.forEach((node)=>{
if(node.type===NodeType.Condition){
condiNodes.push(node as ConditionNode);
}else{
condiNodes.push(...this.getConditions(node as GroupNode));
}
});
return condiNodes;
}
/**
*
* @param parent すべて条件グループを取得する
* @returns
*/
getGroups(parent:GroupNode):number[]{
const groups:number[]=[];
groups.push(parent.index);
parent.children.forEach((node)=>{
if(node.type!==NodeType.Condition){
groups.push(...this.getGroups(node as GroupNode));
}
});
return groups;
}
/**
* Jsonから復元
* @param jsonString
* @returns
*/
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;
}
/**
* JSON文字列に変換する
* @returns
*/
toJson():string{
return JSON.stringify(this.root, (key, value) => {
if (key === 'parent') {
return value ? value.type : null;
}
return value;
});
}
/**
* 条件式を計算する
* @param node
* @param context
* @returns
*/
evaluate(node: INode, context: any): boolean {
if (node.type === NodeType.Condition) {
return this.evaluateCondition(node as ConditionNode, context);
} else if (node.type === NodeType.LogicGroup || node.type === NodeType.Root) {
const groupNode = node as GroupNode;
const results = groupNode.children.map(child => this.evaluate(child, context));
if (groupNode.logicalOperator === LogicalOperator.AND) {
return results.every(result => result);
} else if (groupNode.logicalOperator === LogicalOperator.OR) {
return results.some(result => result);
} else {
throw new Error('Unsupported logical operator');
}
} else {
throw new Error('Unsupported node type');
}
}
/**
* Condition objectの値を取得する
* @param object
* @param context
* @returns
*/
getObjectValue(object:any,context:IContext){
if(!object || typeof object!=="object" || !("objectType" in object)){
return object;
}
if(object.objectType==='field'){
return context.record[object.code].value;
}else if(object.objectType==='var'){
return context.variables[object.varName].value;
}
}
/**
* 比較オブジェクトの値を取得
* @param object
* @returns
*/
getConditionValue(object:any){
if(!object || typeof object!=="object"){
return object;
}
if("label" in object){
return object.label;
}
if("value" in object){
return object.value;
}
if("name" in object){
return object.name;
}
}
/**
* 条件式を計算する
* @param condition
* @param context
* @returns
*/
evaluateCondition(condition: ConditionNode, context: IContext): boolean {
const { object, operator, value } = condition;
const targetValue = this.getObjectValue(object,context);
const conditionValue = this.getConditionValue(value);
switch (operator) {
case Operator.Equal:
case Operator.NotEqual:
case Operator.Greater:
case Operator.GreaterOrEqual:
case Operator.Less:
case Operator.LessOrEqual:
return this.compare(operator,targetValue, conditionValue);
case Operator.Contains:
return this.contains(targetValue,conditionValue);
case Operator.NotContains:
return !this.contains(targetValue,conditionValue);
case Operator.StartWith:
return this.startWith(targetValue,conditionValue);
case Operator.NotStartWith:
return !this.startWith(targetValue,conditionValue);
case Operator.EndWith:
return this.endsWith(targetValue,conditionValue);
case Operator.NotEndWith:
return this.endsWith(targetValue,conditionValue);
default:
throw new Error('Unsupported operator');
}
}
/**
* 比較を実行する
* @param operator
* @param targetValue
* @param value
* @returns
*/
compare(operator: Operator, targetValue: any, value: any): boolean {
// targetValue は日期时value も日期に変換して比較する
if (targetValue instanceof Date) {
const dateValue = new Date(value);
if (!isNaN(dateValue.getTime())) {
value = dateValue;
}
}
//targetValueは数値時value を数値に変換する
else if (typeof targetValue === 'number') {
const numberValue = Number(value);
if (!isNaN(numberValue)) {
value = numberValue;
}
}
else if (typeof targetValue === 'string') {
value = String(value);
}
switch (operator) {
case Operator.Equal:
return targetValue === value;
case Operator.NotEqual:
return targetValue !== value;
case Operator.Greater:
return targetValue > value;
case Operator.GreaterOrEqual:
return targetValue >= value;
case Operator.Less:
return targetValue < value;
case Operator.LessOrEqual:
return targetValue <= value;
default:
throw new Error('Unsupported operator for comparison');
}
}
/**
* 含む計算
* @param targetValue
* @param value
* @returns
*/
contains(targetValue: any, value: any): boolean {
if (typeof targetValue === 'string' && typeof value === 'string') {
return targetValue.includes(value);
}
return false;
}
/**
* StartWith計算
* @param targetValue
* @param value
* @returns
*/
startWith(targetValue: any, value: any): boolean {
if (typeof targetValue === 'string' && typeof value === 'string') {
return targetValue.startsWith(value);
}
return false;
}
/**
* EndsWith計算
* @param targetValue
* @param value
* @returns
*/
endsWith(targetValue: any, value: any): boolean {
if (typeof targetValue === 'string' && typeof value === 'string') {
return targetValue.endsWith(value);
}
return false;
}
}

View File

@@ -3,16 +3,21 @@ import { actionAddins } from "../actions";
import '../actions/must-input';
import '../actions/auto-numbering';
import '../actions/field-shown';
import { ActionFlow,IActionFlow, IActionResult } from "./ActionTypes";
import { ActionFlow,IActionFlow, IActionResult,IContext } from "./ActionTypes";
export class ActionProcess{
eventId:string;
flow:IActionFlow;
event:any;
context:IContext;
constructor(eventId:string,flow:ActionFlow,event:any){
this.eventId=eventId;
this.flow=flow;
this.event=event;
this.context={
record:this.event.record,
variables:{}
};
}
async exec(){
const root = this.flow.getRoot();
@@ -28,7 +33,7 @@ export class ActionProcess{
while(nextAction!==undefined && result.canNext){
const action = actionAddins[nextAction.name];
if(action!==undefined){
result = await action.process(nextAction,this.event)
result = await action.process(nextAction,this.event,this.context);
}
const nextInput = nextAction.outputPoints!==undefined?result.result||'':'';
id=nextAction.nextNodeIds.get(nextInput);

View File

@@ -12,7 +12,7 @@
"component": "SelectBox",
"props": {
"displayName": "表示/非表示",
"options":[
"options": [
"表示",
"非表示"
],
@@ -20,5 +20,14 @@
"name": "show",
"placeholder": ""
}
},
{
"component": "ConditionInput",
"props": {
"displayName": "条件",
"modelValue": "",
"name": "condition",
"placeholder": "対象項目を選択してください"
}
}
]