diff --git a/GUI/src/components/FlowElementsPopup/ConditionBuilderContent.tsx b/GUI/src/components/FlowElementsPopup/ConditionBuilderContent.tsx
index 59a2fffe8..7229640ef 100644
--- a/GUI/src/components/FlowElementsPopup/ConditionBuilderContent.tsx
+++ b/GUI/src/components/FlowElementsPopup/ConditionBuilderContent.tsx
@@ -15,7 +15,7 @@ const ConditionBuilderContent: React.FC = () => {
const isYesNoQuestion = useServiceStore((state) => state.isYesNoQuestion);
const rules = useServiceStore((state) => state.rules);
const node = useServiceStore((state) => state.selectedNode);
- const seedGroup = node?.data?.rules || (Array.isArray(rules) && rules.length > 0 ? rules : undefined);
+ const seedGroup = node?.data?.rules || (rules && rules.children.length > 0 ? rules : undefined);
return (
- {elements?.map((element) =>
- isInstanceOfRule(element) ? (
-
- ) : (
-
- ),
- )}
+ {elements?.map((element: GroupOrRule, index: number) => (
+
+ {index > 0 && (
+ changeConnector(element.id, connector)}
+ onToggleNot={() => toggleConnectorNot(element.id)}
+ />
+ )}
+ {isInstanceOfRule(element) ? (
+
+ ) : (
+
+ )}
+
+ ))}
);
};
diff --git a/GUI/src/components/FlowElementsPopup/RuleBuilder/types.tsx b/GUI/src/components/FlowElementsPopup/RuleBuilder/types.tsx
index 1c517e7fc..90f41bba2 100644
--- a/GUI/src/components/FlowElementsPopup/RuleBuilder/types.tsx
+++ b/GUI/src/components/FlowElementsPopup/RuleBuilder/types.tsx
@@ -8,7 +8,14 @@ export interface RuleGroupBuilderProps {
seedGroup?: Group | GroupOrRule[];
}
-export interface Rule {
+export type GroupType = 'and' | 'or';
+
+interface WithConnector {
+ readonly connector?: GroupType;
+ readonly connectorNot?: boolean;
+}
+
+export interface Rule extends WithConnector {
id: string;
field: string;
operator: string;
@@ -19,20 +26,18 @@ export interface Rule {
isValueManual?: boolean;
}
-export type GroupType = 'and' | 'or';
-
-export interface Group {
+export interface Group extends WithConnector {
id: string;
children: GroupOrRule[];
- type: GroupType;
not: boolean;
+ type?: GroupType;
}
export type GroupOrRule = Group | Rule;
export const isInstanceOfRule = (x: GroupOrRule): boolean => 'operator' in x;
-export const getInitialRule = () => {
+export const getInitialRule = (connector: GroupType = 'and') => {
return {
id: uuidv4(),
field: '',
@@ -42,14 +47,24 @@ export const getInitialRule = () => {
valueDragElement: undefined,
isFieldManual: false,
isValueManual: false,
+ connector,
+ connectorNot: false,
};
};
-export const getInitialGroup = () => {
+export const getInitialGroup = (connector: GroupType = 'and') => {
return {
id: uuidv4(),
children: [getInitialRule()],
- type: 'and',
not: false,
+ connector,
+ connectorNot: false,
} as Group;
};
+
+export const withMigratedConnectors = (children: GroupOrRule[], legacyType?: GroupType): GroupOrRule[] =>
+ children.map((child, index) => ({
+ ...child,
+ connector: child.connector ?? (index === 0 ? undefined : (legacyType ?? 'and')),
+ connectorNot: child.connectorNot ?? false,
+ }));
diff --git a/GUI/src/components/FlowElementsPopup/RuleBuilder/useRuleBuilder.tsx b/GUI/src/components/FlowElementsPopup/RuleBuilder/useRuleBuilder.tsx
index 6e9c0276e..50460ea8f 100644
--- a/GUI/src/components/FlowElementsPopup/RuleBuilder/useRuleBuilder.tsx
+++ b/GUI/src/components/FlowElementsPopup/RuleBuilder/useRuleBuilder.tsx
@@ -1,6 +1,8 @@
import { useEffect, useRef, useState } from 'react';
-import { getInitialGroup, getInitialRule, Group, GroupOrRule, GroupType, Rule } from './types';
+import { getInitialGroup, getInitialRule, Group, GroupOrRule, GroupType, Rule, withMigratedConnectors } from './types';
+
+type SeedGroup = Group | GroupOrRule[] | undefined;
interface UseRuleBuilderProps {
group?: Group;
@@ -10,15 +12,21 @@ interface UseRuleBuilderProps {
}
export const useRuleBuilder = (config: UseRuleBuilderProps) => {
- const getSeedGroupChildren = (seedGroup: Group | GroupOrRule[] | undefined): GroupOrRule[] => {
+ const getSeedGroupChildren = (seedGroup: SeedGroup): GroupOrRule[] => {
if (!seedGroup) return [];
if (Array.isArray(seedGroup)) return seedGroup;
return seedGroup.children;
};
- const elementsInitialValue = config.root ? getSeedGroupChildren(config.seedGroup) : config.group!.children;
+ const getSeedGroupLegacyType = (seedGroup: SeedGroup): GroupType | undefined =>
+ seedGroup && !Array.isArray(seedGroup) ? seedGroup.type : undefined;
+
+ const elementsInitialValue = withMigratedConnectors(
+ config.root ? getSeedGroupChildren(config.seedGroup) : config.group!.children,
+ config.root ? getSeedGroupLegacyType(config.seedGroup) : config.group!.type,
+ );
- const isSeedGroupValid = (seedGroup: Group | GroupOrRule[] | undefined): boolean => {
+ const isSeedGroupValid = (seedGroup: SeedGroup): boolean => {
if (!seedGroup) return false;
if (Array.isArray(seedGroup)) return seedGroup.length > 0;
return seedGroup.children?.length > 0;
@@ -41,6 +49,16 @@ export const useRuleBuilder = (config: UseRuleBuilderProps) => {
const onChangeRef = useRef(config.onChange);
onChangeRef.current = config.onChange;
+ useEffect(() => {
+ if (config.root || !config.group) return;
+ setGroupInfo((prev) => ({
+ ...prev,
+ connector: config.group!.connector,
+ connectorNot: config.group!.connectorNot,
+ }));
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [config.group?.connector, config.group?.connectorNot]);
+
useEffect(() => {
onChangeRef.current({
...groupInfo,
@@ -67,12 +85,13 @@ export const useRuleBuilder = (config: UseRuleBuilderProps) => {
});
};
- const changeType = (type: GroupType) => () => {
- setGroupInfo({ ...groupInfo, type });
+ const changeConnector = (id: string, connector: GroupType) => {
+ setElements(elements.map((x) => (x.id === id ? { ...x, connector } : x)));
};
- const changeToAnd = changeType('and');
- const changeToOr = changeType('or');
+ const toggleConnectorNot = (id: string) => {
+ setElements(elements.map((x) => (x.id === id ? { ...x, connectorNot: !x.connectorNot } : x)));
+ };
const changeRule = (rule: Rule) => setElementById(rule.id, rule);
@@ -90,8 +109,8 @@ export const useRuleBuilder = (config: UseRuleBuilderProps) => {
addGroup,
remove,
toggleNot,
- changeToAnd,
- changeToOr,
+ changeConnector,
+ toggleConnectorNot,
changeRule,
onSubGroupChange,
};
diff --git a/GUI/src/components/FlowElementsPopup/index.tsx b/GUI/src/components/FlowElementsPopup/index.tsx
index 9908c831b..724ea6c76 100644
--- a/GUI/src/components/FlowElementsPopup/index.tsx
+++ b/GUI/src/components/FlowElementsPopup/index.tsx
@@ -138,9 +138,9 @@ const FlowElementsPopup: React.FC = () => {
case StepType.Input:
case StepType.Condition:
if (node.data?.rules && Array.isArray(node.data.rules.children)) {
- useServiceStore.getState().changeRulesNode(node.data.rules.children);
+ useServiceStore.getState().changeRulesNode(node.data.rules);
} else {
- useServiceStore.getState().changeRulesNode([]);
+ useServiceStore.getState().changeRulesNode(undefined);
}
break;
@@ -309,21 +309,19 @@ const FlowElementsPopup: React.FC = () => {
};
const prepareRulesForSaving = (updatedNode: Node) => {
- const rulesArray = Array.isArray(rules) ? rules : [];
- for (const item of rulesArray) {
- const processRuleField = (obj: GroupOrRule) => {
- if ('field' in obj) obj.field = removeTrailingUnderscores(obj.field);
- else {
- for (const child of obj.children) {
- processRuleField(child);
- }
+ const rootGroup = rules ?? getInitialGroup();
+ const processRuleField = (obj: GroupOrRule) => {
+ if ('field' in obj) obj.field = removeTrailingUnderscores(obj.field);
+ else {
+ for (const child of obj.children) {
+ processRuleField(child);
}
- };
- processRuleField(item);
+ }
+ };
+ for (const child of rootGroup.children) {
+ processRuleField(child);
}
- updatedNode.data.rules = node.data.rules
- ? { ...node.data.rules, children: rulesArray }
- : { ...getInitialGroup(), children: rulesArray };
+ updatedNode.data.rules = rootGroup;
};
const saveMultiChoicePopup = (originalNode: Node, updatedNode: Node) => {
diff --git a/GUI/src/services/service-builder.ts b/GUI/src/services/service-builder.ts
index 36dae7aba..074b48562 100644
--- a/GUI/src/services/service-builder.ts
+++ b/GUI/src/services/service-builder.ts
@@ -1,6 +1,6 @@
import { Edge, Node } from '@xyflow/react';
import { AxiosError } from 'axios';
-import { Group, Rule } from 'components/FlowElementsPopup/RuleBuilder/types';
+import { Group, GroupOrRule, GroupType, Rule } from 'components/FlowElementsPopup/RuleBuilder/types';
import { format } from 'date-fns';
import i18next, { t } from 'i18next';
import { NodeHtmlMarkdown, PostProcessResult, TranslatorConfigObject } from 'node-html-markdown';
@@ -252,37 +252,37 @@ const buildConditionString = (group: any, assignedVariableNames: Set): s
return isNumericString(rawField) ? rawField : `"${rawField}"`;
};
+ const buildRuleTerm = (rule: Rule): string => {
+ const rawField = rule.field.replaceAll('${', '').replaceAll('}', '');
+ const absoluteValue = removeWrapperQuotes(rule.value.replaceAll('${', '').replaceAll('}', ''));
+ const value = formatField(absoluteValue);
+ const field = formatField(rawField);
+ return `${field} ${rule.operator} ${value}`;
+ };
+
if ('children' in group) {
const subgroup = group as Group;
if (subgroup.children.length === 0) {
return '';
}
- const conditions = subgroup.children.map((child) => {
- if ('children' in child) {
- return `(${buildConditionString(child, assignedVariableNames)})`;
- } else {
- const rule = child;
- const rawField = rule.field.replaceAll('${', '').replaceAll('}', '');
- const absoluteValue = removeWrapperQuotes(rule.value.replaceAll('${', '').replaceAll('}', ''));
- const value = formatField(absoluteValue);
- const field = formatField(rawField);
- return `${field} ${rule.operator} ${value}`;
- }
- });
+ const legacyType: GroupType = subgroup.type ?? 'and';
- if (subgroup.not) {
- return `!(${subgroup.type === 'and' ? conditions.join(' && ') : conditions.join(' || ')})`;
- } else {
- return subgroup.type === 'and' ? conditions.join(' && ') : conditions.join(' || ');
- }
+ const combined = subgroup.children.reduce((accumulated: string, child: GroupOrRule, index: number) => {
+ const term =
+ 'children' in child ? `(${buildConditionString(child, assignedVariableNames)})` : buildRuleTerm(child as Rule);
+ const signedTerm = child.connectorNot ? `!(${term})` : term;
+
+ if (index === 0) return signedTerm;
+
+ const connector = child.connector ?? legacyType;
+ const operator = connector === 'and' ? '&&' : '||';
+ return `${accumulated} ${operator} ${signedTerm}`;
+ }, '');
+
+ return subgroup.not ? `!(${combined})` : combined;
} else {
- const rule = group as Rule;
- const rawField = rule.field.replaceAll('${', '').replaceAll('}', '');
- const absoluteValue = removeWrapperQuotes(rule.value.replaceAll('${', '').replaceAll('}', ''));
- const value = formatField(absoluteValue);
- const field = formatField(rawField);
- return `${field} ${rule.operator} ${value}`;
+ return buildRuleTerm(group as Rule);
}
};
diff --git a/GUI/src/store/new-services.store.ts b/GUI/src/store/new-services.store.ts
index d59cac1a1..9add4d93e 100644
--- a/GUI/src/store/new-services.store.ts
+++ b/GUI/src/store/new-services.store.ts
@@ -10,7 +10,7 @@ import {
ReactFlowInstance,
} from '@xyflow/react';
import { AxiosResponse } from 'axios';
-import { GroupOrRule } from 'components/FlowElementsPopup/RuleBuilder/types';
+import { Group } from 'components/FlowElementsPopup/RuleBuilder/types';
import i18next from 'i18next';
import {
getAllEndpoints,
@@ -90,13 +90,13 @@ export interface ServiceStoreState {
isNewService: boolean;
serviceState?: ServiceState;
assignElements: Assign[];
- rules: GroupOrRule[];
+ rules: Group | undefined;
isYesNoQuestion: boolean;
stepPreferences: string[];
endpointsResponseVariables: EndpointResponseVariable[];
setIsYesNoQuestion: (value: boolean) => void;
changeAssignNode: (assign: Assign[]) => void;
- changeRulesNode: (rules: GroupOrRule[]) => void;
+ changeRulesNode: (rules: Group | undefined) => void;
markAsNewService: () => void;
unmarkAsNewService: () => void;
setServiceId: (id: string) => void;
@@ -257,7 +257,7 @@ const useServiceStore = create((set, get) => ({
isTestButtonVisible: false,
isTestButtonEnabled: true,
assignElements: [],
- rules: [],
+ rules: undefined,
isYesNoQuestion: false,
stepPreferences: [],
endpointsResponseVariables: [],
@@ -435,7 +435,7 @@ const useServiceStore = create((set, get) => ({
nodes: initialNodes,
isTestButtonEnabled: true,
assignElements: [],
- rules: [],
+ rules: undefined,
isYesNoQuestion: false,
clickedNode: null,
isTestButtonVisible: false,
@@ -447,7 +447,7 @@ const useServiceStore = create((set, get) => ({
useTestServiceStore.getState().reset();
},
resetAssign: () => set({ assignElements: [] }),
- resetRules: () => set({ rules: [], isYesNoQuestion: false }),
+ resetRules: () => set({ rules: undefined, isYesNoQuestion: false }),
loadService: async (id, resetState, search) => {
if (resetState === true) {
get().resetState();