From 9f19ffabf9b0997ec44be7ef4a68080f933b2f21 Mon Sep 17 00:00:00 2001 From: ruwinirathnamalala Date: Thu, 19 Mar 2026 14:45:47 +0530 Subject: [PATCH 01/12] fix for bug - Clear current service state on browser back button press --- GUI/src/pages/ServiceFlowPage.tsx | 5 +++++ GUI/src/store/new-services.store.ts | 2 ++ 2 files changed, 7 insertions(+) diff --git a/GUI/src/pages/ServiceFlowPage.tsx b/GUI/src/pages/ServiceFlowPage.tsx index 33bd1f77..bf74f99f 100644 --- a/GUI/src/pages/ServiceFlowPage.tsx +++ b/GUI/src/pages/ServiceFlowPage.tsx @@ -46,6 +46,11 @@ const ServiceFlowPage: FC = () => { }; void loadData(); + + // Cleanup function to reset state when component unmounts (including browser back button) + return () => { + useServiceStore.getState().resetState(); + }; }, [id]); const edges = useServiceStore((state) => state.edges); diff --git a/GUI/src/store/new-services.store.ts b/GUI/src/store/new-services.store.ts index ae4e16cc..c39ea80d 100644 --- a/GUI/src/store/new-services.store.ts +++ b/GUI/src/store/new-services.store.ts @@ -391,6 +391,8 @@ const useServiceStore = create((set, get) => ({ serviceId: uuid(), description: '', slot: '', + examples: [], + entities: [], secrets: { prod: [], test: [] }, availableVariables: { prod: [], test: [] }, isCommon: false, From 54645fcb3f1cbf2a44bf5fdc73a6d0fe34de85c5 Mon Sep 17 00:00:00 2001 From: ruwinirathnamalala Date: Thu, 19 Mar 2026 16:57:08 +0530 Subject: [PATCH 02/12] Resolver SonarQubes issues in the PR --- GUI/src/pages/ServiceFlowPage.tsx | 6 +++--- GUI/src/store/new-services.store.ts | 33 +++++++++++++++-------------- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/GUI/src/pages/ServiceFlowPage.tsx b/GUI/src/pages/ServiceFlowPage.tsx index bf74f99f..3e354610 100644 --- a/GUI/src/pages/ServiceFlowPage.tsx +++ b/GUI/src/pages/ServiceFlowPage.tsx @@ -74,15 +74,15 @@ const ServiceFlowPage: FC = () => { }} saveOnClick={async () => { setHasUnsavedChanges(false); - if (!id) { + if (id) { + await useServiceStore.getState().loadService(id); + } else { const serviceId = useServiceStore.getState().serviceId; const serviceResponse = await useServiceStore.getState().loadService(serviceId); if (serviceResponse) { useServiceListStore.getState().setSelectedService(serviceResponse?.data); navigate(ROUTES.replaceWithId(ROUTES.EDITSERVICE_ROUTE, serviceId)); } - } else { - await useServiceStore.getState().loadService(id); } }} /> diff --git a/GUI/src/store/new-services.store.ts b/GUI/src/store/new-services.store.ts index c39ea80d..db190b81 100644 --- a/GUI/src/store/new-services.store.ts +++ b/GUI/src/store/new-services.store.ts @@ -229,7 +229,7 @@ const useServiceStore = create((set, get) => ({ unmarkAsNewService: () => set({ isNewService: false }), setServiceId: (id) => set({ serviceId: id }), setNodes: (nodes) => { - if (nodes instanceof Function) { + if (typeof nodes === 'function') { set((state) => { return { nodes: nodes(state.nodes), @@ -240,7 +240,7 @@ const useServiceStore = create((set, get) => ({ } }, setEdges: (edges) => { - if (edges instanceof Function) { + if (typeof edges === 'function') { set((state) => { return { edges: edges(state.edges), @@ -294,17 +294,18 @@ const useServiceStore = create((set, get) => ({ }); } - chips.push({ - name: 'Base Response', - value: `${endpoint?.name.replaceAll(' ', '_')}_res.response.body`, - data: `${endpoint?.name.replaceAll(' ', '_')}_res.response.body`, - }); - - chips.push({ - name: 'Status Code', - value: `${endpoint?.name.replaceAll(' ', '_')}_res.response.statusCodeValue`, - data: `${endpoint?.name.replaceAll(' ', '_')}_res.response.statusCodeValue`, - }); + chips.push( + { + name: 'Base Response', + value: `${endpoint?.name.replaceAll(' ', '_')}_res.response.body`, + data: `${endpoint?.name.replaceAll(' ', '_')}_res.response.body`, + }, + { + name: 'Status Code', + value: `${endpoint?.name.replaceAll(' ', '_')}_res.response.statusCodeValue`, + data: `${endpoint?.name.replaceAll(' ', '_')}_res.response.statusCodeValue`, + } + ); const variable: EndpointResponseVariable = { name: endpoint?.name ?? '', @@ -440,7 +441,7 @@ const useServiceStore = create((set, get) => ({ if (!nodes || nodes.length === 0) nodes = initialNodes; - if (!endpoints || !(endpoints instanceof Array)) endpoints = []; + if (!endpoints || !Array.isArray(endpoints)) endpoints = []; nodes = nodes.map((node: any) => { if (node.type !== 'custom') return node; @@ -679,7 +680,7 @@ const useServiceStore = create((set, get) => ({ title: i18next.t('newService.toast.missingFields'), message: i18next.t('newService.toast.serviceMissingFields'), }); - return Promise.reject(new Error(i18next.t('newService.toast.missingFields') ?? 'Error')); + throw new Error(i18next.t('newService.toast.missingFields') ?? 'Error'); } const { isNewService, onServiceSave } = get(); @@ -687,7 +688,7 @@ const useServiceStore = create((set, get) => ({ try { await onServiceSave(ServiceState.Ready); } catch (e: any) { - return Promise.reject(new Error(i18next.t('toast.cannot-save-flow') ?? (e?.message as string) ?? 'Error')); + throw new Error(i18next.t('toast.cannot-save-flow') ?? (e?.message as string) ?? 'Error'); } if (isNewService) { From 511fd4d7aaed9dc5cd4665a216b28f279347e3b0 Mon Sep 17 00:00:00 2001 From: ruwinirathnamalala Date: Fri, 20 Mar 2026 18:38:33 +0530 Subject: [PATCH 03/12] #932 bug fix - Broken Links & HTML Artifacts in Message to Client --- .../hbs/bot_responses_to_messages.handlebars | 2 +- .../components/Flow/NodeTypes/StepNode.tsx | 13 +++++++++++- GUI/src/components/Markdowify/index.tsx | 13 +++++++++++- GUI/src/components/chat/bot-message.tsx | 1 - GUI/src/components/chat/chat.module.scss | 20 +++++++++++++++++++ GUI/src/services/service-builder.ts | 6 +++--- GUI/src/utils/string-util.ts | 20 +++++++++++++++++++ 7 files changed, 68 insertions(+), 7 deletions(-) diff --git a/DSL/DMapper/services/hbs/bot_responses_to_messages.handlebars b/DSL/DMapper/services/hbs/bot_responses_to_messages.handlebars index aa023019..d0aacf50 100644 --- a/DSL/DMapper/services/hbs/bot_responses_to_messages.handlebars +++ b/DSL/DMapper/services/hbs/bot_responses_to_messages.handlebars @@ -2,7 +2,7 @@ {{#each data.botMessages}} { "chatId": "{{../data.chatId}}", - "content": "{{filterControlCharacters result}}", + "content": "{{{filterControlCharacters (escapeQuotes result)}}}", "buttons": "[{{#each ../data.buttons}}{\"title\": \"{{#if (eq title true)}}Yes{{else if (eq title false)}}No{{else}}{{{title}}}{{/if}}\",\"payload\": \"{{{payload}}}\"}{{#unless @last}},{{/unless}}{{/each}}]", "authorTimestamp": "{{../data.authorTimestamp}}", "authorId": "{{../data.authorId}}", diff --git a/GUI/src/components/Flow/NodeTypes/StepNode.tsx b/GUI/src/components/Flow/NodeTypes/StepNode.tsx index e33b8407..9aae6a26 100644 --- a/GUI/src/components/Flow/NodeTypes/StepNode.tsx +++ b/GUI/src/components/Flow/NodeTypes/StepNode.tsx @@ -3,10 +3,12 @@ import ExclamationBadge from 'components/ExclamationBadge'; import Track from 'components/Track'; import { FC, memo, useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import sanitizeHtml from 'sanitize-html'; import useServiceStore from 'store/new-services.store'; import { StepType } from 'types'; import { NodeDataProps } from 'types/service-flow'; import { validateStep } from 'utils/flow-utils'; +import { decodeHtmlEntities } from 'utils/string-util'; type StepNodeProps = { data: NodeDataProps; @@ -21,8 +23,17 @@ const StepNode: FC = ({ data }) => { fontWeight: 500, }; const createMarkup = (text: string) => { + const decodedText = decodeHtmlEntities(text); + const sanitizedText = sanitizeHtml(decodedText, { + allowedTags: ['a', 'b', 'strong', 'i', 'em', 'u', 'p', 'br', 'ul', 'ol', 'li', 'code'], + allowedAttributes: { + a: ['href', 'target', 'rel'], + }, + disallowedTagsMode: 'discard', + }); + return { - __html: text, + __html: sanitizedText, }; }; diff --git a/GUI/src/components/Markdowify/index.tsx b/GUI/src/components/Markdowify/index.tsx index e4b19252..7ff568a4 100644 --- a/GUI/src/components/Markdowify/index.tsx +++ b/GUI/src/components/Markdowify/index.tsx @@ -72,6 +72,16 @@ const LinkPreview: React.FC<{ const hasSpecialFormat = (m: string) => m.includes('\n\n') && m.indexOf('.') > 0 && m.indexOf(':') > m.indexOf('.'); +const htmlLinkToMarkdown = (value: string): string => + value.replaceAll( + /]*href\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))[^>]*>([\s\S]*?)<\/a>/gi, + (_, href1: string, href2: string, href3: string, label: string) => { + const href = (href1 ?? href2 ?? href3 ?? '').trim(); + const text = sanitizeHtml(label ?? '', { allowedTags: [], allowedAttributes: {} }).trim() || href; + return href ? `[${text}](${href})` : text; + }, + ); + function formatMessage(message?: string): string { const sanitizedMessage = sanitizeHtml(message ?? ''); @@ -87,8 +97,9 @@ function formatMessage(message?: string): string { dataImagePattern, (_, prefix, dataUrl) => `${prefix}[image](${dataUrl})`, ); + const markdownLinksMessage = htmlLinkToMarkdown(finalMessage); - return finalMessage + return markdownLinksMessage .replaceAll(/&#x([0-9A-F]+);/gi, (_, hex: string) => String.fromCharCode(parseInt(hex, 16))) .replaceAll('&', '&') .replaceAll('>', '>') diff --git a/GUI/src/components/chat/bot-message.tsx b/GUI/src/components/chat/bot-message.tsx index 068d6fda..db3441a2 100644 --- a/GUI/src/components/chat/bot-message.tsx +++ b/GUI/src/components/chat/bot-message.tsx @@ -22,7 +22,6 @@ interface ChatMessageProps { const BotMessage = ({ message }: ChatMessageProps) => { const renderContent = useCallback(() => { - if (message.message.startsWith('

')) return message.message.replace('

', '').replace('

', ''); return ; }, [message.message]); diff --git a/GUI/src/components/chat/chat.module.scss b/GUI/src/components/chat/chat.module.scss index 6de0ece8..c93a2966 100644 --- a/GUI/src/components/chat/chat.module.scss +++ b/GUI/src/components/chat/chat.module.scss @@ -141,6 +141,16 @@ .content { border-radius: 6px 48px 48px 29px; background-color: blue; + color: white; + + a { + color: #a8d4ff; + text-decoration: underline; + + &:visited { + color: #c8a8ff; + } + } } .icon { @@ -378,6 +388,16 @@ .main { .content { background-color: get-color(sapphire-blue-12); + color: var(--dark-bg-extra-light); + + a { + color: #a8d4ff; + text-decoration: underline; + + &:visited { + color: #c8a8ff; + } + } } } } diff --git a/GUI/src/services/service-builder.ts b/GUI/src/services/service-builder.ts index 27e70693..71368207 100644 --- a/GUI/src/services/service-builder.ts +++ b/GUI/src/services/service-builder.ts @@ -12,6 +12,7 @@ import { Assign } from 'types/assign'; import { EndpointData } from 'types/endpoint'; import { NodeDataProps } from 'types/service-flow'; import { + decodeHtmlEntities, getLastDigits, isNumericString, removeTrailingUnderscores, @@ -602,11 +603,10 @@ function handleTextField( }); const spacePlaceholder = '___SPACE___'; + const rawMessage = typeof parentNode.data.message === 'string' ? decodeHtmlEntities(parentNode.data.message) : ''; const markdownMessage = htmlToMarkdown .translate( - typeof parentNode.data.message === 'string' - ? parentNode.data.message.replace('{{', '${').replace('}}', '}').replaceAll(' ', spacePlaceholder) - : '', + rawMessage.replace('{{', '${').replace('}}', '}').replaceAll(' ', spacePlaceholder), ) .replaceAll(spacePlaceholder, ' ') .replaceAll(/\\([-~>[\]_*#().!`=<\\])/g, String.raw`\\$1`); diff --git a/GUI/src/utils/string-util.ts b/GUI/src/utils/string-util.ts index 291061fc..1d7957a9 100644 --- a/GUI/src/utils/string-util.ts +++ b/GUI/src/utils/string-util.ts @@ -151,3 +151,23 @@ export function removeWrapperQuotes(str: string): string { return str.substring(start, end + 1); } + +export function decodeHtmlEntities(value: string): string { + if (typeof value !== 'string' || value.length === 0) return value; + + if (typeof document !== 'undefined') { + const textarea = document.createElement('textarea'); + textarea.innerHTML = value; + return textarea.value; + } + + return value + .replaceAll(/&#x([0-9A-F]+);/gi, (_, hex: string) => String.fromCharCode(parseInt(hex, 16))) + .replaceAll(/&#(\d+);/g, (_, code: string) => String.fromCharCode(parseInt(code, 10))) + .replaceAll('&', '&') + .replaceAll('>', '>') + .replaceAll('<', '<') + .replaceAll('"', '"') + .replaceAll(''', "'") + .replaceAll(''', "'"); +} From 60dd974fffb28a9548712947bd10ace700d47e70 Mon Sep 17 00:00:00 2001 From: ruwinirathnamalala Date: Fri, 20 Mar 2026 19:39:57 +0530 Subject: [PATCH 04/12] PR sonar issues fixed --- GUI/src/components/Flow/NodeTypes/StepNode.tsx | 2 +- GUI/src/components/Markdowify/index.tsx | 6 +++--- GUI/src/components/chat/chat.module.scss | 8 ++++---- GUI/src/utils/string-util.ts | 10 +++++----- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/GUI/src/components/Flow/NodeTypes/StepNode.tsx b/GUI/src/components/Flow/NodeTypes/StepNode.tsx index 9aae6a26..ec2edd0e 100644 --- a/GUI/src/components/Flow/NodeTypes/StepNode.tsx +++ b/GUI/src/components/Flow/NodeTypes/StepNode.tsx @@ -63,7 +63,7 @@ const StepNode: FC = ({ data }) => { }, [data, endpoints]); useEffect(() => { - void updateIsTestedAndPassed(); + updateIsTestedAndPassed(); }, [updateIsTestedAndPassed]); return ( diff --git a/GUI/src/components/Markdowify/index.tsx b/GUI/src/components/Markdowify/index.tsx index 7ff568a4..9feed90b 100644 --- a/GUI/src/components/Markdowify/index.tsx +++ b/GUI/src/components/Markdowify/index.tsx @@ -74,7 +74,7 @@ const hasSpecialFormat = (m: string) => m.includes('\n\n') && m.indexOf('.') > 0 const htmlLinkToMarkdown = (value: string): string => value.replaceAll( - /]*href\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))[^>]*>([\s\S]*?)<\/a>/gi, + /]*href\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))[^>]*>((?:[^<]|<(?!\/a>))*)<\/a>/gi, (_, href1: string, href2: string, href3: string, label: string) => { const href = (href1 ?? href2 ?? href3 ?? '').trim(); const text = sanitizeHtml(label ?? '', { allowedTags: [], allowedAttributes: {} }).trim() || href; @@ -100,7 +100,7 @@ function formatMessage(message?: string): string { const markdownLinksMessage = htmlLinkToMarkdown(finalMessage); return markdownLinksMessage - .replaceAll(/&#x([0-9A-F]+);/gi, (_, hex: string) => String.fromCharCode(parseInt(hex, 16))) + .replaceAll(/&#x([0-9A-F]+);/gi, (_, hex: string) => String.fromCodePoint(Number.parseInt(hex, 16))) .replaceAll('&', '&') .replaceAll('>', '>') .replaceAll('<', '<') @@ -116,7 +116,7 @@ function formatMessage(message?: string): string { return `${prefix}${year}. `; } } - return `${prefix}${year}\\. `; + return String.raw`${prefix}${year}\. `; }) .replaceAll(/(?<=\n)\d+\.\s/g, hasSpecialFormat(finalMessage) ? '\n\n$&' : '$&') .replaceAll(/^(\s+)/g, (match) => match.replaceAll(' ', ' ')); diff --git a/GUI/src/components/chat/chat.module.scss b/GUI/src/components/chat/chat.module.scss index c93a2966..ba7fff03 100644 --- a/GUI/src/components/chat/chat.module.scss +++ b/GUI/src/components/chat/chat.module.scss @@ -233,7 +233,7 @@ pointer-events: none; user-select: none; background-color: #f5f5f5; - color: #999; + color: #696969; } } @@ -278,17 +278,17 @@ font-weight: bold; &.success { - color: rgb(0, 138, 0); + color: rgb(0, 100, 0); background: #0f02; } &.error { - color: rgb(170, 0, 0); + color: rgb(153, 0, 0); background: #f002; } &.info { - color: rgb(0, 0, 203); + color: rgb(0, 0, 153); background: #00f2; } diff --git a/GUI/src/utils/string-util.ts b/GUI/src/utils/string-util.ts index 1d7957a9..007fc034 100644 --- a/GUI/src/utils/string-util.ts +++ b/GUI/src/utils/string-util.ts @@ -24,7 +24,7 @@ export const templateToString = (value: string | number) => { }; export const toSnakeCase = (value: string) => { - return value.toLowerCase().trim().replace(/\s+/g, '_').replace(/-+/g, '_').replace(/_+/g, '_'); + return value.toLowerCase().trim().replaceAll(/\s+/g, '_').replaceAll(/-+/g, '_').replaceAll(/_+/g, '_'); }; export const fromSnakeCase = (value: string) => { @@ -96,7 +96,7 @@ export function removeNestedTemplates(str: string): string { changed = false; iterationCount++; - str = str.replace( + str = str.replaceAll( /\$\{([^${}]*)\$\{([^}]*)\}([^}]*)\}/g, (match: string, p1: string, p2: string, p3: string): string => { changed = true; @@ -116,7 +116,7 @@ export function removeNestedTemplates(str: string): string { changed = false; iterationCount++; - str = str.replace( + str = str.replaceAll( /\$\{([^{}]*)\{([^}]*)\}([^}]*)\}/g, (match: string, p1: string, p2: string, p3: string): string => { changed = true; @@ -162,8 +162,8 @@ export function decodeHtmlEntities(value: string): string { } return value - .replaceAll(/&#x([0-9A-F]+);/gi, (_, hex: string) => String.fromCharCode(parseInt(hex, 16))) - .replaceAll(/&#(\d+);/g, (_, code: string) => String.fromCharCode(parseInt(code, 10))) + .replaceAll(/&#x([0-9A-F]+);/gi, (_, hex: string) => String.fromCodePoint(Number.parseInt(hex, 16))) + .replaceAll(/&#(\d+);/g, (_, code: string) => String.fromCodePoint(parseInt(code, 10))) .replaceAll('&', '&') .replaceAll('>', '>') .replaceAll('<', '<') From 53e39eb5670c9ebec5b46dce7e04140e53afe4d6 Mon Sep 17 00:00:00 2001 From: ruwinirathnamalala Date: Fri, 20 Mar 2026 20:39:16 +0530 Subject: [PATCH 05/12] PR sonar issues fixed --- GUI/src/components/chat/chat.module.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GUI/src/components/chat/chat.module.scss b/GUI/src/components/chat/chat.module.scss index ba7fff03..ae998a29 100644 --- a/GUI/src/components/chat/chat.module.scss +++ b/GUI/src/components/chat/chat.module.scss @@ -283,12 +283,12 @@ } &.error { - color: rgb(153, 0, 0); + color: rgb(80, 0, 0); background: #f002; } &.info { - color: rgb(0, 0, 153); + color: rgb(0, 0, 80); background: #00f2; } From 4f72df388cd093834239d87df5ed7e37117495d8 Mon Sep 17 00:00:00 2001 From: ruwinirathnamalala Date: Fri, 20 Mar 2026 20:52:49 +0530 Subject: [PATCH 06/12] PR sonar issues fixed --- GUI/src/utils/string-util.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GUI/src/utils/string-util.ts b/GUI/src/utils/string-util.ts index 007fc034..92093dc7 100644 --- a/GUI/src/utils/string-util.ts +++ b/GUI/src/utils/string-util.ts @@ -163,7 +163,7 @@ export function decodeHtmlEntities(value: string): string { return value .replaceAll(/&#x([0-9A-F]+);/gi, (_, hex: string) => String.fromCodePoint(Number.parseInt(hex, 16))) - .replaceAll(/&#(\d+);/g, (_, code: string) => String.fromCodePoint(parseInt(code, 10))) + .replaceAll(/&#(\d+);/g, (_, code: string) => String.fromCodePoint(Number.parseInt(code, 10))) .replaceAll('&', '&') .replaceAll('>', '>') .replaceAll('<', '<') From 876a63136e42ec4652795fe132fe3c4a8daf61a6 Mon Sep 17 00:00:00 2001 From: ruwinirathnamalala Date: Mon, 23 Mar 2026 12:09:52 +0530 Subject: [PATCH 07/12] PR sonar issues fixed --- .../components/Flow/NodeTypes/StepNode.tsx | 2 +- GUI/src/components/Markdowify/index.tsx | 24 ++++++++++++------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/GUI/src/components/Flow/NodeTypes/StepNode.tsx b/GUI/src/components/Flow/NodeTypes/StepNode.tsx index ec2edd0e..9aae6a26 100644 --- a/GUI/src/components/Flow/NodeTypes/StepNode.tsx +++ b/GUI/src/components/Flow/NodeTypes/StepNode.tsx @@ -63,7 +63,7 @@ const StepNode: FC = ({ data }) => { }, [data, endpoints]); useEffect(() => { - updateIsTestedAndPassed(); + void updateIsTestedAndPassed(); }, [updateIsTestedAndPassed]); return ( diff --git a/GUI/src/components/Markdowify/index.tsx b/GUI/src/components/Markdowify/index.tsx index 9feed90b..55b72aba 100644 --- a/GUI/src/components/Markdowify/index.tsx +++ b/GUI/src/components/Markdowify/index.tsx @@ -72,15 +72,21 @@ const LinkPreview: React.FC<{ const hasSpecialFormat = (m: string) => m.includes('\n\n') && m.indexOf('.') > 0 && m.indexOf(':') > m.indexOf('.'); -const htmlLinkToMarkdown = (value: string): string => - value.replaceAll( - /]*href\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))[^>]*>((?:[^<]|<(?!\/a>))*)<\/a>/gi, - (_, href1: string, href2: string, href3: string, label: string) => { - const href = (href1 ?? href2 ?? href3 ?? '').trim(); - const text = sanitizeHtml(label ?? '', { allowedTags: [], allowedAttributes: {} }).trim() || href; - return href ? `[${text}](${href})` : text; - }, - ); +const htmlLinkToMarkdown = (value: string): string => { + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = value; + const links = tempDiv.querySelectorAll('a'); + + let result = value; + links.forEach((link) => { + const href = link.getAttribute('href') || ''; + const text = link.textContent || href; + const markdown = href ? `[${text}](${href})` : text; + result = result.replace(link.outerHTML, markdown); + }); + + return result; +}; function formatMessage(message?: string): string { const sanitizedMessage = sanitizeHtml(message ?? ''); From f09a79042e97bd6efddba06ebe236b26b2300671 Mon Sep 17 00:00:00 2001 From: ruwinirathnamalala Date: Mon, 23 Mar 2026 14:46:36 +0530 Subject: [PATCH 08/12] PR sonar issues fixed --- GUI/src/components/Flow/NodeTypes/StepNode.tsx | 4 +++- GUI/src/components/chat/chat.module.scss | 4 ++-- GUI/src/services/service-builder.ts | 18 +++++++++--------- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/GUI/src/components/Flow/NodeTypes/StepNode.tsx b/GUI/src/components/Flow/NodeTypes/StepNode.tsx index 9aae6a26..27eb39a7 100644 --- a/GUI/src/components/Flow/NodeTypes/StepNode.tsx +++ b/GUI/src/components/Flow/NodeTypes/StepNode.tsx @@ -63,7 +63,9 @@ const StepNode: FC = ({ data }) => { }, [data, endpoints]); useEffect(() => { - void updateIsTestedAndPassed(); + updateIsTestedAndPassed().catch(() => { + setIsTestedAndPassed(false); + }); }, [updateIsTestedAndPassed]); return ( diff --git a/GUI/src/components/chat/chat.module.scss b/GUI/src/components/chat/chat.module.scss index ae998a29..b7fc359c 100644 --- a/GUI/src/components/chat/chat.module.scss +++ b/GUI/src/components/chat/chat.module.scss @@ -284,12 +284,12 @@ &.error { color: rgb(80, 0, 0); - background: #f002; + background: #ffe3e3; } &.info { color: rgb(0, 0, 80); - background: #00f2; + background: #e3e8ff; } &.normal { diff --git a/GUI/src/services/service-builder.ts b/GUI/src/services/service-builder.ts index 71368207..92309009 100644 --- a/GUI/src/services/service-builder.ts +++ b/GUI/src/services/service-builder.ts @@ -189,7 +189,7 @@ export const saveFlow = async ({ const mcqNodes = nodes.filter( (node) => node.data?.stepType === StepType.MultiChoiceQuestion, - ) as Node[]; + ); if (mcqNodes.length > 0) { const nodesUpToFirstMcq = nodes.slice( @@ -421,7 +421,7 @@ export function getYamlContent( try { allRelations.forEach((r) => { const [parentNodeId, childNodeId] = r.split(','); - const parentNode = nodes.findLast((node) => node.id === parentNodeId) as Node | undefined; + const parentNode = nodes.findLast((node) => node.id === parentNodeId); if ( !parentNode?.type || parentNode.type !== 'custom' || @@ -430,7 +430,7 @@ export function getYamlContent( return; } - const childNode = nodes.find((node) => node.id === childNodeId) as Node | undefined; + const childNode = nodes.find((node) => node.id === childNodeId); const parentStepName = toSnakeCase(parentNode.data.label); if (parentNode.data.stepType === StepType.Textfield) { @@ -631,7 +631,7 @@ function handleTextField( function handleConditionStep( allRelations: any[], parentNodeId: any, - nodes: Node[], + nodes: Node[], parentNode: Node, finishedFlow: Map, parentStepName: string, @@ -640,8 +640,8 @@ function handleConditionStep( const firstChildNode = conditionRelations[0].split(',')[1]; const secondChildNode = conditionRelations[1].split(',')[1]; - const firstChild = nodes.find((node) => node.id === firstChildNode) as Node | undefined; - const secondChild = nodes.find((node) => node.id === secondChildNode) as Node | undefined; + const firstChild = nodes.find((node) => node.id === firstChildNode); + const secondChild = nodes.find((node) => node.id === secondChildNode); const rulesChildren = Array.isArray(parentNode.data.rules?.children) ? parentNode.data.rules.children : []; const invalidRulesExist = hasInvalidRules(rulesChildren); @@ -880,9 +880,9 @@ export const saveFlowClick = async (status: 'draft' | 'ready' = 'ready', showErr const nodes = useServiceStore.getState().nodes as Node[]; await saveFlow({ - name: !name - ? `${t('newService.defaultServiceName').toString()}_${format(new Date(), 'dd_MM_yyyy_HH_mm_ss')}` - : name, + name: name + ? name + : `${t('newService.defaultServiceName').toString()}_${format(new Date(), 'dd_MM_yyyy_HH_mm_ss')}`, edges, nodes, onSuccess: () => { From 8d731e7ac43211f1878dcd129ea6409ba3e57215 Mon Sep 17 00:00:00 2001 From: ruwinirathnamalala Date: Mon, 23 Mar 2026 15:13:28 +0530 Subject: [PATCH 09/12] PR sonar issues fixed --- GUI/src/services/service-builder.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/GUI/src/services/service-builder.ts b/GUI/src/services/service-builder.ts index 92309009..9bb8b98e 100644 --- a/GUI/src/services/service-builder.ts +++ b/GUI/src/services/service-builder.ts @@ -880,9 +880,7 @@ export const saveFlowClick = async (status: 'draft' | 'ready' = 'ready', showErr const nodes = useServiceStore.getState().nodes as Node[]; await saveFlow({ - name: name - ? name - : `${t('newService.defaultServiceName').toString()}_${format(new Date(), 'dd_MM_yyyy_HH_mm_ss')}`, + name: name || `${t('newService.defaultServiceName').toString()}_${format(new Date(), 'dd_MM_yyyy_HH_mm_ss')}`, edges, nodes, onSuccess: () => { From e470d5f1c1e120453061a85cc084b3912277077c Mon Sep 17 00:00:00 2001 From: ruwinirathnamalala Date: Thu, 26 Mar 2026 14:15:57 +0530 Subject: [PATCH 10/12] Fix for bug #944 MCQ nodes share button state causing errors --- .../components/FlowElementsPopup/index.tsx | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/GUI/src/components/FlowElementsPopup/index.tsx b/GUI/src/components/FlowElementsPopup/index.tsx index 9b76c784..39f1b3f3 100644 --- a/GUI/src/components/FlowElementsPopup/index.tsx +++ b/GUI/src/components/FlowElementsPopup/index.tsx @@ -85,6 +85,12 @@ const FlowElementsPopup: React.FC = () => { [], ); + // Copy buttons to avoid shared object references between popup state and node data. + const copyMcqButtons = (buttons: MultiChoiceQuestionButton[]) => + buttons.map((button) => ({ + ...button, + })); + // StepType.Textfield const [textfieldMessage, setTextfieldMessage] = useState(null); const [textfieldMessagePlaceholders, setTextfieldMessagePlaceholders] = useState<{ [key: string]: string }>({}); @@ -101,7 +107,7 @@ const FlowElementsPopup: React.FC = () => { node?.data.multiChoiceQuestion?.question ?? '', ); const [multiChoiceQuestionButtons, setMultiChoiceQuestionButtons] = useState( - node?.data.multiChoiceQuestion?.buttons ?? defaultMultiChoiceQuestionButtons, + copyMcqButtons(node?.data.multiChoiceQuestion?.buttons ?? defaultMultiChoiceQuestionButtons), ); const [dynamicChoices, setDynamicChoices] = useState( node?.data.dynamicChoices ?? defaultDynamicChoices, @@ -136,7 +142,9 @@ const FlowElementsPopup: React.FC = () => { case StepType.MultiChoiceQuestion: setMultiChoiceQuestionQuestion(node.data?.multiChoiceQuestion?.question ?? ''); - setMultiChoiceQuestionButtons(node.data?.multiChoiceQuestion?.buttons ?? defaultMultiChoiceQuestionButtons); + setMultiChoiceQuestionButtons( + copyMcqButtons(node.data?.multiChoiceQuestion?.buttons ?? defaultMultiChoiceQuestionButtons), + ); break; case StepType.DynamicChoices: @@ -163,7 +171,7 @@ const FlowElementsPopup: React.FC = () => { setFileContent(null); setTextfieldMessagePlaceholders({}); setMultiChoiceQuestionQuestion(''); - setMultiChoiceQuestionButtons(defaultMultiChoiceQuestionButtons); + setMultiChoiceQuestionButtons(copyMcqButtons(defaultMultiChoiceQuestionButtons)); setIsSaveEnabled(true); setDynamicChoices(defaultDynamicChoices); useServiceStore.getState().resetSelectedNode(); @@ -187,7 +195,7 @@ const FlowElementsPopup: React.FC = () => { node.data.stepType === StepType.MultiChoiceQuestion ? { question: multiChoiceQuestionQuestion, - buttons: multiChoiceQuestionButtons, + buttons: copyMcqButtons(multiChoiceQuestionButtons), } : undefined, dynamicChoices: node.data.stepType === StepType.DynamicChoices ? dynamicChoices : undefined, @@ -339,8 +347,10 @@ const FlowElementsPopup: React.FC = () => { const saveMultiChoicePopup = (originalNode: Node, updatedNode: Node) => { if (!instance) return; - const currentButtons = originalNode.data.multiChoiceQuestion?.buttons ?? defaultMultiChoiceQuestionButtons; - const newButtons = updatedNode.data.multiChoiceQuestion?.buttons ?? []; + const currentButtons = copyMcqButtons( + originalNode.data.multiChoiceQuestion?.buttons ?? defaultMultiChoiceQuestionButtons, + ); + const newButtons = copyMcqButtons(updatedNode.data.multiChoiceQuestion?.buttons ?? []); const edges = instance.getEdges(); const nodes = instance.getNodes(); @@ -362,7 +372,12 @@ const FlowElementsPopup: React.FC = () => { })); const updatedEdges = edges.map((edge) => { - if (!edge.label || !connectedEdges.some((ce) => ce.id === edge.id)) return edge; + if ( + !edge.label || + edge.source !== originalNode.id || + !connectedEdges.some((ce) => ce.id === edge.id) + ) + return edge; const rename = renamedButtons.find((r) => r.oldTitle === edge.label); if (rename) { return { ...edge, label: rename.newTitle }; @@ -387,10 +402,11 @@ const FlowElementsPopup: React.FC = () => { const buttonsNeedingEdges = addedButtons.filter((btn) => !existingButtonTitles.has(btn.title)); const newEdges = buttonsNeedingEdges.map((button) => { + const ghostNodeId = `${originalNode.id}-ghost-${button.id}`; const newEdge: Edge = { id: `${originalNode.id}->${button.id}`, source: originalNode.id, - target: `ghost-${button.id}`, + target: ghostNodeId, type: 'step', animated: true, deletable: false, From 6cd10d9742ea8c66e6eba564708c91c7b4caa003 Mon Sep 17 00:00:00 2001 From: ruwinirathnamalala Date: Mon, 30 Mar 2026 17:56:10 +0530 Subject: [PATCH 11/12] PR sonar issues fixed --- GUI/src/components/FlowElementsPopup/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GUI/src/components/FlowElementsPopup/index.tsx b/GUI/src/components/FlowElementsPopup/index.tsx index 39f1b3f3..e9f34538 100644 --- a/GUI/src/components/FlowElementsPopup/index.tsx +++ b/GUI/src/components/FlowElementsPopup/index.tsx @@ -225,7 +225,7 @@ const FlowElementsPopup: React.FC = () => { }; const prepareAssignForSaving = (updatedNode: Node) => { - const flatEndpointVariables = endpointsVariables.map((endpoint) => endpoint.chips).flat(); + const flatEndpointVariables = endpointsVariables.flatMap((endpoint) => endpoint.chips); assignElements.forEach((element) => { const key = removeTrailingUnderscores(element.key); element.key = key; From 5431d6f1c47bf06ee9b4d5428e2d2f2924966736 Mon Sep 17 00:00:00 2001 From: ruwinirathnamalala Date: Mon, 30 Mar 2026 18:33:31 +0530 Subject: [PATCH 12/12] Fix for bug #946 and #947 --- DSL/Ruuter/services/POST/services/edit.yml | 29 ++++++++++++++++++++-- GUI/src/services/service-builder.ts | 9 ++++--- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/DSL/Ruuter/services/POST/services/edit.yml b/DSL/Ruuter/services/POST/services/edit.yml index 92346663..a2d661d4 100644 --- a/DSL/Ruuter/services/POST/services/edit.yml +++ b/DSL/Ruuter/services/POST/services/edit.yml @@ -95,7 +95,17 @@ check_name_exists_result: switch: - condition: ${name_exists_res.response.body[0].nameExists} next: return_name_already_exists - next: delete_all_mcq_files + next: delete_old_mcq_files + +delete_old_mcq_files: + call: http.post + args: + url: "[#SERVICE_DMAPPER]/file-manager/delete-all-that-starts-with" + body: + path: "[#RUUTER_SERVICES_PATH]/${type}/services/draft" + keyword: "${get_service_result.response.body[0].name}_" + result: deleteOldRes + next: delete_all_mcq_files delete_all_mcq_files: call: http.post @@ -196,13 +206,28 @@ check_new_structure: use_new_structure: assign: new_structure: ${structure} - next: rename_dsl + next: check_rename_or_delete use_old_structure: assign: new_structure: ${old_structure.value} + next: check_rename_or_delete + +check_rename_or_delete: + switch: + - condition: ${content !== null && old_name !== name} + next: delete_old_main_file next: rename_dsl +delete_old_main_file: + call: http.post + args: + url: "[#SERVICE_DMAPPER]/file-manager/delete" + body: + file_path: "[#RUUTER_SERVICES_PATH]/${type}/[#RUUTER_SERVICES_DIR_PATH]/${old_state}/${old_name}.tmp" + result: delete_old_main_result + next: service_edit + rename_dsl: call: http.post args: diff --git a/GUI/src/services/service-builder.ts b/GUI/src/services/service-builder.ts index 27e70693..cfb34331 100644 --- a/GUI/src/services/service-builder.ts +++ b/GUI/src/services/service-builder.ts @@ -750,9 +750,12 @@ function handleMultiChoiceQuestion( childNode: Node | undefined, serviceName: string, ) { - parentNode.data.multiChoiceQuestion?.buttons.forEach( - (b) => (b.payload = b.payload.replaceAll('/_mcq_', `/${serviceName}_mcq_`)), - ); + const rootServiceName = serviceName.replace(/_mcq_\d+_\d+$/, ''); + parentNode.data.multiChoiceQuestion?.buttons.forEach((b) => { + + b.payload = b.payload.replace(/\/[^/]*_mcq_/, `/${rootServiceName}_mcq_`); + + }); return finishedFlow.set(parentStepName, { assign: {