条件エディタ実装

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

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