- {children}
+
+
+
+
+ {children}
{footer && (
- {footer}
+
+ {footer}
+
)}
diff --git a/GUI/src/hooks/flow/useEdgeAdd.ts b/GUI/src/hooks/flow/useEdgeAdd.ts
index bda5f544c..97402c756 100644
--- a/GUI/src/hooks/flow/useEdgeAdd.ts
+++ b/GUI/src/hooks/flow/useEdgeAdd.ts
@@ -25,7 +25,7 @@ function useEdgeAdd(id: string) {
label: nodeLabel,
onDelete: useServiceStore.getState().onDelete,
onEdit: useServiceStore.getState().handleNodeEdit,
- type: [StepType.FinishingStepEnd, StepType.FinishingStepRedirect].includes(stepType)
+ type: [StepType.DynamicChoices, StepType.FinishingStepEnd, StepType.FinishingStepRedirect].includes(stepType)
? "finishing-step"
: "step",
stepType: stepType,
@@ -35,6 +35,8 @@ function useEdgeAdd(id: string) {
},
className: [StepType.FinishingStepEnd, StepType.FinishingStepRedirect].includes(stepType)
? "finishing-step"
+ : [StepType.DynamicChoices].includes(stepType)
+ ? "dynamic-choices"
: "step",
type: "custom",
};
@@ -49,7 +51,11 @@ function useEdgeAdd(id: string) {
let targetEdge: Edge | null = null;
- if (stepType != StepType.FinishingStepEnd && stepType != StepType.FinishingStepRedirect) {
+ if (
+ stepType != StepType.DynamicChoices &&
+ stepType != StepType.FinishingStepEnd &&
+ stepType != StepType.FinishingStepRedirect
+ ) {
targetEdge = {
id: `${newNodeId}->${edge.target}`,
source: newNodeId,
@@ -64,7 +70,7 @@ function useEdgeAdd(id: string) {
let ghostEdges: Edge[] = [];
if (stepType === StepType.MultiChoiceQuestion || stepType === StepType.Condition || stepType === StepType.Input) {
- const labels = stepType === StepType.MultiChoiceQuestion ? ["Yes", "No"] : ["Success", "Failure"];
+ const labels = stepType === StepType.MultiChoiceQuestion ? ["Jah", "Ei"] : ["Success", "Failure"];
ghostNodes = labels.slice(1).map((_, i) => ({
id: crypto.randomUUID(),
type: "ghost",
diff --git a/GUI/src/hooks/flow/useOnNodeDelete.ts b/GUI/src/hooks/flow/useOnNodeDelete.ts
index 3dc16de1f..692476607 100644
--- a/GUI/src/hooks/flow/useOnNodeDelete.ts
+++ b/GUI/src/hooks/flow/useOnNodeDelete.ts
@@ -202,5 +202,6 @@ const processDeletedNodes = (
onKeepItConfirmed,
hasConnectedNodes,
setDeletedNodes,
+ setNodeToDelete,
};
};
diff --git a/GUI/src/i18n/en/common.json b/GUI/src/i18n/en/common.json
index 8e555764b..652226fd1 100644
--- a/GUI/src/i18n/en/common.json
+++ b/GUI/src/i18n/en/common.json
@@ -285,7 +285,18 @@
"redirectConversationToSupport": "Direct to Customer Support",
"rules": "Rules",
"rasaRules": "Rasa Rules",
- "siga": "SiGa"
+ "siga": "SiGa",
+ "dynamicChoices": {
+ "title": "Dynamic Choices",
+ "list": "List",
+ "serviceName": "Service Name",
+ "key": "Key",
+ "payloadKeys": "Payload Keys",
+ "listTooltip": "The list needed to generate choices\nExample: [{\"name\":\"choice1\",\"price\":12},{\"name\":\"choice2\",\"price\":15}]",
+ "serviceNameTooltip": "The name of the service that will be executed when a choice is made\nExample: test_service",
+ "keyTooltip": "The key will be used as the title for each choice\nExample: name for the above list",
+ "payloadKeysTooltip": "Comma-separated keys to be the data sent to the service when a choice is made\nExample: name,price"
+ }
},
"popup": {
"messageLabel": "Message",
diff --git a/GUI/src/i18n/et/common.json b/GUI/src/i18n/et/common.json
index 5eb608e44..0c93c9278 100644
--- a/GUI/src/i18n/et/common.json
+++ b/GUI/src/i18n/et/common.json
@@ -285,7 +285,18 @@
"redirectConversationToSupport": "Klienditeenindusse suunamine",
"rules": "Reeglid",
"rasaRules": "Rasa reeglid",
- "siga": "Allkirjastamine"
+ "siga": "Allkirjastamine",
+ "dynamicChoices": {
+ "title": "Dünaamilised valikud",
+ "list": "Nimekiri",
+ "serviceName": "Teenuse nimi",
+ "key": "Võti",
+ "payloadKeys": "Andmete võtmed",
+ "listTooltip": "Valikute genereerimiseks vajalik nimekiri\nNäide: [{\"name\":\"choice1\",\"price\":12},{\"name\":\"choice2\",\"price\":15}]",
+ "serviceNameTooltip": "Teenuse nimi, mis valiku tegemisel käivitatakse\nNäide: test_service",
+ "keyTooltip": "Võtit kasutatakse iga valiku pealkirjana\nNäide: name for the above list",
+ "payloadKeysTooltip": "Komadega eraldatud võtmed peavad olema andmed, mis saadetakse teenusele valiku tegemisel\nNäide: name,price"
+ }
},
"popup": {
"messageLabel": "Sõnum",
diff --git a/GUI/src/pages/ServiceFlowPage.scss b/GUI/src/pages/ServiceFlowPage.scss
index 82d3148ad..afd8d33fc 100644
--- a/GUI/src/pages/ServiceFlowPage.scss
+++ b/GUI/src/pages/ServiceFlowPage.scss
@@ -123,6 +123,11 @@
border: 1px solid get-color(jasper-3);
}
+ &.dynamic-choices {
+ background-color: get-color(purple-1);
+ border: 1px solid get-color(purple-3);
+ }
+
&.selected {
border: dashed 1px get-color(black-coral-1);
}
diff --git a/GUI/src/services/flow-builder.ts b/GUI/src/services/flow-builder.ts
index 62ce323e0..4c2701da2 100644
--- a/GUI/src/services/flow-builder.ts
+++ b/GUI/src/services/flow-builder.ts
@@ -280,252 +280,6 @@ const buildRuleEdges = ({
];
};
-export const onNodeDrag = (_event: React.MouseEvent, draggedNode: Node) => {
- // Move the placeholder together with the node being moved
- const edges = useServiceStore.getState().edges;
- const nodes = useServiceStore.getState().nodes;
-
- const draggedEdges = edges.filter((edge) => edge.source === draggedNode.id);
- if (draggedEdges.length === 0) return;
- const placeholders = nodes.filter(
- (node) => draggedEdges.map((edge) => edge.target).includes(node.id) && node.type === "placeholder"
- );
- // only drag placeholders following the node
- if (placeholders.length === 0) return;
-
- useServiceStore.getState().setNodes((prevNodes) =>
- prevNodes.map((prevNode) => {
- const placeholderIndex = placeholders.findIndex((p) => p.id === prevNode.id);
- if (placeholderIndex >= 0) {
- const totalPlaceholders = placeholders.length;
- const baseY = draggedNode.position.y + EDGE_LENGTH * 1.5;
- const baseX = draggedNode.position.x;
- const widthOffset = (draggedNode.width ?? 0) * 0.75;
- const spacing = widthOffset * 1.7;
-
- if (totalPlaceholders === 1) {
- prevNode.position.x = baseX;
- prevNode.position.y = baseY + (draggedNode.height ?? 0);
- } else {
- const middleIndex = Math.floor(totalPlaceholders / 2);
- const offset = (placeholderIndex - middleIndex + (totalPlaceholders % 2 === 0 ? 0.5 : 0)) * spacing;
-
- prevNode.position.x = baseX + offset;
- prevNode.position.y = baseY;
- }
- }
- return prevNode;
- })
- );
-};
-
-export const onDrop = (
- event: React.DragEvent
,
- reactFlowWrapper: React.RefObject,
- setDefaultMessages: (stepType: StepType) => any
-) => {
- // Dragging and dropping the element from the list on the left
- // onto the placeholder node adds it to the flow
-
- const reactFlowInstance = useServiceStore.getState().reactFlowInstance;
-
- event.preventDefault();
- // Find matching placeholder
- if (!reactFlowInstance || !reactFlowWrapper.current) return;
-
- const [label, type, originalDefinedNodeId] = [
- event.dataTransfer.getData("application/reactflow-label"),
- event.dataTransfer.getData("application/reactflow-type") as StepType,
- event.dataTransfer.getData("application/reactflow-originalDefinedNodeId"),
- ];
-
- const position = reactFlowInstance.screenToFlowPosition({
- x: event.clientX,
- y: event.clientY,
- });
-
- const matchingPlaceholder = reactFlowInstance.getNodes().find((node) => {
- if (node.type !== "placeholder") return false;
- return (
- node.position.x <= position.x &&
- position.x <= node.position.x + node.width! &&
- node.position.y <= position.y &&
- position.y <= node.position.y + node.height!
- );
- });
- if (!matchingPlaceholder) return;
- const connectedNodeEdge = reactFlowInstance.getEdges().find((edge) => edge.target === matchingPlaceholder.id);
- if (!connectedNodeEdge) return;
-
- useServiceStore.getState().setNodes((prevNodes) => {
- const newNodeId =
- prevNodes.length > 2
- ? `${Math.max(...useServiceStore.getState().nodes.map((node) => +node.id)) + 1}`
- : matchingPlaceholder.id;
- const newPlaceholderId = Math.max(...useServiceStore.getState().nodes.map((node) => +node.id)) + 2;
-
- const baseY = matchingPlaceholder.position.y + EDGE_LENGTH * 1.5;
- const baseX = matchingPlaceholder.position.x;
- const widthOffset = (matchingPlaceholder.width ?? 0) * 0.75;
-
- useServiceStore.getState().setEdges((prevEdges) => {
- // Point edge from previous node to new node
- const newEdges = [...prevEdges];
- let matchingPlaceholderNextNodeId = undefined;
-
- // Point edge from matching placeholder to new node
- if (prevNodes.length > 2) {
- newEdges.push(
- buildEdge({
- id: `edge-${matchingPlaceholder.id}-${newNodeId + 1}`,
- source: matchingPlaceholder.id,
- sourceHandle: `handle-${matchingPlaceholder.id}-${newNodeId}`,
- target: newNodeId,
- })
- );
-
- // Check if the new node is added in between two nodes
- const previousEdgesOfMatchingPlaceholder = newEdges.filter(
- (edge) => edge.source === matchingPlaceholder.id
- ).length;
- if (previousEdgesOfMatchingPlaceholder > 1) {
- matchingPlaceholderNextNodeId = newEdges.find((edge) => edge.source === matchingPlaceholder.id)?.target;
- newEdges.splice(
- newEdges.findIndex((edge) => edge.source === matchingPlaceholder.id),
- 1
- );
- }
- }
- // Point edge from new node to new placeholder
- newEdges.push(
- buildEdge({
- id: `edge-${newNodeId}-${newPlaceholderId + 1}`,
- source: newNodeId,
- sourceHandle: `handle-${newNodeId}-0`,
- target: `${newPlaceholderId + 1}`,
- })
- );
-
- // In-case there is a node after the matching placeholder, point edge from new placeholder to that node
- if (matchingPlaceholderNextNodeId) {
- newEdges.push(
- buildEdge({
- id: `edge-${newPlaceholderId + 1}-${matchingPlaceholderNextNodeId}`,
- source: `${newPlaceholderId + 1}`,
- sourceHandle: `handle-${newNodeId}-0`,
- target: matchingPlaceholderNextNodeId,
- })
- );
- }
-
- if (type === StepType.Input || type === StepType.Condition || type === StepType.MultiChoiceQuestion) {
- newEdges.push(
- buildEdge({
- id: `edge-${newNodeId}-${newPlaceholderId + 2}`,
- source: newNodeId,
- sourceHandle: `handle-${newNodeId}-1`,
- target: `${newPlaceholderId + 2}`,
- })
- );
- }
-
- return newEdges;
- });
-
- const nodeLabel = getNodeLabel(type, prevNodes, label);
-
- const matchingPlaceholderIndex = prevNodes.findIndex((node) => node.id === matchingPlaceholder.id);
-
- // Add new node in place of old placeholder
- const previousNodes = [...prevNodes.slice(0, matchingPlaceholderIndex + 1)];
- const nextNodes = [...prevNodes.slice(matchingPlaceholderIndex + 1)];
- nextNodes.forEach((node) => {
- node.position.y += EDGE_LENGTH * 1.5;
- });
- const id = parseInt(nodeLabel.split("-").pop()?.trim() ?? '1');
- const newNodes = [
- ...previousNodes,
- {
- id: `${newNodeId}`,
- position:
- prevNodes.length > 2
- ? {
- y: matchingPlaceholder.position.y + EDGE_LENGTH,
- x: matchingPlaceholder.position.x,
- }
- : matchingPlaceholder.position,
- type: "custom",
- data: {
- label: nodeLabel,
- onDelete: useServiceStore.getState().onDelete,
- onEdit: useServiceStore.getState().handleNodeEdit,
- type: [StepType.FinishingStepEnd, StepType.FinishingStepRedirect].includes(type) ? "finishing-step" : "step",
- stepType: type,
- clientInputId: type === StepType.Input ? id : undefined,
- conditionId: type === StepType.Condition ? id : undefined,
- multiChoiceQuestionId:
- type === StepType.MultiChoiceQuestion ? id : undefined,
- assignId: type === StepType.Assign ? id : undefined,
- readonly: [
- StepType.Auth,
- StepType.FinishingStepEnd,
- StepType.FinishingStepRedirect,
- ].includes(type),
- childrenCount: type === StepType.Input || type === StepType.Condition || type === StepType.MultiChoiceQuestion ? 2 : 1,
- setClickedNode: useServiceStore.getState().setClickedNode,
- message: setDefaultMessages(type),
- originalDefinedNodeId: type === StepType.UserDefined ? originalDefinedNodeId : undefined,
- },
- className: [StepType.FinishingStepEnd, StepType.FinishingStepRedirect].includes(type)
- ? "finishing-step"
- : "step",
- },
- ...nextNodes,
- ];
-
- if (
- ![StepType.Input, StepType.Condition, StepType.MultiChoiceQuestion, StepType.FinishingStepEnd, StepType.FinishingStepRedirect].includes(type)
- ) {
- // Add placeholder right below new node
- newNodes.push(
- buildPlaceholder({
- id: `${newPlaceholderId + 1}`,
- matchingPlaceholder,
- position: {
- y: matchingPlaceholder.position.y + EDGE_LENGTH * 1.5,
- x: matchingPlaceholder.position.x,
- },
- })
- );
- }
-
- if ([StepType.MultiChoiceQuestion, StepType.Input, StepType.Condition].includes(type)) {
- const labels =
- type === StepType.MultiChoiceQuestion
- ? ["Yes", "No"]
- : ["serviceFlow.placeholderNodeSuccess", "serviceFlow.placeholderNodeFailure"];
-
- const middleIndex = Math.floor(labels.length / 2);
- const spacing = widthOffset * 1.7;
-
- labels.forEach((label, index) => {
- const offset = (index - middleIndex + (labels.length % 2 === 0 ? 0.5 : 0)) * spacing;
- newNodes.push(
- buildPlaceholder({
- id: `${newPlaceholderId + (index + 1)}`,
- label: label,
- position: { y: baseY, x: baseX + offset },
- })
- );
- });
- }
-
- return newNodes;
- });
-
- useServiceStore.getState().disableTestButton();
-};
-
export const onFlowNodeDragStop = (
event: any,
draggedNode: Node,
@@ -598,19 +352,3 @@ export const onFlowNodeDragStop = (
});
startDragNode.current = undefined;
};
-function getNodeLabel(type: StepType, nodes: Node[], label: string) {
- const prevNodes = nodes.filter((node) => node.data.stepType === type);
- const lastNode = prevNodes[prevNodes.length - 1]?.data;
- switch (type) {
- case StepType.Input:
- return `${label} - ${(lastNode?.clientInputId ?? 0) + 1}`;
- case StepType.Condition:
- return `${label} - ${(lastNode?.conditionId ?? 0) + 1}`;
- case StepType.MultiChoiceQuestion:
- return `${label} - ${(lastNode?.multiChoiceQuestionId ?? 0) + 1}`;
- case StepType.Assign:
- return `${label} - ${(lastNode?.assignId ?? 0) + 1}`;
- default:
- return label;
- }
-}
diff --git a/GUI/src/services/service-builder.ts b/GUI/src/services/service-builder.ts
index dd4588dd0..5780fa6d3 100644
--- a/GUI/src/services/service-builder.ts
+++ b/GUI/src/services/service-builder.ts
@@ -10,7 +10,7 @@ import { StepType } from "types";
import { EndpointData, EndpointVariableData } from "types/endpoint";
import api from "../services/api-dev";
import { NodeDataProps } from "types/service-flow";
-import { getLastDigits, removeTrailingUnderscores, toSnakeCase } from "utils/string-util";
+import { getLastDigits, removeTrailingUnderscores, stringToArray, toSnakeCase } from "utils/string-util";
import { format } from "date-fns";
import { AxiosError } from "axios";
@@ -353,6 +353,10 @@ function getYamlContent(nodes: Node[], edges: Edge[], name: string, description:
return handleMultiChoiceQuestion(finishedFlow, parentStepName, parentNode, childNode);
}
+ if (parentNode.data.stepType === StepType.DynamicChoices) {
+ return handleDynamicChoices(finishedFlow, parentStepName, parentNode, childNode);
+ }
+
if (parentNode.data.stepType === StepType.UserDefined) {
return handleEndpointStep(parentNode, finishedFlow, parentStepName, childNode);
}
@@ -529,7 +533,7 @@ function handleEndpointStep(
const methodType = endpointDefinition?.methodType?.toLowerCase();
const stepConfig: any = {
- call: `http.${methodType}`,
+ call: `http.${methodType ?? 'post'}`,
args: {
url: endpointDefinition?.url?.split("?")[0] ?? "",
},
@@ -578,6 +582,37 @@ function handleMultiChoiceQuestion(
});
}
+function handleDynamicChoices(
+ finishedFlow: Map,
+ parentStepName: string,
+ parentNode: Node,
+ childNode: Node | undefined
+) {
+ const list = parentNode.data.dynamicChoices?.list ?? "";
+ finishedFlow.set(parentStepName, {
+ call: "http.post",
+ args: {
+ url: "[#SERVICE_DMAPPER]/generate/buttons-list",
+ body: {
+ list: stringToArray(list, list),
+ service_name: parentNode.data.dynamicChoices?.serviceName ?? "",
+ key: parentNode.data.dynamicChoices?.key ?? "",
+ payload_prefix: "#service, /POST/",
+ payload_keys: parentNode.data.dynamicChoices?.payloadKeys.split(",") ?? [],
+ },
+ },
+ result: "dynamic_choices_res",
+ next: "assign_dynamic_choices_buttons",
+ });
+
+ return finishedFlow.set("assign_dynamic_choices_buttons", {
+ assign: {
+ buttons: "${dynamic_choices_res.response.body.response ?? []}",
+ },
+ next: childNode ? toSnakeCase(childNode.data.label ?? "format_messages") : "format_messages",
+ });
+}
+
const getMapEntry = (value: string) => {
const secrets = useServiceStore.getState().secrets;
diff --git a/GUI/src/store/new-services.store.ts b/GUI/src/store/new-services.store.ts
index bcebb015d..e1e7af5ee 100644
--- a/GUI/src/store/new-services.store.ts
+++ b/GUI/src/store/new-services.store.ts
@@ -716,7 +716,8 @@ const useServiceStore = create((set, get, store) => ({
prevNode.data.fileName != updatedNode.data.fileName ||
prevNode.data.fileContent != updatedNode.data.fileContent ||
prevNode.data.signOption != updatedNode.data.signOption ||
- prevNode.data.multiChoiceQuestion != updatedNode.data.multiChoiceQuestion
+ prevNode.data.multiChoiceQuestion != updatedNode.data.multiChoiceQuestion ||
+ prevNode.data.dynamicChoices != updatedNode.data.dynamicChoices
) {
useServiceStore.getState().disableTestButton();
}
@@ -731,6 +732,7 @@ const useServiceStore = create((set, get, store) => ({
fileContent: updatedNode.data.fileContent,
signOption: updatedNode.data.signOption,
multiChoiceQuestion: updatedNode.data.multiChoiceQuestion,
+ dynamicChoices: updatedNode.data.dynamicChoices,
endpoint: updatedNode.data.endpoint,
label: updatedNode.data.label,
},
diff --git a/GUI/src/styles/settings/variables/_colors.scss b/GUI/src/styles/settings/variables/_colors.scss
index 4380fbc01..e8bcdb91c 100644
--- a/GUI/src/styles/settings/variables/_colors.scss
+++ b/GUI/src/styles/settings/variables/_colors.scss
@@ -115,6 +115,29 @@ $veera-colors: (
sapphire-blue-19: #00111e,
sapphire-blue-20: #00090f,
+ // Purple
+ purple-0: #f2e7f9,
+ purple-1: #e5d0f3,
+ purple-2: #d8b9ed,
+ purple-3: #cba2e7,
+ purple-4: #be8be1,
+ purple-5: #b374db,
+ purple-6: #a65dd5,
+ purple-7: #9946cf,
+ purple-8: #8c2fd0,
+ purple-9: #7f18ca,
+ purple-10: #7201c4,
+ purple-11: #6a00b8,
+ purple-12: #6200a9,
+ purple-13: #5a009b,
+ purple-14: #52008d,
+ purple-15: #4a007f,
+ purple-16: #420071,
+ purple-17: #3a0063,
+ purple-18: #320055,
+ purple-19: #2a0047,
+ purple-20: #220039,
+
// Sea green
sea-green-0: #ecf4ef,
sea-green-1: #d9e9df,
diff --git a/GUI/src/types/dynamic-choices.ts b/GUI/src/types/dynamic-choices.ts
new file mode 100644
index 000000000..dbae53b25
--- /dev/null
+++ b/GUI/src/types/dynamic-choices.ts
@@ -0,0 +1,6 @@
+export type DynamicChoices = {
+ list: string;
+ serviceName: string;
+ key: string;
+ payloadKeys: string;
+};
diff --git a/GUI/src/types/service-flow.ts b/GUI/src/types/service-flow.ts
index 3fbe275e5..8f34adf72 100644
--- a/GUI/src/types/service-flow.ts
+++ b/GUI/src/types/service-flow.ts
@@ -1,3 +1,4 @@
+import { DynamicChoices } from "./dynamic-choices";
import { EndpointData } from "./endpoint";
import { MultiChoiceQuestion } from "./multi-choice-question";
import { StepType } from "./step-type.enum";
@@ -24,6 +25,7 @@ export type NodeDataProps = {
rules?: any;
assignElements?: any;
multiChoiceQuestion?: MultiChoiceQuestion;
+ dynamicChoices?: DynamicChoices;
childrenCount?: number;
endpoint?: EndpointData
};
diff --git a/GUI/src/types/step-type.enum.ts b/GUI/src/types/step-type.enum.ts
index 710918a1a..356af9703 100644
--- a/GUI/src/types/step-type.enum.ts
+++ b/GUI/src/types/step-type.enum.ts
@@ -16,4 +16,5 @@ export enum StepType {
UserDefined = "user-defined",
RasaRules = "rasa-rules",
SiGa = "siga",
+ DynamicChoices = "dynamic-choices",
}
diff --git a/GUI/src/types/step.ts b/GUI/src/types/step.ts
index e7c5504bc..9f2248eb8 100644
--- a/GUI/src/types/step.ts
+++ b/GUI/src/types/step.ts
@@ -27,4 +27,5 @@ export const stepsLabels: Record = {
[StepType.UserDefined]: "serviceFlow.element.userDefined",
[StepType.RasaRules]: "serviceFlow.element.rasaRules",
[StepType.SiGa]: "serviceFlow.element.siga",
+ [StepType.DynamicChoices]: "serviceFlow.element.dynamicChoices.title",
};
diff --git a/GUI/src/utils/string-util.ts b/GUI/src/utils/string-util.ts
index 23e3a1098..184735709 100644
--- a/GUI/src/utils/string-util.ts
+++ b/GUI/src/utils/string-util.ts
@@ -37,3 +37,15 @@ export const removeTrailingUnderscores = (value: string) => {
while (end > 0 && value[end - 1] === "_") end--;
return value.slice(0, end);
};
+
+export function stringToArray(str: string, fallback: any = []) {
+ try {
+ if (!str || typeof str !== "string" || str.trim() === "") {
+ return fallback;
+ }
+ const parsed = JSON.parse(str);
+ return Array.isArray(parsed) ? parsed : fallback;
+ } catch (e) {
+ return fallback;
+ }
+}