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
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:pro="http://www.liquibase.org/xml/ns/pro"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd
http://www.liquibase.org/xml/ns/pro http://www.liquibase.org/xml/ns/pro/liquibase-pro-latest.xsd">

<changeSet id="20250516152344" author="1AhmedYasser">
<sqlFile path="changelog/migrations/20250516152344-add-dynamic-choices-to-step-type-enum.sql" />
<rollback>
<sqlFile path="changelog/migrations/rollback/20250516152344_rollback.sql" />
</rollback>
</changeSet>
</databaseChangeLog>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-- liquibase formatted sql
-- changeset 1AhmedYasser:20250516152344

ALTER TYPE step_type ADD VALUE 'dynamic-choices';
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- liquibase formatted sql
-- rollback

-- Remove the 'dynamic-choices' value from the step_type enum
ALTER TYPE step_type DROP VALUE 'dynamic-choices';
2 changes: 1 addition & 1 deletion DSL/Resql/services/POST/seed-user-step-preferences.sql
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
INSERT INTO user_step_preference (user_id_code, steps)
VALUES (:user_id_code, '{assign,textfield,condition,multi-choice-question,finishing-step-end,input,auth,open-webpage,file-generate,file-sign,finising-step-redirect,rasa-rules,siga}')
VALUES (:user_id_code, '{assign,textfield,condition,multi-choice-question,dynamic-choices,finishing-step-end,input,auth,open-webpage,file-generate,file-sign,finising-step-redirect,rasa-rules,siga}')
11 changes: 6 additions & 5 deletions DSL/Ruuter/services/POST/rasa/rules/add.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,9 @@ declaration:
namespace: service
allowlist:
body:
- field: rule
- field: data
type: object
description: "Body field 'rule'"
- field: story
type: object
description: "Body field 'story'"
description: "Body field 'data'"
headers:
- field: cookie
type: string
Expand Down Expand Up @@ -42,6 +39,8 @@ getRuleNames:
call: http.get
args:
url: "[#SERVICE_RUUTER]/rasa/rule-names"
headers:
cookie: ${incoming.headers.cookie}
result: ruleResult

validateRuleName:
Expand All @@ -56,6 +55,8 @@ getFileLocations:
call: http.get
args:
url: "[#SERVICE_RUUTER]/internal/return-file-locations"
headers:
cookie: ${incoming.headers.cookie}
result: fileLocations

getRulesFile:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ add_rule:
call: http.post
args:
url: "[#SERVICE_RUUTER]/rasa/rules/add"
headers:
cookie: ${incoming.headers.cookie}
body:
data: ${data}
result: add_rule_res
Expand Down
11 changes: 9 additions & 2 deletions GUI/src/components/ApiEndpointCard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { RequestTab } from "../../types";
import { EndpointData, EndpointEnv, EndpointTab } from "../../types/endpoint";
import useServiceStore from "store/new-services.store";
import { EndpointType } from "types/endpoint/endpoint-type";
import { removeTrailingUnderscores } from "utils/string-util";

type EndpointCardProps = {
endpoint: EndpointData;
Expand Down Expand Up @@ -88,18 +89,24 @@ const ApiEndpointCard: FC<EndpointCardProps> = ({
name="endpointName"
label=""
placeholder={t("newService.endpoint.insertName").toString()}
maxLength={30}
value={endpointName}
disabled={isNameDisabled || selectedTab === EndpointEnv.Test}
onChange={(e) => {
setEndpointName(e.target.value);
const sanitizedValue = e.target.value
.replace(/[^a-zA-Z0-9_\s]/g, "")
.replace(/\s+/g, "_")
.replace(/_+/g, "_");

setEndpointName(sanitizedValue);
const endpointsNames = useServiceStore
.getState()
.endpoints.map((ep) => ep.name)
.filter((name) => name !== endpoint.name);
const isNameExist = endpointsNames.includes(e.target.value);
setNameExists(isNameExist);
onNameExists?.(isNameExist);
onNameChange?.(e.target.value);
onNameChange?.(removeTrailingUnderscores(sanitizedValue));
}}
/>
{nameExists && (
Expand Down
2 changes: 1 addition & 1 deletion GUI/src/components/Flow/EdgeTypes/CustomEdge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ function CustomEdge({
StepType.Assign,
StepType.Textfield,
StepType.MultiChoiceQuestion,
StepType.FinishingStepRedirect,
StepType.DynamicChoices,
StepType.FinishingStepEnd,
];

Expand Down
6 changes: 0 additions & 6 deletions GUI/src/components/Flow/NodeTypes/CustomNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,6 @@ type NodeDataProps = {
};
};

const boxTypeColors: { [key: string]: any } = {
step: "blue",
"finishing-step": "red",
rule: "gray",
};

const CustomNode: FC<NodeProps & NodeDataProps> = (props) => {
const { data, isConnectable, id } = props;
const shouldOffsetHandles = data.childrenCount > 1;
Expand Down
10 changes: 7 additions & 3 deletions GUI/src/components/Flow/NodeTypes/StepNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,12 @@ import CheckBadge from "components/CheckBadge";
import ExclamationBadge from "components/ExclamationBadge";
import Track from "components/Track";
import { StepType } from "types";
import { DynamicChoices } from "types/dynamic-choices";

type NodeDataProps = {
data: {
childrenCount: number;
clientInputId: number;
conditionId: number;
multiChoiceQuestionId: number;
assignId: number;
label: string;
onDelete: (id: string) => void;
onEdit: (id: string) => void;
Expand All @@ -36,6 +34,7 @@ type NodeDataProps = {
rules?: Group;
assignElements?: Assign[];
multiChoiceQuestion?: MultiChoiceQuestion;
dynamicChoices?: DynamicChoices;
};
};

Expand Down Expand Up @@ -74,6 +73,11 @@ const StepNode: FC<NodeDataProps> = ({ data }) => {
if (data.stepType === StepType.MultiChoiceQuestion) {
return !data?.multiChoiceQuestion?.question || data.multiChoiceQuestion?.buttons?.find((e) => e.title === "") != undefined;
}

if (data.stepType === StepType.DynamicChoices) {
return !data?.dynamicChoices?.list || !data?.dynamicChoices?.serviceName || !data?.dynamicChoices?.key;
}

if (data.stepType === StepType.UserDefined) return;
if (data.stepType === StepType.OpenWebpage) return !data.link || !data.linkText;
if (data.stepType === StepType.FileGenerate) return !data.fileName || !data.fileContent;
Expand Down
29 changes: 24 additions & 5 deletions GUI/src/components/FlowBuilder/FlowBuilder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,20 @@ type FlowBuilderProps = {

const FlowBuilder: FC<FlowBuilderProps> = ({ nodes, edges }) => {
useLayout();
const { getNodes, getEdges, setNodes, setEdges } = useReactFlow();
const { getNodes, getEdges, setNodes, setEdges, getNode } = useReactFlow();
const setReactFlowInstance = useServiceStore((state) => state.setReactFlowInstance);
const { t } = useTranslation();
const { onNodesDelete, onEdgesDelete, isDeleteConnectionsModalVisible, setIsDeleteConnectionsModalVisible, onDeleteConfirmed, onKeepItConfirmed, hasConnectedNodes, setDeletedNodes } =
useOnNodesDelete();
const {
onNodesDelete,
onEdgesDelete,
isDeleteConnectionsModalVisible,
setIsDeleteConnectionsModalVisible,
onDeleteConfirmed,
onKeepItConfirmed,
hasConnectedNodes,
setDeletedNodes,
setNodeToDelete,
} = useOnNodesDelete();
const { setHasUnsavedChanges } = useServiceStore();

const onConnect = useCallback(({ source, target }: any) => {
Expand Down Expand Up @@ -60,9 +69,16 @@ const FlowBuilder: FC<FlowBuilderProps> = ({ nodes, edges }) => {
}, []);

const onBeforeDelete = useCallback(
async ({ nodes: nodesToDelete }: { nodes: Node[]; edges: Edge[] }) => {
async ({ nodes: nodesToDelete, edges: edgesToDelete }: { nodes: Node[]; edges: Edge[] }) => {
setDeletedNodes(null);
try {
if (edgesToDelete.length > 0 && nodesToDelete.length === 0) {
const shouldPreventDelete = getNode(edgesToDelete[0].source)?.data.stepType === StepType.MultiChoiceQuestion;
if (shouldPreventDelete) {
return false;
}
}

if (nodesToDelete.length === 0 ||
![StepType.MultiChoiceQuestion, StepType.Condition, StepType.Input]
.includes(nodesToDelete[0]?.data.stepType as StepType)) return true;
Expand Down Expand Up @@ -118,7 +134,10 @@ const FlowBuilder: FC<FlowBuilderProps> = ({ nodes, edges }) => {
<Controls orientation="horizontal" showInteractive={false} />
</ReactFlow>
{isDeleteConnectionsModalVisible && (
<Modal title={t("overview.popup.deleteNodeConnections")} onClose={onKeepItConfirmed}>
<Modal title={t("overview.popup.deleteNodeConnections")} onClose={() => {
setNodeToDelete(null);
setIsDeleteConnectionsModalVisible(false);
}}>
<Track justify="end" gap={16}>
<Button appearance="primary" onClick={onDeleteConfirmed}>
{t("global.delete")}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,18 @@ import { getDragData } from "utils/component-util";

interface AssignElementProps {
element: Assign;
onRemove: (id: string) => void;
onRemove?: (id: string) => void;
onChange: (element: Assign) => void;
manualEdit?: boolean;
isKeyEditable?: boolean;
keyStyle?: React.CSSProperties;
valueStyle?: React.CSSProperties;
}

const AssignElement: React.FC<AssignElementProps> = ({ element, onRemove, onChange }) => {
const AssignElement: React.FC<AssignElementProps> = ({ element, onRemove, onChange, manualEdit = false, isKeyEditable, keyStyle, valueStyle }) => {
const slots = element.slots ?? [];
const [isSecondSlotOpen, setIsSecondSlotOpen] = useState(!!slots[1]);
const [isEditingManually, setIsEditingManually] = useState(element.value && !slots.length);
const [isEditingManually, setIsEditingManually] = useState(manualEdit || element.value && !slots.length);

const changeKey = (e: React.ChangeEvent<HTMLInputElement>) => {
onChange({ ...element, key: e.target.value });
Expand Down Expand Up @@ -62,19 +66,22 @@ const AssignElement: React.FC<AssignElementProps> = ({ element, onRemove, onChan
<FormInput
value={element.key}
name="key"
disabled={isKeyEditable === false}
onChange={changeKey}
onDrop={(e) => e.preventDefault()}
style={keyStyle}
label=""
hideLabel
/>
:
<Track style={{ flex: "1 0 75%", justifyContent: "flex-end" }}>
<Track style={{ flex: "1 0 75%", justifyContent: "flex-end" }} gap={5}>
{isEditingManually ? (
<FormInput
value={element.value}
name="value"
onChange={changeValue}
label=""
style={valueStyle}
hideLabel
onDrop={changeManualInputValue}
/>
Expand Down Expand Up @@ -109,10 +116,11 @@ const AssignElement: React.FC<AssignElementProps> = ({ element, onRemove, onChan
</div>
</Tooltip>
) : null}

<button onClick={() => onRemove(element.id)} className="small-assign-button assign-red">
<Icon icon={<MdDeleteOutline />} />
</button>
{onRemove && (
<button onClick={() => onRemove(element.id)} className="small-assign-button assign-red">
<Icon icon={<MdDeleteOutline />} />
</button>
)}
</Track>
</Track>
);
Expand Down
94 changes: 94 additions & 0 deletions GUI/src/components/FlowElementsPopup/DynamicChoicesContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { FC } from "react";
import Track from "../Track";
import PreviousVariables from "./PreviousVariables";
import { DynamicChoices } from "types/dynamic-choices";
import AssignElement from "./AssignBuilder/assignElement";
import { MdOutlineInfo } from "react-icons/md";
import Tooltip from "components/Tooltip";
import { t } from "i18next";

type DynamicChoicesContentProps = {
readonly nodeId: string;
readonly dynamicChoices?: DynamicChoices;
readonly onDynamicChoicesChange?: (dynamicChoices: DynamicChoices) => void;
};

const fields: Array<{
key: keyof DynamicChoices;
label: string;
tooltip: string;
}> = [
{
key: "list",
label: t("serviceFlow.element.dynamicChoices.list"),
tooltip: t("serviceFlow.element.dynamicChoices.listTooltip"),
},
{
key: "serviceName",
label: t("serviceFlow.element.dynamicChoices.serviceName"),
tooltip: t("serviceFlow.element.dynamicChoices.serviceNameTooltip"),
},
{
key: "key",
label: t("serviceFlow.element.dynamicChoices.key"),
tooltip: t("serviceFlow.element.dynamicChoices.keyTooltip"),
},
{
key: "payloadKeys",
label: t("serviceFlow.element.dynamicChoices.payloadKeys"),
tooltip: t("serviceFlow.element.dynamicChoices.payloadKeysTooltip"),
},
];

const DynamicChoicesContent: FC<DynamicChoicesContentProps> = ({
nodeId,
dynamicChoices = {
list: "",
serviceName: "",
key: "",
payloadKeys: "",
},
onDynamicChoicesChange,
}) => {
const handleChange = (field: keyof DynamicChoices, value: string) => {
onDynamicChoicesChange?.({
...dynamicChoices,
[field]: value,
});
};

return (
<Track direction="vertical" align="stretch" gap={16} style={{ width: "100%" }}>
<Track direction="vertical" align="stretch" gap={16} style={{ padding: "16px", width: "100%" }}>
{fields.map((field) => (
<Track key={field.key} gap={8}>
<AssignElement
key={field.key}
manualEdit={true}
isKeyEditable={false}
keyStyle={{
textAlign: "center",
border: "0.5px",
backgroundColor: "#00f5",
fontSize: "14px",
}}
element={{
id: field.key,
key: field.label,
value: dynamicChoices[field.key],
tooltip: field.tooltip,
}}
onChange={(element) => handleChange(field.key, element.value)}
/>
<Tooltip content={field.tooltip}>
<MdOutlineInfo size={20} color={"#005aa3"} />
</Tooltip>
</Track>
))}
</Track>
<PreviousVariables nodeId={nodeId} />
</Track>
);
};

export default DynamicChoicesContent;
Loading
Loading