@adatechnology/conversations-ui 0.1.0-rc.3 → 0.1.0-rc.31
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.
- package/dist/chunk-CUYYYZWD.js +2317 -0
- package/dist/chunk-DXPSPUWF.js +110 -0
- package/dist/{chunk-OGRRHQQW.js → chunk-WCBDXZ3X.js} +68 -4
- package/dist/flows/index.d.ts +338 -4
- package/dist/flows/index.js +1331 -55
- package/dist/index.d.ts +1074 -42
- package/dist/index.js +3572 -742
- package/dist/preview/index.d.ts +328 -8
- package/dist/preview/index.js +902 -115
- package/dist/styles.css +899 -0
- package/dist/types-De5aN-E_.d.ts +502 -0
- package/package.json +3 -3
- package/src/AudioPlayer.tsx +8 -0
- package/src/AudioRecorderButton.test.tsx +30 -0
- package/src/AudioRecorderButton.tsx +248 -0
- package/src/AudioTranscription.test.tsx +115 -0
- package/src/AudioTranscription.tsx +252 -0
- package/src/Avatar.tsx +14 -3
- package/src/ConversationContextPanel.tsx +218 -44
- package/src/ConversationDocumentsPanel.tsx +347 -24
- package/src/ConversationHeader.test.tsx +66 -0
- package/src/ConversationHeader.tsx +163 -45
- package/src/ConversationListItem.tsx +19 -2
- package/src/ConversationLocalesProvider.tsx +42 -0
- package/src/ConversationRow.tsx +31 -7
- package/src/DocumentsLibrary.tsx +382 -0
- package/src/EmojiPicker.tsx +70 -55
- package/src/FileIcon.test.ts +83 -0
- package/src/FileIcon.tsx +88 -11
- package/src/InteractiveMessage.test.tsx +41 -0
- package/src/InteractiveMessage.tsx +146 -0
- package/src/Lightbox.tsx +18 -3
- package/src/MediaRenderer.tsx +98 -22
- package/src/MessageBubble.test.tsx +41 -0
- package/src/MessageBubble.tsx +75 -6
- package/src/MessageComposer.test.tsx +35 -0
- package/src/MessageComposer.tsx +155 -19
- package/src/RichMessageComposer.test.tsx +113 -0
- package/src/RichMessageComposer.tsx +551 -0
- package/src/SimpleEmojiPicker.tsx +5 -3
- package/src/StatusTicks.tsx +1 -1
- package/src/Toast.tsx +4 -0
- package/src/Tooltip.test.ts +42 -0
- package/src/Tooltip.tsx +164 -0
- package/src/Wallpaper.test.tsx +21 -0
- package/src/Wallpaper.tsx +67 -7
- package/src/WhatsAppMessageEditor.tsx +28 -4
- package/src/WindowExpiredNotice.tsx +12 -4
- package/src/audioRecorderFormat.test.ts +67 -0
- package/src/buildOutput.test.ts +79 -0
- package/src/composer.constant.ts +33 -0
- package/src/conversationTranscript.test.ts +57 -0
- package/src/conversationTranscript.ts +29 -4
- package/src/conversationWindow.ts +7 -5
- package/src/documentTypeLabel.test.ts +57 -0
- package/src/documents/DocumentsWorkspace.tsx +550 -0
- package/src/documents/index.ts +8 -0
- package/src/documents/labels.ts +92 -0
- package/src/emojiCatalog.test.ts +35 -0
- package/src/emojiCatalog.ts +189 -0
- package/src/flows/FlowGroupHeader.tsx +12 -2
- package/src/flows/FlowMapCanvas.tsx +15 -12
- package/src/flows/FlowMapNode.tsx +4 -1
- package/src/flows/FlowNodeCard.tsx +35 -8
- package/src/flows/FlowNodePanel.tsx +149 -38
- package/src/flows/FlowPalette.tsx +13 -3
- package/src/flows/FlowPortalNode.tsx +1 -1
- package/src/flows/FlowWhatsAppPreview.tsx +14 -3
- package/src/flows/FlowsWorkspace.tsx +1023 -0
- package/src/flows/flowCanvasModel.test.ts +312 -0
- package/src/flows/flowCanvasModel.ts +368 -0
- package/src/flows/flowEditorOps.test.ts +241 -0
- package/src/flows/flowEditorOps.ts +177 -0
- package/src/flows/flowGraph.ts +6 -6
- package/src/flows/index.ts +47 -1
- package/src/flows/labels.ts +141 -0
- package/src/flows/workspaceContract.test.ts +95 -0
- package/src/hooks/useContainerWidth.ts +35 -0
- package/src/hooks/useConversationActions.ts +56 -0
- package/src/hooks/useConversationDocuments.ts +11 -7
- package/src/hooks/useConversationList.ts +15 -9
- package/src/hooks/useConversationMessages.ts +2 -2
- package/src/hooks/useConversationRealtime.ts +10 -8
- package/src/hooks/useScrollToLatestMessage.ts +127 -0
- package/src/hooks/useUrlFilterState.ts +107 -0
- package/src/icon.constant.ts +12 -0
- package/src/index.ts +114 -13
- package/src/lib/cn.test.ts +29 -0
- package/src/lib/composer-formatting.test.ts +78 -0
- package/src/lib/composer-formatting.ts +145 -0
- package/src/lib/createMediaUrlResolver.ts +33 -0
- package/src/lib/paginated.test.ts +33 -0
- package/src/lib/paginated.ts +26 -0
- package/src/lib/whatsapp-formatting.test.tsx +37 -0
- package/src/lib/whatsapp-formatting.tsx +28 -3
- package/src/listing/index.tsx +202 -0
- package/src/pagination.constant.ts +10 -0
- package/src/preview/ConversationPreview.tsx +199 -13
- package/src/preview/ConversationSimulatorPanel.test.tsx +55 -0
- package/src/preview/ConversationSimulatorPanel.tsx +89 -0
- package/src/preview/MediaTypesPreview.tsx +87 -0
- package/src/preview/conversationPreviewFailures.test.ts +64 -0
- package/src/preview/createMockConversationsApi.ts +175 -15
- package/src/preview/createPreviewBridgeClient.test.ts +92 -0
- package/src/preview/createPreviewBridgeClient.ts +124 -0
- package/src/preview/createPreviewMediaUploader.ts +82 -0
- package/src/preview/createPreviewWebhookClient.test.ts +96 -0
- package/src/preview/createPreviewWebhookClient.ts +127 -4
- package/src/preview/index.ts +33 -3
- package/src/preview/mediaTypeOf.test.ts +15 -0
- package/src/preview/mockDocumentsSearch.test.ts +57 -0
- package/src/preview/preview.test.ts +5 -3
- package/src/preview/previewFileSamples.test.ts +151 -0
- package/src/preview/previewFileSamples.ts +74 -0
- package/src/preview/previewFixtures.ts +288 -1
- package/src/preview/previewMediaSource.test.ts +62 -0
- package/src/preview/previewMediaSource.ts +91 -0
- package/src/preview/previewMediaUploader.test.ts +61 -0
- package/src/providers/ConversationsProvider.tsx +8 -6
- package/src/providers/types.ts +185 -10
- package/src/quickReply.test.ts +58 -0
- package/src/settings/MessagesWorkspace.tsx +468 -0
- package/src/settings/TopicsForm.tsx +2 -0
- package/src/settings/TranscriptionSettingsForm.test.tsx +81 -0
- package/src/settings/TranscriptionSettingsForm.tsx +190 -0
- package/src/settings/WelcomeFarewellForm.tsx +1 -0
- package/src/settings/WhatsAppCreateTemplateForm.tsx +4 -1
- package/src/settings/WhatsAppTemplateSettingsForm.tsx +5 -2
- package/src/settings/WhatsAppTemplatesSettings.tsx +9 -1
- package/src/styles.css +862 -0
- package/src/types.ts +64 -1
- package/src/useWaitingNotifications.ts +74 -29
- package/src/workspace/BulkTemplateModal.tsx +132 -0
- package/src/workspace/ConversationPane.tsx +432 -0
- package/src/workspace/ConversationsInboxList.tsx +194 -0
- package/src/workspace/ConversationsWorkspace.tsx +346 -0
- package/src/workspace/index.ts +12 -0
- package/src/workspace/labels.test.ts +17 -0
- package/src/workspace/labels.ts +85 -0
- package/src/workspace/useConversationsInbox.ts +332 -0
- package/dist/chunk-N7B24WYD.js +0 -719
- package/dist/chunk-NV2RZ5KT.js +0 -56
- package/dist/types-C0PtaO7S.d.ts +0 -207
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// src/Tooltip.tsx
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
import { createPortal } from "react-dom";
|
|
4
|
+
import { jsx } from "react/jsx-runtime";
|
|
5
|
+
var TOOLTIP_ATTRIBUTE = "data-cv-tooltip";
|
|
6
|
+
var TOOLTIP_DELAY_MS = 120;
|
|
7
|
+
var TOOLTIP_GAP_PX = 8;
|
|
8
|
+
var TOOLTIP_EDGE_PX = 72;
|
|
9
|
+
function tooltipPositionOf({ targetRect, viewportWidth }) {
|
|
10
|
+
const placement = targetRect.top < TOOLTIP_EDGE_PX ? "bottom" : "top";
|
|
11
|
+
const centerX = targetRect.left + targetRect.width / 2;
|
|
12
|
+
return {
|
|
13
|
+
left: Math.min(Math.max(centerX, TOOLTIP_EDGE_PX), viewportWidth - TOOLTIP_EDGE_PX),
|
|
14
|
+
top: placement === "top" ? targetRect.top - TOOLTIP_GAP_PX : targetRect.bottom + TOOLTIP_GAP_PX,
|
|
15
|
+
placement
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function tooltipStateFor(target, text) {
|
|
19
|
+
const position = tooltipPositionOf({
|
|
20
|
+
targetRect: target.getBoundingClientRect(),
|
|
21
|
+
viewportWidth: window.innerWidth
|
|
22
|
+
});
|
|
23
|
+
return { text, ...position };
|
|
24
|
+
}
|
|
25
|
+
var layerOwner;
|
|
26
|
+
function TooltipLayer() {
|
|
27
|
+
const identity = useRef({});
|
|
28
|
+
const [isOwner, setIsOwner] = useState(false);
|
|
29
|
+
const [tooltip, setTooltip] = useState(void 0);
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
const owned = identity.current;
|
|
32
|
+
if (layerOwner && layerOwner !== owned) return void 0;
|
|
33
|
+
layerOwner = owned;
|
|
34
|
+
setIsOwner(true);
|
|
35
|
+
return () => {
|
|
36
|
+
if (layerOwner === owned) layerOwner = void 0;
|
|
37
|
+
};
|
|
38
|
+
}, []);
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (!isOwner) return void 0;
|
|
41
|
+
let timer;
|
|
42
|
+
function cancelPending() {
|
|
43
|
+
if (timer) clearTimeout(timer);
|
|
44
|
+
timer = void 0;
|
|
45
|
+
}
|
|
46
|
+
function hide() {
|
|
47
|
+
cancelPending();
|
|
48
|
+
setTooltip(void 0);
|
|
49
|
+
}
|
|
50
|
+
function handleEnter(event) {
|
|
51
|
+
const origin = event.target;
|
|
52
|
+
if (!(origin instanceof Element)) return;
|
|
53
|
+
const target = origin.closest(`[${TOOLTIP_ATTRIBUTE}]`);
|
|
54
|
+
if (!(target instanceof HTMLElement)) {
|
|
55
|
+
hide();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const text = target.getAttribute(TOOLTIP_ATTRIBUTE);
|
|
59
|
+
if (!text) {
|
|
60
|
+
hide();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
cancelPending();
|
|
64
|
+
timer = setTimeout(() => setTooltip(tooltipStateFor(target, text)), TOOLTIP_DELAY_MS);
|
|
65
|
+
}
|
|
66
|
+
function handleLeaveWindow(event) {
|
|
67
|
+
if (!event.relatedTarget) hide();
|
|
68
|
+
}
|
|
69
|
+
function handleKeyDown(event) {
|
|
70
|
+
if (event.key === "Escape") hide();
|
|
71
|
+
}
|
|
72
|
+
document.addEventListener("pointerover", handleEnter);
|
|
73
|
+
document.addEventListener("pointerout", handleLeaveWindow);
|
|
74
|
+
document.addEventListener("focusin", handleEnter);
|
|
75
|
+
document.addEventListener("pointerdown", hide);
|
|
76
|
+
document.addEventListener("focusout", hide);
|
|
77
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
78
|
+
window.addEventListener("scroll", hide, true);
|
|
79
|
+
window.addEventListener("resize", hide);
|
|
80
|
+
return () => {
|
|
81
|
+
cancelPending();
|
|
82
|
+
document.removeEventListener("pointerover", handleEnter);
|
|
83
|
+
document.removeEventListener("pointerout", handleLeaveWindow);
|
|
84
|
+
document.removeEventListener("focusin", handleEnter);
|
|
85
|
+
document.removeEventListener("pointerdown", hide);
|
|
86
|
+
document.removeEventListener("focusout", hide);
|
|
87
|
+
document.removeEventListener("keydown", handleKeyDown);
|
|
88
|
+
window.removeEventListener("scroll", hide, true);
|
|
89
|
+
window.removeEventListener("resize", hide);
|
|
90
|
+
};
|
|
91
|
+
}, [isOwner]);
|
|
92
|
+
if (!tooltip) return null;
|
|
93
|
+
return createPortal(
|
|
94
|
+
/* @__PURE__ */ jsx(
|
|
95
|
+
"div",
|
|
96
|
+
{
|
|
97
|
+
role: "tooltip",
|
|
98
|
+
className: `cv-tooltip cv-tooltip--${tooltip.placement}`,
|
|
99
|
+
style: { left: tooltip.left, top: tooltip.top },
|
|
100
|
+
children: tooltip.text
|
|
101
|
+
}
|
|
102
|
+
),
|
|
103
|
+
document.body
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export {
|
|
108
|
+
TOOLTIP_ATTRIBUTE,
|
|
109
|
+
TooltipLayer
|
|
110
|
+
};
|
|
@@ -69,9 +69,12 @@ function parseInlineTokens(text) {
|
|
|
69
69
|
if (remaining) result.push({ type: "text", content: remaining });
|
|
70
70
|
break;
|
|
71
71
|
}
|
|
72
|
-
if (nextSpecial
|
|
73
|
-
result.push({ type: "text", content: remaining.slice(0,
|
|
72
|
+
if (nextSpecial === 0) {
|
|
73
|
+
result.push({ type: "text", content: remaining.slice(0, 1) });
|
|
74
|
+
remaining = remaining.slice(1);
|
|
75
|
+
continue;
|
|
74
76
|
}
|
|
77
|
+
result.push({ type: "text", content: remaining.slice(0, nextSpecial) });
|
|
75
78
|
remaining = remaining.slice(nextSpecial);
|
|
76
79
|
}
|
|
77
80
|
return result;
|
|
@@ -103,6 +106,9 @@ function unescapeHtml(text) {
|
|
|
103
106
|
}
|
|
104
107
|
var CODE_TOKEN_MARK = String.fromCharCode(57344);
|
|
105
108
|
var CODE_TOKEN_REGEX = new RegExp(`${CODE_TOKEN_MARK}(\\d+)${CODE_TOKEN_MARK}`, "g");
|
|
109
|
+
var ZERO_WIDTH_SPACE = String.fromCharCode(8203);
|
|
110
|
+
var EMPTY_CODE_REGEX = new RegExp(`<code[^>]*>[${ZERO_WIDTH_SPACE}\\s]*</code>`, "gi");
|
|
111
|
+
var ZERO_WIDTH_SPACE_REGEX = new RegExp(ZERO_WIDTH_SPACE, "g");
|
|
106
112
|
function waToHTML(text) {
|
|
107
113
|
if (!text) return "";
|
|
108
114
|
const codeTokens = [];
|
|
@@ -117,7 +123,7 @@ function waToHTML(text) {
|
|
|
117
123
|
let html = escapeHtml(working);
|
|
118
124
|
html = html.replace(/\*([^*\n]+)\*/g, "<strong>$1</strong>");
|
|
119
125
|
html = html.replace(/_([^_\n]+)_/g, "<em>$1</em>");
|
|
120
|
-
html = html.replace(/~([^~\n]+)~/g, "<
|
|
126
|
+
html = html.replace(/~([^~\n]+)~/g, "<s>$1</s>");
|
|
121
127
|
html = html.replace(/\n/g, "<br>");
|
|
122
128
|
html = html.replace(CODE_TOKEN_REGEX, (_match, indexStr) => {
|
|
123
129
|
const token = codeTokens[Number(indexStr)];
|
|
@@ -132,6 +138,7 @@ function waToHTML(text) {
|
|
|
132
138
|
function htmlToWA(html) {
|
|
133
139
|
if (!html) return "";
|
|
134
140
|
let text = html;
|
|
141
|
+
text = text.replace(EMPTY_CODE_REGEX, "");
|
|
135
142
|
text = text.replace(/<code data-wa="block"[^>]*>([\s\S]*?)<\/code>/gi, (_match, inner) => `\`\`\`${unescapeHtml(inner.replace(/<br\s*\/?>/gi, "\n"))}\`\`\``);
|
|
136
143
|
text = text.replace(/<code data-wa="inline"[^>]*>([\s\S]*?)<\/code>/gi, (_match, inner) => `\`${unescapeHtml(inner)}\``);
|
|
137
144
|
text = text.replace(/<br\s*\/?>/gi, "\n").replace(/<div>/gi, "\n").replace(/<\/div>/gi, "").replace(/<\/p>/gi, "\n").replace(/<p[^>]*>/gi, "");
|
|
@@ -141,9 +148,11 @@ function htmlToWA(html) {
|
|
|
141
148
|
text = text.replace(/<i>(.*?)<\/i>/gi, "_$1_");
|
|
142
149
|
text = text.replace(/<del>(.*?)<\/del>/gi, "~$1~");
|
|
143
150
|
text = text.replace(/<s>(.*?)<\/s>/gi, "~$1~");
|
|
151
|
+
text = text.replace(/<strike>(.*?)<\/strike>/gi, "~$1~");
|
|
144
152
|
text = text.replace(/<code[^>]*>(.*?)<\/code>/gi, "`$1`");
|
|
145
153
|
text = text.replace(/<[^>]+>/g, "");
|
|
146
154
|
text = unescapeHtml(text);
|
|
155
|
+
text = text.replace(ZERO_WIDTH_SPACE_REGEX, "");
|
|
147
156
|
return text.trim();
|
|
148
157
|
}
|
|
149
158
|
function waToHTMLInline(text) {
|
|
@@ -151,9 +160,64 @@ function waToHTMLInline(text) {
|
|
|
151
160
|
return escapeHtml(text).replace(/\*([^*\n]+)\*/g, "<strong>$1</strong>").replace(/_([^_\n]+)_/g, "<em>$1</em>").replace(/~([^~\n]+)~/g, "<del>$1</del>").replace(/`([^`\n]+)`/g, "<code>$1</code>");
|
|
152
161
|
}
|
|
153
162
|
|
|
163
|
+
// src/useDarkMode.ts
|
|
164
|
+
import { useState, useEffect, useCallback } from "react";
|
|
165
|
+
var STORAGE_KEY = "conversations-ui-dark-mode";
|
|
166
|
+
function getInitialDark() {
|
|
167
|
+
if (typeof window === "undefined") return false;
|
|
168
|
+
const stored = localStorage.getItem(STORAGE_KEY);
|
|
169
|
+
if (stored !== null) {
|
|
170
|
+
return stored === "true";
|
|
171
|
+
}
|
|
172
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
173
|
+
}
|
|
174
|
+
function useIsDarkTheme() {
|
|
175
|
+
const [isDark, setIsDark] = useState(
|
|
176
|
+
() => typeof document !== "undefined" && document.documentElement.classList.contains("dark")
|
|
177
|
+
);
|
|
178
|
+
useEffect(() => {
|
|
179
|
+
const root = document.documentElement;
|
|
180
|
+
const observer = new MutationObserver(() => setIsDark(root.classList.contains("dark")));
|
|
181
|
+
observer.observe(root, { attributes: true, attributeFilter: ["class"] });
|
|
182
|
+
setIsDark(root.classList.contains("dark"));
|
|
183
|
+
return () => observer.disconnect();
|
|
184
|
+
}, []);
|
|
185
|
+
return isDark;
|
|
186
|
+
}
|
|
187
|
+
function useDarkMode() {
|
|
188
|
+
const [isDark, setIsDark] = useState(getInitialDark);
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
const root = document.documentElement;
|
|
191
|
+
if (isDark) {
|
|
192
|
+
root.classList.add("dark");
|
|
193
|
+
} else {
|
|
194
|
+
root.classList.remove("dark");
|
|
195
|
+
}
|
|
196
|
+
localStorage.setItem(STORAGE_KEY, String(isDark));
|
|
197
|
+
}, [isDark]);
|
|
198
|
+
useEffect(() => {
|
|
199
|
+
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
|
200
|
+
const handleChange = (event) => {
|
|
201
|
+
const stored = localStorage.getItem(STORAGE_KEY);
|
|
202
|
+
if (stored === null) {
|
|
203
|
+
setIsDark(event.matches);
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
mediaQuery.addEventListener("change", handleChange);
|
|
207
|
+
return () => mediaQuery.removeEventListener("change", handleChange);
|
|
208
|
+
}, []);
|
|
209
|
+
const toggle = useCallback(() => {
|
|
210
|
+
setIsDark((prev) => !prev);
|
|
211
|
+
}, []);
|
|
212
|
+
return { isDark, toggle };
|
|
213
|
+
}
|
|
214
|
+
|
|
154
215
|
export {
|
|
155
216
|
parseWhatsAppFormatting,
|
|
217
|
+
ZERO_WIDTH_SPACE,
|
|
156
218
|
waToHTML,
|
|
157
219
|
htmlToWA,
|
|
158
|
-
waToHTMLInline
|
|
220
|
+
waToHTMLInline,
|
|
221
|
+
useIsDarkTheme,
|
|
222
|
+
useDarkMode
|
|
159
223
|
};
|
package/dist/flows/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
2
3
|
import { NodeProps } from '@xyflow/react';
|
|
3
4
|
import { LucideIcon } from 'lucide-react';
|
|
4
5
|
import { FlowConditionOperator, FlowGraphData, FlowNodeData, FlowNodeType, FlowQuestionType, FlowActionKind } from '@adatechnology/meta-whatsapp-contracts';
|
|
@@ -9,6 +10,7 @@ declare const BUILT_IN_ACTION_KINDS: {
|
|
|
9
10
|
readonly HANDOFF: "handoff";
|
|
10
11
|
readonly RATE_LIMITED_HANDOFF: "rate_limited_handoff";
|
|
11
12
|
readonly SEND_PRODUCT_LIST: "send_product_list";
|
|
13
|
+
readonly SEND_MEDIA: "send_media";
|
|
12
14
|
};
|
|
13
15
|
declare const CROSS_FLOW_PREFIX = "flow:";
|
|
14
16
|
declare const isCrossFlowTarget: (target: string) => boolean;
|
|
@@ -62,11 +64,12 @@ type CollectionChain = {
|
|
|
62
64
|
actionNodeId: string;
|
|
63
65
|
};
|
|
64
66
|
declare function findCollectionChains(graph: FlowGraphData): CollectionChain[];
|
|
65
|
-
declare function slugifyNodeId(label: string, existing:
|
|
67
|
+
declare function slugifyNodeId(label: string, existing: ReadonlySet<string>): string;
|
|
66
68
|
|
|
67
69
|
interface FlowEditorLabels {
|
|
68
70
|
legend: Record<FlowNodeType, string>;
|
|
69
71
|
startNodeTooltip: string;
|
|
72
|
+
detachedNodeTooltip: string;
|
|
70
73
|
liveCountTooltip: (count: number) => string;
|
|
71
74
|
edgeFallbackLabel: string;
|
|
72
75
|
actionKindLabels: Record<string, string>;
|
|
@@ -75,10 +78,15 @@ interface FlowEditorLabels {
|
|
|
75
78
|
nodePanel: {
|
|
76
79
|
title: string;
|
|
77
80
|
contextKey: string;
|
|
81
|
+
nodeName: string;
|
|
82
|
+
nodeNamePlaceholder: string;
|
|
83
|
+
nodeNameHint: string;
|
|
78
84
|
questionType: string;
|
|
79
85
|
question: string;
|
|
80
86
|
options: string;
|
|
81
87
|
addOption: string;
|
|
88
|
+
removeOption: string;
|
|
89
|
+
close: string;
|
|
82
90
|
optionId: string;
|
|
83
91
|
optionLabel: string;
|
|
84
92
|
next: string;
|
|
@@ -109,6 +117,8 @@ interface FlowEditorLabels {
|
|
|
109
117
|
conditionTrue: string;
|
|
110
118
|
conditionFalse: string;
|
|
111
119
|
conditionVariableMissing: string;
|
|
120
|
+
media: string;
|
|
121
|
+
mediaUnavailable: string;
|
|
112
122
|
};
|
|
113
123
|
palette: {
|
|
114
124
|
title: string;
|
|
@@ -121,6 +131,8 @@ interface FlowEditorLabels {
|
|
|
121
131
|
flowMap: {
|
|
122
132
|
nodeCount: (count: number) => string;
|
|
123
133
|
openFlow: string;
|
|
134
|
+
toggleToMap: string;
|
|
135
|
+
toggleToDetail: string;
|
|
124
136
|
};
|
|
125
137
|
flowGroup: {
|
|
126
138
|
focus: string;
|
|
@@ -130,6 +142,66 @@ interface FlowEditorLabels {
|
|
|
130
142
|
tooltip: string;
|
|
131
143
|
goesTo: (label: string) => string;
|
|
132
144
|
};
|
|
145
|
+
collectionChain: {
|
|
146
|
+
feeds: (label: string) => string;
|
|
147
|
+
};
|
|
148
|
+
/**
|
|
149
|
+
* Texto dos problemas encontrados por `validateGraph`. Vive aqui, e não no host, porque a tela
|
|
150
|
+
* composta é quem valida — deixar de fora obrigaria todo produto a repassar o mesmo mapa de
|
|
151
|
+
* funções só para a barra de erros aparecer.
|
|
152
|
+
*/
|
|
153
|
+
validation: FlowValidationLabels;
|
|
154
|
+
/** Barra de cima, estados de carregamento e ações do editor inteiro. */
|
|
155
|
+
workspace: {
|
|
156
|
+
title: string;
|
|
157
|
+
subtitle: string;
|
|
158
|
+
loading: string;
|
|
159
|
+
loadError: string;
|
|
160
|
+
saveGraph: string;
|
|
161
|
+
saving: string;
|
|
162
|
+
saveSuccess: string;
|
|
163
|
+
saveError: string;
|
|
164
|
+
organize: string;
|
|
165
|
+
organizeTooltip: string;
|
|
166
|
+
discardChanges: string;
|
|
167
|
+
discardTooltip: string;
|
|
168
|
+
discardConfirm: string;
|
|
169
|
+
unsavedChangesConfirm: string;
|
|
170
|
+
};
|
|
171
|
+
flowManager: {
|
|
172
|
+
newFlow: string;
|
|
173
|
+
createTitle: string;
|
|
174
|
+
key: string;
|
|
175
|
+
keyHint: string;
|
|
176
|
+
keyInvalid: string;
|
|
177
|
+
label: string;
|
|
178
|
+
showInMenu: string;
|
|
179
|
+
menuOptionLabel: string;
|
|
180
|
+
create: string;
|
|
181
|
+
creating: string;
|
|
182
|
+
deleteFlow: string;
|
|
183
|
+
deleteConfirm: (label: string) => string;
|
|
184
|
+
createError: string;
|
|
185
|
+
deleteError: string;
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
interface FlowValidationLabels {
|
|
189
|
+
title: string;
|
|
190
|
+
errors: (count: number) => string;
|
|
191
|
+
warnings: (count: number) => string;
|
|
192
|
+
noStart: string;
|
|
193
|
+
brokenRef: (from: string, to: string) => string;
|
|
194
|
+
choiceWithoutOptions: (id: string) => string;
|
|
195
|
+
duplicatedOptionId: (id: string, optionId: string) => string;
|
|
196
|
+
optionWithoutTarget: (id: string, optionLabel: string) => string;
|
|
197
|
+
tooManyOptions: (id: string, count: number) => string;
|
|
198
|
+
buttonTitleTooLong: (id: string, label: string) => string;
|
|
199
|
+
listTitleTooLong: (id: string, label: string) => string;
|
|
200
|
+
bodyTooLong: (id: string) => string;
|
|
201
|
+
unreachable: (id: string) => string;
|
|
202
|
+
deadEndQuestion: (id: string) => string;
|
|
203
|
+
conditionIncomplete: (id: string) => string;
|
|
204
|
+
conditionBranchMissing: (id: string, branch: string) => string;
|
|
133
205
|
}
|
|
134
206
|
declare const DEFAULT_FLOW_EDITOR_LABELS: FlowEditorLabels;
|
|
135
207
|
declare function mergeFlowEditorLabels(override?: Partial<FlowEditorLabels>): FlowEditorLabels;
|
|
@@ -140,6 +212,12 @@ type FlowNodeCardData = {
|
|
|
140
212
|
liveCount: number;
|
|
141
213
|
isStart: boolean;
|
|
142
214
|
isSelected: boolean;
|
|
215
|
+
/**
|
|
216
|
+
* Nó sem nenhuma ligação de entrada — o bot nunca chega nele. Contorno tracejado e pulso: sem
|
|
217
|
+
* isso, desconectar um fio ou criar um card solto passa despercebido até o fluxo quebrar em
|
|
218
|
+
* produção.
|
|
219
|
+
*/
|
|
220
|
+
isDetached?: boolean;
|
|
143
221
|
issues: GraphIssue[];
|
|
144
222
|
labels: FlowEditorLabels;
|
|
145
223
|
actionKindIcons?: Record<string, LucideIcon>;
|
|
@@ -233,14 +311,270 @@ interface FlowNodePanelProps {
|
|
|
233
311
|
onChange: (updated: FlowNodeData) => void;
|
|
234
312
|
onDelete: (nodeId: string) => void;
|
|
235
313
|
labels?: Partial<FlowEditorLabels>;
|
|
314
|
+
/**
|
|
315
|
+
* Seletor de arquivos do nó `send_media`, renderizado no lugar da mensagem direta.
|
|
316
|
+
*
|
|
317
|
+
* Slot, e não uma lista de arquivos por prop, porque a biblioteca é do host: upload, permissão e
|
|
318
|
+
* URL assinada são dele, e o painel não tem como buscar nada. Ausente, o nó continua editável —
|
|
319
|
+
* só não dá para anexar por aqui.
|
|
320
|
+
*/
|
|
321
|
+
renderMediaPicker?: (node: FlowNodeData, graph: FlowGraphData) => ReactNode;
|
|
236
322
|
}
|
|
237
|
-
declare function FlowNodePanel({ graph, node, issues, otherFlows, onClose, onChange, onDelete, labels: labelsOverride, }: FlowNodePanelProps): react.JSX.Element;
|
|
323
|
+
declare function FlowNodePanel({ graph, node, issues, otherFlows, onClose, onChange, onDelete, labels: labelsOverride, renderMediaPicker, }: FlowNodePanelProps): react.JSX.Element;
|
|
238
324
|
|
|
239
325
|
interface FlowWhatsAppPreviewProps {
|
|
240
326
|
body: string;
|
|
241
327
|
options?: [string, string][];
|
|
242
328
|
labels?: FlowEditorLabels['nodePanel'];
|
|
243
329
|
}
|
|
244
|
-
declare function FlowWhatsAppPreview({ body, options, labels }: FlowWhatsAppPreviewProps): react.JSX.Element;
|
|
330
|
+
declare function FlowWhatsAppPreview({ body, options, labels, }: FlowWhatsAppPreviewProps): react.JSX.Element;
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Derivações puras do canvas do editor: posições, arestas, contagem de conversas vivas e o nó que
|
|
334
|
+
* nasce de cada item da paleta.
|
|
335
|
+
*
|
|
336
|
+
* Viviam dentro do componente, e é ali que estava o risco. Uma aresta com destino errado não pinta
|
|
337
|
+
* errado — ela leva a conversa do cliente para o lugar errado, e o sintoma aparece longe de quem
|
|
338
|
+
* editou. Um card que o layout joga para fora da área visível se lê como "apaguei sem querer".
|
|
339
|
+
*
|
|
340
|
+
* A topologia sai daqui em forma neutra (`FlowEdgeSpec`) e o estilo fica no componente: o teste cobra
|
|
341
|
+
* o que importa e roda sem navegador nem `@xyflow/react`. Cor de traço ninguém quebra sem ver.
|
|
342
|
+
*/
|
|
343
|
+
|
|
344
|
+
type FlowNodePosition = {
|
|
345
|
+
readonly x: number;
|
|
346
|
+
readonly y: number;
|
|
347
|
+
};
|
|
348
|
+
/** Onde cada conversa viva está parada agora — uma linha por sessão. */
|
|
349
|
+
type FlowLivePosition = {
|
|
350
|
+
readonly currentState: string;
|
|
351
|
+
readonly flow: string | null;
|
|
352
|
+
readonly nodeId: string | null;
|
|
353
|
+
readonly menuNodeId: string | null;
|
|
354
|
+
};
|
|
355
|
+
/**
|
|
356
|
+
* Quantas conversas estão paradas num nó — o formato que o servidor já agrega.
|
|
357
|
+
*
|
|
358
|
+
* `meta-whatsapp-module` responde `GROUP BY (flowKey, currentNodeId)`, e trazer uma linha por
|
|
359
|
+
* sessão só para o editor recontar em memória seria pior em toda base grande. Os dois formatos
|
|
360
|
+
* valem porque produto que já implementou o de sessão não deve quebrar numa release de correção.
|
|
361
|
+
*/
|
|
362
|
+
type FlowLiveNodeCount = {
|
|
363
|
+
readonly flowKey: string;
|
|
364
|
+
readonly nodeId: string;
|
|
365
|
+
readonly count: number;
|
|
366
|
+
};
|
|
367
|
+
type FlowLivePositionInput = FlowLivePosition | FlowLiveNodeCount;
|
|
368
|
+
/**
|
|
369
|
+
* Pseudo-nó que representa um fluxo alvo ainda não mesclado no canvas.
|
|
370
|
+
*
|
|
371
|
+
* Um portal por (nó de origem, fluxo alvo): duas opções do mesmo nó indo para o mesmo fluxo
|
|
372
|
+
* compartilham o portal, senão o card ficaria cercado de caixas idênticas.
|
|
373
|
+
*/
|
|
374
|
+
declare function portalNodeId(sourceNodeId: string, target: string): string;
|
|
375
|
+
declare function chainFrameNodeId(actionNodeId: string): string;
|
|
376
|
+
declare const GROUP_HEADER_NODE_ID = "__group_header__";
|
|
377
|
+
/**
|
|
378
|
+
* Quantas conversas estão paradas em cada nó de um fluxo.
|
|
379
|
+
*
|
|
380
|
+
* A raiz é caso à parte no formato por sessão: o servidor guarda o passo do menu em `menuNodeId`,
|
|
381
|
+
* num campo próprio, e uma conversa no menu não carrega `flow`. Ler `nodeId` ali daria contagem
|
|
382
|
+
* zero na tela mais visitada do editor. No formato agregado a chave do fluxo já vem na linha,
|
|
383
|
+
* então a raiz não tem exceção.
|
|
384
|
+
*/
|
|
385
|
+
declare function countLiveByNode(params: {
|
|
386
|
+
readonly flowKey: string;
|
|
387
|
+
readonly rootFlowKey: string;
|
|
388
|
+
readonly positions: readonly FlowLivePositionInput[] | undefined;
|
|
389
|
+
}): Record<string, number>;
|
|
390
|
+
/**
|
|
391
|
+
* Um layout só para TODOS os nós de TODOS os fluxos abertos juntos.
|
|
392
|
+
*
|
|
393
|
+
* Posicionar cada fluxo à parte e deslocar não resolve: nada impede dois fluxos de ocuparem o mesmo
|
|
394
|
+
* espaço, e a altura real de cada card é ignorada. Aqui o ranqueamento por BFS roda sobre o grafo
|
|
395
|
+
* mesclado inteiro, com os saltos `flow:<key>` já resolvidos para o nó inicial do alvo.
|
|
396
|
+
*/
|
|
397
|
+
declare function computeMergedLayout(params: {
|
|
398
|
+
readonly openKeys: readonly string[];
|
|
399
|
+
readonly graphs: Readonly<Record<string, FlowGraphData>>;
|
|
400
|
+
readonly primaryFlowKey: string;
|
|
401
|
+
}): Map<string, FlowNodePosition>;
|
|
402
|
+
/** Papel visual da aresta. O componente traduz em traço, cor e seta; o modelo só decide qual é. */
|
|
403
|
+
type FlowEdgeKind = 'linear' | 'branch' | 'fallback';
|
|
404
|
+
type FlowEdgeSpec = {
|
|
405
|
+
readonly id: string;
|
|
406
|
+
readonly source: string;
|
|
407
|
+
readonly target: string;
|
|
408
|
+
readonly sourceHandle?: string;
|
|
409
|
+
readonly kind: FlowEdgeKind;
|
|
410
|
+
/** Salto entre fluxos — tracejado, esteja o alvo mesclado ou num portal. */
|
|
411
|
+
readonly crossFlow: boolean;
|
|
412
|
+
readonly live: boolean;
|
|
413
|
+
};
|
|
414
|
+
/**
|
|
415
|
+
* Arestas de todos os fluxos abertos.
|
|
416
|
+
*
|
|
417
|
+
* A regra que não é óbvia: um salto `flow:<key>` vira ligação real até o nó inicial do alvo quando
|
|
418
|
+
* esse fluxo já está no canvas, e só cai no portal quando não está. Sem isso, abrir dois fluxos
|
|
419
|
+
* ligados mostrava uma caixa de portal entre dois cards que já estavam ali do lado.
|
|
420
|
+
*/
|
|
421
|
+
declare function buildFlowEdges(params: {
|
|
422
|
+
readonly openKeys: readonly string[];
|
|
423
|
+
readonly graphs: Readonly<Record<string, FlowGraphData>>;
|
|
424
|
+
readonly rootFlowKey: string;
|
|
425
|
+
readonly livePositions?: readonly FlowLivePositionInput[] | undefined;
|
|
426
|
+
}): FlowEdgeSpec[];
|
|
427
|
+
/** Nós de um fluxo que ninguém aponta — o card ganha contorno tracejado para cobrar a ligação. */
|
|
428
|
+
declare function detachedNodeIds(graph: FlowGraphData): Set<string>;
|
|
429
|
+
/** Retângulo que envolve uma cadeia de coleta, em coordenadas do canvas. */
|
|
430
|
+
declare function chainFrameBounds(params: {
|
|
431
|
+
readonly nodeIds: readonly string[];
|
|
432
|
+
readonly graph: FlowGraphData;
|
|
433
|
+
readonly positionOf: (nodeId: string) => FlowNodePosition;
|
|
434
|
+
readonly padding: number;
|
|
435
|
+
}): {
|
|
436
|
+
x: number;
|
|
437
|
+
y: number;
|
|
438
|
+
width: number;
|
|
439
|
+
height: number;
|
|
440
|
+
};
|
|
441
|
+
/**
|
|
442
|
+
* O nó que nasce de cada item da paleta.
|
|
443
|
+
*
|
|
444
|
+
* `contextKey` igual ao id porque é o que o motor usa para guardar a resposta: deixá-lo vazio faria a
|
|
445
|
+
* pergunta ser feita e a resposta descartada, sem erro em lugar nenhum.
|
|
446
|
+
*/
|
|
447
|
+
declare function newNodeFromSpec(spec: NewNodeSpec, existingIds: ReadonlySet<string>): FlowNodeData;
|
|
448
|
+
|
|
449
|
+
interface CreateFlowInput {
|
|
450
|
+
key: string;
|
|
451
|
+
label: string;
|
|
452
|
+
showInMenu: boolean;
|
|
453
|
+
/** Ausente quando `showInMenu` é falso — não há opção de menu para rotular. */
|
|
454
|
+
menuOptionLabel?: string;
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Backend de fluxos do host. Funções cruas em vez de um cliente HTTP: o pacote roda em produtos com
|
|
458
|
+
* axios, fetch e react-query, e nenhum deles precisa entrar como dependência daqui.
|
|
459
|
+
*/
|
|
460
|
+
interface FlowsWorkspaceApi {
|
|
461
|
+
getGraphs(): Promise<Record<string, FlowGraphData>>;
|
|
462
|
+
saveGraph(key: string, graph: FlowGraphData): Promise<void>;
|
|
463
|
+
/**
|
|
464
|
+
* Criar e excluir fluxo são **opcionais por capacidade**: produto cujos fluxos vêm de um seed
|
|
465
|
+
* versionado não expõe rota para isso, e a tela simplesmente não desenha os botões — em vez de
|
|
466
|
+
* oferecer uma ação que estoura no clique.
|
|
467
|
+
*/
|
|
468
|
+
createFlow?(input: CreateFlowInput): Promise<void>;
|
|
469
|
+
deleteFlow?(key: string): Promise<void>;
|
|
470
|
+
/**
|
|
471
|
+
* Onde estão as conversas vivas. Ausente, os cards não pulsam e nada é consultado.
|
|
472
|
+
*
|
|
473
|
+
* Aceita a linha por sessão e a linha já agregada por nó — `meta-whatsapp-module` responde a
|
|
474
|
+
* segunda, e exigir a primeira deixava os cards parados em todo produto que usa o módulo.
|
|
475
|
+
*/
|
|
476
|
+
getLivePositions?(): Promise<readonly FlowLivePositionInput[]>;
|
|
477
|
+
}
|
|
478
|
+
interface FlowsWorkspaceProps {
|
|
479
|
+
readonly api: FlowsWorkspaceApi;
|
|
480
|
+
/** Fluxo raiz — o que abre por padrão e o único que não pode ser excluído. */
|
|
481
|
+
readonly rootFlowKey?: string;
|
|
482
|
+
readonly labels?: Partial<FlowEditorLabels>;
|
|
483
|
+
/** Kinds de ação do produto oferecidos na paleta (`trigger_simulation`, `abrir_comanda`…). */
|
|
484
|
+
readonly actionOptions?: readonly FlowPaletteActionOption[];
|
|
485
|
+
/** Seletor de arquivos do nó `send_media` — a biblioteca é do host, então entra por slot. */
|
|
486
|
+
readonly renderMediaPicker?: (node: FlowNodeData, graph: FlowGraphData) => ReactNode;
|
|
487
|
+
/** Intervalo do polling de posições vivas. Só tem efeito com `getLivePositions`. */
|
|
488
|
+
readonly livePollIntervalMs?: number;
|
|
489
|
+
/**
|
|
490
|
+
* Título e subtítulo do editor. `false` deixa só a barra de ações.
|
|
491
|
+
*
|
|
492
|
+
* Produto cuja navegação já nomeia a tela mostrava o nome duas vezes, em dois tamanhos, porque a
|
|
493
|
+
* tipografia daqui é do pacote e a de lá é do host. Esconder é a única saída que não força o
|
|
494
|
+
* pacote a adivinhar a escala tipográfica de cada produto.
|
|
495
|
+
*/
|
|
496
|
+
readonly showHeader?: boolean;
|
|
497
|
+
readonly className?: string;
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Editor de fluxograma completo — barra de ações, abas de fluxo, paleta, canvas com fusão
|
|
501
|
+
* editável, painel de nó, mapa de fluxos e diálogos de criar/excluir.
|
|
502
|
+
*
|
|
503
|
+
* É a tela inteira, não as peças: cada produto que remontava esse grid à mão acabava com uma
|
|
504
|
+
* versão diferente do mesmo editor. Customização entra por `labels`, `actionOptions` e
|
|
505
|
+
* `renderMediaPicker` — nunca por cópia do arquivo.
|
|
506
|
+
*/
|
|
507
|
+
declare function FlowsWorkspace({ api, rootFlowKey, labels: labelsOverride, actionOptions, renderMediaPicker, livePollIntervalMs, showHeader, className, }: FlowsWorkspaceProps): react.JSX.Element;
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Copyright (c) 2026 Ada Technology. MIT License.
|
|
511
|
+
*
|
|
512
|
+
* Operações puras de grafo que o editor de fluxo precisa, e que viviam soltas dentro da página de
|
|
513
|
+
* 973 linhas do financiamento.
|
|
514
|
+
*
|
|
515
|
+
* Puras e separadas do hook de propósito: são a parte que dá para testar sem navegador, sem React e
|
|
516
|
+
* sem estado — e três delas (`removeNodeAndCleanRefs`, `resolveConnection`, `mergedFlowKeysFrom`)
|
|
517
|
+
* decidem o que acontece com o fluxo que alguém desenhou. Errar ali não dá erro; dá aresta apontando
|
|
518
|
+
* para nó que não existe mais, ou fluxo que some do canvas.
|
|
519
|
+
*/
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Id de nó no canvas mesclado: vários fluxos dividem o mesmo espaço, e `boas-vindas` pode existir em
|
|
523
|
+
* dois deles. Sem o prefixo, arrastar um card moveria o homônimo do outro fluxo.
|
|
524
|
+
*/
|
|
525
|
+
declare function namespaceNodeId(flowKey: string, nodeId: string): string;
|
|
526
|
+
declare function parseNamespacedId(value: string): {
|
|
527
|
+
flowKey: string;
|
|
528
|
+
nodeId: string;
|
|
529
|
+
};
|
|
530
|
+
/**
|
|
531
|
+
* Apaga o nó E as referências a ele.
|
|
532
|
+
*
|
|
533
|
+
* A limpeza não é cortesia: uma aresta apontando para nó inexistente faz o motor do bot parar a
|
|
534
|
+
* conversa no meio, e o sintoma aparece para o cliente, não para quem editou.
|
|
535
|
+
*/
|
|
536
|
+
declare function removeNodeAndCleanRefs(nodes: Readonly<Record<string, FlowNodeData>>, removedId: string): Record<string, FlowNodeData>;
|
|
537
|
+
/**
|
|
538
|
+
* O fecho transitivo dos fluxos alcançáveis a partir de um — é o conjunto que o canvas abre junto.
|
|
539
|
+
*
|
|
540
|
+
* BFS e não recursão: fluxo que referencia a si mesmo (menu que volta ao menu) é comum, e recursão
|
|
541
|
+
* ingênua estouraria a pilha no caso mais banal que existe.
|
|
542
|
+
*/
|
|
543
|
+
declare function mergedFlowKeysFrom(rootKey: string, graphs: Readonly<Record<string, FlowGraphData>>): readonly string[];
|
|
544
|
+
type ConnectionRequest = {
|
|
545
|
+
readonly source: string;
|
|
546
|
+
readonly target: string;
|
|
547
|
+
readonly sourceHandle?: string | null | undefined;
|
|
548
|
+
};
|
|
549
|
+
type ResolvedConnection = {
|
|
550
|
+
readonly flowKey: string;
|
|
551
|
+
readonly nodeId: string;
|
|
552
|
+
readonly handle: string;
|
|
553
|
+
/** O que gravar no `next`: id de nó local, ou `flow:<key>` quando atravessa fluxo. */
|
|
554
|
+
readonly targetValue: string;
|
|
555
|
+
};
|
|
556
|
+
/**
|
|
557
|
+
* Traduz um arraste de aresta no valor que vai para o `next` — e recusa o que o motor do bot não
|
|
558
|
+
* sabe executar.
|
|
559
|
+
*
|
|
560
|
+
* A regra que não é óbvia: conectar num nó de OUTRO fluxo só funciona se for o nó inicial dele,
|
|
561
|
+
* porque o motor só sabe pular para o começo de um fluxo, não para um nó do meio. Conectar no meio
|
|
562
|
+
* devolve `undefined` — recusa silenciosa é melhor que gravar um salto que o bot vai ignorar em
|
|
563
|
+
* produção, deixando a conversa parada sem ninguém entender por quê.
|
|
564
|
+
*/
|
|
565
|
+
declare function resolveConnection(params: {
|
|
566
|
+
readonly connection: ConnectionRequest;
|
|
567
|
+
readonly graphs: Readonly<Record<string, FlowGraphData>>;
|
|
568
|
+
}): ResolvedConnection | undefined;
|
|
569
|
+
/** Aplica a conexão resolvida no nó. `next` string para saída única, objeto para ramificação. */
|
|
570
|
+
declare function applyConnection(node: FlowNodeData, resolved: ResolvedConnection): FlowNodeData;
|
|
571
|
+
/**
|
|
572
|
+
* Um fluxo está sujo quando o rascunho difere do publicado.
|
|
573
|
+
*
|
|
574
|
+
* Comparação estrutural por JSON: é grosseira, e é suficiente porque o grafo é dado serializável sem
|
|
575
|
+
* ordem significativa de chave — o servidor devolve o que gravou. Comparar campo a campo daria a
|
|
576
|
+
* mesma resposta com mais código para errar.
|
|
577
|
+
*/
|
|
578
|
+
declare function isGraphDirty(working: FlowGraphData | undefined, published: FlowGraphData | undefined): boolean;
|
|
245
579
|
|
|
246
|
-
export { BUILT_IN_ACTION_KINDS, CONDITION_OPERATORS, CROSS_FLOW_PREFIX, type CollectionChain, DEFAULT_FLOW_EDITOR_LABELS, type FlowEditorLabels, FlowGroupFrame, type FlowGroupFrameData, FlowGroupHeader, type FlowGroupHeaderData, FlowMapCanvas, type FlowMapCanvasProps, FlowMapNode, type FlowMapNodeData, FlowNodeCard, type FlowNodeCardData, FlowNodePanel, type FlowNodePanelProps, FlowPalette, type FlowPaletteActionOption, type FlowPaletteProps, FlowPortalNode, type FlowPortalNodeData, FlowWhatsAppPreview, type FlowWhatsAppPreviewProps, 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 };
|
|
580
|
+
export { BUILT_IN_ACTION_KINDS, CONDITION_OPERATORS, CROSS_FLOW_PREFIX, type CollectionChain, type ConnectionRequest, type CreateFlowInput, DEFAULT_FLOW_EDITOR_LABELS, type FlowEdgeKind, type FlowEdgeSpec, type FlowEditorLabels, FlowGroupFrame, type FlowGroupFrameData, FlowGroupHeader, type FlowGroupHeaderData, type FlowLiveNodeCount, type FlowLivePosition, type FlowLivePositionInput, FlowMapCanvas, type FlowMapCanvasProps, FlowMapNode, type FlowMapNodeData, FlowNodeCard, type FlowNodeCardData, FlowNodePanel, type FlowNodePanelProps, type FlowNodePosition, FlowPalette, type FlowPaletteActionOption, type FlowPaletteProps, FlowPortalNode, type FlowPortalNodeData, type FlowValidationLabels, FlowWhatsAppPreview, type FlowWhatsAppPreviewProps, FlowsWorkspace, type FlowsWorkspaceApi, type FlowsWorkspaceProps, GROUP_HEADER_NODE_ID, type GraphIssue, NODE_CARD_WIDTH, type NewNodeSpec, type ResolvedConnection, WHATSAPP_LIMITS, applyConnection, buildFlowEdges, chainFrameBounds, chainFrameNodeId, computeAutoLayout, computeFlowMapLayout, computeMergedLayout, countLiveByNode, crossFlowKey, crossFlowTargetsOf, detachedNodeIds, estimateNodeHeight, findCollectionChains, flowGroupFrameNodeTypes, flowGroupHeaderNodeTypes, flowMapNodeTypes, flowNodeTypes, flowPortalNodeTypes, isCrossFlowTarget, isGraphDirty, mergeFlowEditorLabels, mergedFlowKeysFrom, namespaceNodeId, newNodeFromSpec, nodeLabel, parseNamespacedId, portalNodeId, removeNodeAndCleanRefs, rendersAsButtons, resolveConnection, slugifyNodeId, targetsOf, validateGraph };
|