@adatechnology/conversations-ui 0.1.0-rc.27 → 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.
@@ -64,7 +64,7 @@ type CollectionChain = {
64
64
  actionNodeId: string;
65
65
  };
66
66
  declare function findCollectionChains(graph: FlowGraphData): CollectionChain[];
67
- declare function slugifyNodeId(label: string, existing: Set<string>): string;
67
+ declare function slugifyNodeId(label: string, existing: ReadonlySet<string>): string;
68
68
 
69
69
  interface FlowEditorLabels {
70
70
  legend: Record<FlowNodeType, string>;
@@ -383,4 +383,75 @@ interface FlowsWorkspaceProps {
383
383
  */
384
384
  declare function FlowsWorkspace({ api, rootFlowKey, labels: labelsOverride, actionOptions, renderMediaPicker, livePollIntervalMs, className, }: FlowsWorkspaceProps): react.JSX.Element;
385
385
 
386
- export { BUILT_IN_ACTION_KINDS, CONDITION_OPERATORS, CROSS_FLOW_PREFIX, type CollectionChain, type CreateFlowInput, DEFAULT_FLOW_EDITOR_LABELS, type FlowEditorLabels, FlowGroupFrame, type FlowGroupFrameData, FlowGroupHeader, type FlowGroupHeaderData, type FlowLivePosition, FlowMapCanvas, type FlowMapCanvasProps, FlowMapNode, type FlowMapNodeData, FlowNodeCard, type FlowNodeCardData, FlowNodePanel, type FlowNodePanelProps, FlowPalette, type FlowPaletteActionOption, type FlowPaletteProps, FlowPortalNode, type FlowPortalNodeData, type FlowValidationLabels, FlowWhatsAppPreview, type FlowWhatsAppPreviewProps, FlowsWorkspace, type FlowsWorkspaceApi, type FlowsWorkspaceProps, type GraphIssue, NODE_CARD_WIDTH, type NewNodeSpec, WHATSAPP_LIMITS, computeAutoLayout, computeFlowMapLayout, crossFlowKey, crossFlowTargetsOf, estimateNodeHeight, findCollectionChains, flowGroupFrameNodeTypes, flowGroupHeaderNodeTypes, flowMapNodeTypes, flowNodeTypes, flowPortalNodeTypes, isCrossFlowTarget, mergeFlowEditorLabels, nodeLabel, rendersAsButtons, slugifyNodeId, targetsOf, validateGraph };
386
+ /**
387
+ * Copyright (c) 2026 Ada Technology. MIT License.
388
+ *
389
+ * Operações puras de grafo que o editor de fluxo precisa, e que viviam soltas dentro da página de
390
+ * 973 linhas do financiamento.
391
+ *
392
+ * Puras e separadas do hook de propósito: são a parte que dá para testar sem navegador, sem React e
393
+ * sem estado — e três delas (`removeNodeAndCleanRefs`, `resolveConnection`, `mergedFlowKeysFrom`)
394
+ * decidem o que acontece com o fluxo que alguém desenhou. Errar ali não dá erro; dá aresta apontando
395
+ * para nó que não existe mais, ou fluxo que some do canvas.
396
+ */
397
+
398
+ /**
399
+ * Id de nó no canvas mesclado: vários fluxos dividem o mesmo espaço, e `boas-vindas` pode existir em
400
+ * dois deles. Sem o prefixo, arrastar um card moveria o homônimo do outro fluxo.
401
+ */
402
+ declare function namespaceNodeId(flowKey: string, nodeId: string): string;
403
+ declare function parseNamespacedId(value: string): {
404
+ flowKey: string;
405
+ nodeId: string;
406
+ };
407
+ /**
408
+ * Apaga o nó E as referências a ele.
409
+ *
410
+ * A limpeza não é cortesia: uma aresta apontando para nó inexistente faz o motor do bot parar a
411
+ * conversa no meio, e o sintoma aparece para o cliente, não para quem editou.
412
+ */
413
+ declare function removeNodeAndCleanRefs(nodes: Readonly<Record<string, FlowNodeData>>, removedId: string): Record<string, FlowNodeData>;
414
+ /**
415
+ * O fecho transitivo dos fluxos alcançáveis a partir de um — é o conjunto que o canvas abre junto.
416
+ *
417
+ * BFS e não recursão: fluxo que referencia a si mesmo (menu que volta ao menu) é comum, e recursão
418
+ * ingênua estouraria a pilha no caso mais banal que existe.
419
+ */
420
+ declare function mergedFlowKeysFrom(rootKey: string, graphs: Readonly<Record<string, FlowGraphData>>): readonly string[];
421
+ type ConnectionRequest = {
422
+ readonly source: string;
423
+ readonly target: string;
424
+ readonly sourceHandle?: string | null | undefined;
425
+ };
426
+ type ResolvedConnection = {
427
+ readonly flowKey: string;
428
+ readonly nodeId: string;
429
+ readonly handle: string;
430
+ /** O que gravar no `next`: id de nó local, ou `flow:<key>` quando atravessa fluxo. */
431
+ readonly targetValue: string;
432
+ };
433
+ /**
434
+ * Traduz um arraste de aresta no valor que vai para o `next` — e recusa o que o motor do bot não
435
+ * sabe executar.
436
+ *
437
+ * A regra que não é óbvia: conectar num nó de OUTRO fluxo só funciona se for o nó inicial dele,
438
+ * porque o motor só sabe pular para o começo de um fluxo, não para um nó do meio. Conectar no meio
439
+ * devolve `undefined` — recusa silenciosa é melhor que gravar um salto que o bot vai ignorar em
440
+ * produção, deixando a conversa parada sem ninguém entender por quê.
441
+ */
442
+ declare function resolveConnection(params: {
443
+ readonly connection: ConnectionRequest;
444
+ readonly graphs: Readonly<Record<string, FlowGraphData>>;
445
+ }): ResolvedConnection | undefined;
446
+ /** Aplica a conexão resolvida no nó. `next` string para saída única, objeto para ramificação. */
447
+ declare function applyConnection(node: FlowNodeData, resolved: ResolvedConnection): FlowNodeData;
448
+ /**
449
+ * Um fluxo está sujo quando o rascunho difere do publicado.
450
+ *
451
+ * Comparação estrutural por JSON: é grosseira, e é suficiente porque o grafo é dado serializável sem
452
+ * ordem significativa de chave — o servidor devolve o que gravou. Comparar campo a campo daria a
453
+ * mesma resposta com mais código para errar.
454
+ */
455
+ declare function isGraphDirty(working: FlowGraphData | undefined, published: FlowGraphData | undefined): boolean;
456
+
457
+ export { BUILT_IN_ACTION_KINDS, CONDITION_OPERATORS, CROSS_FLOW_PREFIX, type CollectionChain, type ConnectionRequest, type CreateFlowInput, DEFAULT_FLOW_EDITOR_LABELS, type FlowEditorLabels, FlowGroupFrame, type FlowGroupFrameData, FlowGroupHeader, type FlowGroupHeaderData, type FlowLivePosition, FlowMapCanvas, type FlowMapCanvasProps, FlowMapNode, type FlowMapNodeData, FlowNodeCard, type FlowNodeCardData, FlowNodePanel, type FlowNodePanelProps, FlowPalette, type FlowPaletteActionOption, type FlowPaletteProps, FlowPortalNode, type FlowPortalNodeData, type FlowValidationLabels, FlowWhatsAppPreview, type FlowWhatsAppPreviewProps, FlowsWorkspace, type FlowsWorkspaceApi, type FlowsWorkspaceProps, type GraphIssue, NODE_CARD_WIDTH, type NewNodeSpec, type ResolvedConnection, WHATSAPP_LIMITS, applyConnection, computeAutoLayout, computeFlowMapLayout, crossFlowKey, crossFlowTargetsOf, estimateNodeHeight, findCollectionChains, flowGroupFrameNodeTypes, flowGroupHeaderNodeTypes, flowMapNodeTypes, flowNodeTypes, flowPortalNodeTypes, isCrossFlowTarget, isGraphDirty, mergeFlowEditorLabels, mergedFlowKeysFrom, namespaceNodeId, nodeLabel, parseNamespacedId, removeNodeAndCleanRefs, rendersAsButtons, resolveConnection, slugifyNodeId, targetsOf, validateGraph };
@@ -1267,6 +1267,87 @@ import {
1267
1267
  } from "@xyflow/react";
1268
1268
  import "@xyflow/react/dist/style.css";
1269
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
1270
1351
  import { Fragment as Fragment2, jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
1271
1352
  var RF_NODE_TYPES = {
1272
1353
  ...flowNodeTypes,
@@ -1277,7 +1358,6 @@ var RF_NODE_TYPES = {
1277
1358
  var CHAIN_FRAME_PADDING = 36;
1278
1359
  var COLUMN_GAP = 360;
1279
1360
  var ROW_GAP = 40;
1280
- var NS_SEP = "::";
1281
1361
  var FLOW_KEY_PATTERN = /^[a-z0-9_]{2,40}$/;
1282
1362
  var EDGE_COLOR_LINEAR = "#94a3b8";
1283
1363
  var EDGE_COLOR_BRANCH = "#8b5cf6";
@@ -1289,25 +1369,8 @@ var BACKGROUND_COLOR_DARK2 = "#334155";
1289
1369
  function portalNodeId(sourceId, target) {
1290
1370
  return `__portal__${sourceId}__${target}`;
1291
1371
  }
1292
- function ns(flowKey, nodeId) {
1293
- return `${flowKey}${NS_SEP}${nodeId}`;
1294
- }
1295
- function parseNs(id) {
1296
- const index = id.indexOf(NS_SEP);
1297
- return index === -1 ? { flowKey: "", nodeId: id } : { flowKey: id.slice(0, index), nodeId: id.slice(index + NS_SEP.length) };
1298
- }
1299
1372
  function autoMergeAll(rootKey, graphsSource) {
1300
- const visited = /* @__PURE__ */ new Set();
1301
- const queue = [rootKey];
1302
- while (queue.length > 0) {
1303
- const key = queue.shift();
1304
- if (visited.has(key) || !graphsSource[key]) continue;
1305
- visited.add(key);
1306
- for (const target of crossFlowTargetsOf(graphsSource[key])) {
1307
- if (!visited.has(target) && graphsSource[target]) queue.push(target);
1308
- }
1309
- }
1310
- return [...visited].map((key) => ({ key, offset: { x: 0, y: 0 } }));
1373
+ return mergedFlowKeysFrom(rootKey, graphsSource).map((key) => ({ key, offset: { x: 0, y: 0 } }));
1311
1374
  }
1312
1375
  function computeMergedLayout(openFlows, workingGraphs, primaryFlowKey) {
1313
1376
  const openKeys = new Set(openFlows.map((flow) => flow.key));
@@ -1315,7 +1378,7 @@ function computeMergedLayout(openFlows, workingGraphs, primaryFlowKey) {
1315
1378
  for (const { key } of openFlows) {
1316
1379
  const graph = workingGraphs[key];
1317
1380
  if (!graph) continue;
1318
- 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);
1319
1382
  }
1320
1383
  function forwardEdges(flowKey, node) {
1321
1384
  const result = [];
@@ -1323,16 +1386,16 @@ function computeMergedLayout(openFlows, workingGraphs, primaryFlowKey) {
1323
1386
  if (isCrossFlowTarget(target)) {
1324
1387
  const targetFlowKey = crossFlowKey(target);
1325
1388
  const targetGraph = openKeys.has(targetFlowKey) ? workingGraphs[targetFlowKey] : void 0;
1326
- if (targetGraph) result.push(ns(targetFlowKey, targetGraph.startNodeId));
1389
+ if (targetGraph) result.push(namespaceNodeId(targetFlowKey, targetGraph.startNodeId));
1327
1390
  } else if (workingGraphs[flowKey]?.nodes[target]) {
1328
- result.push(ns(flowKey, target));
1391
+ result.push(namespaceNodeId(flowKey, target));
1329
1392
  }
1330
1393
  }
1331
1394
  return result;
1332
1395
  }
1333
1396
  const rank = /* @__PURE__ */ new Map();
1334
1397
  const primaryGraph = workingGraphs[primaryFlowKey];
1335
- const rootId = primaryGraph ? ns(primaryFlowKey, primaryGraph.startNodeId) : void 0;
1398
+ const rootId = primaryGraph ? namespaceNodeId(primaryFlowKey, primaryGraph.startNodeId) : void 0;
1336
1399
  if (rootId && nodeByNsId.has(rootId)) {
1337
1400
  rank.set(rootId, 0);
1338
1401
  const queue = [rootId];
@@ -1340,7 +1403,7 @@ function computeMergedLayout(openFlows, workingGraphs, primaryFlowKey) {
1340
1403
  const id = queue.shift();
1341
1404
  const node = nodeByNsId.get(id);
1342
1405
  if (!node) continue;
1343
- for (const nextId of forwardEdges(parseNs(id).flowKey, node)) {
1406
+ for (const nextId of forwardEdges(parseNamespacedId(id).flowKey, node)) {
1344
1407
  if (!rank.has(nextId)) {
1345
1408
  rank.set(nextId, rank.get(id) + 1);
1346
1409
  queue.push(nextId);
@@ -1387,6 +1450,7 @@ function buildAllEdges(openFlows, workingGraphs, rootFlowKey, livePositions) {
1387
1450
  for (const [id, node] of Object.entries(graph.nodes)) {
1388
1451
  const isLive = (liveCounts[id] ?? 0) > 0;
1389
1452
  for (const { target, optionId, isDefault } of targetsOf(node)) {
1453
+ if (!target) continue;
1390
1454
  const crossFlow = isCrossFlowTarget(target);
1391
1455
  let targetFlowKey = flowKey;
1392
1456
  let rawTargetId = target;
@@ -1400,8 +1464,8 @@ function buildAllEdges(openFlows, workingGraphs, rootFlowKey, livePositions) {
1400
1464
  rawTargetId = portalNodeId(id, target);
1401
1465
  }
1402
1466
  }
1403
- const source = ns(flowKey, id);
1404
- const edgeTarget = ns(targetFlowKey, rawTargetId);
1467
+ const source = namespaceNodeId(flowKey, id);
1468
+ const edgeTarget = namespaceNodeId(targetFlowKey, rawTargetId);
1405
1469
  if (optionId === void 0 && !isDefault) {
1406
1470
  const color = crossFlow ? EDGE_COLOR_CROSS_FLOW : isLive ? EDGE_COLOR_LIVE : EDGE_COLOR_LINEAR;
1407
1471
  edges.push({
@@ -1468,29 +1532,6 @@ function newNodeFromSpec(spec, existingIds) {
1468
1532
  const id = slugifyNodeId("nova_acao", existingIds);
1469
1533
  return { id, type: "action", actionKind: spec.actionKind };
1470
1534
  }
1471
- function removeNodeAndCleanRefs(nodes, nodeId) {
1472
- const { [nodeId]: _removed, ...rest } = nodes;
1473
- return Object.fromEntries(
1474
- Object.entries(rest).map(([id, node]) => {
1475
- if (!node.next) return [id, node];
1476
- if (typeof node.next === "string") {
1477
- return [id, node.next === nodeId ? { ...node, next: void 0 } : node];
1478
- }
1479
- return [
1480
- id,
1481
- {
1482
- ...node,
1483
- next: {
1484
- byAnswer: Object.fromEntries(
1485
- Object.entries(node.next.byAnswer).map(([key, value]) => [key, value === nodeId ? "" : value])
1486
- ),
1487
- default: node.next.default === nodeId ? "" : node.next.default
1488
- }
1489
- }
1490
- ];
1491
- })
1492
- );
1493
- }
1494
1535
  function extractErrorMessage(error) {
1495
1536
  if (error instanceof Error) return error.message;
1496
1537
  return void 0;
@@ -1667,7 +1708,7 @@ function FlowsWorkspace({
1667
1708
  for (const { key: flowKey, offset } of openFlows) {
1668
1709
  let resolvePosition2 = function(nodeId) {
1669
1710
  if (mergedPositions) {
1670
- const nsId = ns(flowKey, nodeId);
1711
+ const nsId = namespaceNodeId(flowKey, nodeId);
1671
1712
  return renderedPositionsRef.current.get(nsId) ?? mergedPositions.get(nsId) ?? { x: 0, y: 0 };
1672
1713
  }
1673
1714
  const local = graph.nodes[nodeId]?.position ?? fallbackPositions[nodeId] ?? { x: 0, y: 0 };
@@ -1689,7 +1730,7 @@ function FlowsWorkspace({
1689
1730
  for (const node of Object.values(graph.nodes)) {
1690
1731
  const position = resolvePosition2(node.id);
1691
1732
  allNodes.push({
1692
- id: ns(flowKey, node.id),
1733
+ id: namespaceNodeId(flowKey, node.id),
1693
1734
  type: "flowNode",
1694
1735
  position,
1695
1736
  draggable: true,
@@ -1709,7 +1750,7 @@ function FlowsWorkspace({
1709
1750
  const targetFlowKey = crossFlowKey(target);
1710
1751
  if (openFlows.some((flow) => flow.key === targetFlowKey)) return;
1711
1752
  allNodes.push({
1712
- id: ns(flowKey, portalNodeId(node.id, target)),
1753
+ id: namespaceNodeId(flowKey, portalNodeId(node.id, target)),
1713
1754
  type: "flowPortal",
1714
1755
  draggable: false,
1715
1756
  selectable: false,
@@ -1729,7 +1770,7 @@ function FlowsWorkspace({
1729
1770
  const minY = Math.min(...positions.map((point) => point.y));
1730
1771
  const maxY = Math.max(...positions.map((point) => point.y)) + estimateNodeHeight(graph.nodes[chain.actionNodeId]);
1731
1772
  allNodes.push({
1732
- id: ns(flowKey, `__chain__${chain.actionNodeId}`),
1773
+ id: namespaceNodeId(flowKey, `__chain__${chain.actionNodeId}`),
1733
1774
  type: "flowGroupFrame",
1734
1775
  draggable: false,
1735
1776
  selectable: false,
@@ -1747,7 +1788,7 @@ function FlowsWorkspace({
1747
1788
  if (!isPrimary) {
1748
1789
  const startPosition = resolvePosition2(graph.startNodeId);
1749
1790
  allNodes.push({
1750
- id: ns(flowKey, "__group_header__"),
1791
+ id: namespaceNodeId(flowKey, "__group_header__"),
1751
1792
  type: "flowGroupHeader",
1752
1793
  draggable: false,
1753
1794
  selectable: false,
@@ -1799,10 +1840,11 @@ function FlowsWorkspace({
1799
1840
  const onNodeDragStop = useCallback(
1800
1841
  (_event, node) => {
1801
1842
  renderedPositionsRef.current.set(node.id, node.position);
1802
- const { flowKey, nodeId } = parseNs(node.id);
1843
+ const { flowKey, nodeId } = parseNamespacedId(node.id);
1803
1844
  const openFlow = openFlows.find((flow) => flow.key === flowKey);
1804
1845
  if (!openFlow) return;
1805
- 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;
1806
1848
  updateFlow(
1807
1849
  flowKey,
1808
1850
  (graph) => graph.nodes[nodeId] ? { ...graph, nodes: { ...graph.nodes, [nodeId]: { ...graph.nodes[nodeId], position: localPosition } } } : graph
@@ -1812,26 +1854,15 @@ function FlowsWorkspace({
1812
1854
  );
1813
1855
  const onConnect = useCallback(
1814
1856
  (connection) => {
1815
- const { source, sourceHandle, target } = connection;
1816
- if (!source || !target || source === target) return;
1817
- const sourceRef = parseNs(source);
1818
- const targetRef = parseNs(target);
1819
- let targetValue;
1820
- if (targetRef.flowKey === sourceRef.flowKey) {
1821
- targetValue = targetRef.nodeId;
1822
- } else {
1823
- const targetGraph = workingGraphs[targetRef.flowKey];
1824
- if (!targetGraph || targetGraph.startNodeId !== targetRef.nodeId) return;
1825
- targetValue = `${CROSS_FLOW_PREFIX}${targetRef.flowKey}`;
1826
- }
1827
- updateFlow(sourceRef.flowKey, (graph) => {
1828
- 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];
1829
1864
  if (!node) return graph;
1830
- const handle = sourceHandle ?? "next";
1831
- const currentByAnswer = typeof node.next === "object" && node.next ? node.next.byAnswer : {};
1832
- const currentDefault = typeof node.next === "object" && node.next ? node.next.default : "";
1833
- const updatedNode = handle === "next" ? { ...node, next: targetValue } : handle === "__default" ? { ...node, next: { byAnswer: currentByAnswer, default: targetValue } } : { ...node, next: { byAnswer: { ...currentByAnswer, [handle]: targetValue }, default: currentDefault } };
1834
- return { ...graph, nodes: { ...graph.nodes, [sourceRef.nodeId]: updatedNode } };
1865
+ return { ...graph, nodes: { ...graph.nodes, [resolved.nodeId]: applyConnection(node, resolved) } };
1835
1866
  });
1836
1867
  },
1837
1868
  [workingGraphs, updateFlow]
@@ -1843,7 +1874,7 @@ function FlowsWorkspace({
1843
1874
  newNode.position = { x: 0, y: maxY + 170 };
1844
1875
  updateFlow(primaryFlowKey, (graph) => ({ ...graph, nodes: { ...graph.nodes, [newNode.id]: newNode } }));
1845
1876
  setEditingRef({ flowKey: primaryFlowKey, nodeId: newNode.id });
1846
- setPendingFocusNodeId(ns(primaryFlowKey, newNode.id));
1877
+ setPendingFocusNodeId(namespaceNodeId(primaryFlowKey, newNode.id));
1847
1878
  }
1848
1879
  function handleNodePanelChange(updated) {
1849
1880
  if (!editingRef) return;
@@ -1865,7 +1896,7 @@ function FlowsWorkspace({
1865
1896
  ...graph,
1866
1897
  nodes: Object.fromEntries(
1867
1898
  Object.entries(graph.nodes).map(([id, node]) => {
1868
- const position = positions2.get(ns(key, id));
1899
+ const position = positions2.get(namespaceNodeId(key, id));
1869
1900
  return [
1870
1901
  id,
1871
1902
  position ? { ...node, position: { x: position.x - offset.x, y: position.y - offset.y } } : node
@@ -2289,6 +2320,7 @@ export {
2289
2320
  FlowsWorkspace,
2290
2321
  NODE_CARD_WIDTH,
2291
2322
  WHATSAPP_LIMITS,
2323
+ applyConnection,
2292
2324
  computeAutoLayout,
2293
2325
  computeFlowMapLayout,
2294
2326
  crossFlowKey,
@@ -2301,9 +2333,15 @@ export {
2301
2333
  flowNodeTypes,
2302
2334
  flowPortalNodeTypes,
2303
2335
  isCrossFlowTarget,
2336
+ isGraphDirty,
2304
2337
  mergeFlowEditorLabels,
2338
+ mergedFlowKeysFrom,
2339
+ namespaceNodeId,
2305
2340
  nodeLabel,
2341
+ parseNamespacedId,
2342
+ removeNodeAndCleanRefs,
2306
2343
  rendersAsButtons,
2344
+ resolveConnection,
2307
2345
  slugifyNodeId,
2308
2346
  targetsOf,
2309
2347
  validateGraph
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adatechnology/conversations-ui",
3
- "version": "0.1.0-rc.27",
3
+ "version": "0.1.0-rc.28",
4
4
  "description": "WhatsApp conversation UI components — parametrizável por endpoint, tema e feature flags",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -51,7 +51,7 @@
51
51
  "@types/react-dom": "^18 || ^19"
52
52
  },
53
53
  "scripts": {
54
- "build": "tsup src/index.ts src/flows/index.ts src/channel/index.ts src/preview/index.ts src/styles.css --dts --format esm --external react --external react-dom --external @xyflow/react",
54
+ "build": "tsup src/index.ts src/flows/index.ts src/channel/index.ts src/preview/index.ts src/styles.css --dts --clean --format esm --external react --external react-dom --external @xyflow/react",
55
55
  "build:watch": "tsup src/index.ts src/flows/index.ts src/channel/index.ts src/styles.css --watch --dts --format esm --external react --external react-dom --external @xyflow/react",
56
56
  "check": "tsc -p tsconfig.json --noEmit",
57
57
  "test": "bun test"
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Copyright (c) 2026 Ada Technology. MIT License.
3
+ *
4
+ * O único teste que lê o `dist` em vez do fonte.
5
+ *
6
+ * Existe porque o `notification-ui@rc.1` saiu com 19 testes verdes e não renderizava. Dois defeitos,
7
+ * os dois invisíveis para quem importa o fonte:
8
+ *
9
+ * 1. `jsx: react-jsx` chegava ao tsup por um tsconfig com `extends`, e o esbuild NÃO segue `extends`
10
+ * para essa opção. O bundle saiu com `React.createElement` sem `React` no escopo, e o produto
11
+ * quebrou com `ReferenceError: React is not defined`.
12
+ * 2. `splitting: false` com dois entrypoints duplicou o módulo de contexto. O provider de um bundle
13
+ * não era o mesmo objeto do consumidor no outro, e o hook acusava "usado fora do provider" estando
14
+ * dentro de um.
15
+ *
16
+ * Nenhum teste de fonte pega isso: eles importam `./index`, não `dist`. Rode depois do build.
17
+ */
18
+
19
+ import { describe, expect, it } from 'bun:test'
20
+
21
+ const DIST = `${import.meta.dir}/../dist`
22
+
23
+ async function distText(file: string): Promise<string> {
24
+ const handle = Bun.file(`${DIST}/${file}`)
25
+ expect(await handle.exists(), `${file} não existe — rode \`bun run build\` antes`).toBe(true)
26
+ return handle.text()
27
+ }
28
+
29
+ describe('transform de JSX', () => {
30
+ it('usa o runtime automático, e não React.createElement', async () => {
31
+ for (const file of ['index.js', 'flows/index.js', 'preview/index.js']) {
32
+ const content = await distText(file)
33
+
34
+ expect(content, `${file} com createElement`).not.toContain('React.createElement')
35
+ expect(content, `${file} sem jsx-runtime`).toContain('react/jsx-runtime')
36
+ }
37
+ })
38
+ })
39
+
40
+ describe('divisão de código entre entrypoints', () => {
41
+ it('os entrypoints compartilham chunk em vez de duplicar módulo', async () => {
42
+ // Sem `splitting: true`, cada entrypoint carrega a própria cópia dos módulos comuns — e um
43
+ // contexto do React duplicado deixa de ser o mesmo objeto entre provider e consumidor.
44
+ const chunks = [...new Bun.Glob('chunk-*.js').scanSync({ cwd: DIST })]
45
+
46
+ expect(chunks.length, 'nenhum chunk compartilhado gerado').toBeGreaterThan(0)
47
+ })
48
+ })
49
+
50
+ describe('telas compostas chegam ao pacote publicado', () => {
51
+ it('o subpath raiz entrega MessagesWorkspace, no js e nos tipos', async () => {
52
+ expect(await distText('index.js')).toContain('MessagesWorkspace')
53
+ expect(await distText('index.d.ts')).toContain('MessagesWorkspace')
54
+ })
55
+
56
+ it('o subpath /flows entrega FlowsWorkspace, no js e nos tipos', async () => {
57
+ // Export declarado no `index.ts` e ausente do `dist` é o modo de falhar mais barato de cometer e
58
+ // mais caro de descobrir: só aparece no produto, depois de publicar.
59
+ expect(await distText('flows/index.js')).toContain('FlowsWorkspace')
60
+ expect(await distText('flows/index.d.ts')).toContain('FlowsWorkspace')
61
+ })
62
+
63
+ it('o CSS publicado traz as classes que as telas consomem', async () => {
64
+ const css = await distText('styles.css')
65
+
66
+ // A inbox depende destas classes do pacote (o editor de fluxos e a tela de mensagens estilizam por
67
+ // utilitários do host). Publicar sem elas deixa a tela montada e sem layout — e nada falha.
68
+ expect(css).toContain('.cv-workspace')
69
+ expect(css).toContain('.cv-workspace-modal')
70
+ })
71
+ })
72
+
73
+ describe('o bundle do host não paga por @xyflow/react sem pedir', () => {
74
+ it('o subpath raiz não puxa o xyflow', async () => {
75
+ // É o motivo de o editor viver em `/flows`: quem só usa a inbox não carrega a biblioteca de
76
+ // canvas, que é a maior dependência do pacote.
77
+ expect(await distText('index.js')).not.toContain('@xyflow/react')
78
+ })
79
+ })