Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
22c69fe
chore(982): Added mcq branch manual connection support
1AhmedYasser May 19, 2026
b586604
chore(982): Handled buttons long texts
1AhmedYasser May 19, 2026
1d6bd9b
chore(982): Addressed PR Comments
1AhmedYasser May 19, 2026
e583908
chore(1016): stripped html from mcq question
1AhmedYasser May 19, 2026
ebce0db
chore(963): Added Service settings to service import/export
1AhmedYasser May 19, 2026
3743cde
chore(963): Fixed format and lint
1AhmedYasser May 19, 2026
293e115
chore(963): Add import only flow option
1AhmedYasser May 19, 2026
6b26790
fix(963): fix format
1AhmedYasser May 19, 2026
b4ab075
fix(963): Addressed comments
1AhmedYasser May 20, 2026
8feccfe
chore(987): Modified translations and dark mode node selections
1AhmedYasser May 20, 2026
cd1ff0f
fix(987): Fixed Date & time / Separator dropdown going out of bounds
1AhmedYasser May 20, 2026
a84d34a
fix(987): Chat widget dark mode text color
1AhmedYasser May 20, 2026
697cf07
fix(987): Remove test tab from nodes
1AhmedYasser May 20, 2026
0a92a89
chore(987): Modified translations
1AhmedYasser May 20, 2026
6c5feff
chore(987): make edge selection more visible
1AhmedYasser May 20, 2026
df7e183
chore(987): Removed chat content height
1AhmedYasser May 20, 2026
7732a9e
chore(987): Aligned examples and keywords value fields spacing
1AhmedYasser May 20, 2026
99c2140
chore(987): Fixed format
1AhmedYasser May 20, 2026
84a0d6f
Merge pull request #1017 from 1AhmedYasser/strip-html-from-mcq-question
ffrose May 21, 2026
9364041
Merge pull request #1018 from 1AhmedYasser/Include-service-settings-m…
ffrose May 21, 2026
f5fb8e1
Merge pull request #1015 from 1AhmedYasser/Ability-to-Manually-Connec…
ffrose May 21, 2026
5a182a4
Merge pull request #1019 from 1AhmedYasser/UI-Cleanup-and-Dark-Mode-F…
ffrose May 21, 2026
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
4 changes: 4 additions & 0 deletions GUI/src/components/Collapsible/Collapsible.scss
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@
background-color: get-color(white);
border-radius: 0 0 4px 4px;
overflow: hidden;

&[data-state='open'] {
overflow: visible;
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion GUI/src/components/DynamicList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ type DynamicListProps = {
tooltipText?: string;
};

const DynamicList: FC<DynamicListProps> = ({ label, labelWidth = 120, value, onChange, placeholder, tooltipText }) => {
const DynamicList: FC<DynamicListProps> = ({ label, labelWidth = 170, value, onChange, placeholder, tooltipText }) => {
const { t } = useTranslation();
const items = value?.length > 0 ? value : [''];

Expand Down
4 changes: 2 additions & 2 deletions GUI/src/components/ExportServicesModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,8 @@ const ExportServicesModal: FC<ExportServicesModalProps> = ({ isVisible, onClose
try {
const servicesWithStructure = await Promise.all(
selectedServices.map(async (service) => {
const response = await api.post(getServiceById(), { id: service.serviceId, search: '' });
return { ...service, structure: response.data.structure };
const response = await api.post<Service>(getServiceById(), { id: service.serviceId, search: '' });
return response.data;
}),
);
const success = await exportServices(servicesWithStructure);
Expand Down
79 changes: 54 additions & 25 deletions GUI/src/components/Flow/Controls/ImportExportControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,18 @@ import { updateFlowInputRules } from 'services/flow-builder';
import useServiceStore from 'store/new-services.store';
import useToastStore from 'store/toasts.store';
import { FlowData } from 'types/service-flow';
import { appendFlowNodes } from 'utils/append-flow-nodes';
import {
applyServiceSettings,
buildServiceSettingsFromStore,
isValidFlowData,
parseFlowArtifact,
serializeFlowArtifact,
} from 'utils/service-flow-artifact';
import { removeTrailingUnderscores } from 'utils/string-util';

const ImportExportControls: FC = () => {
const { getNodes, getEdges } = useReactFlow();
const { getNodes, getEdges, setNodes, setEdges } = useReactFlow();
const { t } = useTranslation();
const { setHasUnsavedChanges, saveToHistory, setNodes: setStoreNodes, setEdges: setStoreEdges } = useServiceStore();
const fileInputRef = useRef<HTMLInputElement>(null);
Expand All @@ -21,7 +29,7 @@ const ImportExportControls: FC = () => {

const handleExport = useCallback(async () => {
try {
const dataString = JSON.stringify({ nodes: getNodes(), edges: getEdges() });
const dataString = serializeFlowArtifact(getNodes(), getEdges(), buildServiceSettingsFromStore());
const fileName = `${serviceName != undefined && serviceName != '' ? serviceName : 'flow'}_${format(new Date(), 'yyyy_MM_dd_HH_mm_ss')}.json`;

if ('showSaveFilePicker' in window) {
Expand Down Expand Up @@ -56,8 +64,9 @@ const ImportExportControls: FC = () => {
}, [getNodes, getEdges, serviceName, t]);

const applyImportedFlow = useCallback(
(flowData: FlowData) => {
(flowData: FlowData, settings?: FlowData['settings']) => {
if (isValidFlowData(flowData)) {
applyServiceSettings(settings);
const nodes = flowData.nodes.map((node: any) => {
if (node.type !== 'custom') return node;
node.data = {
Expand Down Expand Up @@ -90,13 +99,14 @@ const ImportExportControls: FC = () => {
reader.onload = (e) => {
try {
const content = e.target?.result as string;
const flowData = JSON.parse(content) as FlowData;
const { nodes, edges, settings } = parseFlowArtifact(content);
const flowData = { nodes, edges } as FlowData;
const currentNodes = getNodes().filter((node) => node.type !== 'ghost');

if (currentNodes.length === 1 && currentNodes[0].type === 'start') {
applyImportedFlow(flowData);
applyImportedFlow(flowData, settings);
} else {
setImportedFlowData(flowData);
setImportedFlowData({ ...flowData, settings });
setIsConfirmImportModalVisible(true);
}
} catch (error) {
Expand All @@ -115,28 +125,46 @@ const ImportExportControls: FC = () => {
[getNodes, applyImportedFlow, t],
);

const closeImportModal = useCallback(() => {
setIsConfirmImportModalVisible(false);
setImportedFlowData(null);
}, []);

const handleConfirmImport = useCallback(() => {
if (importedFlowData) {
applyImportedFlow(importedFlowData);
const { settings, ...flowData } = importedFlowData;
applyImportedFlow(flowData, settings);
}
setIsConfirmImportModalVisible(false);
setImportedFlowData(null);
}, [importedFlowData, applyImportedFlow]);
closeImportModal();
}, [importedFlowData, applyImportedFlow, closeImportModal]);

const handleCancelImport = useCallback(() => {
setIsConfirmImportModalVisible(false);
setImportedFlowData(null);
}, []);
const handleImportFlowOnly = useCallback(() => {
if (!importedFlowData) {
closeImportModal();
return;
}

const isValidFlowData = (data: any): data is FlowData => {
return (
data &&
Array.isArray(data.nodes) &&
Array.isArray(data.edges) &&
data.nodes.every((node: any) => node.id && node.type) &&
data.edges.every((edge: any) => edge.id && edge.source && edge.target)
);
};
const { nodes, edges } = appendFlowNodes(getNodes(), getEdges(), importedFlowData.nodes, importedFlowData.edges);
saveToHistory();
setStoreNodes(nodes);
setStoreEdges(edges);
setNodes(nodes);
setEdges(edges);
saveToHistory({ nodes, edges });
setHasUnsavedChanges(true);
closeImportModal();
}, [
importedFlowData,
getNodes,
getEdges,
setNodes,
setEdges,
setStoreNodes,
setStoreEdges,
setHasUnsavedChanges,
saveToHistory,
closeImportModal,
]);

const triggerFileInput = useCallback(() => {
if (fileInputRef.current) {
Expand All @@ -158,12 +186,13 @@ const ImportExportControls: FC = () => {
</Button>
</Track>
{isConfirmImportModalVisible && (
<Modal title={t('serviceFlow.popup.confirmImport')} onClose={handleCancelImport}>
<Modal title={t('serviceFlow.popup.confirmImport')} onClose={closeImportModal}>
<Track justify="end" gap={16}>
<Button appearance="primary" onClick={handleConfirmImport}>
{t('global.proceed')}
</Button>
<Button appearance="secondary" onClick={handleCancelImport}>
<Button onClick={handleImportFlowOnly}>{t('serviceFlow.popup.importFlowOnly')}</Button>
<Button appearance="secondary" onClick={closeImportModal}>
{t('global.cancel')}
</Button>
</Track>
Expand Down
10 changes: 10 additions & 0 deletions GUI/src/components/Flow/McqBranchSelectModal.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.mcq-branch-select-modal__button {
display: inline-flex;
width: fit-content;
max-width: 100%;
overflow: hidden;

.multiple-choice-question-button__text {
max-width: 100%;
}
}
32 changes: 32 additions & 0 deletions GUI/src/components/Flow/McqBranchSelectModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Button, Modal, Track } from 'components';
import { FC } from 'react';
import { useTranslation } from 'react-i18next';
import { McqEmptyBranch } from 'utils/mcq-flow-utils';

import '../FlowElementsPopup/styles.scss';
import './McqBranchSelectModal.scss';

type McqBranchSelectModalProps = {
readonly emptyBranches: McqEmptyBranch[];
readonly onSelect: (branch: McqEmptyBranch) => void;
readonly onClose: () => void;
};

const McqBranchSelectModal: FC<McqBranchSelectModalProps> = ({ emptyBranches, onSelect, onClose }) => {
const { t } = useTranslation();

return (
<Modal title={t('serviceFlow.mcq.selectBranchTitle')} onClose={onClose}>
<p>{t('serviceFlow.mcq.emptyBranchesMessage', { count: emptyBranches.length })}</p>
<Track direction="vertical" gap={8} align="left" style={{ marginTop: 16, width: '100%' }}>
{emptyBranches.map((branch) => (
<Button key={branch.edgeId} className="mcq-branch-select-modal__button" onClick={() => onSelect(branch)}>
<span className="multiple-choice-question-button__text">{branch.label}</span>
</Button>
))}
</Track>
</Modal>
);
};

export default McqBranchSelectModal;
32 changes: 27 additions & 5 deletions GUI/src/components/Flow/NodeTypes/CustomNode.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { Handle, NodeProps, Position, useUpdateNodeInternals } from '@xyflow/react';
import { Handle, NodeProps, Position, useStore, useUpdateNodeInternals } from '@xyflow/react';
import './Node.scss';
import Button from 'components/Button';
import Icon from 'components/Icon';
import Track from 'components/Track';
import React, { FC, useEffect } from 'react';
import React, { FC, useEffect, useMemo } from 'react';
import { MdDeleteOutline, MdOutlineEdit, MdOutlineRemoveRedEye } from 'react-icons/md';
import useServiceStore from 'store/services.store';
import { StepType } from 'types';
import { NodeDataProps } from 'types/service-flow';
import { MCQ_SOURCE_HANDLE_ID, mcqHasEmptyBranches } from 'utils/mcq-flow-utils';

import StepNode from './StepNode';

Expand All @@ -17,13 +19,21 @@ type CustomNodeProps = {
const CustomNode: FC<NodeProps & CustomNodeProps> = (props) => {
const { data, isConnectable, id } = props;
const orientation = useServiceStore((state) => state.orientation);
const shouldOffsetHandles = data.childrenCount > 1;
const isMcq = data.stepType === StepType.MultiChoiceQuestion;
const shouldOffsetHandles = !isMcq && data.childrenCount > 1;

const edges = useStore((state) => state.edges);
const nodes = useStore((state) => state.nodes);

const mcqCanConnect = useMemo(() => !isMcq || mcqHasEmptyBranches(id, nodes, edges), [edges, id, isMcq, nodes]);

const canConnect = isConnectable && mcqCanConnect;

const updateNodeInternals = useUpdateNodeInternals();

useEffect(() => {
updateNodeInternals(id);
}, [data.childrenCount, id, updateNodeInternals, orientation]);
}, [data.childrenCount, id, isMcq, mcqCanConnect, updateNodeInternals, orientation]);

const isFinishingStep = () => {
return data.type === 'finishing-step';
Expand All @@ -38,6 +48,18 @@ const CustomNode: FC<NodeProps & CustomNodeProps> = (props) => {
};

const bottomHandles = (): React.JSX.Element => {
if (isMcq) {
return (
<Handle
id={MCQ_SOURCE_HANDLE_ID}
type="source"
position={getSourcePosition()}
isConnectable={canConnect}
hidden={isFinishingStep()}
/>
);
}

return (
<>
{new Array(data.childrenCount).fill(0).map((_, i) => (
Expand All @@ -64,7 +86,7 @@ const CustomNode: FC<NodeProps & CustomNodeProps> = (props) => {

return (
<>
<Handle type="target" position={getTargetPosition()} isConnectable={isConnectable} />
<Handle type="target" position={getTargetPosition()} isConnectable={canConnect} />
<StepNode data={data} />
{data.stepType !== 'rule' && (
<Track style={{ position: 'fixed', top: 8, right: 8 }}>
Expand Down
48 changes: 19 additions & 29 deletions GUI/src/components/FlowBuilder/FlowBuilder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import LassoSelectionControls from 'components/Flow/Controls/LassoSelectionContr
import UndoRedoControls from 'components/Flow/Controls/UndoRedoControls';
import edgeTypes from 'components/Flow/EdgeTypes';
import { Lasso } from 'components/Flow/LassoSelection/Lasso';
import McqBranchSelectModal from 'components/Flow/McqBranchSelectModal';
import nodeTypes from 'components/Flow/NodeTypes';
import useLayout from 'hooks/flow/useLayout';
import useMcqConnect from 'hooks/flow/useMcqConnect';
import { useOnNodesDelete } from 'hooks/flow/useOnNodeDelete';
import { ChangeEventHandler, FC, useCallback, useEffect, useState } from 'react';
import '@xyflow/react/dist/style.css';
Expand All @@ -17,6 +19,7 @@ import { MdCenterFocusStrong, MdOutlineCenterFocusStrong } from 'react-icons/md'
import useNewServiceStore from 'store/new-services.store';
import useServiceStore from 'store/services.store';
import { StepType } from 'types';
import { applySimpleConnection } from 'utils/mcq-flow-utils';
import '../Flow/LassoSelection/Lasso.css';
import './FlowBuilder.scss';

Expand Down Expand Up @@ -59,42 +62,26 @@ const FlowBuilder: FC<FlowBuilderProps> = ({ nodes, edges }) => {
const { fitView } = useReactFlow();

const { runLayout } = useLayout();
const { pendingConnection, handleConnect, isValidConnection, confirmBranch, cancelBranchSelection } = useMcqConnect();

const onConnect = useCallback(
({ source, target }: any) => {
(connection: { source?: string | null; target?: string | null; sourceHandle?: string | null }) => {
if (handleConnect(connection)) return;

const nodes = getNodes();
const edges = getEdges();

const parentOutgoingEdges = edges.filter((edge) => edge.source === source);

const ghostEdges = parentOutgoingEdges.filter((edge) => {
const targetNode = nodes.find((n) => n.id === edge.target);
return targetNode?.type === 'ghost';
const { nodes: finalNodes, edges: finalEdges } = applySimpleConnection({
nodes,
edges,
connection,
});

let finalNodes = nodes;
let finalEdges = edges;

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

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

setNodes(finalNodes);
setEdges(finalEdges);
setHasUnsavedChanges(true);
saveToHistory({ nodes: finalNodes, edges: finalEdges });
},
[getEdges, getNodes, setEdges, setHasUnsavedChanges, setNodes, saveToHistory],
[getEdges, getNodes, handleConnect, setEdges, setHasUnsavedChanges, setNodes, saveToHistory],
);

const zIndexStyle = { zIndex: 20 };
Expand All @@ -103,10 +90,6 @@ const FlowBuilder: FC<FlowBuilderProps> = ({ nodes, edges }) => {
setColorMode(evt.target.value as ColorMode);
};

const isValidConnection = useCallback((connection: any) => {
return connection.source !== connection.target;
}, []);

const onSelectionChange = useCallback(
({ nodes: selectedNodes }: { nodes: Node[] }) => {
setFlowSelectedNodes(selectedNodes);
Expand Down Expand Up @@ -262,6 +245,13 @@ const FlowBuilder: FC<FlowBuilderProps> = ({ nodes, edges }) => {
</Track>
</Modal>
)}
{pendingConnection && (
<McqBranchSelectModal
emptyBranches={pendingConnection.emptyBranches}
onSelect={confirmBranch}
onClose={cancelBranchSelection}
/>
)}
</>
);
};
Expand Down
Loading
Loading