@adatechnology/conversations-ui 0.1.0-rc.26 → 0.1.0-rc.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/dist/chunk-DXPSPUWF.js +110 -0
  2. package/dist/{chunk-TV4OQRGH.js → chunk-UZJBYD5O.js} +123 -45
  3. package/dist/{chunk-2AYDBWNE.js → chunk-WCBDXZ3X.js} +13 -3
  4. package/dist/flows/index.d.ts +75 -2
  5. package/dist/flows/index.js +176 -95
  6. package/dist/index.d.ts +37 -10
  7. package/dist/index.js +694 -297
  8. package/dist/preview/index.js +3 -3
  9. package/dist/styles.css +119 -6
  10. package/package.json +2 -2
  11. package/src/AudioPlayer.tsx +8 -0
  12. package/src/AudioRecorderButton.test.tsx +3 -3
  13. package/src/AudioRecorderButton.tsx +3 -3
  14. package/src/AudioTranscription.tsx +4 -1
  15. package/src/Avatar.tsx +1 -1
  16. package/src/ConversationContextPanel.tsx +2 -2
  17. package/src/ConversationDocumentsPanel.tsx +11 -6
  18. package/src/ConversationHeader.test.tsx +66 -0
  19. package/src/ConversationHeader.tsx +147 -47
  20. package/src/ConversationListItem.tsx +3 -2
  21. package/src/ConversationRow.tsx +31 -7
  22. package/src/DocumentsLibrary.tsx +9 -4
  23. package/src/EmojiPicker.tsx +2 -1
  24. package/src/InteractiveMessage.tsx +3 -0
  25. package/src/Lightbox.tsx +1 -1
  26. package/src/MediaRenderer.tsx +2 -0
  27. package/src/MessageBubble.test.tsx +41 -0
  28. package/src/MessageBubble.tsx +27 -8
  29. package/src/MessageComposer.tsx +11 -8
  30. package/src/RichMessageComposer.test.tsx +42 -12
  31. package/src/RichMessageComposer.tsx +216 -45
  32. package/src/SimpleEmojiPicker.tsx +5 -3
  33. package/src/StatusTicks.tsx +1 -1
  34. package/src/Toast.tsx +4 -0
  35. package/src/Tooltip.test.ts +42 -0
  36. package/src/Tooltip.tsx +164 -0
  37. package/src/WhatsAppMessageEditor.tsx +4 -4
  38. package/src/WindowExpiredNotice.tsx +12 -4
  39. package/src/buildOutput.test.ts +79 -0
  40. package/src/composer.constant.ts +33 -0
  41. package/src/conversationWindow.ts +7 -5
  42. package/src/documents/DocumentsWorkspace.tsx +10 -6
  43. package/src/flows/FlowGroupHeader.tsx +2 -2
  44. package/src/flows/FlowMapNode.tsx +2 -1
  45. package/src/flows/FlowNodeCard.tsx +3 -3
  46. package/src/flows/FlowNodePanel.tsx +6 -5
  47. package/src/flows/FlowPalette.tsx +7 -1
  48. package/src/flows/FlowPortalNode.tsx +1 -1
  49. package/src/flows/FlowsWorkspace.tsx +69 -102
  50. package/src/flows/flowEditorOps.test.ts +241 -0
  51. package/src/flows/flowEditorOps.ts +177 -0
  52. package/src/flows/flowGraph.ts +1 -1
  53. package/src/flows/index.ts +16 -0
  54. package/src/flows/labels.ts +4 -0
  55. package/src/flows/workspaceContract.test.ts +95 -0
  56. package/src/hooks/useContainerWidth.ts +35 -0
  57. package/src/hooks/useConversationRealtime.ts +10 -8
  58. package/src/icon.constant.ts +12 -0
  59. package/src/index.ts +1 -0
  60. package/src/lib/composer-formatting.test.ts +78 -0
  61. package/src/lib/composer-formatting.ts +145 -0
  62. package/src/lib/whatsapp-formatting.test.tsx +37 -0
  63. package/src/lib/whatsapp-formatting.tsx +28 -3
  64. package/src/listing/index.tsx +5 -1
  65. package/src/pagination.constant.ts +10 -0
  66. package/src/preview/ConversationSimulatorPanel.tsx +1 -1
  67. package/src/providers/ConversationsProvider.tsx +8 -6
  68. package/src/settings/MessagesWorkspace.tsx +3 -0
  69. package/src/settings/TopicsForm.tsx +2 -0
  70. package/src/settings/TranscriptionSettingsForm.test.tsx +1 -1
  71. package/src/settings/TranscriptionSettingsForm.tsx +1 -0
  72. package/src/settings/WelcomeFarewellForm.tsx +1 -0
  73. package/src/settings/WhatsAppCreateTemplateForm.tsx +1 -0
  74. package/src/settings/WhatsAppTemplateSettingsForm.tsx +5 -2
  75. package/src/settings/WhatsAppTemplatesSettings.tsx +2 -0
  76. package/src/styles.css +100 -4
  77. package/src/workspace/BulkTemplateModal.tsx +4 -2
  78. package/src/workspace/ConversationPane.tsx +49 -19
  79. package/src/workspace/ConversationsInboxList.tsx +17 -8
  80. package/src/workspace/ConversationsWorkspace.tsx +24 -16
  81. package/src/workspace/useConversationsInbox.ts +5 -2
@@ -1,7 +1,10 @@
1
+ import {
2
+ TooltipLayer
3
+ } from "../chunk-DXPSPUWF.js";
1
4
  import {
2
5
  parseWhatsAppFormatting,
3
6
  useIsDarkTheme
4
- } from "../chunk-2AYDBWNE.js";
7
+ } from "../chunk-WCBDXZ3X.js";
5
8
 
6
9
  // src/flows/FlowNodeCard.tsx
7
10
  import { Handle, Position } from "@xyflow/react";
@@ -99,7 +102,7 @@ function FlowNodeCard({ data }) {
99
102
  return /* @__PURE__ */ jsxs(
100
103
  "div",
101
104
  {
102
- title: isDetached ? labels.detachedNodeTooltip : label,
105
+ "data-cv-tooltip": isDetached ? labels.detachedNodeTooltip : label,
103
106
  className: `relative rounded-lg border-2 px-3 py-2 w-60 cursor-pointer shadow-sm hover:shadow-md transition-shadow ${NODE_TYPE_COLOR[node.type]} ${isSelected ? "ring-2 ring-blue-500 ring-offset-2 dark:ring-offset-gray-900" : ""} ${isDetached ? "border-dashed !border-amber-400 animate-pulse" : ""}`,
104
107
  onClick: () => onSelect(node.id),
105
108
  children: [
@@ -107,7 +110,7 @@ function FlowNodeCard({ data }) {
107
110
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
108
111
  /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1.5 uppercase tracking-wide font-semibold text-gray-500 dark:text-gray-400 text-xs", children: [
109
112
  /* @__PURE__ */ jsx(Icon, { size: 12, strokeWidth: 2.5 }),
110
- isStart && /* @__PURE__ */ jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-emerald-500", title: labels.startNodeTooltip }),
113
+ isStart && /* @__PURE__ */ jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-emerald-500", "data-cv-tooltip": labels.startNodeTooltip }),
111
114
  labels.legend[node.type]
112
115
  ] }),
113
116
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
@@ -116,7 +119,7 @@ function FlowNodeCard({ data }) {
116
119
  liveCount > 0 && /* @__PURE__ */ jsx(
117
120
  "span",
118
121
  {
119
- title: labels.liveCountTooltip(liveCount),
122
+ "data-cv-tooltip": labels.liveCountTooltip(liveCount),
120
123
  className: "font-bold bg-blue-600 text-white rounded-full px-1.5 py-0.5 text-xs",
121
124
  children: liveCount
122
125
  }
@@ -181,6 +184,8 @@ var DEFAULT_FLOW_EDITOR_LABELS = {
181
184
  question: "Texto da pergunta",
182
185
  options: "Op\xE7\xF5es (choice)",
183
186
  addOption: "Adicionar op\xE7\xE3o",
187
+ removeOption: "Remover op\xE7\xE3o",
188
+ close: "Fechar painel",
184
189
  optionId: "Valor",
185
190
  optionLabel: "Texto exibido",
186
191
  next: "Pr\xF3ximo n\xF3",
@@ -323,12 +328,14 @@ function FlowMapNode({ data }) {
323
328
  /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-1.5 text-emerald-700 dark:text-emerald-300", children: [
324
329
  /* @__PURE__ */ jsx2(GitBranch2, { size: 14 }),
325
330
  /* @__PURE__ */ jsx2("span", { className: "text-sm font-semibold truncate", children: label }),
326
- isRoot && /* @__PURE__ */ jsx2("span", { className: "h-1.5 w-1.5 rounded-full bg-emerald-500 shrink-0", title: labels.startNodeTooltip })
331
+ isRoot && /* @__PURE__ */ jsx2("span", { className: "h-1.5 w-1.5 rounded-full bg-emerald-500 shrink-0", "data-cv-tooltip": labels.startNodeTooltip })
327
332
  ] }),
328
333
  /* @__PURE__ */ jsx2("p", { className: "text-xs text-gray-500 dark:text-gray-400 mt-1", children: labels.flowMap.nodeCount(nodeCount) }),
329
334
  /* @__PURE__ */ jsxs2(
330
335
  "button",
331
336
  {
337
+ "data-cv-tooltip": labels.flowMap.openFlow,
338
+ "aria-label": labels.flowMap.openFlow,
332
339
  type: "button",
333
340
  onClick: onOpen,
334
341
  className: "mt-2 inline-flex items-center gap-1 text-xs font-medium text-blue-600 dark:text-blue-400 hover:underline",
@@ -678,7 +685,8 @@ function FlowGroupHeader({ data }) {
678
685
  {
679
686
  type: "button",
680
687
  onClick: onFocus,
681
- title: labels.flowGroup.focus,
688
+ "data-cv-tooltip": labels.flowGroup.focus,
689
+ "aria-label": labels.flowGroup.focus,
682
690
  className: "hover:text-blue-600 dark:hover:text-blue-400",
683
691
  children: /* @__PURE__ */ jsx5(Maximize22, { size: 12 })
684
692
  }
@@ -688,7 +696,8 @@ function FlowGroupHeader({ data }) {
688
696
  {
689
697
  type: "button",
690
698
  onClick: onClose,
691
- title: labels.flowGroup.close,
699
+ "data-cv-tooltip": labels.flowGroup.close,
700
+ "aria-label": labels.flowGroup.close,
692
701
  className: "hover:text-red-600 dark:hover:text-red-400",
693
702
  children: /* @__PURE__ */ jsx5(X, { size: 12 })
694
703
  }
@@ -707,7 +716,8 @@ function FlowPortalNode({ data }) {
707
716
  "button",
708
717
  {
709
718
  type: "button",
710
- title: labels.crossFlowPortal.tooltip,
719
+ "data-cv-tooltip": labels.crossFlowPortal.tooltip,
720
+ "aria-label": labels.crossFlowPortal.tooltip,
711
721
  onClick: onNavigate,
712
722
  className: "relative flex items-center gap-1.5 rounded-full border-2 border-dashed border-cyan-400 dark:border-cyan-600 bg-cyan-50 dark:bg-cyan-950/40 px-3 py-1.5 text-xs font-medium text-cyan-700 dark:text-cyan-300 hover:bg-cyan-100 dark:hover:bg-cyan-950/70 transition-colors cursor-pointer",
713
723
  children: [
@@ -752,6 +762,8 @@ function FlowPalette({ onAdd, labels: labelsOverride, actionOptions }) {
752
762
  /* @__PURE__ */ jsxs6(
753
763
  "button",
754
764
  {
765
+ "data-cv-tooltip": labels.palette.title,
766
+ "aria-label": labels.palette.title,
755
767
  onClick: () => setOpen((v) => !v),
756
768
  className: "inline-flex items-center gap-1.5 rounded-lg bg-blue-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-blue-700",
757
769
  children: [
@@ -766,6 +778,8 @@ function FlowPalette({ onAdd, labels: labelsOverride, actionOptions }) {
766
778
  /* @__PURE__ */ jsxs6(
767
779
  "button",
768
780
  {
781
+ "data-cv-tooltip": labels.palette.question,
782
+ "aria-label": labels.palette.question,
769
783
  onMouseEnter: () => setSubmenu("question"),
770
784
  onClick: () => setSubmenu(submenu === "question" ? null : "question"),
771
785
  className: "w-full flex items-center justify-between gap-2 px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700",
@@ -782,6 +796,8 @@ function FlowPalette({ onAdd, labels: labelsOverride, actionOptions }) {
782
796
  submenu === "question" && /* @__PURE__ */ jsx7("div", { className: "absolute left-full top-0 ml-1 w-56 rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 shadow-lg py-1", children: QUESTION_TYPES.map((qt) => /* @__PURE__ */ jsx7(
783
797
  "button",
784
798
  {
799
+ "data-cv-tooltip": labels.questionTypeLabels[qt],
800
+ "aria-label": labels.questionTypeLabels[qt],
785
801
  onClick: () => select({ kind: "question", questionType: qt }),
786
802
  className: "w-full text-left px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700",
787
803
  children: labels.questionTypeLabels[qt]
@@ -792,6 +808,8 @@ function FlowPalette({ onAdd, labels: labelsOverride, actionOptions }) {
792
808
  /* @__PURE__ */ jsxs6(
793
809
  "button",
794
810
  {
811
+ "data-cv-tooltip": labels.palette.decision,
812
+ "aria-label": labels.palette.decision,
795
813
  onMouseEnter: () => setSubmenu(null),
796
814
  onClick: () => select({ kind: "decision" }),
797
815
  className: "w-full flex items-center gap-2 px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700",
@@ -808,7 +826,8 @@ function FlowPalette({ onAdd, labels: labelsOverride, actionOptions }) {
808
826
  onMouseEnter: () => setSubmenu(null),
809
827
  onClick: () => select({ kind: "condition" }),
810
828
  className: "w-full flex items-center gap-2 px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700",
811
- title: labels.palette.conditionHint,
829
+ "data-cv-tooltip": labels.palette.conditionHint,
830
+ "aria-label": labels.palette.conditionHint,
812
831
  children: [
813
832
  /* @__PURE__ */ jsx7(Diamond2, { size: 15, className: "text-cyan-500" }),
814
833
  " ",
@@ -820,6 +839,8 @@ function FlowPalette({ onAdd, labels: labelsOverride, actionOptions }) {
820
839
  /* @__PURE__ */ jsxs6(
821
840
  "button",
822
841
  {
842
+ "data-cv-tooltip": labels.palette.action,
843
+ "aria-label": labels.palette.action,
823
844
  onMouseEnter: () => setSubmenu("action"),
824
845
  onClick: () => setSubmenu(submenu === "action" ? null : "action"),
825
846
  className: "w-full flex items-center justify-between gap-2 px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700",
@@ -836,6 +857,8 @@ function FlowPalette({ onAdd, labels: labelsOverride, actionOptions }) {
836
857
  submenu === "action" && /* @__PURE__ */ jsx7("div", { className: "absolute left-full top-0 ml-1 w-64 rounded-xl border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 shadow-lg py-1", children: resolvedActionOptions.map((option) => /* @__PURE__ */ jsx7(
837
858
  "button",
838
859
  {
860
+ "data-cv-tooltip": option.label,
861
+ "aria-label": option.label,
839
862
  onClick: () => select({ kind: "action", actionKind: option.actionKind }),
840
863
  className: "w-full text-left px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700",
841
864
  children: option.label
@@ -1003,7 +1026,7 @@ function FlowNodePanel({
1003
1026
  return /* @__PURE__ */ jsxs8("div", { className: "fixed inset-y-0 right-0 w-96 bg-white dark:bg-gray-800 border-l border-gray-200 dark:border-gray-700 shadow-xl z-50 flex flex-col", children: [
1004
1027
  /* @__PURE__ */ jsxs8("div", { className: "flex items-center justify-between px-4 py-3 border-b border-gray-100 dark:border-gray-700", children: [
1005
1028
  /* @__PURE__ */ jsx9("h3", { className: "text-sm font-semibold text-gray-900 dark:text-gray-100", children: labels.nodePanel.title }),
1006
- /* @__PURE__ */ jsx9("button", { onClick: onClose, className: "text-gray-400 hover:text-gray-600", children: /* @__PURE__ */ jsx9(X2, { size: 18 }) })
1029
+ /* @__PURE__ */ jsx9("button", { "data-cv-tooltip": labels.nodePanel.close, "aria-label": labels.nodePanel.close, onClick: onClose, className: "text-gray-400 hover:text-gray-600", children: /* @__PURE__ */ jsx9(X2, { size: 18 }) })
1007
1030
  ] }),
1008
1031
  /* @__PURE__ */ jsxs8("div", { className: "flex-1 overflow-y-auto p-4 space-y-4", children: [
1009
1032
  nodeIssues.length > 0 && /* @__PURE__ */ jsx9("div", { className: "rounded-lg border border-gray-100 dark:border-gray-700 bg-gray-50 dark:bg-gray-900/40 p-2.5 space-y-1.5", children: nodeIssues.map((issue, i) => /* @__PURE__ */ jsx9(IssueRow, { issue }, i)) }),
@@ -1124,7 +1147,7 @@ function FlowNodePanel({
1124
1147
  (draft.questionType === "choice" || draft.type === "menu") && /* @__PURE__ */ jsxs8("div", { children: [
1125
1148
  /* @__PURE__ */ jsxs8("div", { className: "flex items-center justify-between mb-1.5", children: [
1126
1149
  /* @__PURE__ */ jsx9("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.options }),
1127
- /* @__PURE__ */ jsxs8("button", { onClick: addOption, className: "text-xs text-blue-600 hover:underline flex items-center gap-1", children: [
1150
+ /* @__PURE__ */ jsxs8("button", { "data-cv-tooltip": labels.nodePanel.addOption, "aria-label": labels.nodePanel.addOption, onClick: addOption, className: "text-xs text-blue-600 hover:underline flex items-center gap-1", children: [
1128
1151
  /* @__PURE__ */ jsx9(Plus2, { size: 12 }),
1129
1152
  " ",
1130
1153
  labels.nodePanel.addOption
@@ -1149,7 +1172,7 @@ function FlowNodePanel({
1149
1172
  className: `flex-1 ${INPUT_CLASSNAME}`
1150
1173
  }
1151
1174
  ),
1152
- /* @__PURE__ */ jsx9("button", { onClick: () => removeOption(i), className: "text-gray-400 hover:text-red-600", children: /* @__PURE__ */ jsx9(Trash2, { size: 14 }) })
1175
+ /* @__PURE__ */ jsx9("button", { "data-cv-tooltip": labels.nodePanel.removeOption, "aria-label": labels.nodePanel.removeOption, onClick: () => removeOption(i), className: "text-gray-400 hover:text-red-600", children: /* @__PURE__ */ jsx9(Trash2, { size: 14 }) })
1153
1176
  ] }, i)) })
1154
1177
  ] }),
1155
1178
  !isAction && /* @__PURE__ */ jsxs8("div", { className: "pt-2 border-t border-gray-100 dark:border-gray-700", children: [
@@ -1204,6 +1227,8 @@ function FlowNodePanel({
1204
1227
  /* @__PURE__ */ jsxs8(
1205
1228
  "button",
1206
1229
  {
1230
+ "data-cv-tooltip": labels.nodePanel.save,
1231
+ "aria-label": labels.nodePanel.save,
1207
1232
  onClick: () => onChange(draft),
1208
1233
  className: "flex-1 inline-flex items-center justify-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700",
1209
1234
  children: [
@@ -1219,12 +1244,13 @@ function FlowNodePanel({
1219
1244
  onClick: () => {
1220
1245
  if (window.confirm(labels.nodePanel.deleteConfirm)) onDelete(node.id);
1221
1246
  },
1222
- title: labels.nodePanel.delete,
1247
+ "data-cv-tooltip": labels.nodePanel.delete,
1248
+ "aria-label": labels.nodePanel.delete,
1223
1249
  className: "px-3 py-2 text-sm text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-950/30 rounded-lg",
1224
1250
  children: /* @__PURE__ */ jsx9(Trash2, { size: 14 })
1225
1251
  }
1226
1252
  ),
1227
- /* @__PURE__ */ jsx9("button", { onClick: onClose, className: "px-4 py-2 text-sm text-gray-600 dark:text-gray-300 hover:underline", children: labels.nodePanel.cancel })
1253
+ /* @__PURE__ */ jsx9("button", { "data-cv-tooltip": labels.nodePanel.cancel, "aria-label": labels.nodePanel.cancel, onClick: onClose, className: "px-4 py-2 text-sm text-gray-600 dark:text-gray-300 hover:underline", children: labels.nodePanel.cancel })
1228
1254
  ] })
1229
1255
  ] });
1230
1256
  }
@@ -1241,6 +1267,87 @@ import {
1241
1267
  } from "@xyflow/react";
1242
1268
  import "@xyflow/react/dist/style.css";
1243
1269
  import { Plus as Plus3, Trash2 as Trash22, LayoutGrid, AlertTriangle as AlertTriangle3, AlertCircle as AlertCircle3, Save as Save2, Undo2, Map as MapIcon, Workflow } from "lucide-react";
1270
+
1271
+ // src/flows/flowEditorOps.ts
1272
+ var NAMESPACE_SEPARATOR = "::";
1273
+ function namespaceNodeId(flowKey, nodeId) {
1274
+ return `${flowKey}${NAMESPACE_SEPARATOR}${nodeId}`;
1275
+ }
1276
+ function parseNamespacedId(value) {
1277
+ const index = value.indexOf(NAMESPACE_SEPARATOR);
1278
+ if (index === -1) return { flowKey: "", nodeId: value };
1279
+ return { flowKey: value.slice(0, index), nodeId: value.slice(index + NAMESPACE_SEPARATOR.length) };
1280
+ }
1281
+ function removeNodeAndCleanRefs(nodes, removedId) {
1282
+ const remaining = {};
1283
+ for (const [id, node] of Object.entries(nodes)) {
1284
+ if (id === removedId) continue;
1285
+ remaining[id] = { ...node, next: cleanNext(node.next, removedId) };
1286
+ }
1287
+ return remaining;
1288
+ }
1289
+ function cleanNext(next, removedId) {
1290
+ if (next === void 0) return void 0;
1291
+ if (typeof next === "string") return next === removedId ? "" : next;
1292
+ const byAnswer = {};
1293
+ for (const [answer, target] of Object.entries(next.byAnswer ?? {})) {
1294
+ byAnswer[answer] = target === removedId ? "" : target;
1295
+ }
1296
+ return { byAnswer, default: next.default === removedId ? "" : next.default ?? "" };
1297
+ }
1298
+ function mergedFlowKeysFrom(rootKey, graphs) {
1299
+ const visited = /* @__PURE__ */ new Set();
1300
+ const queue = [rootKey];
1301
+ while (queue.length > 0) {
1302
+ const key = queue.shift();
1303
+ if (visited.has(key) || !graphs[key]) continue;
1304
+ visited.add(key);
1305
+ for (const target of crossFlowTargetsOf(graphs[key])) {
1306
+ if (!visited.has(target) && graphs[target]) queue.push(target);
1307
+ }
1308
+ }
1309
+ return [...visited];
1310
+ }
1311
+ function resolveConnection(params) {
1312
+ const { source, target, sourceHandle } = params.connection;
1313
+ if (!source || !target || source === target) return void 0;
1314
+ const sourceRef = parseNamespacedId(source);
1315
+ const targetRef = parseNamespacedId(target);
1316
+ let targetValue;
1317
+ if (targetRef.flowKey === sourceRef.flowKey) {
1318
+ targetValue = targetRef.nodeId;
1319
+ } else {
1320
+ const targetGraph = params.graphs[targetRef.flowKey];
1321
+ if (!targetGraph || targetGraph.startNodeId !== targetRef.nodeId) return void 0;
1322
+ targetValue = `${CROSS_FLOW_PREFIX}${targetRef.flowKey}`;
1323
+ }
1324
+ return {
1325
+ flowKey: sourceRef.flowKey,
1326
+ nodeId: sourceRef.nodeId,
1327
+ handle: sourceHandle ?? "next",
1328
+ targetValue
1329
+ };
1330
+ }
1331
+ function applyConnection(node, resolved) {
1332
+ const currentNext = typeof node.next === "object" && node.next ? node.next : void 0;
1333
+ if (resolved.handle === "next") return { ...node, next: resolved.targetValue };
1334
+ if (resolved.handle === "__default") {
1335
+ return { ...node, next: { byAnswer: currentNext?.byAnswer ?? {}, default: resolved.targetValue } };
1336
+ }
1337
+ return {
1338
+ ...node,
1339
+ next: {
1340
+ byAnswer: { ...currentNext?.byAnswer ?? {}, [resolved.handle]: resolved.targetValue },
1341
+ default: currentNext?.default ?? ""
1342
+ }
1343
+ };
1344
+ }
1345
+ function isGraphDirty(working, published) {
1346
+ if (!working || !published) return false;
1347
+ return JSON.stringify(working) !== JSON.stringify(published);
1348
+ }
1349
+
1350
+ // src/flows/FlowsWorkspace.tsx
1244
1351
  import { Fragment as Fragment2, jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
1245
1352
  var RF_NODE_TYPES = {
1246
1353
  ...flowNodeTypes,
@@ -1251,7 +1358,6 @@ var RF_NODE_TYPES = {
1251
1358
  var CHAIN_FRAME_PADDING = 36;
1252
1359
  var COLUMN_GAP = 360;
1253
1360
  var ROW_GAP = 40;
1254
- var NS_SEP = "::";
1255
1361
  var FLOW_KEY_PATTERN = /^[a-z0-9_]{2,40}$/;
1256
1362
  var EDGE_COLOR_LINEAR = "#94a3b8";
1257
1363
  var EDGE_COLOR_BRANCH = "#8b5cf6";
@@ -1263,25 +1369,8 @@ var BACKGROUND_COLOR_DARK2 = "#334155";
1263
1369
  function portalNodeId(sourceId, target) {
1264
1370
  return `__portal__${sourceId}__${target}`;
1265
1371
  }
1266
- function ns(flowKey, nodeId) {
1267
- return `${flowKey}${NS_SEP}${nodeId}`;
1268
- }
1269
- function parseNs(id) {
1270
- const index = id.indexOf(NS_SEP);
1271
- return index === -1 ? { flowKey: "", nodeId: id } : { flowKey: id.slice(0, index), nodeId: id.slice(index + NS_SEP.length) };
1272
- }
1273
1372
  function autoMergeAll(rootKey, graphsSource) {
1274
- const visited = /* @__PURE__ */ new Set();
1275
- const queue = [rootKey];
1276
- while (queue.length > 0) {
1277
- const key = queue.shift();
1278
- if (visited.has(key) || !graphsSource[key]) continue;
1279
- visited.add(key);
1280
- for (const target of crossFlowTargetsOf(graphsSource[key])) {
1281
- if (!visited.has(target) && graphsSource[target]) queue.push(target);
1282
- }
1283
- }
1284
- return [...visited].map((key) => ({ key, offset: { x: 0, y: 0 } }));
1373
+ return mergedFlowKeysFrom(rootKey, graphsSource).map((key) => ({ key, offset: { x: 0, y: 0 } }));
1285
1374
  }
1286
1375
  function computeMergedLayout(openFlows, workingGraphs, primaryFlowKey) {
1287
1376
  const openKeys = new Set(openFlows.map((flow) => flow.key));
@@ -1289,7 +1378,7 @@ function computeMergedLayout(openFlows, workingGraphs, primaryFlowKey) {
1289
1378
  for (const { key } of openFlows) {
1290
1379
  const graph = workingGraphs[key];
1291
1380
  if (!graph) continue;
1292
- for (const node of Object.values(graph.nodes)) nodeByNsId.set(ns(key, node.id), node);
1381
+ for (const node of Object.values(graph.nodes)) nodeByNsId.set(namespaceNodeId(key, node.id), node);
1293
1382
  }
1294
1383
  function forwardEdges(flowKey, node) {
1295
1384
  const result = [];
@@ -1297,16 +1386,16 @@ function computeMergedLayout(openFlows, workingGraphs, primaryFlowKey) {
1297
1386
  if (isCrossFlowTarget(target)) {
1298
1387
  const targetFlowKey = crossFlowKey(target);
1299
1388
  const targetGraph = openKeys.has(targetFlowKey) ? workingGraphs[targetFlowKey] : void 0;
1300
- if (targetGraph) result.push(ns(targetFlowKey, targetGraph.startNodeId));
1389
+ if (targetGraph) result.push(namespaceNodeId(targetFlowKey, targetGraph.startNodeId));
1301
1390
  } else if (workingGraphs[flowKey]?.nodes[target]) {
1302
- result.push(ns(flowKey, target));
1391
+ result.push(namespaceNodeId(flowKey, target));
1303
1392
  }
1304
1393
  }
1305
1394
  return result;
1306
1395
  }
1307
1396
  const rank = /* @__PURE__ */ new Map();
1308
1397
  const primaryGraph = workingGraphs[primaryFlowKey];
1309
- const rootId = primaryGraph ? ns(primaryFlowKey, primaryGraph.startNodeId) : void 0;
1398
+ const rootId = primaryGraph ? namespaceNodeId(primaryFlowKey, primaryGraph.startNodeId) : void 0;
1310
1399
  if (rootId && nodeByNsId.has(rootId)) {
1311
1400
  rank.set(rootId, 0);
1312
1401
  const queue = [rootId];
@@ -1314,7 +1403,7 @@ function computeMergedLayout(openFlows, workingGraphs, primaryFlowKey) {
1314
1403
  const id = queue.shift();
1315
1404
  const node = nodeByNsId.get(id);
1316
1405
  if (!node) continue;
1317
- for (const nextId of forwardEdges(parseNs(id).flowKey, node)) {
1406
+ for (const nextId of forwardEdges(parseNamespacedId(id).flowKey, node)) {
1318
1407
  if (!rank.has(nextId)) {
1319
1408
  rank.set(nextId, rank.get(id) + 1);
1320
1409
  queue.push(nextId);
@@ -1361,6 +1450,7 @@ function buildAllEdges(openFlows, workingGraphs, rootFlowKey, livePositions) {
1361
1450
  for (const [id, node] of Object.entries(graph.nodes)) {
1362
1451
  const isLive = (liveCounts[id] ?? 0) > 0;
1363
1452
  for (const { target, optionId, isDefault } of targetsOf(node)) {
1453
+ if (!target) continue;
1364
1454
  const crossFlow = isCrossFlowTarget(target);
1365
1455
  let targetFlowKey = flowKey;
1366
1456
  let rawTargetId = target;
@@ -1374,8 +1464,8 @@ function buildAllEdges(openFlows, workingGraphs, rootFlowKey, livePositions) {
1374
1464
  rawTargetId = portalNodeId(id, target);
1375
1465
  }
1376
1466
  }
1377
- const source = ns(flowKey, id);
1378
- const edgeTarget = ns(targetFlowKey, rawTargetId);
1467
+ const source = namespaceNodeId(flowKey, id);
1468
+ const edgeTarget = namespaceNodeId(targetFlowKey, rawTargetId);
1379
1469
  if (optionId === void 0 && !isDefault) {
1380
1470
  const color = crossFlow ? EDGE_COLOR_CROSS_FLOW : isLive ? EDGE_COLOR_LIVE : EDGE_COLOR_LINEAR;
1381
1471
  edges.push({
@@ -1442,29 +1532,6 @@ function newNodeFromSpec(spec, existingIds) {
1442
1532
  const id = slugifyNodeId("nova_acao", existingIds);
1443
1533
  return { id, type: "action", actionKind: spec.actionKind };
1444
1534
  }
1445
- function removeNodeAndCleanRefs(nodes, nodeId) {
1446
- const { [nodeId]: _removed, ...rest } = nodes;
1447
- return Object.fromEntries(
1448
- Object.entries(rest).map(([id, node]) => {
1449
- if (!node.next) return [id, node];
1450
- if (typeof node.next === "string") {
1451
- return [id, node.next === nodeId ? { ...node, next: void 0 } : node];
1452
- }
1453
- return [
1454
- id,
1455
- {
1456
- ...node,
1457
- next: {
1458
- byAnswer: Object.fromEntries(
1459
- Object.entries(node.next.byAnswer).map(([key, value]) => [key, value === nodeId ? "" : value])
1460
- ),
1461
- default: node.next.default === nodeId ? "" : node.next.default
1462
- }
1463
- }
1464
- ];
1465
- })
1466
- );
1467
- }
1468
1535
  function extractErrorMessage(error) {
1469
1536
  if (error instanceof Error) return error.message;
1470
1537
  return void 0;
@@ -1641,7 +1708,7 @@ function FlowsWorkspace({
1641
1708
  for (const { key: flowKey, offset } of openFlows) {
1642
1709
  let resolvePosition2 = function(nodeId) {
1643
1710
  if (mergedPositions) {
1644
- const nsId = ns(flowKey, nodeId);
1711
+ const nsId = namespaceNodeId(flowKey, nodeId);
1645
1712
  return renderedPositionsRef.current.get(nsId) ?? mergedPositions.get(nsId) ?? { x: 0, y: 0 };
1646
1713
  }
1647
1714
  const local = graph.nodes[nodeId]?.position ?? fallbackPositions[nodeId] ?? { x: 0, y: 0 };
@@ -1663,7 +1730,7 @@ function FlowsWorkspace({
1663
1730
  for (const node of Object.values(graph.nodes)) {
1664
1731
  const position = resolvePosition2(node.id);
1665
1732
  allNodes.push({
1666
- id: ns(flowKey, node.id),
1733
+ id: namespaceNodeId(flowKey, node.id),
1667
1734
  type: "flowNode",
1668
1735
  position,
1669
1736
  draggable: true,
@@ -1683,7 +1750,7 @@ function FlowsWorkspace({
1683
1750
  const targetFlowKey = crossFlowKey(target);
1684
1751
  if (openFlows.some((flow) => flow.key === targetFlowKey)) return;
1685
1752
  allNodes.push({
1686
- id: ns(flowKey, portalNodeId(node.id, target)),
1753
+ id: namespaceNodeId(flowKey, portalNodeId(node.id, target)),
1687
1754
  type: "flowPortal",
1688
1755
  draggable: false,
1689
1756
  selectable: false,
@@ -1703,7 +1770,7 @@ function FlowsWorkspace({
1703
1770
  const minY = Math.min(...positions.map((point) => point.y));
1704
1771
  const maxY = Math.max(...positions.map((point) => point.y)) + estimateNodeHeight(graph.nodes[chain.actionNodeId]);
1705
1772
  allNodes.push({
1706
- id: ns(flowKey, `__chain__${chain.actionNodeId}`),
1773
+ id: namespaceNodeId(flowKey, `__chain__${chain.actionNodeId}`),
1707
1774
  type: "flowGroupFrame",
1708
1775
  draggable: false,
1709
1776
  selectable: false,
@@ -1721,7 +1788,7 @@ function FlowsWorkspace({
1721
1788
  if (!isPrimary) {
1722
1789
  const startPosition = resolvePosition2(graph.startNodeId);
1723
1790
  allNodes.push({
1724
- id: ns(flowKey, "__group_header__"),
1791
+ id: namespaceNodeId(flowKey, "__group_header__"),
1725
1792
  type: "flowGroupHeader",
1726
1793
  draggable: false,
1727
1794
  selectable: false,
@@ -1773,10 +1840,11 @@ function FlowsWorkspace({
1773
1840
  const onNodeDragStop = useCallback(
1774
1841
  (_event, node) => {
1775
1842
  renderedPositionsRef.current.set(node.id, node.position);
1776
- const { flowKey, nodeId } = parseNs(node.id);
1843
+ const { flowKey, nodeId } = parseNamespacedId(node.id);
1777
1844
  const openFlow = openFlows.find((flow) => flow.key === flowKey);
1778
1845
  if (!openFlow) return;
1779
- const localPosition = { x: node.position.x - openFlow.offset.x, y: node.position.y - openFlow.offset.y };
1846
+ const drawnWithOffset = openFlows.length === 1;
1847
+ const localPosition = drawnWithOffset ? { x: node.position.x - openFlow.offset.x, y: node.position.y - openFlow.offset.y } : node.position;
1780
1848
  updateFlow(
1781
1849
  flowKey,
1782
1850
  (graph) => graph.nodes[nodeId] ? { ...graph, nodes: { ...graph.nodes, [nodeId]: { ...graph.nodes[nodeId], position: localPosition } } } : graph
@@ -1786,26 +1854,15 @@ function FlowsWorkspace({
1786
1854
  );
1787
1855
  const onConnect = useCallback(
1788
1856
  (connection) => {
1789
- const { source, sourceHandle, target } = connection;
1790
- if (!source || !target || source === target) return;
1791
- const sourceRef = parseNs(source);
1792
- const targetRef = parseNs(target);
1793
- let targetValue;
1794
- if (targetRef.flowKey === sourceRef.flowKey) {
1795
- targetValue = targetRef.nodeId;
1796
- } else {
1797
- const targetGraph = workingGraphs[targetRef.flowKey];
1798
- if (!targetGraph || targetGraph.startNodeId !== targetRef.nodeId) return;
1799
- targetValue = `${CROSS_FLOW_PREFIX}${targetRef.flowKey}`;
1800
- }
1801
- updateFlow(sourceRef.flowKey, (graph) => {
1802
- const node = graph.nodes[sourceRef.nodeId];
1857
+ const resolved = resolveConnection({
1858
+ connection: { source: connection.source, target: connection.target, sourceHandle: connection.sourceHandle },
1859
+ graphs: workingGraphs
1860
+ });
1861
+ if (!resolved) return;
1862
+ updateFlow(resolved.flowKey, (graph) => {
1863
+ const node = graph.nodes[resolved.nodeId];
1803
1864
  if (!node) return graph;
1804
- const handle = sourceHandle ?? "next";
1805
- const currentByAnswer = typeof node.next === "object" && node.next ? node.next.byAnswer : {};
1806
- const currentDefault = typeof node.next === "object" && node.next ? node.next.default : "";
1807
- const updatedNode = handle === "next" ? { ...node, next: targetValue } : handle === "__default" ? { ...node, next: { byAnswer: currentByAnswer, default: targetValue } } : { ...node, next: { byAnswer: { ...currentByAnswer, [handle]: targetValue }, default: currentDefault } };
1808
- return { ...graph, nodes: { ...graph.nodes, [sourceRef.nodeId]: updatedNode } };
1865
+ return { ...graph, nodes: { ...graph.nodes, [resolved.nodeId]: applyConnection(node, resolved) } };
1809
1866
  });
1810
1867
  },
1811
1868
  [workingGraphs, updateFlow]
@@ -1817,7 +1874,7 @@ function FlowsWorkspace({
1817
1874
  newNode.position = { x: 0, y: maxY + 170 };
1818
1875
  updateFlow(primaryFlowKey, (graph) => ({ ...graph, nodes: { ...graph.nodes, [newNode.id]: newNode } }));
1819
1876
  setEditingRef({ flowKey: primaryFlowKey, nodeId: newNode.id });
1820
- setPendingFocusNodeId(ns(primaryFlowKey, newNode.id));
1877
+ setPendingFocusNodeId(namespaceNodeId(primaryFlowKey, newNode.id));
1821
1878
  }
1822
1879
  function handleNodePanelChange(updated) {
1823
1880
  if (!editingRef) return;
@@ -1839,7 +1896,7 @@ function FlowsWorkspace({
1839
1896
  ...graph,
1840
1897
  nodes: Object.fromEntries(
1841
1898
  Object.entries(graph.nodes).map(([id, node]) => {
1842
- const position = positions2.get(ns(key, id));
1899
+ const position = positions2.get(namespaceNodeId(key, id));
1843
1900
  return [
1844
1901
  id,
1845
1902
  position ? { ...node, position: { x: position.x - offset.x, y: position.y - offset.y } } : node
@@ -1930,6 +1987,7 @@ function FlowsWorkspace({
1930
1987
  const canCreateFlow = Boolean(api.createFlow);
1931
1988
  const canDeleteFlow = Boolean(api.deleteFlow) && primaryFlowKey !== rootFlowKey;
1932
1989
  return /* @__PURE__ */ jsxs9("div", { className: `space-y-4 h-full flex flex-col ${className ?? ""}`, children: [
1990
+ /* @__PURE__ */ jsx10(TooltipLayer, {}),
1933
1991
  /* @__PURE__ */ jsxs9("div", { className: "flex items-center justify-between flex-wrap gap-3", children: [
1934
1992
  /* @__PURE__ */ jsxs9("div", { children: [
1935
1993
  /* @__PURE__ */ jsx10("h2", { className: "text-2xl font-bold text-gray-900 dark:text-gray-100", children: labels.workspace.title }),
@@ -1941,6 +1999,8 @@ function FlowsWorkspace({
1941
1999
  /* @__PURE__ */ jsxs9(
1942
2000
  "button",
1943
2001
  {
2002
+ "data-cv-tooltip": viewMode === "map" ? labels.flowMap.toggleToDetail : labels.flowMap.toggleToMap,
2003
+ "aria-label": viewMode === "map" ? labels.flowMap.toggleToDetail : labels.flowMap.toggleToMap,
1944
2004
  type: "button",
1945
2005
  onClick: () => setViewMode((mode) => mode === "map" ? "detail" : "map"),
1946
2006
  className: OUTLINE_BUTTON,
@@ -1957,7 +2017,8 @@ function FlowsWorkspace({
1957
2017
  type: "button",
1958
2018
  onClick: handleOrganize,
1959
2019
  className: OUTLINE_BUTTON,
1960
- title: labels.workspace.organizeTooltip,
2020
+ "data-cv-tooltip": labels.workspace.organizeTooltip,
2021
+ "aria-label": labels.workspace.organizeTooltip,
1961
2022
  disabled: !primaryGraph,
1962
2023
  children: [
1963
2024
  /* @__PURE__ */ jsx10(LayoutGrid, { size: 14, "aria-hidden": "true" }),
@@ -1972,7 +2033,8 @@ function FlowsWorkspace({
1972
2033
  type: "button",
1973
2034
  onClick: handleDiscardChanges,
1974
2035
  className: OUTLINE_BUTTON,
1975
- title: labels.workspace.discardTooltip,
2036
+ "data-cv-tooltip": labels.workspace.discardTooltip,
2037
+ "aria-label": labels.workspace.discardTooltip,
1976
2038
  disabled: !isDirty || saveState === "saving",
1977
2039
  children: [
1978
2040
  /* @__PURE__ */ jsx10(Undo2, { size: 14, "aria-hidden": "true" }),
@@ -1984,6 +2046,8 @@ function FlowsWorkspace({
1984
2046
  /* @__PURE__ */ jsxs9(
1985
2047
  "button",
1986
2048
  {
2049
+ "data-cv-tooltip": labels.workspace.saveGraph,
2050
+ "aria-label": labels.workspace.saveGraph,
1987
2051
  type: "button",
1988
2052
  onClick: () => void handlePublish(),
1989
2053
  className: PRIMARY_BUTTON,
@@ -2002,6 +2066,8 @@ function FlowsWorkspace({
2002
2066
  graphs && Object.values(graphs).map((graph) => /* @__PURE__ */ jsx10(
2003
2067
  "button",
2004
2068
  {
2069
+ "data-cv-tooltip": graph.label,
2070
+ "aria-label": graph.label,
2005
2071
  type: "button",
2006
2072
  onClick: () => focusFlow(graph.key),
2007
2073
  className: `px-3 py-1.5 rounded-full text-xs font-medium border transition-colors ${primaryFlowKey === graph.key ? "bg-blue-600 text-white border-blue-600" : "bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-300 border-gray-200 dark:border-gray-700 hover:border-blue-300"}`,
@@ -2012,6 +2078,8 @@ function FlowsWorkspace({
2012
2078
  canCreateFlow && /* @__PURE__ */ jsxs9(
2013
2079
  "button",
2014
2080
  {
2081
+ "data-cv-tooltip": labels.flowManager.newFlow,
2082
+ "aria-label": labels.flowManager.newFlow,
2015
2083
  type: "button",
2016
2084
  onClick: () => {
2017
2085
  setFlowMutationState({ pending: false });
@@ -2038,6 +2106,8 @@ function FlowsWorkspace({
2038
2106
  primaryGraph && canDeleteFlow && /* @__PURE__ */ jsxs9(
2039
2107
  "button",
2040
2108
  {
2109
+ "data-cv-tooltip": labels.flowManager.deleteFlow,
2110
+ "aria-label": labels.flowManager.deleteFlow,
2041
2111
  type: "button",
2042
2112
  onClick: () => {
2043
2113
  setFlowMutationState({ pending: false });
@@ -2171,10 +2241,12 @@ function FlowsWorkspace({
2171
2241
  flowMutationState.error && /* @__PURE__ */ jsx10("p", { className: "text-xs text-red-600", children: flowMutationState.error })
2172
2242
  ] }),
2173
2243
  /* @__PURE__ */ jsxs9("div", { className: "flex justify-end gap-2 mt-4", children: [
2174
- /* @__PURE__ */ jsx10("button", { type: "button", className: OUTLINE_BUTTON, onClick: () => setShowCreateDialog(false), children: labels.nodePanel.cancel }),
2244
+ /* @__PURE__ */ jsx10("button", { "data-cv-tooltip": labels.nodePanel.cancel, "aria-label": labels.nodePanel.cancel, type: "button", className: OUTLINE_BUTTON, onClick: () => setShowCreateDialog(false), children: labels.nodePanel.cancel }),
2175
2245
  /* @__PURE__ */ jsx10(
2176
2246
  "button",
2177
2247
  {
2248
+ "data-cv-tooltip": labels.flowManager.create,
2249
+ "aria-label": labels.flowManager.create,
2178
2250
  type: "button",
2179
2251
  className: PRIMARY_BUTTON,
2180
2252
  onClick: () => void handleCreateFlow(),
@@ -2188,10 +2260,12 @@ function FlowsWorkspace({
2188
2260
  /* @__PURE__ */ jsx10("p", { className: "text-sm text-gray-600 dark:text-gray-300", children: primaryGraph ? labels.flowManager.deleteConfirm(primaryGraph.label) : "" }),
2189
2261
  flowMutationState.error && /* @__PURE__ */ jsx10("p", { className: "text-xs text-red-600 mt-2", children: flowMutationState.error }),
2190
2262
  /* @__PURE__ */ jsxs9("div", { className: "flex justify-end gap-2 mt-4", children: [
2191
- /* @__PURE__ */ jsx10("button", { type: "button", className: OUTLINE_BUTTON, onClick: () => setShowDeleteDialog(false), children: labels.nodePanel.cancel }),
2263
+ /* @__PURE__ */ jsx10("button", { "data-cv-tooltip": labels.nodePanel.cancel, "aria-label": labels.nodePanel.cancel, type: "button", className: OUTLINE_BUTTON, onClick: () => setShowDeleteDialog(false), children: labels.nodePanel.cancel }),
2192
2264
  /* @__PURE__ */ jsxs9(
2193
2265
  "button",
2194
2266
  {
2267
+ "data-cv-tooltip": labels.flowManager.deleteFlow,
2268
+ "aria-label": labels.flowManager.deleteFlow,
2195
2269
  type: "button",
2196
2270
  className: "inline-flex items-center gap-1.5 rounded-lg bg-red-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-red-700 disabled:opacity-40",
2197
2271
  onClick: () => void handleDeleteFlow(),
@@ -2246,6 +2320,7 @@ export {
2246
2320
  FlowsWorkspace,
2247
2321
  NODE_CARD_WIDTH,
2248
2322
  WHATSAPP_LIMITS,
2323
+ applyConnection,
2249
2324
  computeAutoLayout,
2250
2325
  computeFlowMapLayout,
2251
2326
  crossFlowKey,
@@ -2258,9 +2333,15 @@ export {
2258
2333
  flowNodeTypes,
2259
2334
  flowPortalNodeTypes,
2260
2335
  isCrossFlowTarget,
2336
+ isGraphDirty,
2261
2337
  mergeFlowEditorLabels,
2338
+ mergedFlowKeysFrom,
2339
+ namespaceNodeId,
2262
2340
  nodeLabel,
2341
+ parseNamespacedId,
2342
+ removeNodeAndCleanRefs,
2263
2343
  rendersAsButtons,
2344
+ resolveConnection,
2264
2345
  slugifyNodeId,
2265
2346
  targetsOf,
2266
2347
  validateGraph