Skip to content
Open
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="20260526120000" author="1AhmedYasser">
<sqlFile path="changelog/migrations/20260526120000_add-jump-to-service-to-step-type-enum.sql" />
<rollback>
<sqlFile path="changelog/migrations/rollback/20260526120000_rollback.sql" />
</rollback>
</changeSet>
</databaseChangeLog>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-- liquibase formatted sql
-- changeset 1AhmedYasser:20260526120000

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

-- Remove the 'jump-to-service' value from the step_type enum
ALTER TYPE step_type DROP VALUE 'jump-to-service';
15 changes: 9 additions & 6 deletions DSL/Resql/services/POST/get-active-services-list.sql
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@

SELECT id,
name,
current_state AS state,
ruuter_type AS type
FROM services
SELECT service_id, name
FROM (
SELECT DISTINCT ON (service_id) service_id, name, current_state
FROM services
WHERE NOT deleted
AND is_common = false
ORDER BY service_id, id DESC
) latest
WHERE current_state = 'active'
ORDER BY id ASC;
ORDER BY name ASC;
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
Expand Up @@ -2,7 +2,7 @@
-- Uses WHERE NOT EXISTS to prevent race condition: if multiple requests arrive simultaneously
-- and both see no preferences, only one will insert, preventing duplicate records
INSERT INTO user_step_preference (user_id_code, steps)
SELECT :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}'::step_type[]
SELECT :user_id_code, '{assign,textfield,condition,multi-choice-question,dynamic-choices,finishing-step-end,jump-to-service,input,auth,open-webpage,file-generate,file-sign,finising-step-redirect,rasa-rules,siga}'::step_type[]
WHERE NOT EXISTS (
SELECT 1 FROM user_step_preference WHERE user_id_code = :user_id_code
)
5 changes: 2 additions & 3 deletions DSL/Resql/services/POST/update-user-step-preferences.sql
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
UPDATE user_step_preference
SET
steps = :steps::step_type[],
endpoints = :endpoints::uuid[]
WHERE user_id_code = :user_id_code;
steps = :steps::step_type[]
WHERE user_id_code = :user_id_code;
5 changes: 0 additions & 5 deletions DSL/Ruuter/services/POST/steps/preferences.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,10 @@ declaration:
- field: steps
type: string
description: "Body field 'steps'"
- field: endpoints
type: string
description: "Body field 'endpoints'"

extractRequestData:
assign:
steps: ${incoming.body.steps.join(",")}
endpoints: ${incoming.body.endpoints.join(",")}

get_user_info:
call: http.post
Expand Down Expand Up @@ -46,7 +42,6 @@ update_user_step_preferences:
url: "[#SERVICE_RESQL]/update-user-step-preferences"
body:
steps: "{${steps}}"
endpoints: "{${endpoints}}"
user_id_code: ${idCode}
result: update_preferences_res

Expand Down
39 changes: 39 additions & 0 deletions DSL/Ruuter/services/TEMPLATES/jump-to-service.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
declaration:
call: declare
version: 0.1
description: "Jump to another active service and pass input to it"
method: post
accepts: json
returns: json
namespace: service
allowlist:
body:
- field: serviceName
type: string
description: "Name of the target active service to trigger"
- field: input
type: object
description: "Key-value inputs to pass to the target service"

check_for_body:
switch:
- condition: ${incoming.body == null || incoming.body.serviceName == null || incoming.body.serviceName == ""}
next: missing_service_name
next: trigger_service

trigger_service:
call: http.post
args:
url: "[#SERVICE_RUUTER]/services/active/${incoming.body.serviceName}"
body:
input: ${incoming.body.input}
result: trigger_result

return_value:
return: ${trigger_result.response.body}
next: end

missing_service_name:
status: 400
return: 'serviceName - missing'
next: end
1 change: 1 addition & 0 deletions GUI/src/components/Flow/Controls/CopyPasteControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
return;
}

const selectedNodeIds = selectedNodes.map((node) => node.id);

Check warning on line 53 in GUI/src/components/Flow/Controls/CopyPasteControls.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

`selectedNodeIds` should be a `Set`, and use `selectedNodeIds.has()` to check existence or non-existence.

See more on https://sonarcloud.io/project/issues?id=buerokratt_Service-Module&issues=AZ5tVuJ8NX90yzli4Wow&open=AZ5tVuJ8NX90yzli4Wow&pullRequest=1040
const internalEdges = getEdges().filter(
(edge) => selectedNodeIds.includes(edge.source) && selectedNodeIds.includes(edge.target),
);
Expand Down Expand Up @@ -259,6 +259,7 @@
if (
stepType === StepType.FinishingStepEnd ||
stepType === StepType.FinishingStepRedirect ||
stepType === StepType.JumpToService ||
stepType === StepType.DynamicChoices ||
stepType === StepType.Condition ||
stepType === StepType.Input ||
Expand Down Expand Up @@ -352,7 +353,7 @@

if (isDialogOpen) return;

const isMac = navigator.userAgent.toUpperCase().indexOf('MAC') >= 0;

Check warning on line 356 in GUI/src/components/Flow/Controls/CopyPasteControls.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use `.includes()`, rather than `.indexOf()`, when checking for existence.

See more on https://sonarcloud.io/project/issues?id=buerokratt_Service-Module&issues=AZ5tVuJ8NX90yzli4Wox&open=AZ5tVuJ8NX90yzli4Wox&pullRequest=1040
const isCtrlOrCmd = isMac ? event.metaKey : event.ctrlKey;

if (isCtrlOrCmd && event.key === 'c' && !event.shiftKey) {
Expand Down
1 change: 1 addition & 0 deletions GUI/src/components/Flow/EdgeTypes/CustomEdge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ function CustomEdge({
StepType.MultiChoiceQuestion,
StepType.DynamicChoices,
StepType.FinishingStepEnd,
StepType.JumpToService,
];

if (allowedSteps.includes(preference as StepType)) {
Expand Down
3 changes: 3 additions & 0 deletions GUI/src/components/Flow/NodeTypes/StepNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,9 @@ const StepNode: FC<StepNodeProps> = ({ data }) => {
{data.stepType === StepType.FinishingStepRedirect && (
<p style={boldText}>{t('serviceFlow.popup.redirectToCustomerSupport')}</p>
)}
{data.stepType === StepType.JumpToService && data.jumpToService?.serviceName && (
<p style={boldText}>&rarr; {data.jumpToService.serviceName}</p>
)}
{data.stepType === StepType.Rule && (
<p>
{data.name && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const createNewElement = () => {
};

export const useAssignBuilder = ({ seedGroup, onChange }: UseAssignBuilderProps) => {
const [elements, setElements] = useState<Assign[]>(seedGroup);
const [elements, setElements] = useState<Assign[]>(seedGroup ?? []);

useEffect(() => {
onChange(elements);
Expand Down
89 changes: 89 additions & 0 deletions GUI/src/components/FlowElementsPopup/JumpToServiceContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { Node } from '@xyflow/react';
import { FormSelect } from 'components/FormElements';
import { FC, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { getActiveServicesList } from 'resources/api-constants';
import api from 'services/api-dev';
import { Assign } from 'types/assign';
import { JumpToService } from 'types/jump-to-service';
import { NodeDataProps } from 'types/service-flow';

import Track from '../Track';
import AssignBuilder from './AssignBuilder';
import PreviousVariables from './PreviousVariables';

type ServiceOption = { name: string; serviceId: string };

type JumpToServiceContentProps = {
readonly node: Node<NodeDataProps>;
readonly jumpToService: JumpToService;
readonly onChange: (value: JumpToService) => void;
};

const JumpToServiceContent: FC<JumpToServiceContentProps> = ({ node, jumpToService, onChange }) => {
const { t } = useTranslation();
const [services, setServices] = useState<ServiceOption[]>([]);
const [isLoading, setIsLoading] = useState(true);

useEffect(() => {
api
.get(getActiveServicesList())
.then((res) => {
const data: { serviceId: string; name: string }[] = Array.isArray(res.data) ? res.data : [];
setServices(data.map((s) => ({ name: s.name, serviceId: s.serviceId })));
})
.catch(console.error)
.finally(() => setIsLoading(false));
}, []);

const serviceOptions = services.map((s) => ({ label: s.name, value: { name: s.name, serviceId: s.serviceId } }));

const defaultServiceValue =
services.find((s) => s.serviceId === jumpToService.serviceId || s.name === jumpToService.serviceName) ?? null;

const handleServiceChange = (selection: { label: string; value: { name: string; serviceId: string } } | null) => {
if (!selection) return;
const service = services.find((s) => s.serviceId === selection.value.serviceId);
onChange({
...jumpToService,
serviceName: service?.name ?? selection.label,
serviceId: service?.serviceId,
});
};

const handleInputChange = (input: Assign[]) => {
onChange({ ...jumpToService, input });
};

return (
<Track direction="vertical" align="stretch" gap={16}>
<Track direction="vertical" align="stretch" gap={16} style={{ padding: '16px 16px 0' }}>
{isLoading ? (
<Track justify="center">
<div className="loader" />
</Track>
) : (
<>
<FormSelect
label={t('serviceFlow.element.jumpToService.targetService')}
name="targetService"
placeholder={t('serviceFlow.element.jumpToService.selectService')}
options={serviceOptions}
defaultValue={defaultServiceValue ?? undefined}
onSelectionChange={handleServiceChange}
/>
{services.length === 0 && (
<p style={{ color: '#888', fontSize: '13px' }}>
{t('serviceFlow.element.jumpToService.noActiveServices')}
</p>
)}
</>
)}
</Track>
<AssignBuilder seedGroup={jumpToService.input} onChange={handleInputChange} />
<PreviousVariables node={node} />
</Track>
);
};

export default JumpToServiceContent;
20 changes: 18 additions & 2 deletions GUI/src/components/FlowElementsPopup/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import useServiceListStore from 'store/services.store';
import useToastStore from 'store/toasts.store';
import { DynamicChoices } from 'types/dynamic-choices';
import { EndpointData } from 'types/endpoint';
import { JumpToService } from 'types/jump-to-service';
import { MultiChoiceQuestionButton } from 'types/multi-choice-question';
import { NodeDataProps } from 'types/service-flow';
import { getValueByPath } from 'utils/object-util';
Expand All @@ -31,14 +32,15 @@ import DynamicChoicesContent from './DynamicChoicesContent';
import EndConversationContent from './EndConversationContent';
import FileGenerateContent from './FileGenerateContent';
import FileSignContent from './FileSignContent';
import JumpToServiceContent from './JumpToServiceContent';
import MultiChoiceQuestionContent from './MultiChoiceQuestionContent';
import OpenWebPageContent from './OpenWebPageContent';
import OpenWebPageTestContent from './OpenWebPageTestContent';
import RasaRulesContent from './RasaRulesContent';
import TextfieldContent from './TextfieldContent';
import TextfieldTestContent from './TextfieldTestContent';
import { StepType } from '../../types';
import { getInitialGroup, GroupOrRule } from './RuleBuilder/types';
import TextfieldContent from './TextfieldContent';
import './styles.scss';

const FlowElementsPopup: React.FC = () => {
Expand Down Expand Up @@ -120,6 +122,9 @@ const FlowElementsPopup: React.FC = () => {
node?.data.dynamicChoices ?? defaultDynamicChoices,
);

const defaultJumpToService: JumpToService = useMemo(() => ({ serviceName: '', input: [] }), []);
const [jumpToService, setJumpToService] = useState<JumpToService>(node?.data.jumpToService ?? defaultJumpToService);

const [nodeEndpoint, setNodeEndpoint] = useState<EndpointData | undefined>(node?.data.endpoint);
const [title, setTitle] = useState(node?.data.label ?? '');
const [titleError, setTitleError] = useState<string | undefined>(undefined);
Expand Down Expand Up @@ -158,10 +163,14 @@ const FlowElementsPopup: React.FC = () => {
setDynamicChoices(node.data?.dynamicChoices ?? defaultDynamicChoices);
break;

case StepType.JumpToService:
setJumpToService(node.data?.jumpToService ?? defaultJumpToService);
break;

default:
break;
}
}, [defaultDynamicChoices, defaultMultiChoiceQuestionButtons, node, stepType]);
}, [defaultDynamicChoices, defaultJumpToService, defaultMultiChoiceQuestionButtons, node, stepType]);

if (!node) return <></>;

Expand All @@ -179,6 +188,7 @@ const FlowElementsPopup: React.FC = () => {
setMultiChoiceQuestionButtons(copyMcqButtons(defaultMultiChoiceQuestionButtons));
setIsSaveEnabled(true);
setDynamicChoices(defaultDynamicChoices);
setJumpToService(defaultJumpToService);
useServiceStore.getState().resetSelectedNode();
useServiceStore.getState().resetRules();
useServiceStore.getState().resetAssign();
Expand All @@ -204,6 +214,7 @@ const FlowElementsPopup: React.FC = () => {
}
: undefined,
dynamicChoices: node.data.stepType === StepType.DynamicChoices ? dynamicChoices : undefined,
jumpToService: node.data.stepType === StepType.JumpToService ? jumpToService : undefined,
endpoint: nodeEndpoint ?? node.data?.endpoint,
testingPassed: undefined,
childrenCount: node.data.stepType === StepType.MultiChoiceQuestion ? 1 : node.data.childrenCount,
Expand Down Expand Up @@ -506,6 +517,11 @@ const FlowElementsPopup: React.FC = () => {
onDynamicChoicesChange={setDynamicChoices}
/>
)}
{stepType === StepType.JumpToService && (
<DndProvider backend={HTML5Backend}>
<JumpToServiceContent node={node} jumpToService={jumpToService} onChange={setJumpToService} />
</DndProvider>
)}
{stepType === StepType.UserDefined && (
<ApiContent
node={node}
Expand Down
Loading
Loading