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
Expand Up @@ -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}}",
Expand Down
29 changes: 27 additions & 2 deletions DSL/Ruuter/services/POST/services/edit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
17 changes: 15 additions & 2 deletions GUI/src/components/Flow/NodeTypes/StepNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -21,8 +23,17 @@ const StepNode: FC<StepNodeProps> = ({ 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,
};
};

Expand Down Expand Up @@ -52,7 +63,9 @@ const StepNode: FC<StepNodeProps> = ({ data }) => {
}, [data, endpoints]);

useEffect(() => {
void updateIsTestedAndPassed();
updateIsTestedAndPassed().catch(() => {
setIsTestedAndPassed(false);
});
}, [updateIsTestedAndPassed]);

return (
Expand Down
34 changes: 25 additions & 9 deletions GUI/src/components/FlowElementsPopup/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string | null>(null);
const [textfieldMessagePlaceholders, setTextfieldMessagePlaceholders] = useState<{ [key: string]: string }>({});
Expand All @@ -101,7 +107,7 @@ const FlowElementsPopup: React.FC = () => {
node?.data.multiChoiceQuestion?.question ?? '',
);
const [multiChoiceQuestionButtons, setMultiChoiceQuestionButtons] = useState<MultiChoiceQuestionButton[]>(
node?.data.multiChoiceQuestion?.buttons ?? defaultMultiChoiceQuestionButtons,
copyMcqButtons(node?.data.multiChoiceQuestion?.buttons ?? defaultMultiChoiceQuestionButtons),
);
const [dynamicChoices, setDynamicChoices] = useState<DynamicChoices>(
node?.data.dynamicChoices ?? defaultDynamicChoices,
Expand Down Expand Up @@ -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:
Expand All @@ -163,7 +171,7 @@ const FlowElementsPopup: React.FC = () => {
setFileContent(null);
setTextfieldMessagePlaceholders({});
setMultiChoiceQuestionQuestion('');
setMultiChoiceQuestionButtons(defaultMultiChoiceQuestionButtons);
setMultiChoiceQuestionButtons(copyMcqButtons(defaultMultiChoiceQuestionButtons));
setIsSaveEnabled(true);
setDynamicChoices(defaultDynamicChoices);
useServiceStore.getState().resetSelectedNode();
Expand All @@ -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,
Expand Down Expand Up @@ -217,7 +225,7 @@ const FlowElementsPopup: React.FC = () => {
};

const prepareAssignForSaving = (updatedNode: Node<NodeDataProps>) => {
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;
Expand Down Expand Up @@ -339,8 +347,10 @@ const FlowElementsPopup: React.FC = () => {
const saveMultiChoicePopup = (originalNode: Node<NodeDataProps>, updatedNode: Node<NodeDataProps>) => {
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();
Expand All @@ -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 };
Expand All @@ -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,
Expand Down
23 changes: 20 additions & 3 deletions GUI/src/components/Markdowify/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,22 @@ const LinkPreview: React.FC<{

const hasSpecialFormat = (m: string) => m.includes('\n\n') && m.indexOf('.') > 0 && m.indexOf(':') > m.indexOf('.');

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 ?? '');

Expand All @@ -87,9 +103,10 @@ function formatMessage(message?: string): string {
dataImagePattern,
(_, prefix, dataUrl) => `${prefix}[image](${dataUrl})`,
);
const markdownLinksMessage = htmlLinkToMarkdown(finalMessage);

return finalMessage
.replaceAll(/&#x([0-9A-F]+);/gi, (_, hex: string) => String.fromCharCode(parseInt(hex, 16)))
return markdownLinksMessage
.replaceAll(/&#x([0-9A-F]+);/gi, (_, hex: string) => String.fromCodePoint(Number.parseInt(hex, 16)))
.replaceAll('&amp;', '&')
.replaceAll('&gt;', '>')
.replaceAll('&lt;', '<')
Expand All @@ -105,7 +122,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(' ', '&nbsp;'));
Expand Down
1 change: 0 additions & 1 deletion GUI/src/components/chat/bot-message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ interface ChatMessageProps {

const BotMessage = ({ message }: ChatMessageProps) => {
const renderContent = useCallback(() => {
if (message.message.startsWith('<p>')) return message.message.replace('<p>', '').replace('</p>', '');
return <Markdownify message={message.message} />;
}, [message.message]);

Expand Down
32 changes: 26 additions & 6 deletions GUI/src/components/chat/chat.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -223,7 +233,7 @@
pointer-events: none;
user-select: none;
background-color: #f5f5f5;
color: #999;
color: #696969;
}
}

Expand Down Expand Up @@ -268,18 +278,18 @@
font-weight: bold;

&.success {
color: rgb(0, 138, 0);
color: rgb(0, 100, 0);
background: #0f02;
}

&.error {
color: rgb(170, 0, 0);
background: #f002;
color: rgb(80, 0, 0);
background: #ffe3e3;
}

&.info {
color: rgb(0, 0, 203);
background: #00f2;
color: rgb(0, 0, 80);
background: #e3e8ff;
}

&.normal {
Expand Down Expand Up @@ -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;
}
}
}
}
}
Expand Down
11 changes: 8 additions & 3 deletions GUI/src/pages/ServiceFlowPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -69,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);
}
}}
/>
Expand Down
Loading