Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 23 additions & 5 deletions GUI/src/components/Flow/Controls/CopyPasteControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ interface CopyPasteControlsProps {
const CopyPasteControls: FC<CopyPasteControlsProps> = ({ onNodesDelete }) => {
const { getNodes, getEdges, setNodes, setEdges } = useReactFlow();
const { t } = useTranslation();
const { setHasUnsavedChanges, saveToHistory } = useServiceStore();
const { setHasUnsavedChanges, saveToHistory, setNodes: setStoreNodes, setEdges: setStoreEdges } = useServiceStore();
const [hasClipboardData, setHasClipboardData] = useState<boolean>(false);
const selectedNodes = useServiceStore((state) => state.flowSelectedNodes);
const reactFlowInstance = useServiceStore.getState().reactFlowInstance;
Expand Down Expand Up @@ -147,9 +147,11 @@ const CopyPasteControls: FC<CopyPasteControlsProps> = ({ onNodesDelete }) => {
const uniqueLabel = generateUniqueLabel(node.data.label as string, allExistingNodes);
processedLabels.add(uniqueLabel);

const position = node.position ?? { x: 0, y: 0 };
return {
...node,
id: newId,
position: { x: position.x, y: position.y },
selected: false,
data: {
...node.data,
Expand Down Expand Up @@ -293,15 +295,31 @@ const CopyPasteControls: FC<CopyPasteControlsProps> = ({ onNodesDelete }) => {
}
});

setNodes((prevNodes) => [...prevNodes, ...newNodes, ...ghostNodes]);
setEdges((prevEdges) => [...prevEdges, ...newEdges, ...ghostEdges]);
const currentEdges = getEdges();
const finalNodes = [...currentNodes, ...newNodes, ...ghostNodes];
const finalEdges = [...currentEdges, ...newEdges, ...ghostEdges];
setStoreNodes(finalNodes);
setStoreEdges(finalEdges);
setNodes(finalNodes);
setEdges(finalEdges);
setHasUnsavedChanges(true);
saveToHistory();
saveToHistory({ nodes: finalNodes, edges: finalEdges });

useToastStore
.getState()
.success({ title: t('serviceFlow.nodesPasted', { count: newNodes.length, s: newNodes.length > 1 ? 's' : '' }) });
}, [fallbackClipboardData, getNodes, setNodes, setEdges, setHasUnsavedChanges, t, saveToHistory]);
}, [
fallbackClipboardData,
getEdges,
getNodes,
setNodes,
setEdges,
setStoreNodes,
setStoreEdges,
setHasUnsavedChanges,
t,
saveToHistory,
]);

const cutNodes = useCallback(async () => {
if (selectedNodes.length === 0) {
Expand Down
12 changes: 7 additions & 5 deletions GUI/src/components/Flow/Controls/ImportExportControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ import { FlowData } from 'types/service-flow';
import { removeTrailingUnderscores } from 'utils/string-util';

const ImportExportControls: FC = () => {
const { getNodes, getEdges, setNodes, setEdges } = useReactFlow();
const { getNodes, getEdges } = useReactFlow();
const { t } = useTranslation();
const { setHasUnsavedChanges } = useServiceStore();
const { setHasUnsavedChanges, saveToHistory, setNodes: setStoreNodes, setEdges: setStoreEdges } = useServiceStore();
const fileInputRef = useRef<HTMLInputElement>(null);
const serviceName = useServiceStore((state) => removeTrailingUnderscores(state.serviceNameDashed()));
const [isConfirmImportModalVisible, setIsConfirmImportModalVisible] = useState(false);
Expand Down Expand Up @@ -69,14 +69,16 @@ const ImportExportControls: FC = () => {
};
return node;
});
setNodes(nodes);
setEdges(flowData.edges);
saveToHistory();
setStoreNodes(nodes);
setStoreEdges(flowData.edges);
saveToHistory({ nodes, edges: flowData.edges });
setHasUnsavedChanges(true);
} else {
useToastStore.getState().error({ title: t('global.notificationError'), message: t('serviceFlow.parseError') });
}
},
[setNodes, setEdges, setHasUnsavedChanges, t],
[setStoreNodes, setStoreEdges, setHasUnsavedChanges, saveToHistory, t],
);

const handleImport = useCallback(
Expand Down
34 changes: 18 additions & 16 deletions GUI/src/components/FlowBuilder/FlowBuilder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,25 +72,27 @@ const FlowBuilder: FC<FlowBuilderProps> = ({ nodes, edges }) => {
return targetNode?.type === 'ghost';
});

let finalNodes = nodes;
let finalEdges = edges;

if (ghostEdges.length > 0) {
const ghostNodeIds = new Set(ghostEdges.map((edge) => edge.target));
const updatedEdges = edges.filter((edge) => !ghostEdges.includes(edge));
const updatedNodes = nodes.filter((node) => !ghostNodeIds.has(node.id));
setNodes(updatedNodes);
setEdges(updatedEdges);
finalEdges = edges.filter((edge) => !ghostEdges.includes(edge));
finalNodes = nodes.filter((node) => !ghostNodeIds.has(node.id));
}

setEdges((eds) => [
...eds,
{
id: `${source}->${target}`,
source: source,
target: target,
type: 'step',
},
]);
const newEdge = {
id: `${source}->${target}`,
source: source,
target: target,
type: 'step',
};
finalEdges = [...finalEdges, newEdge];

setNodes(finalNodes);
setEdges(finalEdges);
setHasUnsavedChanges(true);
saveToHistory();
saveToHistory({ nodes: finalNodes, edges: finalEdges });
},
[getEdges, getNodes, setEdges, setHasUnsavedChanges, setNodes, saveToHistory],
);
Expand Down Expand Up @@ -176,13 +178,13 @@ const FlowBuilder: FC<FlowBuilderProps> = ({ nodes, edges }) => {
onEdgesDelete={(edges) => {
onEdgesDelete(edges);
setHasUnsavedChanges(true);
saveToHistory();
setTimeout(() => saveToHistory(), 0);
}}
onBeforeDelete={onBeforeDelete}
onNodesDelete={(nodes) => {
onNodesDelete(nodes);
setHasUnsavedChanges(true);
saveToHistory();
setTimeout(() => saveToHistory(), 0);
}}
fitView
fitViewOptions={{ padding: 5 }}
Expand Down
13 changes: 10 additions & 3 deletions GUI/src/hooks/flow/useEdgeAdd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,16 @@ function useEdgeAdd(id: string) {
return newNodes;
});

setTimeout(() => {
useServiceStore.getState().saveToHistory();
}, 0);
const isFinishingStep = [
StepType.DynamicChoices,
StepType.FinishingStepEnd,
StepType.FinishingStepRedirect,
].includes(stepType);
if (!isFinishingStep) {
setTimeout(() => {
useServiceStore.getState().saveToHistory();
}, 0);
}
};
return handleEdgeClick;
}
Expand Down
37 changes: 29 additions & 8 deletions GUI/src/services/service-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,26 @@ const hasInvalidElements = (elements: any[]): boolean => {
});
};

const buildConditionString = (group: any): string => {
const getAssignedVariableNames = (nodes: Node[]): Set<string> => {
const names = new Set(['chatId', 'authorId', 'input', 'buttons', 'res']);
for (const node of nodes) {
const data = node.data as NodeDataProps | undefined;
if (data?.stepType === StepType.Assign && Array.isArray(data.assignElements)) {
for (const e of data.assignElements) {
const key = e.key?.replaceAll('${', '').replaceAll('}', '').trim();
if (key) names.add(key);
}
}
}
return names;
};

const buildConditionString = (group: any, assignedVariableNames: Set<string>): string => {
const formatField = (rawField: string): string => {
if (assignedVariableNames.has(rawField)) return rawField;
return isNumericString(rawField) ? rawField : `"${rawField}"`;
};

if ('children' in group) {
const subgroup = group as Group;
if (subgroup.children.length === 0) {
Expand All @@ -122,12 +141,14 @@ const buildConditionString = (group: any): string => {

const conditions = subgroup.children.map((child) => {
if ('children' in child) {
return `(${buildConditionString(child)})`;
return `(${buildConditionString(child, assignedVariableNames)})`;
} else {
const rule = child;
const rawField = rule.field.replaceAll('${', '').replaceAll('}', '');
const absoluteValue = removeWrapperQuotes(rule.value.replaceAll('${', '').replaceAll('}', ''));
const value = isNumericString(absoluteValue) ? absoluteValue : `"${absoluteValue}"`;
return `${rule.field.replaceAll('${', '').replaceAll('}', '')} ${rule.operator} ${value}`;
const field = formatField(rawField);
return `${field} ${rule.operator} ${value}`;
}
});

Expand All @@ -138,9 +159,11 @@ const buildConditionString = (group: any): string => {
}
} else {
const rule = group as Rule;
const rawField = rule.field.replaceAll('${', '').replaceAll('}', '');
const absoluteValue = removeWrapperQuotes(rule.value.replaceAll('${', '').replaceAll('}', ''));
const value = isNumericString(absoluteValue) ? absoluteValue : `"${absoluteValue}"`;
return `${rule.field.replaceAll('${', '').replaceAll('}', '')} ${rule.operator} ${value}`;
const field = formatField(rawField);
return `${field} ${rule.operator} ${value}`;
}
};

Expand Down Expand Up @@ -615,10 +638,11 @@ function handleConditionStep(
throw new Error(i18next.t('toast.missing-condition-rules') ?? 'Error');
}

const assignedVariableNames = getAssignedVariableNames(nodes);
finishedFlow.set(parentStepName, {
switch: [
{
condition: `\${${buildConditionString(parentNode.data.rules)}}`,
condition: `\${${buildConditionString(parentNode.data.rules, assignedVariableNames)}}`,
next: toSnakeCase(firstChild?.data?.label ?? '') ?? '',
},
],
Expand Down Expand Up @@ -860,9 +884,6 @@ export const saveFlowClick = async (status: 'draft' | 'ready' = 'ready', showErr
title: i18next.t('newService.toast.failed'),
message: e.response?.status === 409 ? t('newService.toast.serviceNameAlreadyExists') : e?.message,
});
throw new Error(
e.response?.status === 409 ? t('newService.toast.serviceNameAlreadyExists').toString() : e?.message,
);
},
description,
slot,
Expand Down
33 changes: 22 additions & 11 deletions GUI/src/store/new-services.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ export interface ServiceStoreState {
handleProgrammaticNavigation: (to: string) => boolean;
history: { nodes: Node[]; edges: Edge[] }[];
historyIndex: number;
saveToHistory: () => void;
saveToHistory: (state?: { nodes: Node[]; edges: Edge[] }) => void;
undo: () => void;
redo: () => void;
canUndo: () => boolean;
Expand Down Expand Up @@ -820,13 +820,18 @@ const useServiceStore = create<ServiceStoreState>((set, get) => ({
});
});
},
saveToHistory: () => {
saveToHistory: (stateOverride?: { nodes: Node[]; edges: Edge[] }) => {
const { nodes, edges, history, historyIndex } = get();

const currentState = {
nodes: JSON.parse(JSON.stringify(nodes)),
edges: JSON.parse(JSON.stringify(edges)),
};
const currentState = stateOverride
? {
nodes: JSON.parse(JSON.stringify(stateOverride.nodes)),
edges: JSON.parse(JSON.stringify(stateOverride.edges)),
}
: {
nodes: JSON.parse(JSON.stringify(nodes)),
edges: JSON.parse(JSON.stringify(edges)),
};

const lastState = history[historyIndex];

Expand All @@ -839,11 +844,13 @@ const useServiceStore = create<ServiceStoreState>((set, get) => ({
return;
}

history.push(currentState);
const truncatedHistory = historyIndex < history.length - 1 ? history.slice(0, historyIndex + 1) : history;

truncatedHistory.push(currentState);

set({
history,
historyIndex: historyIndex + 1,
history: truncatedHistory,
historyIndex: truncatedHistory.length - 1,
hasUnsavedChanges: true,
});
},
Expand All @@ -855,8 +862,10 @@ const useServiceStore = create<ServiceStoreState>((set, get) => ({

nodes = nodes.map((node: any) => {
if (node.type !== 'custom') return node;
const { stepType, ...restData } = node.data;
node.data = {
...node.data,
...restData,
stepType,
onDelete: get().onDelete,
setClickedNode: get().setClickedNode,
onEdit: get().handleNodeEdit,
Expand All @@ -882,8 +891,10 @@ const useServiceStore = create<ServiceStoreState>((set, get) => ({

nodes = nodes.map((node: any) => {
if (node.type !== 'custom') return node;
const { stepType, ...restData } = node.data;
node.data = {
...node.data,
...restData,
stepType,
onDelete: get().onDelete,
setClickedNode: get().setClickedNode,
onEdit: get().handleNodeEdit,
Expand Down