@adatechnology/conversations-ui 0.1.0-rc.3 → 0.1.0-rc.30
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-DXPSPUWF.js +110 -0
- package/dist/chunk-GY472G6E.js +2316 -0
- package/dist/{chunk-OGRRHQQW.js → chunk-WCBDXZ3X.js} +68 -4
- package/dist/flows/index.d.ts +311 -4
- package/dist/flows/index.js +1322 -55
- package/dist/index.d.ts +1074 -42
- package/dist/index.js +3571 -742
- package/dist/preview/index.d.ts +328 -8
- package/dist/preview/index.js +902 -115
- package/dist/styles.css +819 -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 +543 -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 +1003 -0
- package/src/flows/flowCanvasModel.test.ts +293 -0
- package/src/flows/flowCanvasModel.ts +342 -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 +40 -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 +783 -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
package/dist/flows/index.js
CHANGED
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
} from "../chunk-
|
|
2
|
+
TooltipLayer
|
|
3
|
+
} from "../chunk-DXPSPUWF.js";
|
|
4
4
|
import {
|
|
5
|
-
parseWhatsAppFormatting
|
|
6
|
-
|
|
5
|
+
parseWhatsAppFormatting,
|
|
6
|
+
useIsDarkTheme
|
|
7
|
+
} from "../chunk-WCBDXZ3X.js";
|
|
7
8
|
|
|
8
9
|
// src/flows/FlowNodeCard.tsx
|
|
9
10
|
import { Handle, Position } from "@xyflow/react";
|
|
10
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
MessageCircleQuestion,
|
|
13
|
+
GitBranch,
|
|
14
|
+
Zap,
|
|
15
|
+
ListTree,
|
|
16
|
+
Diamond,
|
|
17
|
+
AlertTriangle,
|
|
18
|
+
AlertCircle,
|
|
19
|
+
Headset,
|
|
20
|
+
Clock3,
|
|
21
|
+
ShoppingBag
|
|
22
|
+
} from "lucide-react";
|
|
11
23
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
12
24
|
var NODE_TYPE_COLOR = {
|
|
13
25
|
question: "border-blue-300 bg-blue-50 dark:bg-blue-950/40 dark:border-blue-800",
|
|
@@ -30,6 +42,7 @@ var DEFAULT_ACTION_KIND_ICON = {
|
|
|
30
42
|
};
|
|
31
43
|
function nodeLabel(node, labels) {
|
|
32
44
|
if (!node) return "\u2014";
|
|
45
|
+
if (node.label) return node.label;
|
|
33
46
|
if (node.type === "action") {
|
|
34
47
|
const actionKindLabel = node.actionKind ? labels.actionKindLabels[node.actionKind] : void 0;
|
|
35
48
|
return node.directMessage || node.fallbackMessage || actionKindLabel || node.actionKind || node.id;
|
|
@@ -59,7 +72,13 @@ function sourceRows(node, labels) {
|
|
|
59
72
|
}
|
|
60
73
|
function SourceRow({ label, isDefault, handleId }) {
|
|
61
74
|
return /* @__PURE__ */ jsxs("div", { className: "relative flex items-center gap-1.5 rounded-md border border-gray-200 dark:border-gray-600 bg-white/70 dark:bg-gray-900/40 px-2 py-1 pr-3", children: [
|
|
62
|
-
/* @__PURE__ */ jsx(
|
|
75
|
+
/* @__PURE__ */ jsx(
|
|
76
|
+
"span",
|
|
77
|
+
{
|
|
78
|
+
className: `text-xs truncate flex-1 ${isDefault ? "italic text-gray-400 dark:text-gray-500" : "text-gray-700 dark:text-gray-200"}`,
|
|
79
|
+
children: label
|
|
80
|
+
}
|
|
81
|
+
),
|
|
63
82
|
/* @__PURE__ */ jsx(
|
|
64
83
|
Handle,
|
|
65
84
|
{
|
|
@@ -73,7 +92,7 @@ function SourceRow({ label, isDefault, handleId }) {
|
|
|
73
92
|
] });
|
|
74
93
|
}
|
|
75
94
|
function FlowNodeCard({ data }) {
|
|
76
|
-
const { node, liveCount, isStart, isSelected, issues, labels, actionKindIcons, onSelect } = data;
|
|
95
|
+
const { node, liveCount, isStart, isSelected, isDetached, issues, labels, actionKindIcons, onSelect } = data;
|
|
77
96
|
const label = nodeLabel(node, labels);
|
|
78
97
|
const iconMap = { ...DEFAULT_ACTION_KIND_ICON, ...actionKindIcons };
|
|
79
98
|
const Icon = node.type === "action" && node.actionKind ? iconMap[node.actionKind] ?? NODE_TYPE_ICON[node.type] : NODE_TYPE_ICON[node.type];
|
|
@@ -83,15 +102,15 @@ function FlowNodeCard({ data }) {
|
|
|
83
102
|
return /* @__PURE__ */ jsxs(
|
|
84
103
|
"div",
|
|
85
104
|
{
|
|
86
|
-
|
|
87
|
-
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" : ""}`,
|
|
105
|
+
"data-cv-tooltip": isDetached ? labels.detachedNodeTooltip : label,
|
|
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" : ""}`,
|
|
88
107
|
onClick: () => onSelect(node.id),
|
|
89
108
|
children: [
|
|
90
109
|
/* @__PURE__ */ jsx(Handle, { type: "target", position: Position.Top, id: "target", className: "!bg-gray-400 dark:!bg-gray-500" }),
|
|
91
110
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
|
|
92
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: [
|
|
93
112
|
/* @__PURE__ */ jsx(Icon, { size: 12, strokeWidth: 2.5 }),
|
|
94
|
-
isStart && /* @__PURE__ */ jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-emerald-500",
|
|
113
|
+
isStart && /* @__PURE__ */ jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-emerald-500", "data-cv-tooltip": labels.startNodeTooltip }),
|
|
95
114
|
labels.legend[node.type]
|
|
96
115
|
] }),
|
|
97
116
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
|
|
@@ -100,7 +119,7 @@ function FlowNodeCard({ data }) {
|
|
|
100
119
|
liveCount > 0 && /* @__PURE__ */ jsx(
|
|
101
120
|
"span",
|
|
102
121
|
{
|
|
103
|
-
|
|
122
|
+
"data-cv-tooltip": labels.liveCountTooltip(liveCount),
|
|
104
123
|
className: "font-bold bg-blue-600 text-white rounded-full px-1.5 py-0.5 text-xs",
|
|
105
124
|
children: liveCount
|
|
106
125
|
}
|
|
@@ -129,12 +148,14 @@ var DEFAULT_FLOW_EDITOR_LABELS = {
|
|
|
129
148
|
condition: "Condi\xE7\xE3o"
|
|
130
149
|
},
|
|
131
150
|
startNodeTooltip: "In\xEDcio do fluxo",
|
|
151
|
+
detachedNodeTooltip: "Sem liga\xE7\xE3o de entrada \u2014 o bot n\xE3o chega neste n\xF3. Puxe um fio de outro card at\xE9 ele.",
|
|
132
152
|
liveCountTooltip: (count) => `${count} conversa(s) ativa(s) aqui agora`,
|
|
133
153
|
edgeFallbackLabel: "outro",
|
|
134
154
|
actionKindLabels: {
|
|
135
155
|
handoff: "Encaminhar para atendimento",
|
|
136
156
|
rate_limited_handoff: "Encaminhar (limite de simula\xE7\xF5es atingido)",
|
|
137
|
-
send_product_list: "Enviar cat\xE1logo de produtos"
|
|
157
|
+
send_product_list: "Enviar cat\xE1logo de produtos",
|
|
158
|
+
send_media: "Enviar arquivos da biblioteca"
|
|
138
159
|
},
|
|
139
160
|
conditionOperatorLabels: {
|
|
140
161
|
">": "maior que",
|
|
@@ -156,10 +177,15 @@ var DEFAULT_FLOW_EDITOR_LABELS = {
|
|
|
156
177
|
nodePanel: {
|
|
157
178
|
title: "Editar n\xF3",
|
|
158
179
|
contextKey: "Chave (contexto)",
|
|
180
|
+
nodeName: "Nome do n\xF3 (opcional)",
|
|
181
|
+
nodeNamePlaceholder: "Ex.: Enviar tabela de pre\xE7os",
|
|
182
|
+
nodeNameHint: "S\xF3 aparece no editor \u2014 o cliente n\xE3o v\xEA.",
|
|
159
183
|
questionType: "Tipo de resposta",
|
|
160
184
|
question: "Texto da pergunta",
|
|
161
185
|
options: "Op\xE7\xF5es (choice)",
|
|
162
186
|
addOption: "Adicionar op\xE7\xE3o",
|
|
187
|
+
removeOption: "Remover op\xE7\xE3o",
|
|
188
|
+
close: "Fechar painel",
|
|
163
189
|
optionId: "Valor",
|
|
164
190
|
optionLabel: "Texto exibido",
|
|
165
191
|
next: "Pr\xF3ximo n\xF3",
|
|
@@ -189,7 +215,9 @@ var DEFAULT_FLOW_EDITOR_LABELS = {
|
|
|
189
215
|
conditionValue: "Valor de compara\xE7\xE3o",
|
|
190
216
|
conditionTrue: "Se verdadeiro \u2192",
|
|
191
217
|
conditionFalse: "Se falso \u2192",
|
|
192
|
-
conditionVariableMissing: "Se a vari\xE1vel ainda n\xE3o foi coletada \u2192"
|
|
218
|
+
conditionVariableMissing: "Se a vari\xE1vel ainda n\xE3o foi coletada \u2192",
|
|
219
|
+
media: "Arquivos enviados neste ponto",
|
|
220
|
+
mediaUnavailable: "A biblioteca de arquivos n\xE3o est\xE1 dispon\xEDvel neste painel."
|
|
193
221
|
},
|
|
194
222
|
palette: {
|
|
195
223
|
title: "Adicionar ao fluxo",
|
|
@@ -201,7 +229,9 @@ var DEFAULT_FLOW_EDITOR_LABELS = {
|
|
|
201
229
|
},
|
|
202
230
|
flowMap: {
|
|
203
231
|
nodeCount: (count) => `${count} n\xF3(s)`,
|
|
204
|
-
openFlow: "Abrir fluxo"
|
|
232
|
+
openFlow: "Abrir fluxo",
|
|
233
|
+
toggleToMap: "Mapa de fluxos",
|
|
234
|
+
toggleToDetail: "Voltar ao editor"
|
|
205
235
|
},
|
|
206
236
|
flowGroup: {
|
|
207
237
|
focus: "Focar neste fluxo",
|
|
@@ -210,6 +240,59 @@ var DEFAULT_FLOW_EDITOR_LABELS = {
|
|
|
210
240
|
crossFlowPortal: {
|
|
211
241
|
tooltip: "Clique para abrir esse fluxo aqui do lado, ligado ao ponto de onde ele \xE9 chamado",
|
|
212
242
|
goesTo: (label) => `\u21AA Vai para: ${label}`
|
|
243
|
+
},
|
|
244
|
+
collectionChain: {
|
|
245
|
+
feeds: (label) => `Alimenta: ${label}`
|
|
246
|
+
},
|
|
247
|
+
validation: {
|
|
248
|
+
title: "Antes de publicar",
|
|
249
|
+
errors: (count) => `${count} erro(s) \u2014 corrija antes de salvar`,
|
|
250
|
+
warnings: (count) => `${count} aviso(s)`,
|
|
251
|
+
noStart: "O fluxo precisa de um n\xF3 inicial v\xE1lido.",
|
|
252
|
+
brokenRef: (from, to) => `N\xF3 "${from}": liga\xE7\xE3o aponta para "${to}", que n\xE3o existe.`,
|
|
253
|
+
choiceWithoutOptions: (id) => `N\xF3 "${id}": escolha sem nenhuma op\xE7\xE3o.`,
|
|
254
|
+
duplicatedOptionId: (id, optionId) => `N\xF3 "${id}": valor de op\xE7\xE3o "${optionId}" duplicado.`,
|
|
255
|
+
optionWithoutTarget: (id, label) => `N\xF3 "${id}": op\xE7\xE3o "${label}" n\xE3o tem destino definido.`,
|
|
256
|
+
tooManyOptions: (id, count) => `N\xF3 "${id}": ${count} op\xE7\xF5es \u2014 o WhatsApp aceita no m\xE1ximo 10 em lista.`,
|
|
257
|
+
buttonTitleTooLong: (id, label) => `N\xF3 "${id}": bot\xE3o "${label}" passa de 20 caracteres.`,
|
|
258
|
+
listTitleTooLong: (id, label) => `N\xF3 "${id}": item de lista "${label}" passa de 24 caracteres.`,
|
|
259
|
+
bodyTooLong: (id) => `N\xF3 "${id}": texto passa de 1024 caracteres.`,
|
|
260
|
+
unreachable: (id) => `N\xF3 "${id}" \xE9 inalcan\xE7\xE1vel a partir do in\xEDcio do fluxo.`,
|
|
261
|
+
deadEndQuestion: (id) => `N\xF3 "${id}": pergunta sem pr\xF3ximo passo definido.`,
|
|
262
|
+
conditionIncomplete: (id) => `N\xF3 "${id}": condi\xE7\xE3o incompleta \u2014 defina vari\xE1vel, operador e valor.`,
|
|
263
|
+
conditionBranchMissing: (id, branch) => `N\xF3 "${id}": ramo "${branch === "true" ? "Verdadeiro" : "Falso"}" sem destino definido.`
|
|
264
|
+
},
|
|
265
|
+
workspace: {
|
|
266
|
+
title: "Fluxos do Bot",
|
|
267
|
+
subtitle: "Blueprint visual dos fluxos de conversa, sincronizado com o que est\xE1 em produ\xE7\xE3o.",
|
|
268
|
+
loading: "Carregando fluxos\u2026",
|
|
269
|
+
loadError: "N\xE3o foi poss\xEDvel carregar os fluxos.",
|
|
270
|
+
saveGraph: "Publicar altera\xE7\xF5es",
|
|
271
|
+
saving: "Publicando\u2026",
|
|
272
|
+
saveSuccess: "Fluxo publicado! O bot j\xE1 est\xE1 usando a vers\xE3o nova.",
|
|
273
|
+
saveError: "N\xE3o foi poss\xEDvel salvar \u2014 verifique se todos os destinos apontam para n\xF3s existentes.",
|
|
274
|
+
organize: "Organizar",
|
|
275
|
+
organizeTooltip: "Reorganiza os n\xF3s automaticamente e salva as novas posi\xE7\xF5es",
|
|
276
|
+
discardChanges: "Desfazer altera\xE7\xF5es",
|
|
277
|
+
discardTooltip: "Devolve os fluxos abertos ao que est\xE1 publicado, descartando o que n\xE3o foi salvo",
|
|
278
|
+
discardConfirm: "Descartar todas as altera\xE7\xF5es n\xE3o publicadas e voltar ao fluxo que est\xE1 no ar?",
|
|
279
|
+
unsavedChangesConfirm: "Voc\xEA tem altera\xE7\xF5es n\xE3o publicadas neste fluxo. Trocar de fluxo agora descarta essas edi\xE7\xF5es. Continuar?"
|
|
280
|
+
},
|
|
281
|
+
flowManager: {
|
|
282
|
+
newFlow: "Novo fluxo",
|
|
283
|
+
createTitle: "Criar novo fluxo",
|
|
284
|
+
key: "Identificador \xFAnico",
|
|
285
|
+
keyHint: "letras min\xFAsculas, n\xFAmeros e _ (ex.: promocoes_semana)",
|
|
286
|
+
keyInvalid: "Use apenas letras min\xFAsculas, n\xFAmeros e _ (2 a 40 caracteres)",
|
|
287
|
+
label: "Nome exibido",
|
|
288
|
+
showInMenu: "Exibir como op\xE7\xE3o no menu principal do bot",
|
|
289
|
+
menuOptionLabel: "Texto da op\xE7\xE3o no menu",
|
|
290
|
+
create: "Criar fluxo",
|
|
291
|
+
creating: "Criando\u2026",
|
|
292
|
+
deleteFlow: "Excluir fluxo",
|
|
293
|
+
deleteConfirm: (label) => `Excluir o fluxo "${label}"? Esta a\xE7\xE3o n\xE3o pode ser desfeita.`,
|
|
294
|
+
createError: "N\xE3o foi poss\xEDvel criar o fluxo.",
|
|
295
|
+
deleteError: "N\xE3o foi poss\xEDvel excluir o fluxo."
|
|
213
296
|
}
|
|
214
297
|
};
|
|
215
298
|
function mergeFlowEditorLabels(override) {
|
|
@@ -228,7 +311,11 @@ function mergeFlowEditorLabels(override) {
|
|
|
228
311
|
palette: { ...DEFAULT_FLOW_EDITOR_LABELS.palette, ...override.palette },
|
|
229
312
|
flowMap: { ...DEFAULT_FLOW_EDITOR_LABELS.flowMap, ...override.flowMap },
|
|
230
313
|
flowGroup: { ...DEFAULT_FLOW_EDITOR_LABELS.flowGroup, ...override.flowGroup },
|
|
231
|
-
crossFlowPortal: { ...DEFAULT_FLOW_EDITOR_LABELS.crossFlowPortal, ...override.crossFlowPortal }
|
|
314
|
+
crossFlowPortal: { ...DEFAULT_FLOW_EDITOR_LABELS.crossFlowPortal, ...override.crossFlowPortal },
|
|
315
|
+
collectionChain: { ...DEFAULT_FLOW_EDITOR_LABELS.collectionChain, ...override.collectionChain },
|
|
316
|
+
validation: { ...DEFAULT_FLOW_EDITOR_LABELS.validation, ...override.validation },
|
|
317
|
+
workspace: { ...DEFAULT_FLOW_EDITOR_LABELS.workspace, ...override.workspace },
|
|
318
|
+
flowManager: { ...DEFAULT_FLOW_EDITOR_LABELS.flowManager, ...override.flowManager }
|
|
232
319
|
};
|
|
233
320
|
}
|
|
234
321
|
|
|
@@ -241,12 +328,14 @@ function FlowMapNode({ data }) {
|
|
|
241
328
|
/* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-1.5 text-emerald-700 dark:text-emerald-300", children: [
|
|
242
329
|
/* @__PURE__ */ jsx2(GitBranch2, { size: 14 }),
|
|
243
330
|
/* @__PURE__ */ jsx2("span", { className: "text-sm font-semibold truncate", children: label }),
|
|
244
|
-
isRoot && /* @__PURE__ */ jsx2("span", { className: "h-1.5 w-1.5 rounded-full bg-emerald-500 shrink-0",
|
|
331
|
+
isRoot && /* @__PURE__ */ jsx2("span", { className: "h-1.5 w-1.5 rounded-full bg-emerald-500 shrink-0", "data-cv-tooltip": labels.startNodeTooltip })
|
|
245
332
|
] }),
|
|
246
333
|
/* @__PURE__ */ jsx2("p", { className: "text-xs text-gray-500 dark:text-gray-400 mt-1", children: labels.flowMap.nodeCount(nodeCount) }),
|
|
247
334
|
/* @__PURE__ */ jsxs2(
|
|
248
335
|
"button",
|
|
249
336
|
{
|
|
337
|
+
"data-cv-tooltip": labels.flowMap.openFlow,
|
|
338
|
+
"aria-label": labels.flowMap.openFlow,
|
|
250
339
|
type: "button",
|
|
251
340
|
onClick: onOpen,
|
|
252
341
|
className: "mt-2 inline-flex items-center gap-1 text-xs font-medium text-blue-600 dark:text-blue-400 hover:underline",
|
|
@@ -268,12 +357,9 @@ import { ReactFlow, Background, Controls, MarkerType } from "@xyflow/react";
|
|
|
268
357
|
import "@xyflow/react/dist/style.css";
|
|
269
358
|
|
|
270
359
|
// src/flows/flowGraph.ts
|
|
360
|
+
import { FLOW_ACTION_KIND } from "@adatechnology/meta-whatsapp-contracts";
|
|
271
361
|
var CONDITION_OPERATORS = [">", ">=", "<", "<=", "==", "!=", "contains"];
|
|
272
|
-
var BUILT_IN_ACTION_KINDS =
|
|
273
|
-
HANDOFF: "handoff",
|
|
274
|
-
RATE_LIMITED_HANDOFF: "rate_limited_handoff",
|
|
275
|
-
SEND_PRODUCT_LIST: "send_product_list"
|
|
276
|
-
};
|
|
362
|
+
var BUILT_IN_ACTION_KINDS = FLOW_ACTION_KIND;
|
|
277
363
|
var CROSS_FLOW_PREFIX = "flow:";
|
|
278
364
|
var isCrossFlowTarget = (target) => target.startsWith(CROSS_FLOW_PREFIX);
|
|
279
365
|
var crossFlowKey = (target) => target.slice(CROSS_FLOW_PREFIX.length);
|
|
@@ -529,18 +615,20 @@ function FlowMapCanvas({ graphs, rootKey, onOpenFlow, labels: labelsOverride })
|
|
|
529
615
|
const isDark = useIsDarkTheme();
|
|
530
616
|
const positions = useMemo(() => computeFlowMapLayout(graphs, rootKey), [graphs, rootKey]);
|
|
531
617
|
const nodes = useMemo(
|
|
532
|
-
() => Object.values(graphs).map(
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
618
|
+
() => Object.values(graphs).map(
|
|
619
|
+
(g) => ({
|
|
620
|
+
id: g.key,
|
|
621
|
+
type: "flowMapNode",
|
|
622
|
+
position: positions[g.key] ?? { x: 0, y: 0 },
|
|
623
|
+
data: {
|
|
624
|
+
label: g.label,
|
|
625
|
+
nodeCount: Object.keys(g.nodes).length,
|
|
626
|
+
isRoot: g.key === rootKey,
|
|
627
|
+
labels,
|
|
628
|
+
onOpen: () => onOpenFlow(g.key)
|
|
629
|
+
}
|
|
630
|
+
})
|
|
631
|
+
),
|
|
544
632
|
[graphs, positions, rootKey, onOpenFlow, labels]
|
|
545
633
|
);
|
|
546
634
|
const edges = useMemo(() => {
|
|
@@ -592,8 +680,28 @@ function FlowGroupHeader({ data }) {
|
|
|
592
680
|
const { label, labels = DEFAULT_FLOW_EDITOR_LABELS, onFocus, onClose } = data;
|
|
593
681
|
return /* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-2 rounded-full border border-cyan-300 dark:border-cyan-700 bg-cyan-50 dark:bg-cyan-950/50 px-3 py-1 text-xs font-medium text-cyan-800 dark:text-cyan-200 shadow-sm whitespace-nowrap", children: [
|
|
594
682
|
/* @__PURE__ */ jsx5("span", { children: label }),
|
|
595
|
-
/* @__PURE__ */ jsx5(
|
|
596
|
-
|
|
683
|
+
/* @__PURE__ */ jsx5(
|
|
684
|
+
"button",
|
|
685
|
+
{
|
|
686
|
+
type: "button",
|
|
687
|
+
onClick: onFocus,
|
|
688
|
+
"data-cv-tooltip": labels.flowGroup.focus,
|
|
689
|
+
"aria-label": labels.flowGroup.focus,
|
|
690
|
+
className: "hover:text-blue-600 dark:hover:text-blue-400",
|
|
691
|
+
children: /* @__PURE__ */ jsx5(Maximize22, { size: 12 })
|
|
692
|
+
}
|
|
693
|
+
),
|
|
694
|
+
/* @__PURE__ */ jsx5(
|
|
695
|
+
"button",
|
|
696
|
+
{
|
|
697
|
+
type: "button",
|
|
698
|
+
onClick: onClose,
|
|
699
|
+
"data-cv-tooltip": labels.flowGroup.close,
|
|
700
|
+
"aria-label": labels.flowGroup.close,
|
|
701
|
+
className: "hover:text-red-600 dark:hover:text-red-400",
|
|
702
|
+
children: /* @__PURE__ */ jsx5(X, { size: 12 })
|
|
703
|
+
}
|
|
704
|
+
)
|
|
597
705
|
] });
|
|
598
706
|
}
|
|
599
707
|
var flowGroupHeaderNodeTypes = { flowGroupHeader: FlowGroupHeader };
|
|
@@ -608,7 +716,8 @@ function FlowPortalNode({ data }) {
|
|
|
608
716
|
"button",
|
|
609
717
|
{
|
|
610
718
|
type: "button",
|
|
611
|
-
|
|
719
|
+
"data-cv-tooltip": labels.crossFlowPortal.tooltip,
|
|
720
|
+
"aria-label": labels.crossFlowPortal.tooltip,
|
|
612
721
|
onClick: onNavigate,
|
|
613
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",
|
|
614
723
|
children: [
|
|
@@ -653,6 +762,8 @@ function FlowPalette({ onAdd, labels: labelsOverride, actionOptions }) {
|
|
|
653
762
|
/* @__PURE__ */ jsxs6(
|
|
654
763
|
"button",
|
|
655
764
|
{
|
|
765
|
+
"data-cv-tooltip": labels.palette.title,
|
|
766
|
+
"aria-label": labels.palette.title,
|
|
656
767
|
onClick: () => setOpen((v) => !v),
|
|
657
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",
|
|
658
769
|
children: [
|
|
@@ -667,6 +778,8 @@ function FlowPalette({ onAdd, labels: labelsOverride, actionOptions }) {
|
|
|
667
778
|
/* @__PURE__ */ jsxs6(
|
|
668
779
|
"button",
|
|
669
780
|
{
|
|
781
|
+
"data-cv-tooltip": labels.palette.question,
|
|
782
|
+
"aria-label": labels.palette.question,
|
|
670
783
|
onMouseEnter: () => setSubmenu("question"),
|
|
671
784
|
onClick: () => setSubmenu(submenu === "question" ? null : "question"),
|
|
672
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",
|
|
@@ -683,6 +796,8 @@ function FlowPalette({ onAdd, labels: labelsOverride, actionOptions }) {
|
|
|
683
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(
|
|
684
797
|
"button",
|
|
685
798
|
{
|
|
799
|
+
"data-cv-tooltip": labels.questionTypeLabels[qt],
|
|
800
|
+
"aria-label": labels.questionTypeLabels[qt],
|
|
686
801
|
onClick: () => select({ kind: "question", questionType: qt }),
|
|
687
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",
|
|
688
803
|
children: labels.questionTypeLabels[qt]
|
|
@@ -693,6 +808,8 @@ function FlowPalette({ onAdd, labels: labelsOverride, actionOptions }) {
|
|
|
693
808
|
/* @__PURE__ */ jsxs6(
|
|
694
809
|
"button",
|
|
695
810
|
{
|
|
811
|
+
"data-cv-tooltip": labels.palette.decision,
|
|
812
|
+
"aria-label": labels.palette.decision,
|
|
696
813
|
onMouseEnter: () => setSubmenu(null),
|
|
697
814
|
onClick: () => select({ kind: "decision" }),
|
|
698
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",
|
|
@@ -709,7 +826,8 @@ function FlowPalette({ onAdd, labels: labelsOverride, actionOptions }) {
|
|
|
709
826
|
onMouseEnter: () => setSubmenu(null),
|
|
710
827
|
onClick: () => select({ kind: "condition" }),
|
|
711
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",
|
|
712
|
-
|
|
829
|
+
"data-cv-tooltip": labels.palette.conditionHint,
|
|
830
|
+
"aria-label": labels.palette.conditionHint,
|
|
713
831
|
children: [
|
|
714
832
|
/* @__PURE__ */ jsx7(Diamond2, { size: 15, className: "text-cyan-500" }),
|
|
715
833
|
" ",
|
|
@@ -721,6 +839,8 @@ function FlowPalette({ onAdd, labels: labelsOverride, actionOptions }) {
|
|
|
721
839
|
/* @__PURE__ */ jsxs6(
|
|
722
840
|
"button",
|
|
723
841
|
{
|
|
842
|
+
"data-cv-tooltip": labels.palette.action,
|
|
843
|
+
"aria-label": labels.palette.action,
|
|
724
844
|
onMouseEnter: () => setSubmenu("action"),
|
|
725
845
|
onClick: () => setSubmenu(submenu === "action" ? null : "action"),
|
|
726
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",
|
|
@@ -737,6 +857,8 @@ function FlowPalette({ onAdd, labels: labelsOverride, actionOptions }) {
|
|
|
737
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(
|
|
738
858
|
"button",
|
|
739
859
|
{
|
|
860
|
+
"data-cv-tooltip": option.label,
|
|
861
|
+
"aria-label": option.label,
|
|
740
862
|
onClick: () => select({ kind: "action", actionKind: option.actionKind }),
|
|
741
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",
|
|
742
864
|
children: option.label
|
|
@@ -755,7 +877,11 @@ import { Plus as Plus2, Trash2, Save, X as X2, AlertTriangle as AlertTriangle2,
|
|
|
755
877
|
// src/flows/FlowWhatsAppPreview.tsx
|
|
756
878
|
import { List } from "lucide-react";
|
|
757
879
|
import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
758
|
-
function FlowWhatsAppPreview({
|
|
880
|
+
function FlowWhatsAppPreview({
|
|
881
|
+
body,
|
|
882
|
+
options,
|
|
883
|
+
labels = DEFAULT_FLOW_EDITOR_LABELS.nodePanel
|
|
884
|
+
}) {
|
|
759
885
|
if (!body && (!options || options.length === 0)) {
|
|
760
886
|
return /* @__PURE__ */ jsx8("p", { className: "text-xs text-gray-400 dark:text-gray-500 italic px-1", children: labels.previewPlaceholder });
|
|
761
887
|
}
|
|
@@ -771,7 +897,14 @@ function FlowWhatsAppPreview({ body, options, labels = DEFAULT_FLOW_EDITOR_LABEL
|
|
|
771
897
|
labels.previewListButton
|
|
772
898
|
] }) })
|
|
773
899
|
] }),
|
|
774
|
-
hasOptions && usesButtons && /* @__PURE__ */ jsx8("div", { className: "mt-1 space-y-1", children: options.map(([id, label]) => /* @__PURE__ */ jsx8(
|
|
900
|
+
hasOptions && usesButtons && /* @__PURE__ */ jsx8("div", { className: "mt-1 space-y-1", children: options.map(([id, label]) => /* @__PURE__ */ jsx8(
|
|
901
|
+
"div",
|
|
902
|
+
{
|
|
903
|
+
className: "rounded-lg bg-white dark:bg-gray-700 py-1.5 text-center text-sm font-medium text-cyan-600 dark:text-cyan-400 shadow-sm",
|
|
904
|
+
children: label || /* @__PURE__ */ jsx8("span", { className: "italic text-gray-400", children: labels.previewEmptyOption })
|
|
905
|
+
},
|
|
906
|
+
id
|
|
907
|
+
)) }),
|
|
775
908
|
hasOptions && !usesButtons && /* @__PURE__ */ jsx8("div", { className: "mt-1.5 rounded-lg bg-white dark:bg-gray-700 shadow-sm divide-y divide-gray-100 dark:divide-gray-600 overflow-hidden", children: options.slice(0, WHATSAPP_LIMITS.MAX_LIST_ROWS).map(([id, label]) => /* @__PURE__ */ jsxs7("div", { className: "flex items-center gap-2 px-3 py-1.5 text-sm text-gray-800 dark:text-gray-100", children: [
|
|
776
909
|
/* @__PURE__ */ jsx8("span", { className: "h-3.5 w-3.5 rounded-full border border-gray-300 dark:border-gray-500 shrink-0" }),
|
|
777
910
|
label || /* @__PURE__ */ jsx8("span", { className: "italic text-gray-400", children: labels.previewEmptyOption })
|
|
@@ -833,17 +966,23 @@ function FlowNodePanel({
|
|
|
833
966
|
onClose,
|
|
834
967
|
onChange,
|
|
835
968
|
onDelete,
|
|
836
|
-
labels: labelsOverride
|
|
969
|
+
labels: labelsOverride,
|
|
970
|
+
renderMediaPicker
|
|
837
971
|
}) {
|
|
838
972
|
const labels = { ...DEFAULT_FLOW_EDITOR_LABELS, ...labelsOverride };
|
|
839
973
|
const [draft, setDraft] = useState2(node);
|
|
840
974
|
const otherNodeIds = Object.keys(graph.nodes).filter((id) => id !== node.id);
|
|
841
975
|
const isFixedLogic = draft.type === "entrada_choice";
|
|
842
976
|
const isAction = draft.type === "action";
|
|
977
|
+
const isSendMedia = isAction && draft.actionKind === BUILT_IN_ACTION_KINDS.SEND_MEDIA;
|
|
843
978
|
const isCondition = draft.type === "condition";
|
|
844
979
|
const isStart = graph.startNodeId === node.id;
|
|
845
980
|
const nodeIssues = issues.filter((i) => i.nodeId === node.id);
|
|
846
|
-
const knownContextKeys = [
|
|
981
|
+
const knownContextKeys = [
|
|
982
|
+
...new Set(
|
|
983
|
+
Object.values(graph.nodes).map((n) => n.contextKey).filter((key) => !!key)
|
|
984
|
+
)
|
|
985
|
+
];
|
|
847
986
|
const conditionAnswerIds = isCondition ? ["true", "false"] : (draft.options ?? []).map(([id]) => id);
|
|
848
987
|
function updateNextString(value) {
|
|
849
988
|
setDraft((prev) => ({ ...prev, next: value }));
|
|
@@ -870,7 +1009,10 @@ function FlowNodePanel({
|
|
|
870
1009
|
});
|
|
871
1010
|
}
|
|
872
1011
|
function addOption() {
|
|
873
|
-
setDraft((prev) => ({
|
|
1012
|
+
setDraft((prev) => ({
|
|
1013
|
+
...prev,
|
|
1014
|
+
options: [...prev.options ?? [], [String((prev.options?.length ?? 0) + 1), "Nova op\xE7\xE3o"]]
|
|
1015
|
+
}));
|
|
874
1016
|
}
|
|
875
1017
|
function removeOption(index) {
|
|
876
1018
|
setDraft((prev) => ({ ...prev, options: (prev.options ?? []).filter((_, i) => i !== index) }));
|
|
@@ -884,16 +1026,36 @@ function FlowNodePanel({
|
|
|
884
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: [
|
|
885
1027
|
/* @__PURE__ */ jsxs8("div", { className: "flex items-center justify-between px-4 py-3 border-b border-gray-100 dark:border-gray-700", children: [
|
|
886
1028
|
/* @__PURE__ */ jsx9("h3", { className: "text-sm font-semibold text-gray-900 dark:text-gray-100", children: labels.nodePanel.title }),
|
|
887
|
-
/* @__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 }) })
|
|
888
1030
|
] }),
|
|
889
1031
|
/* @__PURE__ */ jsxs8("div", { className: "flex-1 overflow-y-auto p-4 space-y-4", children: [
|
|
890
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)) }),
|
|
891
1033
|
isFixedLogic && /* @__PURE__ */ jsx9("p", { className: "text-xs text-purple-700 dark:text-purple-400 bg-purple-50 dark:bg-purple-950/30 rounded-lg p-2", children: labels.nodePanel.fixedLogicNotice }),
|
|
892
1034
|
isAction && /* @__PURE__ */ jsx9("p", { className: "text-xs text-orange-700 dark:text-orange-400 bg-orange-50 dark:bg-orange-950/30 rounded-lg p-2", children: labels.nodePanel.actionNotice }),
|
|
893
1035
|
isCondition && /* @__PURE__ */ jsx9("p", { className: "text-xs text-cyan-700 dark:text-cyan-400 bg-cyan-50 dark:bg-cyan-950/30 rounded-lg p-2", children: labels.nodePanel.conditionNotice }),
|
|
1036
|
+
/* @__PURE__ */ jsxs8("div", { children: [
|
|
1037
|
+
/* @__PURE__ */ jsx9("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.nodeName }),
|
|
1038
|
+
/* @__PURE__ */ jsx9(
|
|
1039
|
+
"input",
|
|
1040
|
+
{
|
|
1041
|
+
value: draft.label ?? "",
|
|
1042
|
+
placeholder: labels.nodePanel.nodeNamePlaceholder,
|
|
1043
|
+
onChange: (event) => setDraft((prev) => ({ ...prev, label: event.target.value || void 0 })),
|
|
1044
|
+
className: `w-full mt-1 ${INPUT_CLASSNAME}`
|
|
1045
|
+
}
|
|
1046
|
+
),
|
|
1047
|
+
/* @__PURE__ */ jsx9("p", { className: "text-[11px] text-gray-400 dark:text-gray-500 mt-1", children: labels.nodePanel.nodeNameHint })
|
|
1048
|
+
] }),
|
|
894
1049
|
draft.contextKey && /* @__PURE__ */ jsxs8("div", { children: [
|
|
895
1050
|
/* @__PURE__ */ jsx9("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.contextKey }),
|
|
896
|
-
/* @__PURE__ */ jsx9(
|
|
1051
|
+
/* @__PURE__ */ jsx9(
|
|
1052
|
+
"input",
|
|
1053
|
+
{
|
|
1054
|
+
value: draft.contextKey,
|
|
1055
|
+
disabled: true,
|
|
1056
|
+
className: "w-full mt-1 rounded-xl border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 px-3 py-2.5 text-sm text-gray-500"
|
|
1057
|
+
}
|
|
1058
|
+
)
|
|
897
1059
|
] }),
|
|
898
1060
|
!isFixedLogic && !isAction && !isCondition && /* @__PURE__ */ jsxs8("div", { children: [
|
|
899
1061
|
/* @__PURE__ */ jsx9("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.questionType }),
|
|
@@ -937,7 +1099,10 @@ function FlowNodePanel({
|
|
|
937
1099
|
"select",
|
|
938
1100
|
{
|
|
939
1101
|
value: draft.conditionOperator ?? ">",
|
|
940
|
-
onChange: (e) => setDraft((prev) => ({
|
|
1102
|
+
onChange: (e) => setDraft((prev) => ({
|
|
1103
|
+
...prev,
|
|
1104
|
+
conditionOperator: e.target.value
|
|
1105
|
+
})),
|
|
941
1106
|
className: `w-full mt-1 ${SELECT_CLASSNAME}`,
|
|
942
1107
|
children: CONDITION_OPERATORS.map((operator) => /* @__PURE__ */ jsx9("option", { value: operator, children: labels.conditionOperatorLabels[operator] ?? operator }, operator))
|
|
943
1108
|
}
|
|
@@ -965,7 +1130,11 @@ function FlowNodePanel({
|
|
|
965
1130
|
labels
|
|
966
1131
|
}
|
|
967
1132
|
),
|
|
968
|
-
|
|
1133
|
+
isSendMedia && /* @__PURE__ */ jsxs8("div", { children: [
|
|
1134
|
+
/* @__PURE__ */ jsx9("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.media }),
|
|
1135
|
+
/* @__PURE__ */ jsx9("div", { className: "mt-1", children: renderMediaPicker?.(node, graph) ?? /* @__PURE__ */ jsx9("p", { className: "text-xs text-gray-400", children: labels.nodePanel.mediaUnavailable }) })
|
|
1136
|
+
] }),
|
|
1137
|
+
isAction && draft.actionKind !== "send_product_list" && !isSendMedia && /* @__PURE__ */ jsx9(
|
|
969
1138
|
WhatsAppTextField,
|
|
970
1139
|
{
|
|
971
1140
|
label: labels.nodePanel.directMessage,
|
|
@@ -978,7 +1147,7 @@ function FlowNodePanel({
|
|
|
978
1147
|
(draft.questionType === "choice" || draft.type === "menu") && /* @__PURE__ */ jsxs8("div", { children: [
|
|
979
1148
|
/* @__PURE__ */ jsxs8("div", { className: "flex items-center justify-between mb-1.5", children: [
|
|
980
1149
|
/* @__PURE__ */ jsx9("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.options }),
|
|
981
|
-
/* @__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: [
|
|
982
1151
|
/* @__PURE__ */ jsx9(Plus2, { size: 12 }),
|
|
983
1152
|
" ",
|
|
984
1153
|
labels.nodePanel.addOption
|
|
@@ -1003,7 +1172,7 @@ function FlowNodePanel({
|
|
|
1003
1172
|
className: `flex-1 ${INPUT_CLASSNAME}`
|
|
1004
1173
|
}
|
|
1005
1174
|
),
|
|
1006
|
-
/* @__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 }) })
|
|
1007
1176
|
] }, i)) })
|
|
1008
1177
|
] }),
|
|
1009
1178
|
!isAction && /* @__PURE__ */ jsxs8("div", { className: "pt-2 border-t border-gray-100 dark:border-gray-700", children: [
|
|
@@ -1055,26 +1224,1107 @@ function FlowNodePanel({
|
|
|
1055
1224
|
] })
|
|
1056
1225
|
] }),
|
|
1057
1226
|
/* @__PURE__ */ jsxs8("div", { className: "p-4 border-t border-gray-100 dark:border-gray-700 flex gap-2", children: [
|
|
1058
|
-
/* @__PURE__ */ jsxs8(
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1227
|
+
/* @__PURE__ */ jsxs8(
|
|
1228
|
+
"button",
|
|
1229
|
+
{
|
|
1230
|
+
"data-cv-tooltip": labels.nodePanel.save,
|
|
1231
|
+
"aria-label": labels.nodePanel.save,
|
|
1232
|
+
onClick: () => onChange(draft),
|
|
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",
|
|
1234
|
+
children: [
|
|
1235
|
+
/* @__PURE__ */ jsx9(Save, { size: 14 }),
|
|
1236
|
+
" ",
|
|
1237
|
+
labels.nodePanel.save
|
|
1238
|
+
]
|
|
1239
|
+
}
|
|
1240
|
+
),
|
|
1063
1241
|
!isStart && /* @__PURE__ */ jsx9(
|
|
1064
1242
|
"button",
|
|
1065
1243
|
{
|
|
1066
1244
|
onClick: () => {
|
|
1067
1245
|
if (window.confirm(labels.nodePanel.deleteConfirm)) onDelete(node.id);
|
|
1068
1246
|
},
|
|
1069
|
-
|
|
1247
|
+
"data-cv-tooltip": labels.nodePanel.delete,
|
|
1248
|
+
"aria-label": labels.nodePanel.delete,
|
|
1070
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",
|
|
1071
1250
|
children: /* @__PURE__ */ jsx9(Trash2, { size: 14 })
|
|
1072
1251
|
}
|
|
1073
1252
|
),
|
|
1074
|
-
/* @__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 })
|
|
1254
|
+
] })
|
|
1255
|
+
] });
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
// src/flows/FlowsWorkspace.tsx
|
|
1259
|
+
import { useCallback, useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2, useState as useState3 } from "react";
|
|
1260
|
+
import {
|
|
1261
|
+
ReactFlow as ReactFlow2,
|
|
1262
|
+
Background as Background2,
|
|
1263
|
+
Controls as Controls2,
|
|
1264
|
+
MiniMap,
|
|
1265
|
+
MarkerType as MarkerType2,
|
|
1266
|
+
applyNodeChanges
|
|
1267
|
+
} from "@xyflow/react";
|
|
1268
|
+
import "@xyflow/react/dist/style.css";
|
|
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/flowCanvasModel.ts
|
|
1351
|
+
var COLUMN_GAP = 360;
|
|
1352
|
+
var ROW_GAP = 40;
|
|
1353
|
+
var CHAIN_LABEL_ROOM = 24;
|
|
1354
|
+
function portalNodeId(sourceNodeId, target) {
|
|
1355
|
+
return `__portal__${sourceNodeId}__${target}`;
|
|
1356
|
+
}
|
|
1357
|
+
function chainFrameNodeId(actionNodeId) {
|
|
1358
|
+
return `__chain__${actionNodeId}`;
|
|
1359
|
+
}
|
|
1360
|
+
var GROUP_HEADER_NODE_ID = "__group_header__";
|
|
1361
|
+
function countLiveByNode(params) {
|
|
1362
|
+
const { flowKey, rootFlowKey, positions } = params;
|
|
1363
|
+
const isRoot = flowKey === rootFlowKey;
|
|
1364
|
+
const counts = {};
|
|
1365
|
+
for (const position of positions ?? []) {
|
|
1366
|
+
if (position.flow !== flowKey && !isRoot) continue;
|
|
1367
|
+
const nodeId = isRoot ? position.menuNodeId : position.nodeId;
|
|
1368
|
+
if (!nodeId) continue;
|
|
1369
|
+
counts[nodeId] = (counts[nodeId] ?? 0) + 1;
|
|
1370
|
+
}
|
|
1371
|
+
return counts;
|
|
1372
|
+
}
|
|
1373
|
+
function computeMergedLayout(params) {
|
|
1374
|
+
const { openKeys, graphs, primaryFlowKey } = params;
|
|
1375
|
+
const open = new Set(openKeys);
|
|
1376
|
+
const nodeById = /* @__PURE__ */ new Map();
|
|
1377
|
+
for (const key of openKeys) {
|
|
1378
|
+
for (const node of Object.values(graphs[key]?.nodes ?? {})) {
|
|
1379
|
+
nodeById.set(namespaceNodeId(key, node.id), node);
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
function forwardEdges(flowKey, node) {
|
|
1383
|
+
const result = [];
|
|
1384
|
+
for (const { target } of targetsOf(node)) {
|
|
1385
|
+
if (isCrossFlowTarget(target)) {
|
|
1386
|
+
const targetKey = crossFlowKey(target);
|
|
1387
|
+
const targetGraph = open.has(targetKey) ? graphs[targetKey] : void 0;
|
|
1388
|
+
if (targetGraph) result.push(namespaceNodeId(targetKey, targetGraph.startNodeId));
|
|
1389
|
+
} else if (graphs[flowKey]?.nodes[target]) {
|
|
1390
|
+
result.push(namespaceNodeId(flowKey, target));
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
return result;
|
|
1394
|
+
}
|
|
1395
|
+
const rank = /* @__PURE__ */ new Map();
|
|
1396
|
+
const primaryGraph = graphs[primaryFlowKey];
|
|
1397
|
+
const rootId = primaryGraph ? namespaceNodeId(primaryFlowKey, primaryGraph.startNodeId) : void 0;
|
|
1398
|
+
if (rootId && nodeById.has(rootId)) {
|
|
1399
|
+
rank.set(rootId, 0);
|
|
1400
|
+
const queue = [rootId];
|
|
1401
|
+
while (queue.length > 0) {
|
|
1402
|
+
const id = queue.shift();
|
|
1403
|
+
const node = nodeById.get(id);
|
|
1404
|
+
if (!node) continue;
|
|
1405
|
+
for (const nextId of forwardEdges(parseNamespacedId(id).flowKey, node)) {
|
|
1406
|
+
if (!rank.has(nextId)) {
|
|
1407
|
+
rank.set(nextId, rank.get(id) + 1);
|
|
1408
|
+
queue.push(nextId);
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
const strayRank = Math.max(0, ...rank.values()) + 1;
|
|
1414
|
+
for (const id of nodeById.keys()) {
|
|
1415
|
+
if (!rank.has(id)) rank.set(id, strayRank);
|
|
1416
|
+
}
|
|
1417
|
+
const layers = /* @__PURE__ */ new Map();
|
|
1418
|
+
for (const [id, value] of rank) {
|
|
1419
|
+
const layer = layers.get(value);
|
|
1420
|
+
if (layer) layer.push(id);
|
|
1421
|
+
else layers.set(value, [id]);
|
|
1422
|
+
}
|
|
1423
|
+
const positions = /* @__PURE__ */ new Map();
|
|
1424
|
+
for (const value of [...layers.keys()].sort((a, b) => a - b)) {
|
|
1425
|
+
let cursorY = 0;
|
|
1426
|
+
for (const id of layers.get(value)) {
|
|
1427
|
+
positions.set(id, { x: value * COLUMN_GAP, y: cursorY });
|
|
1428
|
+
cursorY += estimateNodeHeight(nodeById.get(id)) + ROW_GAP;
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
return positions;
|
|
1432
|
+
}
|
|
1433
|
+
function buildFlowEdges(params) {
|
|
1434
|
+
const { openKeys, graphs, rootFlowKey, livePositions } = params;
|
|
1435
|
+
const open = new Set(openKeys);
|
|
1436
|
+
const edges = [];
|
|
1437
|
+
for (const flowKey of openKeys) {
|
|
1438
|
+
const graph = graphs[flowKey];
|
|
1439
|
+
if (!graph) continue;
|
|
1440
|
+
const liveCounts = countLiveByNode({ flowKey, rootFlowKey, positions: livePositions });
|
|
1441
|
+
for (const [nodeId, node] of Object.entries(graph.nodes)) {
|
|
1442
|
+
const live = (liveCounts[nodeId] ?? 0) > 0;
|
|
1443
|
+
for (const { target, optionId, isDefault } of targetsOf(node)) {
|
|
1444
|
+
if (!target) continue;
|
|
1445
|
+
const crossFlow = isCrossFlowTarget(target);
|
|
1446
|
+
let targetFlowKey = flowKey;
|
|
1447
|
+
let targetNodeId = target;
|
|
1448
|
+
if (crossFlow) {
|
|
1449
|
+
const wantedKey = crossFlowKey(target);
|
|
1450
|
+
const targetGraph = open.has(wantedKey) ? graphs[wantedKey] : void 0;
|
|
1451
|
+
if (targetGraph) {
|
|
1452
|
+
targetFlowKey = wantedKey;
|
|
1453
|
+
targetNodeId = targetGraph.startNodeId;
|
|
1454
|
+
} else {
|
|
1455
|
+
targetNodeId = portalNodeId(nodeId, target);
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
const source = namespaceNodeId(flowKey, nodeId);
|
|
1459
|
+
const edgeTarget = namespaceNodeId(targetFlowKey, targetNodeId);
|
|
1460
|
+
if (isDefault) {
|
|
1461
|
+
edges.push({
|
|
1462
|
+
id: `${source}->${edgeTarget}-default`,
|
|
1463
|
+
source,
|
|
1464
|
+
target: edgeTarget,
|
|
1465
|
+
sourceHandle: "__default",
|
|
1466
|
+
kind: "fallback",
|
|
1467
|
+
crossFlow,
|
|
1468
|
+
// Fallback não anima: é o caminho que ninguém escolheu, e piscar sugeria que a conversa
|
|
1469
|
+
// estava passando por ali.
|
|
1470
|
+
live: false
|
|
1471
|
+
});
|
|
1472
|
+
continue;
|
|
1473
|
+
}
|
|
1474
|
+
if (optionId === void 0) {
|
|
1475
|
+
edges.push({ id: `${source}->${edgeTarget}`, source, target: edgeTarget, kind: "linear", crossFlow, live });
|
|
1476
|
+
continue;
|
|
1477
|
+
}
|
|
1478
|
+
edges.push({
|
|
1479
|
+
id: `${source}->${edgeTarget}-${optionId}`,
|
|
1480
|
+
source,
|
|
1481
|
+
target: edgeTarget,
|
|
1482
|
+
sourceHandle: optionId,
|
|
1483
|
+
kind: "branch",
|
|
1484
|
+
crossFlow,
|
|
1485
|
+
live
|
|
1486
|
+
});
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
return edges;
|
|
1491
|
+
}
|
|
1492
|
+
function detachedNodeIds(graph) {
|
|
1493
|
+
const connected = /* @__PURE__ */ new Set();
|
|
1494
|
+
for (const node of Object.values(graph.nodes)) {
|
|
1495
|
+
for (const { target } of targetsOf(node)) {
|
|
1496
|
+
if (!isCrossFlowTarget(target)) connected.add(target);
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
const detached = /* @__PURE__ */ new Set();
|
|
1500
|
+
for (const nodeId of Object.keys(graph.nodes)) {
|
|
1501
|
+
if (nodeId !== graph.startNodeId && !connected.has(nodeId)) detached.add(nodeId);
|
|
1502
|
+
}
|
|
1503
|
+
return detached;
|
|
1504
|
+
}
|
|
1505
|
+
function chainFrameBounds(params) {
|
|
1506
|
+
const { nodeIds, graph, positionOf, padding } = params;
|
|
1507
|
+
const positions = nodeIds.map(positionOf);
|
|
1508
|
+
const minX = Math.min(...positions.map((each) => each.x));
|
|
1509
|
+
const maxX = Math.max(...positions.map((each) => each.x)) + NODE_CARD_WIDTH;
|
|
1510
|
+
const minY = Math.min(...positions.map((each) => each.y));
|
|
1511
|
+
const maxY = Math.max(...nodeIds.map((id, index) => positions[index].y + estimateNodeHeight(graph.nodes[id])));
|
|
1512
|
+
return {
|
|
1513
|
+
x: minX - padding,
|
|
1514
|
+
y: minY - padding - CHAIN_LABEL_ROOM,
|
|
1515
|
+
width: maxX - minX + padding * 2,
|
|
1516
|
+
height: maxY - minY + padding * 2 + CHAIN_LABEL_ROOM
|
|
1517
|
+
};
|
|
1518
|
+
}
|
|
1519
|
+
function newNodeFromSpec(spec, existingIds) {
|
|
1520
|
+
if (spec.kind === "question") {
|
|
1521
|
+
const id2 = slugifyNodeId("nova_pergunta", existingIds);
|
|
1522
|
+
return { id: id2, type: "question", questionType: spec.questionType, contextKey: id2, question: "" };
|
|
1523
|
+
}
|
|
1524
|
+
if (spec.kind === "decision") {
|
|
1525
|
+
const id2 = slugifyNodeId("nova_decisao", existingIds);
|
|
1526
|
+
return {
|
|
1527
|
+
id: id2,
|
|
1528
|
+
type: "question",
|
|
1529
|
+
questionType: "choice",
|
|
1530
|
+
contextKey: id2,
|
|
1531
|
+
question: "",
|
|
1532
|
+
options: [
|
|
1533
|
+
["1", "Op\xE7\xE3o 1"],
|
|
1534
|
+
["2", "Op\xE7\xE3o 2"]
|
|
1535
|
+
]
|
|
1536
|
+
};
|
|
1537
|
+
}
|
|
1538
|
+
if (spec.kind === "condition") {
|
|
1539
|
+
const id2 = slugifyNodeId("nova_condicao", existingIds);
|
|
1540
|
+
return { id: id2, type: "condition", conditionOperator: ">" };
|
|
1541
|
+
}
|
|
1542
|
+
const id = slugifyNodeId("nova_acao", existingIds);
|
|
1543
|
+
return { id, type: "action", actionKind: spec.actionKind };
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
// src/flows/FlowsWorkspace.tsx
|
|
1547
|
+
import { Fragment as Fragment2, jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
1548
|
+
var RF_NODE_TYPES = {
|
|
1549
|
+
...flowNodeTypes,
|
|
1550
|
+
...flowPortalNodeTypes,
|
|
1551
|
+
...flowGroupHeaderNodeTypes,
|
|
1552
|
+
...flowGroupFrameNodeTypes
|
|
1553
|
+
};
|
|
1554
|
+
var CHAIN_FRAME_PADDING = 36;
|
|
1555
|
+
var FLOW_KEY_PATTERN = /^[a-z0-9_]{2,40}$/;
|
|
1556
|
+
var EDGE_COLOR_LINEAR = "#94a3b8";
|
|
1557
|
+
var EDGE_COLOR_BRANCH = "#8b5cf6";
|
|
1558
|
+
var EDGE_COLOR_FALLBACK = "#cbd5e1";
|
|
1559
|
+
var EDGE_COLOR_LIVE = "#3b82f6";
|
|
1560
|
+
var EDGE_COLOR_CROSS_FLOW = "#06b6d4";
|
|
1561
|
+
var BACKGROUND_COLOR_LIGHT2 = "#cbd5e1";
|
|
1562
|
+
var BACKGROUND_COLOR_DARK2 = "#334155";
|
|
1563
|
+
function styleEdge(spec) {
|
|
1564
|
+
const color = spec.crossFlow ? EDGE_COLOR_CROSS_FLOW : spec.kind === "fallback" ? EDGE_COLOR_FALLBACK : spec.live ? EDGE_COLOR_LIVE : spec.kind === "branch" ? EDGE_COLOR_BRANCH : EDGE_COLOR_LINEAR;
|
|
1565
|
+
const baseWidth = spec.kind === "branch" ? 1.75 : 1.5;
|
|
1566
|
+
const dash = spec.crossFlow ? "3 3" : spec.kind === "fallback" ? "5 4" : void 0;
|
|
1567
|
+
const markerSize = spec.kind === "fallback" ? 16 : 18;
|
|
1568
|
+
return {
|
|
1569
|
+
id: spec.id,
|
|
1570
|
+
source: spec.source,
|
|
1571
|
+
target: spec.target,
|
|
1572
|
+
...spec.sourceHandle === void 0 ? {} : { sourceHandle: spec.sourceHandle },
|
|
1573
|
+
type: "bezier",
|
|
1574
|
+
animated: spec.live,
|
|
1575
|
+
style: {
|
|
1576
|
+
stroke: color,
|
|
1577
|
+
strokeWidth: spec.live && !spec.crossFlow ? 2.5 : baseWidth,
|
|
1578
|
+
...dash === void 0 ? {} : { strokeDasharray: dash }
|
|
1579
|
+
},
|
|
1580
|
+
markerEnd: { type: MarkerType2.ArrowClosed, color, width: markerSize, height: markerSize }
|
|
1581
|
+
};
|
|
1582
|
+
}
|
|
1583
|
+
function extractErrorMessage(error) {
|
|
1584
|
+
if (error instanceof Error) return error.message;
|
|
1585
|
+
return void 0;
|
|
1586
|
+
}
|
|
1587
|
+
var OUTLINE_BUTTON = "inline-flex items-center gap-1.5 rounded-lg border border-gray-200 dark:border-gray-700 px-3 py-1.5 text-xs font-medium text-gray-600 dark:text-gray-300 hover:border-blue-300 disabled:opacity-40 disabled:cursor-not-allowed";
|
|
1588
|
+
var PRIMARY_BUTTON = "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 disabled:opacity-40 disabled:cursor-not-allowed";
|
|
1589
|
+
var DIALOG_INPUT = "w-full border border-gray-200 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-400";
|
|
1590
|
+
function FlowsWorkspace({
|
|
1591
|
+
api,
|
|
1592
|
+
rootFlowKey = "menu",
|
|
1593
|
+
labels: labelsOverride,
|
|
1594
|
+
actionOptions,
|
|
1595
|
+
renderMediaPicker,
|
|
1596
|
+
livePollIntervalMs = 5e3,
|
|
1597
|
+
className
|
|
1598
|
+
}) {
|
|
1599
|
+
const labels = useMemo2(() => mergeFlowEditorLabels(labelsOverride), [labelsOverride]);
|
|
1600
|
+
const isDark = useIsDarkTheme();
|
|
1601
|
+
const [graphs, setGraphs] = useState3(void 0);
|
|
1602
|
+
const [loadState, setLoadState] = useState3("loading");
|
|
1603
|
+
const [livePositions, setLivePositions] = useState3(void 0);
|
|
1604
|
+
const [viewMode, setViewMode] = useState3("detail");
|
|
1605
|
+
const [openFlowKeys, setOpenFlowKeys] = useState3([rootFlowKey]);
|
|
1606
|
+
const [hasAutoMerged, setHasAutoMerged] = useState3(false);
|
|
1607
|
+
const [workingGraphs, setWorkingGraphs] = useState3({});
|
|
1608
|
+
const [editingRef, setEditingRef] = useState3(null);
|
|
1609
|
+
const [saveState, setSaveState] = useState3("idle");
|
|
1610
|
+
const [saveErrorMessage, setSaveErrorMessage] = useState3(void 0);
|
|
1611
|
+
const [showCreateDialog, setShowCreateDialog] = useState3(false);
|
|
1612
|
+
const [showDeleteDialog, setShowDeleteDialog] = useState3(false);
|
|
1613
|
+
const [newFlow, setNewFlow] = useState3({ key: "", label: "", showInMenu: false, menuOptionLabel: "" });
|
|
1614
|
+
const [flowMutationState, setFlowMutationState] = useState3({ pending: false });
|
|
1615
|
+
const [rfNodes, setRfNodes] = useState3([]);
|
|
1616
|
+
const [flowInstance, setFlowInstance] = useState3(null);
|
|
1617
|
+
const [pendingFocusNodeId, setPendingFocusNodeId] = useState3(null);
|
|
1618
|
+
const reloadGraphs = useCallback(async () => {
|
|
1619
|
+
try {
|
|
1620
|
+
const loaded = await api.getGraphs();
|
|
1621
|
+
setGraphs(loaded);
|
|
1622
|
+
setLoadState("ready");
|
|
1623
|
+
return loaded;
|
|
1624
|
+
} catch {
|
|
1625
|
+
setLoadState("error");
|
|
1626
|
+
return void 0;
|
|
1627
|
+
}
|
|
1628
|
+
}, [api]);
|
|
1629
|
+
useEffect2(() => {
|
|
1630
|
+
void reloadGraphs();
|
|
1631
|
+
}, [reloadGraphs]);
|
|
1632
|
+
useEffect2(() => {
|
|
1633
|
+
const fetchLive = api.getLivePositions;
|
|
1634
|
+
if (!fetchLive) return;
|
|
1635
|
+
let active = true;
|
|
1636
|
+
async function poll() {
|
|
1637
|
+
try {
|
|
1638
|
+
const positions = await fetchLive();
|
|
1639
|
+
if (active) setLivePositions(positions);
|
|
1640
|
+
} catch {
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
void poll();
|
|
1644
|
+
const timer = setInterval(() => void poll(), livePollIntervalMs);
|
|
1645
|
+
return () => {
|
|
1646
|
+
active = false;
|
|
1647
|
+
clearInterval(timer);
|
|
1648
|
+
};
|
|
1649
|
+
}, [api, livePollIntervalMs]);
|
|
1650
|
+
const primaryFlowKey = openFlowKeys[0] ?? rootFlowKey;
|
|
1651
|
+
const primaryGraph = workingGraphs[primaryFlowKey];
|
|
1652
|
+
useEffect2(() => {
|
|
1653
|
+
if (!graphs || hasAutoMerged) return;
|
|
1654
|
+
setOpenFlowKeys(mergedFlowKeysFrom(rootFlowKey, graphs));
|
|
1655
|
+
setHasAutoMerged(true);
|
|
1656
|
+
}, [graphs, hasAutoMerged, rootFlowKey]);
|
|
1657
|
+
useEffect2(() => {
|
|
1658
|
+
if (!graphs) return;
|
|
1659
|
+
setWorkingGraphs((prev) => {
|
|
1660
|
+
let changed = false;
|
|
1661
|
+
const next = { ...prev };
|
|
1662
|
+
for (const key of openFlowKeys) {
|
|
1663
|
+
if (!next[key] && graphs[key]) {
|
|
1664
|
+
next[key] = graphs[key];
|
|
1665
|
+
changed = true;
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
return changed ? next : prev;
|
|
1669
|
+
});
|
|
1670
|
+
}, [graphs, openFlowKeys]);
|
|
1671
|
+
const isFlowDirty = useCallback(
|
|
1672
|
+
(key) => {
|
|
1673
|
+
const working = workingGraphs[key];
|
|
1674
|
+
const server = graphs?.[key];
|
|
1675
|
+
if (!working || !server) return false;
|
|
1676
|
+
return JSON.stringify(working) !== JSON.stringify(server);
|
|
1677
|
+
},
|
|
1678
|
+
[workingGraphs, graphs]
|
|
1679
|
+
);
|
|
1680
|
+
const issuesByFlow = useMemo2(() => {
|
|
1681
|
+
const map = {};
|
|
1682
|
+
for (const key of openFlowKeys) {
|
|
1683
|
+
const graph = workingGraphs[key];
|
|
1684
|
+
if (graph) map[key] = validateGraph(graph, labels.validation);
|
|
1685
|
+
}
|
|
1686
|
+
return map;
|
|
1687
|
+
}, [openFlowKeys, workingGraphs, labels]);
|
|
1688
|
+
const errorCount = Object.values(issuesByFlow).reduce(
|
|
1689
|
+
(sum, list) => sum + list.filter((issue) => issue.severity === "error").length,
|
|
1690
|
+
0
|
|
1691
|
+
);
|
|
1692
|
+
const warningCount = Object.values(issuesByFlow).reduce(
|
|
1693
|
+
(sum, list) => sum + list.filter((issue) => issue.severity === "warning").length,
|
|
1694
|
+
0
|
|
1695
|
+
);
|
|
1696
|
+
const dirtyKeys = openFlowKeys.filter(isFlowDirty);
|
|
1697
|
+
const isDirty = dirtyKeys.length > 0;
|
|
1698
|
+
const updateFlow = useCallback((flowKey, updater) => {
|
|
1699
|
+
setWorkingGraphs((prev) => {
|
|
1700
|
+
const graph = prev[flowKey];
|
|
1701
|
+
if (!graph) return prev;
|
|
1702
|
+
return { ...prev, [flowKey]: updater(graph) };
|
|
1703
|
+
});
|
|
1704
|
+
}, []);
|
|
1705
|
+
const focusFlow = useCallback(
|
|
1706
|
+
(key) => {
|
|
1707
|
+
const others = openFlowKeys.filter((each) => each !== key);
|
|
1708
|
+
if (others.some(isFlowDirty) && !window.confirm(labels.workspace.unsavedChangesConfirm)) return;
|
|
1709
|
+
setOpenFlowKeys(graphs ? mergedFlowKeysFrom(key, graphs) : [key]);
|
|
1710
|
+
if (editingRef && editingRef.flowKey !== key) setEditingRef(null);
|
|
1711
|
+
},
|
|
1712
|
+
[openFlowKeys, isFlowDirty, editingRef, graphs, labels]
|
|
1713
|
+
);
|
|
1714
|
+
const closeFlow = useCallback(
|
|
1715
|
+
(key) => {
|
|
1716
|
+
if (isFlowDirty(key) && !window.confirm(labels.workspace.unsavedChangesConfirm)) return;
|
|
1717
|
+
setOpenFlowKeys((prev) => prev.filter((each) => each !== key));
|
|
1718
|
+
setWorkingGraphs((prev) => {
|
|
1719
|
+
const { [key]: _removed, ...rest } = prev;
|
|
1720
|
+
return rest;
|
|
1721
|
+
});
|
|
1722
|
+
if (editingRef?.flowKey === key) setEditingRef(null);
|
|
1723
|
+
},
|
|
1724
|
+
[isFlowDirty, editingRef, labels]
|
|
1725
|
+
);
|
|
1726
|
+
const mergeFlow = useCallback((targetFlowKey) => {
|
|
1727
|
+
setOpenFlowKeys((prev) => prev.includes(targetFlowKey) ? prev : [...prev, targetFlowKey]);
|
|
1728
|
+
}, []);
|
|
1729
|
+
const isMerged = openFlowKeys.length > 1;
|
|
1730
|
+
const mergedPositions = useMemo2(
|
|
1731
|
+
() => isMerged ? computeMergedLayout({ openKeys: openFlowKeys, graphs: workingGraphs, primaryFlowKey }) : null,
|
|
1732
|
+
[isMerged, openFlowKeys, workingGraphs, primaryFlowKey]
|
|
1733
|
+
);
|
|
1734
|
+
const renderedPositionsRef = useRef2(/* @__PURE__ */ new Map());
|
|
1735
|
+
const derivedNodes = useMemo2(() => {
|
|
1736
|
+
const allNodes = [];
|
|
1737
|
+
for (const flowKey of openFlowKeys) {
|
|
1738
|
+
let resolvePosition2 = function(nodeId) {
|
|
1739
|
+
if (mergedPositions) {
|
|
1740
|
+
const nsId = namespaceNodeId(flowKey, nodeId);
|
|
1741
|
+
return renderedPositionsRef.current.get(nsId) ?? mergedPositions.get(nsId) ?? { x: 0, y: 0 };
|
|
1742
|
+
}
|
|
1743
|
+
return graph.nodes[nodeId]?.position ?? fallbackPositions[nodeId] ?? { x: 0, y: 0 };
|
|
1744
|
+
};
|
|
1745
|
+
var resolvePosition = resolvePosition2;
|
|
1746
|
+
const graph = workingGraphs[flowKey];
|
|
1747
|
+
if (!graph) continue;
|
|
1748
|
+
const fallbackPositions = mergedPositions ? {} : computeAutoLayout(graph);
|
|
1749
|
+
const liveCounts = countLiveByNode({ flowKey, rootFlowKey, positions: livePositions });
|
|
1750
|
+
const flowIssues = issuesByFlow[flowKey] ?? [];
|
|
1751
|
+
const isPrimary = flowKey === primaryFlowKey;
|
|
1752
|
+
const connectedTargets = /* @__PURE__ */ new Set();
|
|
1753
|
+
for (const candidate of Object.values(graph.nodes)) {
|
|
1754
|
+
for (const { target } of targetsOf(candidate)) {
|
|
1755
|
+
if (!isCrossFlowTarget(target)) connectedTargets.add(target);
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
for (const node of Object.values(graph.nodes)) {
|
|
1759
|
+
const position = resolvePosition2(node.id);
|
|
1760
|
+
allNodes.push({
|
|
1761
|
+
id: namespaceNodeId(flowKey, node.id),
|
|
1762
|
+
type: "flowNode",
|
|
1763
|
+
position,
|
|
1764
|
+
draggable: true,
|
|
1765
|
+
data: {
|
|
1766
|
+
node,
|
|
1767
|
+
liveCount: liveCounts[node.id] ?? 0,
|
|
1768
|
+
isStart: node.id === graph.startNodeId,
|
|
1769
|
+
isSelected: editingRef?.flowKey === flowKey && editingRef?.nodeId === node.id,
|
|
1770
|
+
isDetached: node.id !== graph.startNodeId && !connectedTargets.has(node.id),
|
|
1771
|
+
issues: flowIssues,
|
|
1772
|
+
labels,
|
|
1773
|
+
onSelect: (nodeId) => setEditingRef({ flowKey, nodeId })
|
|
1774
|
+
}
|
|
1775
|
+
});
|
|
1776
|
+
const crossFlowTargets = [...new Set(targetsOf(node).map((edge) => edge.target).filter(isCrossFlowTarget))];
|
|
1777
|
+
crossFlowTargets.forEach((target, index) => {
|
|
1778
|
+
const targetFlowKey = crossFlowKey(target);
|
|
1779
|
+
if (openFlowKeys.includes(targetFlowKey)) return;
|
|
1780
|
+
allNodes.push({
|
|
1781
|
+
id: namespaceNodeId(flowKey, portalNodeId(node.id, target)),
|
|
1782
|
+
type: "flowPortal",
|
|
1783
|
+
draggable: false,
|
|
1784
|
+
selectable: false,
|
|
1785
|
+
position: { x: position.x + 320, y: position.y + index * 70 },
|
|
1786
|
+
data: {
|
|
1787
|
+
label: graphs?.[targetFlowKey]?.label ?? targetFlowKey,
|
|
1788
|
+
onNavigate: () => mergeFlow(targetFlowKey)
|
|
1789
|
+
}
|
|
1790
|
+
});
|
|
1791
|
+
});
|
|
1792
|
+
}
|
|
1793
|
+
for (const chain of findCollectionChains(graph)) {
|
|
1794
|
+
const chainNodeIds = [...chain.nodeIds, chain.actionNodeId];
|
|
1795
|
+
const positions = chainNodeIds.map((id) => resolvePosition2(id));
|
|
1796
|
+
const minX = Math.min(...positions.map((point) => point.x));
|
|
1797
|
+
const maxX = Math.max(...positions.map((point) => point.x)) + NODE_CARD_WIDTH;
|
|
1798
|
+
const minY = Math.min(...positions.map((point) => point.y));
|
|
1799
|
+
const maxY = Math.max(...positions.map((point) => point.y)) + estimateNodeHeight(graph.nodes[chain.actionNodeId]);
|
|
1800
|
+
allNodes.push({
|
|
1801
|
+
id: namespaceNodeId(flowKey, `__chain__${chain.actionNodeId}`),
|
|
1802
|
+
type: "flowGroupFrame",
|
|
1803
|
+
draggable: false,
|
|
1804
|
+
selectable: false,
|
|
1805
|
+
zIndex: -1,
|
|
1806
|
+
position: { x: minX - CHAIN_FRAME_PADDING, y: minY - CHAIN_FRAME_PADDING - 24 },
|
|
1807
|
+
style: {
|
|
1808
|
+
width: maxX - minX + CHAIN_FRAME_PADDING * 2,
|
|
1809
|
+
height: maxY - minY + CHAIN_FRAME_PADDING * 2 + 24
|
|
1810
|
+
},
|
|
1811
|
+
data: {
|
|
1812
|
+
label: labels.collectionChain.feeds(nodeLabel(graph.nodes[chain.actionNodeId], labels))
|
|
1813
|
+
}
|
|
1814
|
+
});
|
|
1815
|
+
}
|
|
1816
|
+
if (!isPrimary) {
|
|
1817
|
+
const startPosition = resolvePosition2(graph.startNodeId);
|
|
1818
|
+
allNodes.push({
|
|
1819
|
+
id: namespaceNodeId(flowKey, "__group_header__"),
|
|
1820
|
+
type: "flowGroupHeader",
|
|
1821
|
+
draggable: false,
|
|
1822
|
+
selectable: false,
|
|
1823
|
+
position: { x: startPosition.x, y: startPosition.y - 60 },
|
|
1824
|
+
data: {
|
|
1825
|
+
label: graph.label,
|
|
1826
|
+
onFocus: () => focusFlow(flowKey),
|
|
1827
|
+
onClose: () => closeFlow(flowKey)
|
|
1828
|
+
}
|
|
1829
|
+
});
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
return allNodes;
|
|
1833
|
+
}, [
|
|
1834
|
+
openFlowKeys,
|
|
1835
|
+
workingGraphs,
|
|
1836
|
+
mergedPositions,
|
|
1837
|
+
livePositions,
|
|
1838
|
+
issuesByFlow,
|
|
1839
|
+
primaryFlowKey,
|
|
1840
|
+
rootFlowKey,
|
|
1841
|
+
editingRef,
|
|
1842
|
+
graphs,
|
|
1843
|
+
mergeFlow,
|
|
1844
|
+
focusFlow,
|
|
1845
|
+
closeFlow,
|
|
1846
|
+
labels
|
|
1847
|
+
]);
|
|
1848
|
+
const edges = useMemo2(
|
|
1849
|
+
() => buildFlowEdges({ openKeys: openFlowKeys, graphs: workingGraphs, rootFlowKey, livePositions }).map(styleEdge),
|
|
1850
|
+
[openFlowKeys, workingGraphs, rootFlowKey, livePositions]
|
|
1851
|
+
);
|
|
1852
|
+
useEffect2(() => {
|
|
1853
|
+
setRfNodes(derivedNodes);
|
|
1854
|
+
for (const node of derivedNodes) {
|
|
1855
|
+
if (node.type === "flowNode") renderedPositionsRef.current.set(node.id, node.position);
|
|
1856
|
+
}
|
|
1857
|
+
}, [derivedNodes]);
|
|
1858
|
+
useEffect2(() => {
|
|
1859
|
+
if (!pendingFocusNodeId || !flowInstance) return;
|
|
1860
|
+
const target = rfNodes.find((node) => node.id === pendingFocusNodeId);
|
|
1861
|
+
if (!target) return;
|
|
1862
|
+
flowInstance.setCenter(target.position.x + NODE_CARD_WIDTH / 2, target.position.y, { zoom: 1, duration: 400 });
|
|
1863
|
+
setPendingFocusNodeId(null);
|
|
1864
|
+
}, [pendingFocusNodeId, flowInstance, rfNodes]);
|
|
1865
|
+
const onNodesChange = useCallback((changes) => {
|
|
1866
|
+
setRfNodes((current) => applyNodeChanges(changes, current));
|
|
1867
|
+
}, []);
|
|
1868
|
+
const onNodeDragStop = useCallback(
|
|
1869
|
+
(_event, node) => {
|
|
1870
|
+
renderedPositionsRef.current.set(node.id, node.position);
|
|
1871
|
+
const { flowKey, nodeId } = parseNamespacedId(node.id);
|
|
1872
|
+
if (!openFlowKeys.includes(flowKey)) return;
|
|
1873
|
+
updateFlow(
|
|
1874
|
+
flowKey,
|
|
1875
|
+
(graph) => graph.nodes[nodeId] ? { ...graph, nodes: { ...graph.nodes, [nodeId]: { ...graph.nodes[nodeId], position: node.position } } } : graph
|
|
1876
|
+
);
|
|
1877
|
+
},
|
|
1878
|
+
[openFlowKeys, updateFlow]
|
|
1879
|
+
);
|
|
1880
|
+
const onConnect = useCallback(
|
|
1881
|
+
(connection) => {
|
|
1882
|
+
const resolved = resolveConnection({
|
|
1883
|
+
connection: { source: connection.source, target: connection.target, sourceHandle: connection.sourceHandle },
|
|
1884
|
+
graphs: workingGraphs
|
|
1885
|
+
});
|
|
1886
|
+
if (!resolved) return;
|
|
1887
|
+
updateFlow(resolved.flowKey, (graph) => {
|
|
1888
|
+
const node = graph.nodes[resolved.nodeId];
|
|
1889
|
+
if (!node) return graph;
|
|
1890
|
+
return { ...graph, nodes: { ...graph.nodes, [resolved.nodeId]: applyConnection(node, resolved) } };
|
|
1891
|
+
});
|
|
1892
|
+
},
|
|
1893
|
+
[workingGraphs, updateFlow]
|
|
1894
|
+
);
|
|
1895
|
+
function handleAddNode(spec) {
|
|
1896
|
+
if (!primaryGraph) return;
|
|
1897
|
+
const newNode = newNodeFromSpec(spec, new Set(Object.keys(primaryGraph.nodes)));
|
|
1898
|
+
const maxY = Math.max(0, ...Object.values(primaryGraph.nodes).map((node) => node.position?.y ?? 0));
|
|
1899
|
+
newNode.position = { x: 0, y: maxY + 170 };
|
|
1900
|
+
updateFlow(primaryFlowKey, (graph) => ({ ...graph, nodes: { ...graph.nodes, [newNode.id]: newNode } }));
|
|
1901
|
+
setEditingRef({ flowKey: primaryFlowKey, nodeId: newNode.id });
|
|
1902
|
+
setPendingFocusNodeId(namespaceNodeId(primaryFlowKey, newNode.id));
|
|
1903
|
+
}
|
|
1904
|
+
function handleNodePanelChange(updated) {
|
|
1905
|
+
if (!editingRef) return;
|
|
1906
|
+
updateFlow(editingRef.flowKey, (graph) => ({ ...graph, nodes: { ...graph.nodes, [updated.id]: updated } }));
|
|
1907
|
+
setEditingRef(null);
|
|
1908
|
+
}
|
|
1909
|
+
function handleNodeDelete(nodeId) {
|
|
1910
|
+
if (!editingRef) return;
|
|
1911
|
+
updateFlow(editingRef.flowKey, (graph) => ({ ...graph, nodes: removeNodeAndCleanRefs(graph.nodes, nodeId) }));
|
|
1912
|
+
setEditingRef(null);
|
|
1913
|
+
}
|
|
1914
|
+
function handleOrganize() {
|
|
1915
|
+
if (!primaryGraph) return;
|
|
1916
|
+
renderedPositionsRef.current.clear();
|
|
1917
|
+
if (isMerged) {
|
|
1918
|
+
const positions2 = computeMergedLayout({ openKeys: openFlowKeys, graphs: workingGraphs, primaryFlowKey });
|
|
1919
|
+
for (const key of openFlowKeys) {
|
|
1920
|
+
updateFlow(key, (graph) => ({
|
|
1921
|
+
...graph,
|
|
1922
|
+
nodes: Object.fromEntries(
|
|
1923
|
+
Object.entries(graph.nodes).map(([id, node]) => {
|
|
1924
|
+
const position = positions2.get(namespaceNodeId(key, id));
|
|
1925
|
+
return [id, position ? { ...node, position } : node];
|
|
1926
|
+
})
|
|
1927
|
+
)
|
|
1928
|
+
}));
|
|
1929
|
+
}
|
|
1930
|
+
return;
|
|
1931
|
+
}
|
|
1932
|
+
const positions = computeAutoLayout(primaryGraph);
|
|
1933
|
+
updateFlow(primaryFlowKey, (graph) => ({
|
|
1934
|
+
...graph,
|
|
1935
|
+
nodes: Object.fromEntries(
|
|
1936
|
+
Object.entries(graph.nodes).map(([id, node]) => [id, { ...node, position: positions[id] ?? node.position }])
|
|
1937
|
+
)
|
|
1938
|
+
}));
|
|
1939
|
+
}
|
|
1940
|
+
function handleDiscardChanges() {
|
|
1941
|
+
if (!graphs || !isDirty) return;
|
|
1942
|
+
if (!window.confirm(labels.workspace.discardConfirm)) return;
|
|
1943
|
+
setWorkingGraphs((prev) => {
|
|
1944
|
+
const next = { ...prev };
|
|
1945
|
+
for (const key of dirtyKeys) {
|
|
1946
|
+
const published = graphs[key];
|
|
1947
|
+
if (published) next[key] = published;
|
|
1948
|
+
}
|
|
1949
|
+
return next;
|
|
1950
|
+
});
|
|
1951
|
+
renderedPositionsRef.current.clear();
|
|
1952
|
+
setEditingRef(null);
|
|
1953
|
+
}
|
|
1954
|
+
async function handlePublish() {
|
|
1955
|
+
if (dirtyKeys.length === 0 || errorCount > 0) return;
|
|
1956
|
+
setSaveState("saving");
|
|
1957
|
+
setSaveErrorMessage(void 0);
|
|
1958
|
+
try {
|
|
1959
|
+
await Promise.all(dirtyKeys.map((key) => api.saveGraph(key, workingGraphs[key])));
|
|
1960
|
+
await reloadGraphs();
|
|
1961
|
+
setSaveState("success");
|
|
1962
|
+
setTimeout(() => setSaveState("idle"), 3e3);
|
|
1963
|
+
} catch (error) {
|
|
1964
|
+
setSaveState("error");
|
|
1965
|
+
setSaveErrorMessage(extractErrorMessage(error));
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
async function handleCreateFlow() {
|
|
1969
|
+
const createFlow = api.createFlow;
|
|
1970
|
+
if (!createFlow) return;
|
|
1971
|
+
setFlowMutationState({ pending: true });
|
|
1972
|
+
try {
|
|
1973
|
+
await createFlow({
|
|
1974
|
+
key: newFlow.key,
|
|
1975
|
+
label: newFlow.label,
|
|
1976
|
+
showInMenu: newFlow.showInMenu,
|
|
1977
|
+
...newFlow.showInMenu ? { menuOptionLabel: newFlow.menuOptionLabel || newFlow.label } : {}
|
|
1978
|
+
});
|
|
1979
|
+
await reloadGraphs();
|
|
1980
|
+
setOpenFlowKeys([newFlow.key]);
|
|
1981
|
+
setShowCreateDialog(false);
|
|
1982
|
+
setNewFlow({ key: "", label: "", showInMenu: false, menuOptionLabel: "" });
|
|
1983
|
+
setFlowMutationState({ pending: false });
|
|
1984
|
+
} catch (error) {
|
|
1985
|
+
setFlowMutationState({ pending: false, error: extractErrorMessage(error) ?? labels.flowManager.createError });
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
async function handleDeleteFlow() {
|
|
1989
|
+
const deleteFlow = api.deleteFlow;
|
|
1990
|
+
if (!deleteFlow || !primaryGraph) return;
|
|
1991
|
+
setFlowMutationState({ pending: true });
|
|
1992
|
+
try {
|
|
1993
|
+
await deleteFlow(primaryGraph.key);
|
|
1994
|
+
const reloaded = await reloadGraphs();
|
|
1995
|
+
setOpenFlowKeys(reloaded ? mergedFlowKeysFrom(rootFlowKey, reloaded) : [rootFlowKey]);
|
|
1996
|
+
setShowDeleteDialog(false);
|
|
1997
|
+
setFlowMutationState({ pending: false });
|
|
1998
|
+
} catch (error) {
|
|
1999
|
+
setFlowMutationState({ pending: false, error: extractErrorMessage(error) ?? labels.flowManager.deleteError });
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
const editingGraph = editingRef ? workingGraphs[editingRef.flowKey] : null;
|
|
2003
|
+
const editingNode = editingRef && editingGraph ? editingGraph.nodes[editingRef.nodeId] : null;
|
|
2004
|
+
const otherFlows = useMemo2(
|
|
2005
|
+
() => Object.values(graphs ?? {}).filter((graph) => graph.key !== editingRef?.flowKey).map((graph) => ({ key: graph.key, label: graph.label })),
|
|
2006
|
+
[graphs, editingRef?.flowKey]
|
|
2007
|
+
);
|
|
2008
|
+
const keyIsValid = FLOW_KEY_PATTERN.test(newFlow.key);
|
|
2009
|
+
const canCreateFlow = Boolean(api.createFlow);
|
|
2010
|
+
const canDeleteFlow = Boolean(api.deleteFlow) && primaryFlowKey !== rootFlowKey;
|
|
2011
|
+
return /* @__PURE__ */ jsxs9("div", { className: `space-y-4 h-full flex flex-col ${className ?? ""}`, children: [
|
|
2012
|
+
/* @__PURE__ */ jsx10(TooltipLayer, {}),
|
|
2013
|
+
/* @__PURE__ */ jsxs9("div", { className: "flex items-center justify-between flex-wrap gap-3", children: [
|
|
2014
|
+
/* @__PURE__ */ jsxs9("div", { children: [
|
|
2015
|
+
/* @__PURE__ */ jsx10("h2", { className: "text-2xl font-bold text-gray-900 dark:text-gray-100", children: labels.workspace.title }),
|
|
2016
|
+
/* @__PURE__ */ jsx10("p", { className: "text-gray-500 dark:text-gray-400 text-sm mt-1", children: labels.workspace.subtitle })
|
|
2017
|
+
] }),
|
|
2018
|
+
/* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-3", children: [
|
|
2019
|
+
saveState === "success" && /* @__PURE__ */ jsx10("span", { className: "text-sm text-green-600", children: labels.workspace.saveSuccess }),
|
|
2020
|
+
saveState === "error" && /* @__PURE__ */ jsx10("span", { className: "text-sm text-red-600", children: saveErrorMessage ?? labels.workspace.saveError }),
|
|
2021
|
+
/* @__PURE__ */ jsxs9(
|
|
2022
|
+
"button",
|
|
2023
|
+
{
|
|
2024
|
+
"data-cv-tooltip": viewMode === "map" ? labels.flowMap.toggleToDetail : labels.flowMap.toggleToMap,
|
|
2025
|
+
"aria-label": viewMode === "map" ? labels.flowMap.toggleToDetail : labels.flowMap.toggleToMap,
|
|
2026
|
+
type: "button",
|
|
2027
|
+
onClick: () => setViewMode((mode) => mode === "map" ? "detail" : "map"),
|
|
2028
|
+
className: OUTLINE_BUTTON,
|
|
2029
|
+
children: [
|
|
2030
|
+
viewMode === "map" ? /* @__PURE__ */ jsx10(Workflow, { size: 14, "aria-hidden": "true" }) : /* @__PURE__ */ jsx10(MapIcon, { size: 14, "aria-hidden": "true" }),
|
|
2031
|
+
viewMode === "map" ? labels.flowMap.toggleToDetail : labels.flowMap.toggleToMap
|
|
2032
|
+
]
|
|
2033
|
+
}
|
|
2034
|
+
),
|
|
2035
|
+
viewMode === "detail" && /* @__PURE__ */ jsxs9(Fragment2, { children: [
|
|
2036
|
+
/* @__PURE__ */ jsxs9(
|
|
2037
|
+
"button",
|
|
2038
|
+
{
|
|
2039
|
+
type: "button",
|
|
2040
|
+
onClick: handleOrganize,
|
|
2041
|
+
className: OUTLINE_BUTTON,
|
|
2042
|
+
"data-cv-tooltip": labels.workspace.organizeTooltip,
|
|
2043
|
+
"aria-label": labels.workspace.organizeTooltip,
|
|
2044
|
+
disabled: !primaryGraph,
|
|
2045
|
+
children: [
|
|
2046
|
+
/* @__PURE__ */ jsx10(LayoutGrid, { size: 14, "aria-hidden": "true" }),
|
|
2047
|
+
" ",
|
|
2048
|
+
labels.workspace.organize
|
|
2049
|
+
]
|
|
2050
|
+
}
|
|
2051
|
+
),
|
|
2052
|
+
/* @__PURE__ */ jsxs9(
|
|
2053
|
+
"button",
|
|
2054
|
+
{
|
|
2055
|
+
type: "button",
|
|
2056
|
+
onClick: handleDiscardChanges,
|
|
2057
|
+
className: OUTLINE_BUTTON,
|
|
2058
|
+
"data-cv-tooltip": labels.workspace.discardTooltip,
|
|
2059
|
+
"aria-label": labels.workspace.discardTooltip,
|
|
2060
|
+
disabled: !isDirty || saveState === "saving",
|
|
2061
|
+
children: [
|
|
2062
|
+
/* @__PURE__ */ jsx10(Undo2, { size: 14, "aria-hidden": "true" }),
|
|
2063
|
+
" ",
|
|
2064
|
+
labels.workspace.discardChanges
|
|
2065
|
+
]
|
|
2066
|
+
}
|
|
2067
|
+
),
|
|
2068
|
+
/* @__PURE__ */ jsxs9(
|
|
2069
|
+
"button",
|
|
2070
|
+
{
|
|
2071
|
+
"data-cv-tooltip": labels.workspace.saveGraph,
|
|
2072
|
+
"aria-label": labels.workspace.saveGraph,
|
|
2073
|
+
type: "button",
|
|
2074
|
+
onClick: () => void handlePublish(),
|
|
2075
|
+
className: PRIMARY_BUTTON,
|
|
2076
|
+
disabled: !isDirty || errorCount > 0 || saveState === "saving",
|
|
2077
|
+
children: [
|
|
2078
|
+
/* @__PURE__ */ jsx10(Save2, { size: 14, "aria-hidden": "true" }),
|
|
2079
|
+
saveState === "saving" ? labels.workspace.saving : labels.workspace.saveGraph
|
|
2080
|
+
]
|
|
2081
|
+
}
|
|
2082
|
+
)
|
|
2083
|
+
] })
|
|
2084
|
+
] })
|
|
2085
|
+
] }),
|
|
2086
|
+
viewMode === "detail" && /* @__PURE__ */ jsxs9("div", { className: "flex items-center justify-between gap-2 flex-wrap", children: [
|
|
2087
|
+
/* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-2 flex-wrap", children: [
|
|
2088
|
+
graphs && Object.values(graphs).map((graph) => /* @__PURE__ */ jsx10(
|
|
2089
|
+
"button",
|
|
2090
|
+
{
|
|
2091
|
+
"data-cv-tooltip": graph.label,
|
|
2092
|
+
"aria-label": graph.label,
|
|
2093
|
+
type: "button",
|
|
2094
|
+
onClick: () => focusFlow(graph.key),
|
|
2095
|
+
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"}`,
|
|
2096
|
+
children: graph.label
|
|
2097
|
+
},
|
|
2098
|
+
graph.key
|
|
2099
|
+
)),
|
|
2100
|
+
canCreateFlow && /* @__PURE__ */ jsxs9(
|
|
2101
|
+
"button",
|
|
2102
|
+
{
|
|
2103
|
+
"data-cv-tooltip": labels.flowManager.newFlow,
|
|
2104
|
+
"aria-label": labels.flowManager.newFlow,
|
|
2105
|
+
type: "button",
|
|
2106
|
+
onClick: () => {
|
|
2107
|
+
setFlowMutationState({ pending: false });
|
|
2108
|
+
setShowCreateDialog(true);
|
|
2109
|
+
},
|
|
2110
|
+
className: "inline-flex items-center gap-1 px-3 py-1.5 rounded-full text-xs font-medium border border-dashed border-gray-300 dark:border-gray-600 text-gray-500 dark:text-gray-400 hover:border-blue-400 hover:text-blue-600",
|
|
2111
|
+
children: [
|
|
2112
|
+
/* @__PURE__ */ jsx10(Plus3, { size: 12, "aria-hidden": "true" }),
|
|
2113
|
+
" ",
|
|
2114
|
+
labels.flowManager.newFlow
|
|
2115
|
+
]
|
|
2116
|
+
}
|
|
2117
|
+
)
|
|
2118
|
+
] }),
|
|
2119
|
+
/* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-2", children: [
|
|
2120
|
+
primaryGraph && /* @__PURE__ */ jsx10(
|
|
2121
|
+
FlowPalette,
|
|
2122
|
+
{
|
|
2123
|
+
onAdd: handleAddNode,
|
|
2124
|
+
labels,
|
|
2125
|
+
...actionOptions ? { actionOptions: [...actionOptions] } : {}
|
|
2126
|
+
}
|
|
2127
|
+
),
|
|
2128
|
+
primaryGraph && canDeleteFlow && /* @__PURE__ */ jsxs9(
|
|
2129
|
+
"button",
|
|
2130
|
+
{
|
|
2131
|
+
"data-cv-tooltip": labels.flowManager.deleteFlow,
|
|
2132
|
+
"aria-label": labels.flowManager.deleteFlow,
|
|
2133
|
+
type: "button",
|
|
2134
|
+
onClick: () => {
|
|
2135
|
+
setFlowMutationState({ pending: false });
|
|
2136
|
+
setShowDeleteDialog(true);
|
|
2137
|
+
},
|
|
2138
|
+
className: "inline-flex items-center gap-1.5 rounded-lg border border-red-200 dark:border-red-900 px-3 py-1.5 text-xs font-medium text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-950/30",
|
|
2139
|
+
children: [
|
|
2140
|
+
/* @__PURE__ */ jsx10(Trash22, { size: 13, "aria-hidden": "true" }),
|
|
2141
|
+
" ",
|
|
2142
|
+
labels.flowManager.deleteFlow
|
|
2143
|
+
]
|
|
2144
|
+
}
|
|
2145
|
+
)
|
|
2146
|
+
] })
|
|
2147
|
+
] }),
|
|
2148
|
+
viewMode === "detail" && (errorCount > 0 || warningCount > 0) && /* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-4 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/60 px-3 py-2 text-xs", children: [
|
|
2149
|
+
/* @__PURE__ */ jsxs9("span", { className: "font-medium text-gray-600 dark:text-gray-300", children: [
|
|
2150
|
+
labels.validation.title,
|
|
2151
|
+
":"
|
|
2152
|
+
] }),
|
|
2153
|
+
errorCount > 0 && /* @__PURE__ */ jsxs9("span", { className: "flex items-center gap-1 text-red-600 dark:text-red-400 font-medium", children: [
|
|
2154
|
+
/* @__PURE__ */ jsx10(AlertCircle3, { size: 13, "aria-hidden": "true" }),
|
|
2155
|
+
" ",
|
|
2156
|
+
labels.validation.errors(errorCount)
|
|
2157
|
+
] }),
|
|
2158
|
+
warningCount > 0 && /* @__PURE__ */ jsxs9("span", { className: "flex items-center gap-1 text-amber-600 dark:text-amber-400", children: [
|
|
2159
|
+
/* @__PURE__ */ jsx10(AlertTriangle3, { size: 13, "aria-hidden": "true" }),
|
|
2160
|
+
" ",
|
|
2161
|
+
labels.validation.warnings(warningCount)
|
|
2162
|
+
] })
|
|
2163
|
+
] }),
|
|
2164
|
+
/* @__PURE__ */ jsxs9("div", { className: "flex-1 rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden relative", children: [
|
|
2165
|
+
loadState === "loading" && /* @__PURE__ */ jsx10("p", { className: "text-center text-gray-400 py-12", children: labels.workspace.loading }),
|
|
2166
|
+
loadState === "error" && /* @__PURE__ */ jsx10("p", { className: "text-center text-red-500 py-12", children: labels.workspace.loadError }),
|
|
2167
|
+
loadState === "ready" && graphs && viewMode === "map" && /* @__PURE__ */ jsx10(
|
|
2168
|
+
FlowMapCanvas,
|
|
2169
|
+
{
|
|
2170
|
+
graphs,
|
|
2171
|
+
rootKey: rootFlowKey,
|
|
2172
|
+
labels,
|
|
2173
|
+
onOpenFlow: (key) => {
|
|
2174
|
+
focusFlow(key);
|
|
2175
|
+
setViewMode("detail");
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
),
|
|
2179
|
+
loadState === "ready" && viewMode === "detail" && primaryGraph && /* @__PURE__ */ jsxs9(
|
|
2180
|
+
ReactFlow2,
|
|
2181
|
+
{
|
|
2182
|
+
nodes: rfNodes,
|
|
2183
|
+
edges,
|
|
2184
|
+
nodeTypes: RF_NODE_TYPES,
|
|
2185
|
+
onNodesChange,
|
|
2186
|
+
onNodeDragStop,
|
|
2187
|
+
onConnect,
|
|
2188
|
+
onInit: setFlowInstance,
|
|
2189
|
+
fitView: true,
|
|
2190
|
+
proOptions: { hideAttribution: true },
|
|
2191
|
+
colorMode: isDark ? "dark" : "light",
|
|
2192
|
+
children: [
|
|
2193
|
+
/* @__PURE__ */ jsx10(Background2, { color: isDark ? BACKGROUND_COLOR_DARK2 : BACKGROUND_COLOR_LIGHT2 }),
|
|
2194
|
+
/* @__PURE__ */ jsx10(Controls2, {}),
|
|
2195
|
+
/* @__PURE__ */ jsx10(MiniMap, { pannable: true, zoomable: true, className: "!bg-white dark:!bg-gray-800" })
|
|
2196
|
+
]
|
|
2197
|
+
}
|
|
2198
|
+
)
|
|
2199
|
+
] }),
|
|
2200
|
+
editingNode && editingGraph && /* @__PURE__ */ jsx10(
|
|
2201
|
+
FlowNodePanel,
|
|
2202
|
+
{
|
|
2203
|
+
graph: editingGraph,
|
|
2204
|
+
node: editingNode,
|
|
2205
|
+
issues: editingRef ? issuesByFlow[editingRef.flowKey] ?? [] : [],
|
|
2206
|
+
otherFlows,
|
|
2207
|
+
labels,
|
|
2208
|
+
onClose: () => setEditingRef(null),
|
|
2209
|
+
onChange: handleNodePanelChange,
|
|
2210
|
+
onDelete: handleNodeDelete,
|
|
2211
|
+
...renderMediaPicker ? { renderMediaPicker } : {}
|
|
2212
|
+
},
|
|
2213
|
+
`${editingRef?.flowKey}:${editingRef?.nodeId}`
|
|
2214
|
+
),
|
|
2215
|
+
showCreateDialog && canCreateFlow && /* @__PURE__ */ jsxs9(FlowDialog, { title: labels.flowManager.createTitle, onClose: () => setShowCreateDialog(false), children: [
|
|
2216
|
+
/* @__PURE__ */ jsxs9("div", { className: "space-y-3", children: [
|
|
2217
|
+
/* @__PURE__ */ jsxs9("div", { children: [
|
|
2218
|
+
/* @__PURE__ */ jsx10("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.flowManager.label }),
|
|
2219
|
+
/* @__PURE__ */ jsx10(
|
|
2220
|
+
"input",
|
|
2221
|
+
{
|
|
2222
|
+
value: newFlow.label,
|
|
2223
|
+
onChange: (event) => setNewFlow((prev) => ({ ...prev, label: event.target.value })),
|
|
2224
|
+
className: `mt-1 ${DIALOG_INPUT}`
|
|
2225
|
+
}
|
|
2226
|
+
)
|
|
2227
|
+
] }),
|
|
2228
|
+
/* @__PURE__ */ jsxs9("div", { children: [
|
|
2229
|
+
/* @__PURE__ */ jsx10("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.flowManager.key }),
|
|
2230
|
+
/* @__PURE__ */ jsx10(
|
|
2231
|
+
"input",
|
|
2232
|
+
{
|
|
2233
|
+
value: newFlow.key,
|
|
2234
|
+
onChange: (event) => setNewFlow((prev) => ({ ...prev, key: event.target.value.toLowerCase() })),
|
|
2235
|
+
className: `mt-1 ${DIALOG_INPUT}`
|
|
2236
|
+
}
|
|
2237
|
+
),
|
|
2238
|
+
/* @__PURE__ */ jsx10("p", { className: "text-[11px] text-gray-400 mt-1", children: newFlow.key && !keyIsValid ? labels.flowManager.keyInvalid : labels.flowManager.keyHint })
|
|
2239
|
+
] }),
|
|
2240
|
+
/* @__PURE__ */ jsxs9("label", { className: "flex items-center gap-2 text-sm text-gray-700 dark:text-gray-200", children: [
|
|
2241
|
+
/* @__PURE__ */ jsx10(
|
|
2242
|
+
"input",
|
|
2243
|
+
{
|
|
2244
|
+
type: "checkbox",
|
|
2245
|
+
checked: newFlow.showInMenu,
|
|
2246
|
+
onChange: (event) => setNewFlow((prev) => ({ ...prev, showInMenu: event.target.checked }))
|
|
2247
|
+
}
|
|
2248
|
+
),
|
|
2249
|
+
labels.flowManager.showInMenu
|
|
2250
|
+
] }),
|
|
2251
|
+
newFlow.showInMenu && /* @__PURE__ */ jsxs9("div", { children: [
|
|
2252
|
+
/* @__PURE__ */ jsx10("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.flowManager.menuOptionLabel }),
|
|
2253
|
+
/* @__PURE__ */ jsx10(
|
|
2254
|
+
"input",
|
|
2255
|
+
{
|
|
2256
|
+
value: newFlow.menuOptionLabel,
|
|
2257
|
+
onChange: (event) => setNewFlow((prev) => ({ ...prev, menuOptionLabel: event.target.value })),
|
|
2258
|
+
placeholder: newFlow.label,
|
|
2259
|
+
className: `mt-1 ${DIALOG_INPUT}`
|
|
2260
|
+
}
|
|
2261
|
+
)
|
|
2262
|
+
] }),
|
|
2263
|
+
flowMutationState.error && /* @__PURE__ */ jsx10("p", { className: "text-xs text-red-600", children: flowMutationState.error })
|
|
2264
|
+
] }),
|
|
2265
|
+
/* @__PURE__ */ jsxs9("div", { className: "flex justify-end gap-2 mt-4", children: [
|
|
2266
|
+
/* @__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 }),
|
|
2267
|
+
/* @__PURE__ */ jsx10(
|
|
2268
|
+
"button",
|
|
2269
|
+
{
|
|
2270
|
+
"data-cv-tooltip": labels.flowManager.create,
|
|
2271
|
+
"aria-label": labels.flowManager.create,
|
|
2272
|
+
type: "button",
|
|
2273
|
+
className: PRIMARY_BUTTON,
|
|
2274
|
+
onClick: () => void handleCreateFlow(),
|
|
2275
|
+
disabled: !keyIsValid || !newFlow.label || flowMutationState.pending,
|
|
2276
|
+
children: flowMutationState.pending ? labels.flowManager.creating : labels.flowManager.create
|
|
2277
|
+
}
|
|
2278
|
+
)
|
|
2279
|
+
] })
|
|
2280
|
+
] }),
|
|
2281
|
+
showDeleteDialog && canDeleteFlow && /* @__PURE__ */ jsxs9(FlowDialog, { title: labels.flowManager.deleteFlow, onClose: () => setShowDeleteDialog(false), children: [
|
|
2282
|
+
/* @__PURE__ */ jsx10("p", { className: "text-sm text-gray-600 dark:text-gray-300", children: primaryGraph ? labels.flowManager.deleteConfirm(primaryGraph.label) : "" }),
|
|
2283
|
+
flowMutationState.error && /* @__PURE__ */ jsx10("p", { className: "text-xs text-red-600 mt-2", children: flowMutationState.error }),
|
|
2284
|
+
/* @__PURE__ */ jsxs9("div", { className: "flex justify-end gap-2 mt-4", children: [
|
|
2285
|
+
/* @__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 }),
|
|
2286
|
+
/* @__PURE__ */ jsxs9(
|
|
2287
|
+
"button",
|
|
2288
|
+
{
|
|
2289
|
+
"data-cv-tooltip": labels.flowManager.deleteFlow,
|
|
2290
|
+
"aria-label": labels.flowManager.deleteFlow,
|
|
2291
|
+
type: "button",
|
|
2292
|
+
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",
|
|
2293
|
+
onClick: () => void handleDeleteFlow(),
|
|
2294
|
+
disabled: flowMutationState.pending,
|
|
2295
|
+
children: [
|
|
2296
|
+
/* @__PURE__ */ jsx10(Trash22, { size: 13, "aria-hidden": "true" }),
|
|
2297
|
+
" ",
|
|
2298
|
+
labels.flowManager.deleteFlow
|
|
2299
|
+
]
|
|
2300
|
+
}
|
|
2301
|
+
)
|
|
2302
|
+
] })
|
|
1075
2303
|
] })
|
|
1076
2304
|
] });
|
|
1077
2305
|
}
|
|
2306
|
+
function FlowDialog({
|
|
2307
|
+
title,
|
|
2308
|
+
onClose,
|
|
2309
|
+
children
|
|
2310
|
+
}) {
|
|
2311
|
+
return /* @__PURE__ */ jsxs9("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/40 px-4", children: [
|
|
2312
|
+
/* @__PURE__ */ jsx10("div", { className: "absolute inset-0", onClick: onClose, "aria-hidden": "true" }),
|
|
2313
|
+
/* @__PURE__ */ jsxs9(
|
|
2314
|
+
"div",
|
|
2315
|
+
{
|
|
2316
|
+
role: "dialog",
|
|
2317
|
+
"aria-modal": "true",
|
|
2318
|
+
"aria-label": title,
|
|
2319
|
+
className: "relative w-full max-w-md rounded-2xl bg-white dark:bg-gray-800 p-5 shadow-xl",
|
|
2320
|
+
children: [
|
|
2321
|
+
/* @__PURE__ */ jsx10("h3", { className: "text-base font-semibold text-gray-900 dark:text-gray-100 mb-3", children: title }),
|
|
2322
|
+
children
|
|
2323
|
+
]
|
|
2324
|
+
}
|
|
2325
|
+
)
|
|
2326
|
+
] });
|
|
2327
|
+
}
|
|
1078
2328
|
export {
|
|
1079
2329
|
BUILT_IN_ACTION_KINDS,
|
|
1080
2330
|
CONDITION_OPERATORS,
|
|
@@ -1089,12 +2339,21 @@ export {
|
|
|
1089
2339
|
FlowPalette,
|
|
1090
2340
|
FlowPortalNode,
|
|
1091
2341
|
FlowWhatsAppPreview,
|
|
2342
|
+
FlowsWorkspace,
|
|
2343
|
+
GROUP_HEADER_NODE_ID,
|
|
1092
2344
|
NODE_CARD_WIDTH,
|
|
1093
2345
|
WHATSAPP_LIMITS,
|
|
2346
|
+
applyConnection,
|
|
2347
|
+
buildFlowEdges,
|
|
2348
|
+
chainFrameBounds,
|
|
2349
|
+
chainFrameNodeId,
|
|
1094
2350
|
computeAutoLayout,
|
|
1095
2351
|
computeFlowMapLayout,
|
|
2352
|
+
computeMergedLayout,
|
|
2353
|
+
countLiveByNode,
|
|
1096
2354
|
crossFlowKey,
|
|
1097
2355
|
crossFlowTargetsOf,
|
|
2356
|
+
detachedNodeIds,
|
|
1098
2357
|
estimateNodeHeight,
|
|
1099
2358
|
findCollectionChains,
|
|
1100
2359
|
flowGroupFrameNodeTypes,
|
|
@@ -1103,9 +2362,17 @@ export {
|
|
|
1103
2362
|
flowNodeTypes,
|
|
1104
2363
|
flowPortalNodeTypes,
|
|
1105
2364
|
isCrossFlowTarget,
|
|
2365
|
+
isGraphDirty,
|
|
1106
2366
|
mergeFlowEditorLabels,
|
|
2367
|
+
mergedFlowKeysFrom,
|
|
2368
|
+
namespaceNodeId,
|
|
2369
|
+
newNodeFromSpec,
|
|
1107
2370
|
nodeLabel,
|
|
2371
|
+
parseNamespacedId,
|
|
2372
|
+
portalNodeId,
|
|
2373
|
+
removeNodeAndCleanRefs,
|
|
1108
2374
|
rendersAsButtons,
|
|
2375
|
+
resolveConnection,
|
|
1109
2376
|
slugifyNodeId,
|
|
1110
2377
|
targetsOf,
|
|
1111
2378
|
validateGraph
|