条件エディタ実装
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
473
plugin/kintone-addins/src/types/Conditions.ts
Normal file
473
plugin/kintone-addins/src/types/Conditions.ts
Normal 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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user