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
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;
};
Comment thread
trevorling marked this conversation as resolved.

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
4 changes: 3 additions & 1 deletion GUI/src/components/FlowElementsPopup/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { DynamicChoices } from 'types/dynamic-choices';
import { EndpointData } from 'types/endpoint';
import { MultiChoiceQuestionButton } from 'types/multi-choice-question';
import { NodeDataProps } from 'types/service-flow';
import { generateUniqueId } from 'utils/flow-utils';
import { getValueByPath } from 'utils/object-util';
import {
getLastDigits,
Expand Down Expand Up @@ -207,6 +206,7 @@ const FlowElementsPopup: React.FC = () => {
dynamicChoices: node.data.stepType === StepType.DynamicChoices ? dynamicChoices : undefined,
endpoint: nodeEndpoint ?? node.data?.endpoint,
testingPassed: undefined,
childrenCount: node.data.stepType === StepType.MultiChoiceQuestion ? 1 : node.data.childrenCount,
},
};

Expand Down Expand Up @@ -395,6 +395,8 @@ const FlowElementsPopup: React.FC = () => {
draggable: false,
}));

updatedNode.data.childrenCount = 1;

let finalNodes = [...nodes.filter((n) => n.id !== updatedNode.id), updatedNode, ...newGhostNodes];
let finalEdges = [...filteredEdges, ...newEdges];

Expand Down
9 changes: 9 additions & 0 deletions GUI/src/hooks/flow/useEdgeAdd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@

const newNodeId = generateUniqueId();

const defaultMcqButtons = [
{ id: '1', title: 'Jah', payload: '' },
{ id: '2', title: 'Ei', payload: '' },
Comment thread
trevorling marked this conversation as resolved.
];

const multiChoiceQuestion =
stepType === StepType.MultiChoiceQuestion ? { question: '', buttons: defaultMcqButtons } : undefined;

const insertNode = {
id: newNodeId,
position: { x: targetNode.position.x, y: targetNode.position.y },
Expand All @@ -31,6 +39,7 @@
stepType: stepType,
readonly: [StepType.Auth, StepType.FinishingStepEnd, StepType.FinishingStepRedirect].includes(stepType),
endpoint: step.data,
multiChoiceQuestion,
setClickedNode: useServiceStore.getState().setClickedNode,
},
className: (() => {
Expand Down Expand Up @@ -101,7 +110,7 @@
});

if (targetEdge) {
targetEdge.label = labels[labels.length - 1] ?? '+';

Check warning on line 113 in GUI/src/hooks/flow/useEdgeAdd.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `.at(…)` over `[….length - index]`.

See more on https://sonarcloud.io/project/issues?id=buerokratt_Service-Module&issues=AZ4_7SdELvVbuPZ3O5ci&open=AZ4_7SdELvVbuPZ3O5ci&pullRequest=1015
}
}

Expand Down
121 changes: 121 additions & 0 deletions GUI/src/hooks/flow/useMcqConnect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { Connection, useReactFlow } from '@xyflow/react';
import { useCallback, useState } from 'react';
import useServiceStore from 'store/new-services.store';
import {
applyMcqBranchConnection,
applySimpleConnection,
getEmptyMcqBranches,
getMcqNodeIdFromConnection,
McqEmptyBranch,
} from 'utils/mcq-flow-utils';

export type PendingMcqConnection = {
readonly connection: Connection;
readonly emptyBranches: McqEmptyBranch[];
};
Comment thread
trevorling marked this conversation as resolved.

function useMcqConnect() {
const { getNodes, getEdges, setNodes, setEdges, getNode } = useReactFlow();
const saveToHistory = useServiceStore((state) => state.saveToHistory);
const setHasUnsavedChanges = useServiceStore((state) => state.setHasUnsavedChanges);
const [pendingConnection, setPendingConnection] = useState<PendingMcqConnection | null>(null);

const commitConnection = useCallback(
(nodes: ReturnType<typeof getNodes>, edges: ReturnType<typeof getEdges>) => {
setNodes(nodes);
setEdges(edges);
setHasUnsavedChanges(true);
saveToHistory({ nodes, edges });
},
[saveToHistory, setEdges, setHasUnsavedChanges, setNodes],
);

const applyMcqOutgoingConnection = useCallback(
(connection: Connection, branch: McqEmptyBranch) => {
const { source, target } = connection;
if (!source || !target) return;

const nodes = getNodes();
const edges = getEdges();
const result = applyMcqBranchConnection({
nodes,
edges,
mcqId: source,
targetId: target,
branch,
});
commitConnection(result.nodes, result.edges);
},
[commitConnection, getEdges, getNodes],
);

const applyMcqIncomingConnection = useCallback(
(connection: Connection) => {
const nodes = getNodes();
const edges = getEdges();
const result = applySimpleConnection({ nodes, edges, connection });
commitConnection(result.nodes, result.edges);
},
[commitConnection, getEdges, getNodes],
);

const handleConnect = useCallback(
(connection: Connection) => {
const mcqId = getMcqNodeIdFromConnection(connection, getNode);
if (!mcqId) return false;

const nodes = getNodes();
const edges = getEdges();
const emptyBranches = getEmptyMcqBranches(mcqId, nodes, edges);
if (emptyBranches.length === 0) return true;

if (connection.source === mcqId) {
if (emptyBranches.length === 1) {
applyMcqOutgoingConnection(connection, emptyBranches[0]);
} else {
setPendingConnection({ connection, emptyBranches });
}
return true;
}

applyMcqIncomingConnection(connection);
return true;
},
[applyMcqIncomingConnection, applyMcqOutgoingConnection, getEdges, getNode, getNodes],
);

const confirmBranch = useCallback(
(branch: McqEmptyBranch) => {
if (!pendingConnection) return;
applyMcqOutgoingConnection(pendingConnection.connection, branch);
setPendingConnection(null);
},
[applyMcqOutgoingConnection, pendingConnection],
);

const cancelBranchSelection = useCallback(() => {
setPendingConnection(null);
}, []);

const isValidConnection = useCallback(
(connection: Connection) => {
if (connection.source === connection.target) return false;

const mcqId = getMcqNodeIdFromConnection(connection, getNode);
if (!mcqId) return true;

return getEmptyMcqBranches(mcqId, getNodes(), getEdges()).length > 0;
},
[getEdges, getNode, getNodes],
);

return {
pendingConnection,
handleConnect,
isValidConnection,
confirmBranch,
cancelBranchSelection,
};
}

export default useMcqConnect;
4 changes: 4 additions & 0 deletions GUI/src/i18n/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,10 @@
"openObjectEditor": "Open object editor",
"confirmImport": "The current service will be rewritten!"
},
"mcq": {
"selectBranchTitle": "Select branch",
"emptyBranchesMessage": "This multi-choice question has {{count}} empty branches. Which branch do you want to connect?"
},
"previousVariables": {
"assignElements": "Assign Elements",
"dateAndTime": {
Expand Down
Loading
Loading