@rendra-ui/web 2.8.0 → 2.9.0
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/README.md +3 -3
- package/dist/base.css +1 -1
- package/dist/{button-CrSlVIsY.js → button-BcCBrlnt.js} +6 -6
- package/dist/{button-CrSlVIsY.js.map → button-BcCBrlnt.js.map} +1 -1
- package/dist/chart.js +7 -7
- package/dist/cli/auditar.js +1 -1
- package/dist/cli/codigos.js +2 -2
- package/dist/cli/executar-auditar.js +1 -1
- package/dist/cli/help.js +1 -1
- package/dist/cli/trocar.js +2 -2
- package/dist/cli/typescript-loader.js +1 -1
- package/dist/{cn-BlJHpkDs.js → cn-CpdCvlkP.js} +2 -2
- package/dist/{cn-BlJHpkDs.js.map → cn-CpdCvlkP.js.map} +1 -1
- package/dist/{components-6RpoJ2Od.js → components-Bus_Jh2R.js} +7 -4
- package/dist/components-Bus_Jh2R.js.map +1 -0
- package/dist/components.css +67 -26
- package/dist/document-viewer.js +4 -4
- package/dist/{dropdown-menu-BUGKtR6k.js → dropdown-menu-B3L1XSDf.js} +5 -5
- package/dist/{dropdown-menu-BUGKtR6k.js.map → dropdown-menu-B3L1XSDf.js.map} +1 -1
- package/dist/{brand-feedback-icon-CVux4pNh.js → empty-state-Bcx4M0Me.js} +64 -4
- package/dist/empty-state-Bcx4M0Me.js.map +1 -0
- package/dist/{features-max-BCIy1Ic-.js → features-max-CoyJ_kuK.js} +1 -1
- package/dist/flow.js +2818 -868
- package/dist/flow.js.map +1 -1
- package/dist/index.js +80 -465
- package/dist/index.js.map +1 -1
- package/dist/{motion-context-PJPj6X4g.js → motion-context-DWFl2xRr.js} +4 -4
- package/dist/{motion-context-PJPj6X4g.js.map → motion-context-DWFl2xRr.js.map} +1 -1
- package/dist/{motion-on-Bocf4Smc.js → motion-on-zwsBqYZL.js} +2 -2
- package/dist/{motion-on-Bocf4Smc.js.map → motion-on-zwsBqYZL.js.map} +1 -1
- package/dist/motion.js +2 -2
- package/dist/{rendra-provider-Do_qWjJC.js → rendra-provider-DuIhBatD.js} +3 -3
- package/dist/{rendra-provider-Do_qWjJC.js.map → rendra-provider-DuIhBatD.js.map} +1 -1
- package/dist/rich-text-editor.js +4 -4
- package/dist/router-bridge.js +2 -2
- package/dist/{timeline-xrMHeV92.js → toast-C7qYslCp.js} +464 -80
- package/dist/toast-C7qYslCp.js.map +1 -0
- package/dist/tokens.css +1 -1
- package/dist/{tooltip-to0h6aPe.js → tooltip-BSPO-bH4.js} +5 -5
- package/dist/{tooltip-to0h6aPe.js.map → tooltip-BSPO-bH4.js.map} +1 -1
- package/dist/types/components/ui/flow-config-panels.d.ts +10 -0
- package/dist/types/components/ui/flow-editor.d.ts +5 -0
- package/dist/types/components/ui/flow-sidebar.d.ts +21 -0
- package/dist/types/components/ui/flow-simulation-panel.d.ts +20 -0
- package/dist/types/components/ui/flow-viewer.d.ts +27 -10
- package/dist/types/flow.d.ts +1 -1
- package/dist/types/lib/flow-blocks.d.ts +122 -0
- package/dist/types/lib/flow-edit.d.ts +88 -0
- package/dist/types/lib/flow-layout.d.ts +3 -0
- package/dist/types/lib/flow-sim.d.ts +24 -0
- package/dist/types/lib/flow.d.ts +30 -80
- package/dist/{use-breakpoint-CM-yeDHJ.js → use-breakpoint-BGmY3ir-.js} +2 -2
- package/dist/{use-breakpoint-CM-yeDHJ.js.map → use-breakpoint-BGmY3ir-.js.map} +1 -1
- package/dist/{use-motion-presets-I4StrsCi.js → use-motion-presets-CmMLBQLI.js} +3 -3
- package/dist/{use-motion-presets-I4StrsCi.js.map → use-motion-presets-CmMLBQLI.js.map} +1 -1
- package/dist/widget-grid.js +2 -2
- package/package.json +2 -1
- package/dist/brand-feedback-icon-CVux4pNh.js.map +0 -1
- package/dist/components-6RpoJ2Od.js.map +0 -1
- package/dist/empty-state-i4e-LacG.js +0 -67
- package/dist/empty-state-i4e-LacG.js.map +0 -1
- package/dist/timeline-xrMHeV92.js.map +0 -1
package/dist/flow.js
CHANGED
|
@@ -1,51 +1,31 @@
|
|
|
1
|
-
/*! Rendra Design System v2.
|
|
2
|
-
import { t as cn } from "./cn-
|
|
3
|
-
import { i as useMotionLib } from "./motion-context-
|
|
4
|
-
import { t as useMotionPresets } from "./use-motion-presets-
|
|
5
|
-
import { B as
|
|
6
|
-
import { t as Button } from "./button-
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
1
|
+
/*! Rendra Design System v2.9.0 | MIT | © 2026 Bruno Magalhaes | brunomagalhaes.me */
|
|
2
|
+
import { t as cn } from "./cn-CpdCvlkP.js";
|
|
3
|
+
import { i as useMotionLib } from "./motion-context-DWFl2xRr.js";
|
|
4
|
+
import { t as useMotionPresets } from "./use-motion-presets-CmMLBQLI.js";
|
|
5
|
+
import { B as Alert, R as InfoHint, V as ActionBar, a as RadioGroup, b as Input, c as Select, g as Drawer, i as Textarea, l as Field, n as toast, o as useFillHeight, r as Timeline, s as Tabs, v as Checkbox, z as Modal } from "./toast-C7qYslCp.js";
|
|
6
|
+
import { t as Button } from "./button-BcCBrlnt.js";
|
|
7
|
+
import { t as EmptyState } from "./empty-state-Bcx4M0Me.js";
|
|
8
|
+
import { n as useBreakpoint } from "./use-breakpoint-BGmY3ir-.js";
|
|
9
|
+
import { CalendarClock, CheckCircle2, Cog, Copy, Flag, FlaskConical, GitBranch, Hourglass, ListChecks, Mail, Maximize2, MessageCircle, Play, Plus, Search, Send, SquarePen, Trash2, TriangleAlert, UserPlus, Webhook, X, Zap, ZoomIn, ZoomOut } from "lucide-react";
|
|
10
|
+
import { createContext, useCallback, useContext, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
10
11
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
11
|
-
import { ConnectionLineType, Handle, MarkerType, Position, ReactFlow, ReactFlowProvider, applyEdgeChanges, applyNodeChanges, useReactFlow, useStore } from "@xyflow/react";
|
|
12
|
+
import { BaseEdge, ConnectionLineType, EdgeLabelRenderer, Handle, MarkerType, Position, ReactFlow, ReactFlowProvider, applyEdgeChanges, applyNodeChanges, getBezierPath, getNodesBounds, useReactFlow, useStore, useUpdateNodeInternals } from "@xyflow/react";
|
|
12
13
|
import "@xyflow/react/dist/base.css";
|
|
13
|
-
//#region src/lib/flow.ts
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
"acao",
|
|
17
|
-
"condicao",
|
|
18
|
-
"espera",
|
|
19
|
-
"fim"
|
|
20
|
-
];
|
|
14
|
+
//#region src/lib/flow-blocks.ts
|
|
15
|
+
/** Altura mínima de uma linha de saída da condição em px (alvo de toque, `min-h-touch`). */
|
|
16
|
+
var ROW_H = 44;
|
|
21
17
|
var FLOW_WAIT_UNITS = [
|
|
22
18
|
"segundos",
|
|
23
19
|
"minutos",
|
|
24
20
|
"horas",
|
|
25
21
|
"dias"
|
|
26
22
|
];
|
|
27
|
-
/** Nome do tipo de bloco, como aparece na interface. */
|
|
28
|
-
var FLOW_TYPE_NAME = {
|
|
29
|
-
gatilho: "Gatilho",
|
|
30
|
-
acao: "Ação",
|
|
31
|
-
condicao: "Condição",
|
|
32
|
-
espera: "Espera",
|
|
33
|
-
fim: "Fim"
|
|
34
|
-
};
|
|
35
|
-
var BRANCH_NAME = {
|
|
36
|
-
sim: "Sim",
|
|
37
|
-
nao: "Não"
|
|
38
|
-
};
|
|
39
23
|
var WAIT_SINGULAR = {
|
|
40
24
|
segundos: "segundo",
|
|
41
25
|
minutos: "minuto",
|
|
42
26
|
horas: "hora",
|
|
43
27
|
dias: "dia"
|
|
44
28
|
};
|
|
45
|
-
var DEFAULT_WAIT = {
|
|
46
|
-
duration: 5,
|
|
47
|
-
unit: "minutos"
|
|
48
|
-
};
|
|
49
29
|
var isUnit = (v) => typeof v === "string" && FLOW_WAIT_UNITS.includes(v);
|
|
50
30
|
/** A duração do bloco de espera ("5 minutos"), ou `null` se o config não a define. */
|
|
51
31
|
function formatWait(config) {
|
|
@@ -59,23 +39,689 @@ function describeWait(config) {
|
|
|
59
39
|
const wait = formatWait(config);
|
|
60
40
|
return wait ? `Aguardaria ${wait}` : "Aguardaria o tempo definido no bloco";
|
|
61
41
|
}
|
|
42
|
+
var none = () => ({});
|
|
43
|
+
var FLOW_BLOCKS = {
|
|
44
|
+
"webhook-entrada": {
|
|
45
|
+
type: "webhook-entrada",
|
|
46
|
+
group: "gatilhos",
|
|
47
|
+
name: "Receber webhook",
|
|
48
|
+
description: "Inicia o fluxo quando outro sistema envia dados.",
|
|
49
|
+
hint: "dados de outro sistema, endereço e mapeamento de campos",
|
|
50
|
+
defaultConfig: () => ({
|
|
51
|
+
method: "POST",
|
|
52
|
+
path: "/hooks/novo-evento",
|
|
53
|
+
responseCode: 200,
|
|
54
|
+
received: false,
|
|
55
|
+
mapping: []
|
|
56
|
+
}),
|
|
57
|
+
required: [{
|
|
58
|
+
key: "path",
|
|
59
|
+
label: "Endereço"
|
|
60
|
+
}]
|
|
61
|
+
},
|
|
62
|
+
agendamento: {
|
|
63
|
+
type: "agendamento",
|
|
64
|
+
group: "gatilhos",
|
|
65
|
+
name: "Agendamento",
|
|
66
|
+
description: "Inicia o fluxo em dia e horário definidos.",
|
|
67
|
+
hint: "diário, semanal ou mensal, em um horário",
|
|
68
|
+
defaultConfig: () => ({
|
|
69
|
+
frequency: "diario",
|
|
70
|
+
time: "09:00"
|
|
71
|
+
}),
|
|
72
|
+
required: [{
|
|
73
|
+
key: "time",
|
|
74
|
+
label: "Horário"
|
|
75
|
+
}]
|
|
76
|
+
},
|
|
77
|
+
"registro-criado": {
|
|
78
|
+
type: "registro-criado",
|
|
79
|
+
group: "gatilhos",
|
|
80
|
+
name: "Registro criado",
|
|
81
|
+
description: "Inicia o fluxo quando um registro novo é criado.",
|
|
82
|
+
hint: "cliente, pedido ou negócio criado",
|
|
83
|
+
defaultConfig: () => ({ record: "cliente" }),
|
|
84
|
+
required: []
|
|
85
|
+
},
|
|
86
|
+
whatsapp: {
|
|
87
|
+
type: "whatsapp",
|
|
88
|
+
group: "mensagens",
|
|
89
|
+
name: "Enviar WhatsApp",
|
|
90
|
+
description: "Envia uma mensagem de WhatsApp.",
|
|
91
|
+
hint: "mensagem de texto para um telefone",
|
|
92
|
+
defaultConfig: () => ({
|
|
93
|
+
to: "",
|
|
94
|
+
message: ""
|
|
95
|
+
}),
|
|
96
|
+
required: [{
|
|
97
|
+
key: "to",
|
|
98
|
+
label: "Para"
|
|
99
|
+
}, {
|
|
100
|
+
key: "message",
|
|
101
|
+
label: "Mensagem"
|
|
102
|
+
}]
|
|
103
|
+
},
|
|
104
|
+
email: {
|
|
105
|
+
type: "email",
|
|
106
|
+
group: "mensagens",
|
|
107
|
+
name: "Enviar e-mail",
|
|
108
|
+
description: "Envia um e-mail.",
|
|
109
|
+
hint: "mensagem com assunto para um endereço",
|
|
110
|
+
defaultConfig: () => ({
|
|
111
|
+
to: "",
|
|
112
|
+
subject: "",
|
|
113
|
+
message: ""
|
|
114
|
+
}),
|
|
115
|
+
required: [
|
|
116
|
+
{
|
|
117
|
+
key: "to",
|
|
118
|
+
label: "Para"
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
key: "subject",
|
|
122
|
+
label: "Assunto"
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
key: "message",
|
|
126
|
+
label: "Mensagem"
|
|
127
|
+
}
|
|
128
|
+
]
|
|
129
|
+
},
|
|
130
|
+
condicao: {
|
|
131
|
+
type: "condicao",
|
|
132
|
+
group: "condicionais",
|
|
133
|
+
name: "Condição",
|
|
134
|
+
description: "Escolhe o caminho pelas regras.",
|
|
135
|
+
hint: "regras com campo, operador e valor, e a saída Senão",
|
|
136
|
+
defaultConfig: () => ({ rules: [{
|
|
137
|
+
id: "r1",
|
|
138
|
+
field: "",
|
|
139
|
+
operator: "igual",
|
|
140
|
+
value: ""
|
|
141
|
+
}] }),
|
|
142
|
+
required: []
|
|
143
|
+
},
|
|
144
|
+
espera: {
|
|
145
|
+
type: "espera",
|
|
146
|
+
group: "condicionais",
|
|
147
|
+
name: "Esperar",
|
|
148
|
+
description: "Aguarda um tempo antes de seguir.",
|
|
149
|
+
hint: "pausa por segundos, minutos, horas ou dias",
|
|
150
|
+
defaultConfig: () => ({
|
|
151
|
+
duration: 5,
|
|
152
|
+
unit: "minutos"
|
|
153
|
+
}),
|
|
154
|
+
required: []
|
|
155
|
+
},
|
|
156
|
+
"webhook-saida": {
|
|
157
|
+
type: "webhook-saida",
|
|
158
|
+
group: "integracoes",
|
|
159
|
+
name: "Enviar webhook",
|
|
160
|
+
description: "Envia dados para outro sistema.",
|
|
161
|
+
hint: "requisição com corpo montado das variáveis",
|
|
162
|
+
defaultConfig: () => ({
|
|
163
|
+
method: "POST",
|
|
164
|
+
url: "",
|
|
165
|
+
headers: [],
|
|
166
|
+
body: []
|
|
167
|
+
}),
|
|
168
|
+
required: [{
|
|
169
|
+
key: "url",
|
|
170
|
+
label: "Endereço"
|
|
171
|
+
}]
|
|
172
|
+
},
|
|
173
|
+
tarefa: {
|
|
174
|
+
type: "tarefa",
|
|
175
|
+
group: "acoes",
|
|
176
|
+
name: "Criar tarefa",
|
|
177
|
+
description: "Cria uma tarefa para uma pessoa.",
|
|
178
|
+
hint: "título, responsável e prazo em dias",
|
|
179
|
+
defaultConfig: () => ({
|
|
180
|
+
title: "",
|
|
181
|
+
assignee: "",
|
|
182
|
+
dueDays: 1
|
|
183
|
+
}),
|
|
184
|
+
required: [{
|
|
185
|
+
key: "title",
|
|
186
|
+
label: "Título"
|
|
187
|
+
}]
|
|
188
|
+
},
|
|
189
|
+
gatilho: {
|
|
190
|
+
type: "gatilho",
|
|
191
|
+
group: "gatilhos",
|
|
192
|
+
name: "Gatilho personalizado",
|
|
193
|
+
description: "Gatilho sem configuração.",
|
|
194
|
+
hint: "",
|
|
195
|
+
defaultConfig: none,
|
|
196
|
+
required: [],
|
|
197
|
+
hidden: true
|
|
198
|
+
},
|
|
199
|
+
acao: {
|
|
200
|
+
type: "acao",
|
|
201
|
+
group: "acoes",
|
|
202
|
+
name: "Ação personalizada",
|
|
203
|
+
description: "Ação sem configuração.",
|
|
204
|
+
hint: "",
|
|
205
|
+
defaultConfig: none,
|
|
206
|
+
required: [],
|
|
207
|
+
hidden: true
|
|
208
|
+
},
|
|
209
|
+
fim: {
|
|
210
|
+
type: "fim",
|
|
211
|
+
group: "acoes",
|
|
212
|
+
name: "Fim",
|
|
213
|
+
description: "Encerra o caminho.",
|
|
214
|
+
hint: "",
|
|
215
|
+
defaultConfig: none,
|
|
216
|
+
required: [],
|
|
217
|
+
hidden: true
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
var FLOW_BLOCK_GROUPS = [
|
|
221
|
+
{
|
|
222
|
+
id: "gatilhos",
|
|
223
|
+
name: "Gatilhos",
|
|
224
|
+
types: [
|
|
225
|
+
"webhook-entrada",
|
|
226
|
+
"agendamento",
|
|
227
|
+
"registro-criado"
|
|
228
|
+
]
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
id: "mensagens",
|
|
232
|
+
name: "Mensagens",
|
|
233
|
+
types: ["whatsapp", "email"]
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
id: "condicionais",
|
|
237
|
+
name: "Condicionais",
|
|
238
|
+
types: ["condicao", "espera"]
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
id: "integracoes",
|
|
242
|
+
name: "Integrações",
|
|
243
|
+
types: ["webhook-saida"]
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
id: "acoes",
|
|
247
|
+
name: "Ações internas",
|
|
248
|
+
types: ["tarefa"]
|
|
249
|
+
}
|
|
250
|
+
];
|
|
251
|
+
/** Nome do tipo de bloco, como aparece na interface. */
|
|
252
|
+
var FLOW_TYPE_NAME = Object.fromEntries(Object.keys(FLOW_BLOCKS).map((t) => [t, FLOW_BLOCKS[t].name]));
|
|
253
|
+
/** Gatilho é todo bloco do grupo Gatilhos (inclui o legado `gatilho`). */
|
|
254
|
+
var isTrigger = (type) => FLOW_BLOCKS[type].group === "gatilhos";
|
|
255
|
+
var FLOW_SYSTEM_FIELDS = [
|
|
256
|
+
{
|
|
257
|
+
value: "cliente.nome",
|
|
258
|
+
label: "Cliente › Nome"
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
value: "cliente.email",
|
|
262
|
+
label: "Cliente › E-mail"
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
value: "cliente.telefone",
|
|
266
|
+
label: "Cliente › Telefone"
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
value: "cliente.plano",
|
|
270
|
+
label: "Cliente › Plano"
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
value: "negocio.valor",
|
|
274
|
+
label: "Negócio › Valor"
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
value: "pedido.id",
|
|
278
|
+
label: "Pedido › Número"
|
|
279
|
+
}
|
|
280
|
+
];
|
|
281
|
+
/** O JSON de exemplo que "chega" no Receber webhook (simulado). */
|
|
282
|
+
var FLOW_SAMPLE_PAYLOAD = {
|
|
283
|
+
cliente: {
|
|
284
|
+
nome: "Ana Souza",
|
|
285
|
+
email: "ana@exemplo.com",
|
|
286
|
+
telefone: "+55 11 98888-0000",
|
|
287
|
+
plano: "pro"
|
|
288
|
+
},
|
|
289
|
+
pedido: {
|
|
290
|
+
id: "PED-1042",
|
|
291
|
+
valor: 1250,
|
|
292
|
+
criado_em: "2026-10-24"
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
/** Achata o JSON em caminhos (`cliente.nome`) e valores em texto, na ordem de leitura. */
|
|
296
|
+
function flattenPayload(payload, prefix = "") {
|
|
297
|
+
if (payload && typeof payload === "object" && !Array.isArray(payload)) return Object.entries(payload).flatMap(([k, v]) => flattenPayload(v, prefix ? `${prefix}.${k}` : k));
|
|
298
|
+
return [{
|
|
299
|
+
path: prefix,
|
|
300
|
+
value: String(payload)
|
|
301
|
+
}];
|
|
302
|
+
}
|
|
303
|
+
var SAMPLE_VALUES = {
|
|
304
|
+
...Object.fromEntries(flattenPayload(FLOW_SAMPLE_PAYLOAD).map((e) => [e.path, e.value])),
|
|
305
|
+
"negocio.titulo": "Proposta Plano Pro",
|
|
306
|
+
"negocio.valor": "1250",
|
|
307
|
+
"agendamento.data": "24/10/2026"
|
|
308
|
+
};
|
|
309
|
+
var RECORD_VARIABLES = {
|
|
310
|
+
cliente: [
|
|
311
|
+
"cliente.nome",
|
|
312
|
+
"cliente.email",
|
|
313
|
+
"cliente.telefone"
|
|
314
|
+
],
|
|
315
|
+
pedido: [
|
|
316
|
+
"pedido.id",
|
|
317
|
+
"pedido.valor",
|
|
318
|
+
"cliente.nome"
|
|
319
|
+
],
|
|
320
|
+
negocio: [
|
|
321
|
+
"negocio.titulo",
|
|
322
|
+
"negocio.valor",
|
|
323
|
+
"cliente.nome"
|
|
324
|
+
]
|
|
325
|
+
};
|
|
326
|
+
var VARIABLE_LABEL = {
|
|
327
|
+
...Object.fromEntries(FLOW_SYSTEM_FIELDS.map((f) => [f.value, f.label])),
|
|
328
|
+
"pedido.valor": "Pedido › Valor",
|
|
329
|
+
"negocio.titulo": "Negócio › Título",
|
|
330
|
+
"agendamento.data": "Agendamento › Data"
|
|
331
|
+
};
|
|
332
|
+
var asList = (v) => Array.isArray(v) ? v.filter((x) => !!x && typeof x === "object") : [];
|
|
333
|
+
/** Variáveis que os blocos podem usar: as do gatilho do fluxo (sem escopo por caminho). */
|
|
334
|
+
function flowVariables(flow) {
|
|
335
|
+
const trigger = flow.nodes.find((n) => isTrigger(n.type));
|
|
336
|
+
if (!trigger) return [];
|
|
337
|
+
const cfg = trigger.data.config;
|
|
338
|
+
if (trigger.type === "webhook-entrada") return asList(cfg?.mapping).filter((m) => typeof m.from === "string" && typeof m.to === "string" && m.to !== "").map((m) => ({
|
|
339
|
+
id: m.from,
|
|
340
|
+
label: VARIABLE_LABEL[m.to] ?? m.to
|
|
341
|
+
}));
|
|
342
|
+
if (trigger.type === "registro-criado") return (RECORD_VARIABLES[String(cfg?.record)] ?? RECORD_VARIABLES.cliente).map((id) => ({
|
|
343
|
+
id,
|
|
344
|
+
label: VARIABLE_LABEL[id] ?? id
|
|
345
|
+
}));
|
|
346
|
+
if (trigger.type === "agendamento") return [{
|
|
347
|
+
id: "agendamento.data",
|
|
348
|
+
label: VARIABLE_LABEL["agendamento.data"]
|
|
349
|
+
}];
|
|
350
|
+
return [];
|
|
351
|
+
}
|
|
352
|
+
/** Valores de exemplo de cada variável do fluxo (usados no teste e na simulação). */
|
|
353
|
+
function flowSampleData(flow) {
|
|
354
|
+
return Object.fromEntries(flowVariables(flow).map((v) => [v.id, SAMPLE_VALUES[v.id] ?? ""]));
|
|
355
|
+
}
|
|
356
|
+
var VARIABLE = /\{\{\s*([\w.]+)\s*\}\}/g;
|
|
357
|
+
/** Parte o texto em trechos e variáveis `{{x}}` (as variáveis viram chips no card). */
|
|
358
|
+
function splitVariables(text) {
|
|
359
|
+
const parts = [];
|
|
360
|
+
let last = 0;
|
|
361
|
+
for (const m of text.matchAll(VARIABLE)) {
|
|
362
|
+
if (m.index > last) parts.push({
|
|
363
|
+
kind: "text",
|
|
364
|
+
value: text.slice(last, m.index)
|
|
365
|
+
});
|
|
366
|
+
parts.push({
|
|
367
|
+
kind: "var",
|
|
368
|
+
value: m[1]
|
|
369
|
+
});
|
|
370
|
+
last = m.index + m[0].length;
|
|
371
|
+
}
|
|
372
|
+
if (last < text.length) parts.push({
|
|
373
|
+
kind: "text",
|
|
374
|
+
value: text.slice(last)
|
|
375
|
+
});
|
|
376
|
+
return parts;
|
|
377
|
+
}
|
|
378
|
+
/** Troca cada `{{x}}` pelo valor de exemplo; variável sem valor fica como está. */
|
|
379
|
+
function renderTemplate(text, data) {
|
|
380
|
+
return text.replace(VARIABLE, (whole, id) => data[id] ?? whole);
|
|
381
|
+
}
|
|
382
|
+
var FLOW_RULE_OPERATORS = [
|
|
383
|
+
{
|
|
384
|
+
value: "igual",
|
|
385
|
+
label: "é igual a",
|
|
386
|
+
symbol: "="
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
value: "diferente",
|
|
390
|
+
label: "é diferente de",
|
|
391
|
+
symbol: "≠"
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
value: "maior",
|
|
395
|
+
label: "é maior que",
|
|
396
|
+
symbol: ">"
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
value: "menor",
|
|
400
|
+
label: "é menor que",
|
|
401
|
+
symbol: "<"
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
value: "contem",
|
|
405
|
+
label: "contém",
|
|
406
|
+
symbol: "contém"
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
value: "vazio",
|
|
410
|
+
label: "está vazio",
|
|
411
|
+
symbol: "vazio"
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
value: "nao-vazio",
|
|
415
|
+
label: "não está vazio",
|
|
416
|
+
symbol: "preenchido"
|
|
417
|
+
}
|
|
418
|
+
];
|
|
419
|
+
/** Operadores que dispensam o valor. */
|
|
420
|
+
var operatorHasValue = (op) => op !== "vazio" && op !== "nao-vazio";
|
|
421
|
+
var operatorOf = (op) => FLOW_RULE_OPERATORS.find((o) => o.value === op);
|
|
422
|
+
var isRuleComplete = (rule) => rule.field.trim() !== "" && !!operatorOf(rule.operator) && (!operatorHasValue(rule.operator) || rule.value.trim() !== "");
|
|
423
|
+
/** "pedido.valor é maior que 500". */
|
|
424
|
+
function ruleSummary(rule) {
|
|
425
|
+
if (!isRuleComplete(rule)) return "Regra incompleta";
|
|
426
|
+
const op = operatorOf(rule.operator).label;
|
|
427
|
+
return operatorHasValue(rule.operator) ? `${rule.field} ${op} ${rule.value}` : `${rule.field} ${op}`;
|
|
428
|
+
}
|
|
429
|
+
/** As regras do bloco condição (lista vazia se o config não as traz). */
|
|
430
|
+
function rulesOf(node) {
|
|
431
|
+
return asList(node.data.config?.rules).map((r) => ({
|
|
432
|
+
id: String(r.id ?? ""),
|
|
433
|
+
...typeof r.label === "string" ? { label: r.label } : {},
|
|
434
|
+
field: String(r.field ?? ""),
|
|
435
|
+
operator: operatorOf(r.operator) ? r.operator : "igual",
|
|
436
|
+
value: String(r.value ?? "")
|
|
437
|
+
}));
|
|
438
|
+
}
|
|
439
|
+
/** Nome da saída: o rótulo da regra (ou "Regra N") e "Senão". */
|
|
440
|
+
function branchName(node, branch) {
|
|
441
|
+
if (branch === "senao") return "Senão";
|
|
442
|
+
const rules = rulesOf(node);
|
|
443
|
+
const i = rules.findIndex((r) => r.id === branch);
|
|
444
|
+
return i < 0 ? branch : rules[i].label || `Regra ${i + 1}`;
|
|
445
|
+
}
|
|
446
|
+
/** Menor `rN` livre para uma regra nova. */
|
|
447
|
+
function nextRuleId(rules) {
|
|
448
|
+
const taken = new Set(rules.map((r) => r.id));
|
|
449
|
+
let n = 1;
|
|
450
|
+
while (taken.has(`r${n}`)) n++;
|
|
451
|
+
return `r${n}`;
|
|
452
|
+
}
|
|
453
|
+
/** Texto curto na ligação que sai de uma condição ("> 500", "= pro", "Senão"). */
|
|
454
|
+
function edgeLabel(flow, edge) {
|
|
455
|
+
const from = flow.nodes.find((n) => n.id === edge.source);
|
|
456
|
+
if (!from || from.type !== "condicao" || !edge.branch) return null;
|
|
457
|
+
if (edge.branch === "senao") return "Senão";
|
|
458
|
+
const rule = rulesOf(from).find((r) => r.id === edge.branch);
|
|
459
|
+
if (!rule) return null;
|
|
460
|
+
if (!isRuleComplete(rule)) return branchName(from, edge.branch);
|
|
461
|
+
const symbol = operatorOf(rule.operator).symbol;
|
|
462
|
+
return operatorHasValue(rule.operator) ? `${symbol} ${rule.value}` : symbol;
|
|
463
|
+
}
|
|
464
|
+
/** Saídas de um bloco: as regras e "Senão" na condição; uma só nos demais. */
|
|
465
|
+
function outputIds(node) {
|
|
466
|
+
return node.type === "condicao" ? [...rulesOf(node).map((r) => r.id), "senao"] : [void 0];
|
|
467
|
+
}
|
|
468
|
+
var FREQUENCY$1 = {
|
|
469
|
+
diario: "Diário",
|
|
470
|
+
semanal: "Semanal",
|
|
471
|
+
mensal: "Mensal"
|
|
472
|
+
};
|
|
473
|
+
var RECORD_LABEL = {
|
|
474
|
+
cliente: "Cliente",
|
|
475
|
+
pedido: "Pedido",
|
|
476
|
+
negocio: "Negócio"
|
|
477
|
+
};
|
|
478
|
+
/** O texto, ou vazio quando o valor não é texto. */
|
|
479
|
+
var str = (v) => typeof v === "string" ? v : "";
|
|
480
|
+
/** Linhas "rótulo e valor" do resumo no card (variáveis `{{x}}` viram chips). */
|
|
481
|
+
function summarizeConfig(node) {
|
|
482
|
+
const c = node.data.config;
|
|
483
|
+
switch (node.type) {
|
|
484
|
+
case "webhook-entrada": {
|
|
485
|
+
const mapped = asList(c?.mapping).filter((m) => str(m.to) !== "").map((m) => `{{${str(m.from)}}}`).join(" ");
|
|
486
|
+
return [{
|
|
487
|
+
label: "Método e endereço",
|
|
488
|
+
value: `${str(c?.method) || "POST"} ${str(c?.path)}`.trim()
|
|
489
|
+
}, {
|
|
490
|
+
label: "Campos mapeados",
|
|
491
|
+
value: mapped
|
|
492
|
+
}];
|
|
493
|
+
}
|
|
494
|
+
case "agendamento": return [{
|
|
495
|
+
label: "Frequência",
|
|
496
|
+
value: FREQUENCY$1[str(c?.frequency)] ?? ""
|
|
497
|
+
}, {
|
|
498
|
+
label: "Horário",
|
|
499
|
+
value: str(c?.time)
|
|
500
|
+
}];
|
|
501
|
+
case "registro-criado": return [{
|
|
502
|
+
label: "Registro",
|
|
503
|
+
value: RECORD_LABEL[str(c?.record)] ?? ""
|
|
504
|
+
}];
|
|
505
|
+
case "whatsapp": return [{
|
|
506
|
+
label: "Para",
|
|
507
|
+
value: str(c?.to)
|
|
508
|
+
}, {
|
|
509
|
+
label: "Mensagem",
|
|
510
|
+
value: str(c?.message)
|
|
511
|
+
}];
|
|
512
|
+
case "email": return [
|
|
513
|
+
{
|
|
514
|
+
label: "Para",
|
|
515
|
+
value: str(c?.to)
|
|
516
|
+
},
|
|
517
|
+
{
|
|
518
|
+
label: "Assunto",
|
|
519
|
+
value: str(c?.subject)
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
label: "Mensagem",
|
|
523
|
+
value: str(c?.message)
|
|
524
|
+
}
|
|
525
|
+
];
|
|
526
|
+
case "espera": return [{
|
|
527
|
+
label: "Tempo",
|
|
528
|
+
value: formatWait(c) ?? ""
|
|
529
|
+
}];
|
|
530
|
+
case "webhook-saida": return [{
|
|
531
|
+
label: "Método e endereço",
|
|
532
|
+
value: `${str(c?.method) || "POST"} ${str(c?.url)}`.trim()
|
|
533
|
+
}, {
|
|
534
|
+
label: "Corpo",
|
|
535
|
+
value: asList(c?.body).map((b) => str(b.value)).filter(Boolean).join(" ")
|
|
536
|
+
}];
|
|
537
|
+
case "tarefa": {
|
|
538
|
+
const days = typeof c?.dueDays === "number" ? c.dueDays : 0;
|
|
539
|
+
return [
|
|
540
|
+
{
|
|
541
|
+
label: "Título",
|
|
542
|
+
value: str(c?.title)
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
label: "Responsável",
|
|
546
|
+
value: str(c?.assignee)
|
|
547
|
+
},
|
|
548
|
+
{
|
|
549
|
+
label: "Prazo",
|
|
550
|
+
value: `${days} ${days === 1 ? "dia" : "dias"}`
|
|
551
|
+
}
|
|
552
|
+
];
|
|
553
|
+
}
|
|
554
|
+
default: return [];
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
/** Requisição simulada do Enviar webhook, com os valores de exemplo no corpo. */
|
|
558
|
+
function buildWebhookRequest(config, data) {
|
|
559
|
+
const body = {};
|
|
560
|
+
for (const b of asList(config?.body)) if (str(b.key) !== "") body[str(b.key)] = renderTemplate(str(b.value), data);
|
|
561
|
+
return {
|
|
562
|
+
method: str(config?.method) || "POST",
|
|
563
|
+
url: str(config?.url),
|
|
564
|
+
body
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
/** Linhas que o valor do resumo ocupa no card (chips largos quebram em mais de uma). */
|
|
568
|
+
function valueLines(value) {
|
|
569
|
+
let width = 0;
|
|
570
|
+
for (const part of splitVariables(value)) width += part.kind === "var" ? (part.value.length + 4) * 6.6 + 20 : part.value.length * 6.4;
|
|
571
|
+
return Math.max(1, Math.ceil(width / 290));
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Altura estimada do card em px (medida no navegador e arredondada para cima), usada enquanto o
|
|
575
|
+
* React Flow ainda não mediu o bloco: na arrumação automática e ao procurar espaço livre.
|
|
576
|
+
*/
|
|
577
|
+
function estimateNodeHeight(node) {
|
|
578
|
+
if (node.type === "condicao") return 88 + ROW_H * (rulesOf(node).length + 1);
|
|
579
|
+
const rows = summarizeConfig(node);
|
|
580
|
+
let h = 86;
|
|
581
|
+
if (rows.length > 0) h += 25 + rows.reduce((sum, r) => sum + 20 + 24 * valueLines(r.value), 0) + 8 * (rows.length - 1);
|
|
582
|
+
if (node.type === "webhook-entrada" && node.data.config?.received === true) h += 44;
|
|
583
|
+
return h;
|
|
584
|
+
}
|
|
585
|
+
//#endregion
|
|
586
|
+
//#region src/lib/flow-layout.ts
|
|
587
|
+
/** Camada de cada bloco (maior distância do início sem passar pelas ligações que fecham ciclo). */
|
|
588
|
+
function layoutFlow(flow) {
|
|
589
|
+
const byId = new Map(flow.nodes.map((n) => [n.id, n]));
|
|
590
|
+
const edges = flow.edges.filter((e) => byId.has(e.source) && byId.has(e.target));
|
|
591
|
+
const out = /* @__PURE__ */ new Map();
|
|
592
|
+
for (const n of flow.nodes) {
|
|
593
|
+
const mine = edges.filter((e) => e.source === n.id);
|
|
594
|
+
const order = outputIds(n);
|
|
595
|
+
mine.sort((a, b) => order.indexOf(a.branch) - order.indexOf(b.branch));
|
|
596
|
+
out.set(n.id, mine);
|
|
597
|
+
}
|
|
598
|
+
const hasIncoming = new Set(edges.map((e) => e.target));
|
|
599
|
+
let roots = flow.nodes.filter((n) => isTrigger(n.type)).map((n) => n.id);
|
|
600
|
+
if (roots.length === 0) roots = flow.nodes.filter((n) => !hasIncoming.has(n.id)).map((n) => n.id);
|
|
601
|
+
if (roots.length === 0 && flow.nodes[0]) roots = [flow.nodes[0].id];
|
|
602
|
+
const state = /* @__PURE__ */ new Map();
|
|
603
|
+
const preorder = /* @__PURE__ */ new Map();
|
|
604
|
+
const post = [];
|
|
605
|
+
const back = /* @__PURE__ */ new Set();
|
|
606
|
+
const visit = (id) => {
|
|
607
|
+
state.set(id, 1);
|
|
608
|
+
preorder.set(id, preorder.size);
|
|
609
|
+
for (const e of out.get(id) ?? []) if (state.get(e.target) === 1) back.add(e.id);
|
|
610
|
+
else if (!state.has(e.target)) visit(e.target);
|
|
611
|
+
state.set(id, 2);
|
|
612
|
+
post.push(id);
|
|
613
|
+
};
|
|
614
|
+
for (const r of roots) if (!state.has(r)) visit(r);
|
|
615
|
+
const layer = new Map(roots.map((r) => [r, 0]));
|
|
616
|
+
for (const id of [...post].reverse()) for (const e of out.get(id) ?? []) if (!back.has(e.id)) layer.set(e.target, Math.max(layer.get(e.target) ?? 0, layer.get(id) + 1));
|
|
617
|
+
const unreachedLayer = Math.max(-1, ...layer.values()) + 1;
|
|
618
|
+
const columns = /* @__PURE__ */ new Map();
|
|
619
|
+
const ordered = [...flow.nodes].sort((a, b) => {
|
|
620
|
+
const pa = preorder.get(a.id) ?? Infinity;
|
|
621
|
+
const pb = preorder.get(b.id) ?? Infinity;
|
|
622
|
+
return pa === pb ? flow.nodes.indexOf(a) - flow.nodes.indexOf(b) : pa - pb;
|
|
623
|
+
});
|
|
624
|
+
for (const n of ordered) {
|
|
625
|
+
const l = layer.get(n.id) ?? unreachedLayer;
|
|
626
|
+
columns.set(l, [...columns.get(l) ?? [], n.id]);
|
|
627
|
+
}
|
|
628
|
+
const position = /* @__PURE__ */ new Map();
|
|
629
|
+
for (const [l, ids] of columns) {
|
|
630
|
+
let y = 0;
|
|
631
|
+
for (const id of ids) {
|
|
632
|
+
position.set(id, {
|
|
633
|
+
x: l * 432,
|
|
634
|
+
y
|
|
635
|
+
});
|
|
636
|
+
y += estimateNodeHeight(byId.get(id)) + 32;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
return {
|
|
640
|
+
...flow,
|
|
641
|
+
nodes: flow.nodes.map((n) => ({
|
|
642
|
+
...n,
|
|
643
|
+
position: position.get(n.id)
|
|
644
|
+
}))
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
//#endregion
|
|
648
|
+
//#region src/lib/flow.ts
|
|
649
|
+
var V2_TYPES = [
|
|
650
|
+
"webhook-entrada",
|
|
651
|
+
"agendamento",
|
|
652
|
+
"registro-criado",
|
|
653
|
+
"whatsapp",
|
|
654
|
+
"email",
|
|
655
|
+
"condicao",
|
|
656
|
+
"espera",
|
|
657
|
+
"webhook-saida",
|
|
658
|
+
"tarefa",
|
|
659
|
+
"gatilho",
|
|
660
|
+
"acao"
|
|
661
|
+
];
|
|
662
|
+
var LEGACY_TYPES = [
|
|
663
|
+
"gatilho",
|
|
664
|
+
"acao",
|
|
665
|
+
"condicao",
|
|
666
|
+
"espera",
|
|
667
|
+
"fim"
|
|
668
|
+
];
|
|
62
669
|
/** Nome do bloco para mensagens e rótulos: o rótulo, ou o tipo quando ele está vazio. */
|
|
63
670
|
var nodeName = (n) => n.data.label.trim() || FLOW_TYPE_NAME[n.type];
|
|
671
|
+
function renameEdge(e) {
|
|
672
|
+
return {
|
|
673
|
+
id: `${e.source}:senao->${e.target}`,
|
|
674
|
+
source: e.source,
|
|
675
|
+
target: e.target,
|
|
676
|
+
branch: "senao"
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Leva um fluxo da 2.7.0 (sem `version`) ao formato v2: tira os blocos Fim, a condição vira uma
|
|
681
|
+
* regra "Sim" mais "Senão", e os blocos são arrumados da esquerda para a direita. Quem já é v2
|
|
682
|
+
* volta igual.
|
|
683
|
+
*/
|
|
684
|
+
function migrateFlow(flow) {
|
|
685
|
+
if (flow.version === 2) return flow;
|
|
686
|
+
const fim = new Set(flow.nodes.filter((n) => n.type === "fim").map((n) => n.id));
|
|
687
|
+
return layoutFlow({
|
|
688
|
+
version: 2,
|
|
689
|
+
nodes: flow.nodes.filter((n) => !fim.has(n.id)).map((n) => n.type === "condicao" && !Array.isArray(n.data.config?.rules) ? {
|
|
690
|
+
...n,
|
|
691
|
+
data: {
|
|
692
|
+
...n.data,
|
|
693
|
+
config: {
|
|
694
|
+
...n.data.config,
|
|
695
|
+
rules: [{
|
|
696
|
+
id: "sim",
|
|
697
|
+
label: "Sim",
|
|
698
|
+
field: "",
|
|
699
|
+
operator: "igual",
|
|
700
|
+
value: ""
|
|
701
|
+
}]
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
} : n),
|
|
705
|
+
edges: flow.edges.filter((e) => !fim.has(e.source) && !fim.has(e.target)).map((e) => e.branch === "nao" ? renameEdge(e) : e)
|
|
706
|
+
});
|
|
707
|
+
}
|
|
64
708
|
/** Ligações que saem de cada bloco, na ordem do fluxo. */
|
|
65
709
|
function outgoing(flow) {
|
|
66
710
|
const map = /* @__PURE__ */ new Map();
|
|
67
711
|
for (const e of flow.edges) map.set(e.source, [...map.get(e.source) ?? [], e]);
|
|
68
712
|
return map;
|
|
69
713
|
}
|
|
714
|
+
var filled = (v) => typeof v === "string" && v.trim() !== "";
|
|
70
715
|
/** Problemas do fluxo, em ordem estável: a mesma entrada dá sempre a mesma lista. */
|
|
71
|
-
function validateFlow(
|
|
716
|
+
function validateFlow(input) {
|
|
717
|
+
const flow = migrateFlow(input);
|
|
72
718
|
const issues = [];
|
|
73
719
|
const byId = new Map(flow.nodes.map((n) => [n.id, n]));
|
|
74
720
|
const out = outgoing(flow);
|
|
75
|
-
const triggers = flow.nodes.filter((n) => n.type
|
|
721
|
+
const triggers = flow.nodes.filter((n) => isTrigger(n.type));
|
|
76
722
|
if (triggers.length === 0) issues.push({
|
|
77
723
|
code: "sem-gatilho",
|
|
78
|
-
message: "O fluxo precisa de um
|
|
724
|
+
message: "O fluxo precisa de um gatilho para começar."
|
|
79
725
|
});
|
|
80
726
|
else if (triggers.length > 1) issues.push({
|
|
81
727
|
code: "varios-gatilhos",
|
|
@@ -93,10 +739,10 @@ function validateFlow(flow) {
|
|
|
93
739
|
});
|
|
94
740
|
continue;
|
|
95
741
|
}
|
|
96
|
-
if (from.type === "condicao" ?
|
|
742
|
+
if (!(from.type === "condicao" ? !!e.branch && (e.branch === "senao" || rulesOf(from).some((r) => r.id === e.branch)) : !e.branch)) {
|
|
97
743
|
issues.push({
|
|
98
744
|
code: "ramo-invalido",
|
|
99
|
-
message: from.type === "condicao" ? `Uma saída da condição "${nodeName(from)}" não
|
|
745
|
+
message: from.type === "condicao" ? `Uma saída da condição "${nodeName(from)}" não corresponde a nenhuma regra nem ao "Senão".` : `A ligação que sai de "${nodeName(from)}" não pode ter saída de regra: isso só vale para condição.`,
|
|
100
746
|
nodeId: from.id,
|
|
101
747
|
edgeId: e.id
|
|
102
748
|
});
|
|
@@ -107,40 +753,48 @@ function validateFlow(flow) {
|
|
|
107
753
|
for (const n of flow.nodes) {
|
|
108
754
|
const edges = out.get(n.id) ?? [];
|
|
109
755
|
const name = nodeName(n);
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
} else if (n.type === "condicao") {
|
|
117
|
-
const sim = edges.filter((e) => e.branch === "sim").length;
|
|
118
|
-
const nao = edges.filter((e) => e.branch === "nao").length;
|
|
119
|
-
if (sim === 0) issues.push({
|
|
120
|
-
code: "condicao-sem-sim",
|
|
121
|
-
message: `A condição "${name}" precisa de uma saída "Sim".`,
|
|
756
|
+
const cfg = n.data.config;
|
|
757
|
+
if (n.type === "condicao") {
|
|
758
|
+
const rules = rulesOf(n);
|
|
759
|
+
if (rules.length === 0) issues.push({
|
|
760
|
+
code: "condicao-sem-regra",
|
|
761
|
+
message: `A condição "${name}" precisa de ao menos uma regra.`,
|
|
122
762
|
nodeId: n.id
|
|
123
763
|
});
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
764
|
+
rules.forEach((r, i) => {
|
|
765
|
+
if (!isRuleComplete(r)) issues.push({
|
|
766
|
+
code: "regra-incompleta",
|
|
767
|
+
message: `A regra ${i + 1} da condição "${name}" precisa de campo, operador e valor.`,
|
|
768
|
+
nodeId: n.id
|
|
769
|
+
});
|
|
128
770
|
});
|
|
129
|
-
|
|
771
|
+
const seen = /* @__PURE__ */ new Set();
|
|
772
|
+
let repeated = false;
|
|
773
|
+
for (const e of edges) {
|
|
774
|
+
if (!e.branch) continue;
|
|
775
|
+
if (seen.has(e.branch)) repeated = true;
|
|
776
|
+
seen.add(e.branch);
|
|
777
|
+
}
|
|
778
|
+
if (repeated) issues.push({
|
|
130
779
|
code: "condicao-saida-repetida",
|
|
131
|
-
message: `A condição "${name}" tem mais de uma
|
|
780
|
+
message: `A condição "${name}" tem mais de uma ligação na mesma saída.`,
|
|
132
781
|
nodeId: n.id
|
|
133
782
|
});
|
|
134
|
-
} else if (edges.length
|
|
135
|
-
code: "sem-saida",
|
|
136
|
-
message: `O bloco "${name}" não leva a nenhum próximo bloco.`,
|
|
137
|
-
nodeId: n.id
|
|
138
|
-
});
|
|
139
|
-
else if (edges.length > 1) issues.push({
|
|
783
|
+
} else if (edges.length > 1) issues.push({
|
|
140
784
|
code: "varias-saidas",
|
|
141
785
|
message: `O bloco "${name}" tem mais de uma saída; só a condição pode ter.`,
|
|
142
786
|
nodeId: n.id
|
|
143
787
|
});
|
|
788
|
+
for (const f of FLOW_BLOCKS[n.type].required) if (!filled(cfg?.[f.key])) issues.push({
|
|
789
|
+
code: "config-obrigatoria-vazia",
|
|
790
|
+
message: `O bloco "${name}" precisa do campo "${f.label}".`,
|
|
791
|
+
nodeId: n.id
|
|
792
|
+
});
|
|
793
|
+
if (n.type === "espera" && !(typeof cfg?.duration === "number" && cfg.duration > 0)) issues.push({
|
|
794
|
+
code: "config-obrigatoria-vazia",
|
|
795
|
+
message: `O bloco "${name}" precisa de uma duração maior que zero.`,
|
|
796
|
+
nodeId: n.id
|
|
797
|
+
});
|
|
144
798
|
}
|
|
145
799
|
const next = /* @__PURE__ */ new Map();
|
|
146
800
|
for (const e of sound) next.set(e.source, [...next.get(e.source) ?? [], e.target]);
|
|
@@ -183,8 +837,12 @@ var sortKeys = (v) => {
|
|
|
183
837
|
if (v && typeof v === "object") return Object.fromEntries(Object.keys(v).sort().map((k) => [k, sortKeys(v[k])]));
|
|
184
838
|
return v;
|
|
185
839
|
};
|
|
186
|
-
/**
|
|
187
|
-
|
|
840
|
+
/**
|
|
841
|
+
* JSON do fluxo (sempre no formato v2, com `version` na primeira chave) e a ordem das chaves
|
|
842
|
+
* fixa: o mesmo fluxo dá sempre o mesmo texto. Um fluxo da 2.7.0 é convertido antes.
|
|
843
|
+
*/
|
|
844
|
+
function serializeFlow(input) {
|
|
845
|
+
const flow = migrateFlow(input);
|
|
188
846
|
const nodes = flow.nodes.map((n) => ({
|
|
189
847
|
id: n.id,
|
|
190
848
|
type: n.type,
|
|
@@ -208,12 +866,16 @@ function serializeFlow(flow) {
|
|
|
208
866
|
target: e.target
|
|
209
867
|
});
|
|
210
868
|
return JSON.stringify({
|
|
869
|
+
version: 2,
|
|
211
870
|
nodes,
|
|
212
871
|
edges
|
|
213
872
|
}, null, 2);
|
|
214
873
|
}
|
|
215
874
|
var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
216
|
-
/**
|
|
875
|
+
/**
|
|
876
|
+
* Lê o JSON de um fluxo. Nunca lança; campos que o contrato não conhece são ignorados. Texto sem
|
|
877
|
+
* `version` é lido pelas regras da 2.7.0 e convertido; `version: 2` segue as regras do formato novo.
|
|
878
|
+
*/
|
|
217
879
|
function parseFlow(text) {
|
|
218
880
|
let raw;
|
|
219
881
|
try {
|
|
@@ -228,10 +890,16 @@ function parseFlow(text) {
|
|
|
228
890
|
ok: false,
|
|
229
891
|
errors: ["O JSON do fluxo precisa ser um objeto."]
|
|
230
892
|
};
|
|
893
|
+
if (raw.version !== void 0 && raw.version !== 2) return {
|
|
894
|
+
ok: false,
|
|
895
|
+
errors: ["Versão do formato não suportada (use 2 ou deixe sem versão)."]
|
|
896
|
+
};
|
|
231
897
|
if (!Array.isArray(raw.nodes) || !Array.isArray(raw.edges)) return {
|
|
232
898
|
ok: false,
|
|
233
899
|
errors: [`O campo "${!Array.isArray(raw.nodes) ? "nodes" : "edges"}" precisa ser uma lista.`]
|
|
234
900
|
};
|
|
901
|
+
const v2 = raw.version === 2;
|
|
902
|
+
const types = v2 ? V2_TYPES : LEGACY_TYPES;
|
|
235
903
|
const errors = [];
|
|
236
904
|
const nodes = [];
|
|
237
905
|
const ids = /* @__PURE__ */ new Set();
|
|
@@ -247,7 +915,7 @@ function parseFlow(text) {
|
|
|
247
915
|
errors.push(`${at}: o id "${id}" está repetido.`);
|
|
248
916
|
ok = false;
|
|
249
917
|
} else ids.add(id);
|
|
250
|
-
if (!
|
|
918
|
+
if (!types.includes(type)) {
|
|
251
919
|
errors.push(`${at}: tipo desconhecido (${String(type)}).`);
|
|
252
920
|
ok = false;
|
|
253
921
|
}
|
|
@@ -298,9 +966,11 @@ function parseFlow(text) {
|
|
|
298
966
|
errors.push(`${at}: o id "${id}" está repetido.`);
|
|
299
967
|
ok = false;
|
|
300
968
|
}
|
|
301
|
-
if (branch !== void 0
|
|
302
|
-
|
|
303
|
-
|
|
969
|
+
if (branch !== void 0) {
|
|
970
|
+
if (!(v2 ? typeof branch === "string" && branch !== "" : branch === "sim" || branch === "nao")) {
|
|
971
|
+
errors.push(v2 ? `${at}: "branch" precisa ser o id de uma regra ou "senao".` : `${at}: "branch" só pode ser "sim" ou "nao".`);
|
|
972
|
+
ok = false;
|
|
973
|
+
}
|
|
304
974
|
}
|
|
305
975
|
if (ok) {
|
|
306
976
|
edgeIds.add(id);
|
|
@@ -316,310 +986,155 @@ function parseFlow(text) {
|
|
|
316
986
|
});
|
|
317
987
|
}
|
|
318
988
|
});
|
|
319
|
-
|
|
989
|
+
if (errors.length > 0) return {
|
|
320
990
|
ok: false,
|
|
321
991
|
errors
|
|
322
|
-
}
|
|
992
|
+
};
|
|
993
|
+
return {
|
|
323
994
|
ok: true,
|
|
324
|
-
flow: {
|
|
995
|
+
flow: migrateFlow(v2 ? {
|
|
996
|
+
version: 2,
|
|
325
997
|
nodes,
|
|
326
998
|
edges
|
|
327
|
-
}
|
|
999
|
+
} : {
|
|
1000
|
+
nodes,
|
|
1001
|
+
edges
|
|
1002
|
+
})
|
|
328
1003
|
};
|
|
329
1004
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
1005
|
+
//#endregion
|
|
1006
|
+
//#region src/components/ui/flow-viewer.tsx
|
|
1007
|
+
var FlowActionsContext = createContext(null);
|
|
1008
|
+
var FLOW_ICON = {
|
|
1009
|
+
"webhook-entrada": Webhook,
|
|
1010
|
+
agendamento: CalendarClock,
|
|
1011
|
+
"registro-criado": UserPlus,
|
|
1012
|
+
whatsapp: MessageCircle,
|
|
1013
|
+
email: Mail,
|
|
1014
|
+
condicao: GitBranch,
|
|
1015
|
+
espera: Hourglass,
|
|
1016
|
+
"webhook-saida": Send,
|
|
1017
|
+
tarefa: ListChecks,
|
|
1018
|
+
gatilho: Zap,
|
|
1019
|
+
acao: Cog,
|
|
1020
|
+
fim: Flag
|
|
1021
|
+
};
|
|
1022
|
+
var TONE = {
|
|
1023
|
+
gatilhos: "bg-primary-soft text-primary-soft-foreground",
|
|
1024
|
+
mensagens: "bg-success-soft text-success-soft-foreground",
|
|
1025
|
+
condicionais: "bg-warning-soft text-warning-soft-foreground",
|
|
1026
|
+
integracoes: "bg-info-soft text-info-soft-foreground",
|
|
1027
|
+
acoes: "bg-muted text-muted-foreground"
|
|
1028
|
+
};
|
|
1029
|
+
/** Cor do ícone do bloco, por grupo (também usada na barra de blocos). */
|
|
1030
|
+
var flowTone = (type) => TONE[FLOW_BLOCKS[type].group];
|
|
1031
|
+
var DIRECTION = {
|
|
1032
|
+
up: "para cima",
|
|
1033
|
+
down: "para baixo",
|
|
1034
|
+
left: "para a esquerda",
|
|
1035
|
+
right: "para a direita"
|
|
1036
|
+
};
|
|
1037
|
+
/** Os textos que a biblioteca escreve no DOM (leitor de tela), em português. */
|
|
1038
|
+
var FLOW_ARIA = {
|
|
1039
|
+
"node.a11yDescription.default": "Pressione Enter para abrir o bloco, Espaço para selecioná-lo e Delete para apagá-lo. Esc cancela.",
|
|
1040
|
+
"node.a11yDescription.keyboardDisabled": "Pressione Enter para abrir o bloco, Espaço para selecioná-lo e Delete para apagá-lo. Esc cancela.",
|
|
1041
|
+
"node.a11yDescription.ariaLiveMessage": ({ direction, x, y }) => `Bloco movido ${DIRECTION[direction] ?? direction}. Nova posição, x: ${x}, y: ${y}`,
|
|
1042
|
+
"edge.a11yDescription.default": "Pressione Espaço para selecionar a ligação e Delete para apagá-la. Esc cancela."
|
|
1043
|
+
};
|
|
1044
|
+
/** Os blocos do Rendra como nós do React Flow (a biblioteca só vê isto). */
|
|
1045
|
+
function toRfNodes(flow, opts = {}) {
|
|
1046
|
+
const byNode = /* @__PURE__ */ new Map();
|
|
1047
|
+
for (const issue of opts.issues ?? []) if (issue.nodeId) byNode.set(issue.nodeId, [...byNode.get(issue.nodeId) ?? [], issue.message]);
|
|
1048
|
+
return flow.nodes.map((n) => ({
|
|
1049
|
+
id: n.id,
|
|
1050
|
+
type: "bloco",
|
|
1051
|
+
position: n.position,
|
|
1052
|
+
ariaLabel: n.data.label,
|
|
1053
|
+
domAttributes: { "aria-roledescription": "bloco" },
|
|
1054
|
+
data: {
|
|
1055
|
+
label: n.data.label,
|
|
1056
|
+
kind: n.type,
|
|
1057
|
+
...n.data.config ? { config: n.data.config } : {},
|
|
1058
|
+
issues: byNode.get(n.id) ?? [],
|
|
1059
|
+
active: opts.activeId === n.id
|
|
1060
|
+
}
|
|
1061
|
+
}));
|
|
1062
|
+
}
|
|
1063
|
+
/** As ligações do Rendra como arestas do React Flow, com a descrição em português. */
|
|
1064
|
+
function toRfEdges(flow) {
|
|
1065
|
+
const nameOf = new Map(flow.nodes.map((n) => [n.id, nodeName(n)]));
|
|
1066
|
+
return flow.edges.map((e) => {
|
|
1067
|
+
const label = edgeLabel(flow, e);
|
|
1068
|
+
const from = nameOf.get(e.source) ?? e.source;
|
|
1069
|
+
const to = nameOf.get(e.target) ?? e.target;
|
|
1070
|
+
return {
|
|
1071
|
+
id: e.id,
|
|
1072
|
+
source: e.source,
|
|
1073
|
+
target: e.target,
|
|
1074
|
+
type: "ligacao",
|
|
1075
|
+
animated: true,
|
|
1076
|
+
...e.branch ? { sourceHandle: e.branch } : {},
|
|
1077
|
+
ariaLabel: `De "${from}" para "${to}"${label ? `, se ${label}` : ""}`,
|
|
1078
|
+
domAttributes: { "aria-roledescription": "ligação" },
|
|
1079
|
+
data: {
|
|
1080
|
+
label,
|
|
1081
|
+
from,
|
|
1082
|
+
to
|
|
1083
|
+
}
|
|
1084
|
+
};
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
function Port({ type, id, isConnectable, position = type === "target" ? Position.Left : Position.Right }) {
|
|
1088
|
+
return /* @__PURE__ */ jsx(Handle, {
|
|
337
1089
|
id,
|
|
338
1090
|
type,
|
|
339
|
-
position
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
}
|
|
347
|
-
};
|
|
348
|
-
return {
|
|
349
|
-
flow: {
|
|
350
|
-
...flow,
|
|
351
|
-
nodes: [...flow.nodes, node]
|
|
352
|
-
},
|
|
353
|
-
id
|
|
354
|
-
};
|
|
355
|
-
}
|
|
356
|
-
/** Apaga o bloco e as ligações que chegam ou saem dele. */
|
|
357
|
-
function removeNode(flow, id) {
|
|
358
|
-
if (!flow.nodes.some((n) => n.id === id)) return flow;
|
|
359
|
-
return {
|
|
360
|
-
nodes: flow.nodes.filter((n) => n.id !== id),
|
|
361
|
-
edges: flow.edges.filter((e) => e.source !== id && e.target !== id)
|
|
362
|
-
};
|
|
1091
|
+
position,
|
|
1092
|
+
className: "size-touch",
|
|
1093
|
+
isConnectable,
|
|
1094
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
1095
|
+
"aria-hidden": true,
|
|
1096
|
+
"data-flow-dot": true,
|
|
1097
|
+
className: "pointer-events-none absolute inset-0 m-auto size-3 rounded-full border-2 border-card bg-primary-text"
|
|
1098
|
+
})
|
|
1099
|
+
});
|
|
363
1100
|
}
|
|
364
|
-
function
|
|
365
|
-
return {
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
};
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
y: position.y
|
|
378
|
-
}
|
|
379
|
-
} : n)
|
|
380
|
-
};
|
|
381
|
-
}
|
|
382
|
-
/** Troca o rótulo e/ou o config do bloco (o config informado substitui o anterior). */
|
|
383
|
-
function updateNodeData(flow, id, patch) {
|
|
384
|
-
return {
|
|
385
|
-
...flow,
|
|
386
|
-
nodes: flow.nodes.map((n) => {
|
|
387
|
-
if (n.id !== id) return n;
|
|
388
|
-
const label = patch.label ?? n.data.label;
|
|
389
|
-
const config = patch.config ?? n.data.config;
|
|
390
|
-
return {
|
|
391
|
-
...n,
|
|
392
|
-
data: config ? {
|
|
393
|
-
label,
|
|
394
|
-
config
|
|
395
|
-
} : { label }
|
|
396
|
-
};
|
|
397
|
-
})
|
|
398
|
-
};
|
|
399
|
-
}
|
|
400
|
-
var edgeId = (source, target, branch) => branch ? `${source}:${branch}->${target}` : `${source}->${target}`;
|
|
401
|
-
/**
|
|
402
|
-
* Liga dois blocos. A segunda saída de um bloco comum (ou do mesmo ramo da condição) substitui
|
|
403
|
-
* a primeira. Recusa, com o motivo, o que o fluxo nunca aceita.
|
|
404
|
-
*/
|
|
405
|
-
function connectNodes(flow, source, target, branch) {
|
|
406
|
-
const from = flow.nodes.find((n) => n.id === source);
|
|
407
|
-
const to = flow.nodes.find((n) => n.id === target);
|
|
408
|
-
if (!from || !to) return {
|
|
409
|
-
ok: false,
|
|
410
|
-
reason: "Esse bloco não existe mais."
|
|
411
|
-
};
|
|
412
|
-
if (source === target) return {
|
|
413
|
-
ok: false,
|
|
414
|
-
reason: "Um bloco não pode ligar a ele mesmo."
|
|
415
|
-
};
|
|
416
|
-
if (from.type === "fim") return {
|
|
417
|
-
ok: false,
|
|
418
|
-
reason: "O bloco Fim termina o fluxo e não tem saída."
|
|
419
|
-
};
|
|
420
|
-
if (to.type === "gatilho") return {
|
|
421
|
-
ok: false,
|
|
422
|
-
reason: "O gatilho começa o fluxo e não recebe ligação."
|
|
423
|
-
};
|
|
424
|
-
if (from.type === "condicao" && !branch) return {
|
|
425
|
-
ok: false,
|
|
426
|
-
reason: "Escolha a saída \"Sim\" ou \"Não\" da condição."
|
|
427
|
-
};
|
|
428
|
-
if (from.type !== "condicao" && branch) return {
|
|
429
|
-
ok: false,
|
|
430
|
-
reason: "\"Sim\" e \"Não\" só existem na condição."
|
|
431
|
-
};
|
|
432
|
-
const id = edgeId(source, target, branch);
|
|
433
|
-
if (flow.edges.some((e) => e.id === id)) return {
|
|
434
|
-
ok: false,
|
|
435
|
-
reason: "Essa ligação já existe."
|
|
436
|
-
};
|
|
437
|
-
const kept = flow.edges.filter((e) => !(e.source === source && (from.type === "condicao" ? e.branch === branch : true)));
|
|
438
|
-
const edge = branch ? {
|
|
439
|
-
id,
|
|
440
|
-
source,
|
|
441
|
-
target,
|
|
442
|
-
branch
|
|
443
|
-
} : {
|
|
444
|
-
id,
|
|
445
|
-
source,
|
|
446
|
-
target
|
|
447
|
-
};
|
|
448
|
-
return {
|
|
449
|
-
ok: true,
|
|
450
|
-
flow: {
|
|
451
|
-
...flow,
|
|
452
|
-
edges: [...kept, edge]
|
|
453
|
-
}
|
|
454
|
-
};
|
|
455
|
-
}
|
|
456
|
-
/**
|
|
457
|
-
* Define (ou, com `target` nulo, tira) o próximo bloco de `id`; na condição, o do ramo dado.
|
|
458
|
-
* Uma ligação recusada devolve o próprio fluxo, sem mudança.
|
|
459
|
-
*/
|
|
460
|
-
function setNext(flow, id, target, branch) {
|
|
461
|
-
if (target === null) return {
|
|
462
|
-
...flow,
|
|
463
|
-
edges: flow.edges.filter((e) => !(e.source === id && (branch ? e.branch === branch : true)))
|
|
464
|
-
};
|
|
465
|
-
const r = connectNodes(flow, id, target, branch);
|
|
466
|
-
return r.ok ? r.flow : flow;
|
|
467
|
-
}
|
|
468
|
-
/** Blocos que `id` pode ter como próximo: nunca ele mesmo nem o gatilho. */
|
|
469
|
-
function nextCandidates(flow, id) {
|
|
470
|
-
const from = flow.nodes.find((n) => n.id === id);
|
|
471
|
-
if (!from || from.type === "fim") return [];
|
|
472
|
-
return flow.nodes.filter((n) => n.id !== id && n.type !== "gatilho");
|
|
473
|
-
}
|
|
474
|
-
function resetSimulation() {
|
|
475
|
-
return {
|
|
476
|
-
status: "idle",
|
|
477
|
-
steps: [],
|
|
478
|
-
currentId: null,
|
|
479
|
-
issues: []
|
|
480
|
-
};
|
|
481
|
-
}
|
|
482
|
-
/** Começa no gatilho. Fluxo inválido não inicia: devolve `error` com os problemas. */
|
|
483
|
-
function startSimulation(flow) {
|
|
484
|
-
const issues = validateFlow(flow);
|
|
485
|
-
if (issues.length > 0) return {
|
|
486
|
-
status: "error",
|
|
487
|
-
steps: [],
|
|
488
|
-
currentId: null,
|
|
489
|
-
issues
|
|
490
|
-
};
|
|
491
|
-
return {
|
|
492
|
-
status: "running",
|
|
493
|
-
steps: [],
|
|
494
|
-
currentId: flow.nodes.find((n) => n.type === "gatilho").id,
|
|
495
|
-
issues: []
|
|
496
|
-
};
|
|
497
|
-
}
|
|
498
|
-
/**
|
|
499
|
-
* Executa o bloco ativo (um passo) e passa ao próximo. Na condição é preciso escolher o ramo:
|
|
500
|
-
* sem escolha válida nada anda. A espera é instantânea; só o texto diz quanto aguardaria.
|
|
501
|
-
*/
|
|
502
|
-
function advanceSimulation(flow, state, choice) {
|
|
503
|
-
if (state.status !== "running" && state.status !== "waiting-choice" || !state.currentId) return state;
|
|
504
|
-
const node = flow.nodes.find((n) => n.id === state.currentId);
|
|
505
|
-
const edges = node ? flow.edges.filter((e) => e.source === node.id) : [];
|
|
506
|
-
const fail = () => ({
|
|
507
|
-
...state,
|
|
508
|
-
status: "error",
|
|
509
|
-
currentId: null,
|
|
510
|
-
issues: [{
|
|
511
|
-
code: "fluxo-mudou",
|
|
512
|
-
message: "O fluxo mudou durante o teste. Reinicie para testar de novo."
|
|
513
|
-
}]
|
|
514
|
-
});
|
|
515
|
-
if (!node) return fail();
|
|
516
|
-
let detail;
|
|
517
|
-
let target;
|
|
518
|
-
if (node.type === "condicao") {
|
|
519
|
-
if (choice !== "sim" && choice !== "nao") return state;
|
|
520
|
-
target = edges.find((e) => e.branch === choice)?.target;
|
|
521
|
-
detail = `Escolha: ${BRANCH_NAME[choice]}`;
|
|
522
|
-
if (!target) return fail();
|
|
523
|
-
} else if (node.type !== "fim") {
|
|
524
|
-
target = edges[0]?.target;
|
|
525
|
-
if (node.type === "espera") detail = describeWait(node.data.config);
|
|
526
|
-
if (!target) return fail();
|
|
527
|
-
}
|
|
528
|
-
const step = {
|
|
529
|
-
nodeId: node.id,
|
|
530
|
-
type: node.type,
|
|
531
|
-
label: nodeName(node),
|
|
532
|
-
status: "succeeded",
|
|
533
|
-
...detail ? { detail } : {}
|
|
534
|
-
};
|
|
535
|
-
const steps = [...state.steps, step];
|
|
536
|
-
if (!target) return {
|
|
537
|
-
status: "finished",
|
|
538
|
-
steps,
|
|
539
|
-
currentId: null,
|
|
540
|
-
issues: []
|
|
541
|
-
};
|
|
542
|
-
const next = flow.nodes.find((n) => n.id === target);
|
|
543
|
-
if (!next) return fail();
|
|
544
|
-
return {
|
|
545
|
-
status: next.type === "condicao" ? "waiting-choice" : "running",
|
|
546
|
-
steps,
|
|
547
|
-
currentId: target,
|
|
548
|
-
issues: []
|
|
549
|
-
};
|
|
550
|
-
}
|
|
551
|
-
//#endregion
|
|
552
|
-
//#region src/components/ui/flow-viewer.tsx
|
|
553
|
-
var FLOW_ICON = {
|
|
554
|
-
gatilho: Zap,
|
|
555
|
-
acao: Play,
|
|
556
|
-
condicao: GitBranch,
|
|
557
|
-
espera: Clock,
|
|
558
|
-
fim: Flag
|
|
559
|
-
};
|
|
560
|
-
var TONE = {
|
|
561
|
-
gatilho: "bg-primary-soft text-primary-soft-foreground",
|
|
562
|
-
acao: "bg-info-soft text-info-soft-foreground",
|
|
563
|
-
condicao: "bg-warning-soft text-warning-soft-foreground",
|
|
564
|
-
espera: "bg-muted text-muted-foreground",
|
|
565
|
-
fim: "bg-success-soft text-success-soft-foreground"
|
|
566
|
-
};
|
|
567
|
-
var DIRECTION = {
|
|
568
|
-
up: "para cima",
|
|
569
|
-
down: "para baixo",
|
|
570
|
-
left: "para a esquerda",
|
|
571
|
-
right: "para a direita"
|
|
572
|
-
};
|
|
573
|
-
/** Os textos que a biblioteca escreve no DOM (leitor de tela), em português. */
|
|
574
|
-
var FLOW_ARIA = {
|
|
575
|
-
"node.a11yDescription.default": "Pressione Enter para abrir o bloco, Espaço para selecioná-lo e Delete para apagá-lo. Esc cancela.",
|
|
576
|
-
"node.a11yDescription.keyboardDisabled": "Pressione Enter para abrir o bloco, Espaço para selecioná-lo e Delete para apagá-lo. Esc cancela.",
|
|
577
|
-
"node.a11yDescription.ariaLiveMessage": ({ direction, x, y }) => `Bloco movido ${DIRECTION[direction] ?? direction}. Nova posição, x: ${x}, y: ${y}`,
|
|
578
|
-
"edge.a11yDescription.default": "Pressione Espaço para selecionar a ligação e Delete para apagá-la. Esc cancela."
|
|
579
|
-
};
|
|
580
|
-
/** Os blocos do Rendra como nós do React Flow (a biblioteca só vê isto). */
|
|
581
|
-
function toRfNodes(flow, opts) {
|
|
582
|
-
const byNode = /* @__PURE__ */ new Map();
|
|
583
|
-
for (const issue of opts.issues ?? []) if (issue.nodeId) byNode.set(issue.nodeId, [...byNode.get(issue.nodeId) ?? [], issue.message]);
|
|
584
|
-
return flow.nodes.map((n) => ({
|
|
585
|
-
id: n.id,
|
|
586
|
-
type: "bloco",
|
|
587
|
-
position: n.position,
|
|
588
|
-
ariaLabel: n.data.label,
|
|
589
|
-
domAttributes: { "aria-roledescription": "bloco" },
|
|
590
|
-
data: {
|
|
591
|
-
label: n.data.label,
|
|
592
|
-
kind: n.type,
|
|
593
|
-
...n.data.config ? { config: n.data.config } : {},
|
|
594
|
-
issues: byNode.get(n.id) ?? [],
|
|
595
|
-
active: opts.activeId === n.id,
|
|
596
|
-
handles: opts.handles,
|
|
597
|
-
edit: opts.edit
|
|
598
|
-
}
|
|
599
|
-
}));
|
|
600
|
-
}
|
|
601
|
-
/** As ligações do Rendra como arestas do React Flow, com a descrição em português. */
|
|
602
|
-
function toRfEdges(flow) {
|
|
603
|
-
const labelOf = new Map(flow.nodes.map((n) => [n.id, nodeName(n)]));
|
|
604
|
-
return flow.edges.map((e) => ({
|
|
605
|
-
id: e.id,
|
|
606
|
-
source: e.source,
|
|
607
|
-
target: e.target,
|
|
608
|
-
...e.branch ? { sourceHandle: e.branch } : {},
|
|
609
|
-
ariaLabel: `De "${labelOf.get(e.source) ?? e.source}" para "${labelOf.get(e.target) ?? e.target}"${e.branch ? `, se ${e.branch === "sim" ? "sim" : "não"}` : ""}`,
|
|
610
|
-
domAttributes: { "aria-roledescription": "ligação" }
|
|
611
|
-
}));
|
|
1101
|
+
function Value({ text }) {
|
|
1102
|
+
if (!text) return /* @__PURE__ */ jsx("span", {
|
|
1103
|
+
className: "text-xs text-muted-foreground",
|
|
1104
|
+
children: "Não definido"
|
|
1105
|
+
});
|
|
1106
|
+
return /* @__PURE__ */ jsx("span", {
|
|
1107
|
+
className: "flex flex-wrap items-center gap-1 text-xs break-words",
|
|
1108
|
+
children: splitVariables(text).map((part, i) => part.kind === "var" ? /* @__PURE__ */ jsx("span", {
|
|
1109
|
+
"data-flow-chip": true,
|
|
1110
|
+
className: "rounded-item bg-primary-soft px-2 py-1 text-primary-soft-foreground",
|
|
1111
|
+
children: `{{${part.value}}}`
|
|
1112
|
+
}, i) : /* @__PURE__ */ jsx("span", { children: part.value }, i))
|
|
1113
|
+
});
|
|
612
1114
|
}
|
|
613
1115
|
function FlowBlock({ id, data, selected, isConnectable }) {
|
|
614
1116
|
const lib = useMotionLib();
|
|
615
1117
|
const presets = useMotionPresets();
|
|
1118
|
+
const actions = useContext(FlowActionsContext);
|
|
616
1119
|
const uid = useId();
|
|
617
1120
|
const Icon = FLOW_ICON[data.kind];
|
|
618
|
-
const
|
|
1121
|
+
const def = FLOW_BLOCKS[data.kind];
|
|
1122
|
+
const shape = {
|
|
1123
|
+
type: data.kind,
|
|
1124
|
+
data: {
|
|
1125
|
+
label: data.label,
|
|
1126
|
+
config: data.config
|
|
1127
|
+
}
|
|
1128
|
+
};
|
|
1129
|
+
const name = nodeName(shape);
|
|
1130
|
+
const summary = summarizeConfig(shape);
|
|
1131
|
+
const rules = data.kind === "condicao" ? rulesOf(shape) : [];
|
|
1132
|
+
const received = data.kind === "webhook-entrada" && data.config?.received === true;
|
|
619
1133
|
const invalid = data.issues.length > 0;
|
|
620
1134
|
const issueId = `${uid}-problema`;
|
|
621
|
-
const
|
|
1135
|
+
const descId = `${uid}-descricao`;
|
|
622
1136
|
const ring = "pointer-events-none absolute -inset-1 rounded-block border-2 border-primary";
|
|
1137
|
+
const action = "nodrag nopan min-h-touch min-w-touch";
|
|
623
1138
|
return /* @__PURE__ */ jsxs("div", {
|
|
624
1139
|
"data-flow-node": true,
|
|
625
1140
|
"data-kind": data.kind,
|
|
@@ -627,8 +1142,8 @@ function FlowBlock({ id, data, selected, isConnectable }) {
|
|
|
627
1142
|
"data-active": data.active ? "true" : void 0,
|
|
628
1143
|
"data-invalid": invalid ? "true" : void 0,
|
|
629
1144
|
"data-selected": selected ? "true" : void 0,
|
|
630
|
-
"aria-describedby": invalid ? issueId :
|
|
631
|
-
className: cn("relative w-flow-node rounded-block border bg-card
|
|
1145
|
+
"aria-describedby": invalid ? `${descId} ${issueId}` : descId,
|
|
1146
|
+
className: cn("relative w-flow-node rounded-block border bg-card text-card-foreground shadow-sm", invalid && "border-destructive", data.active && "border-primary", selected && "ring-2 ring-primary"),
|
|
632
1147
|
children: [
|
|
633
1148
|
data.active && (lib ? /* @__PURE__ */ jsx(lib.m.span, {
|
|
634
1149
|
"aria-hidden": true,
|
|
@@ -648,17 +1163,15 @@ function FlowBlock({ id, data, selected, isConnectable }) {
|
|
|
648
1163
|
"aria-hidden": true,
|
|
649
1164
|
className: ring
|
|
650
1165
|
})),
|
|
651
|
-
|
|
1166
|
+
!(def.group === "gatilhos") && /* @__PURE__ */ jsx(Port, {
|
|
652
1167
|
type: "target",
|
|
653
|
-
position: Position.Top,
|
|
654
|
-
className: handle,
|
|
655
1168
|
isConnectable
|
|
656
1169
|
}),
|
|
657
1170
|
/* @__PURE__ */ jsxs("div", {
|
|
658
|
-
className: "flex items-start gap-3",
|
|
1171
|
+
className: "flex items-start gap-3 p-3 pb-2",
|
|
659
1172
|
children: [
|
|
660
1173
|
/* @__PURE__ */ jsx("span", {
|
|
661
|
-
className: cn("flex size-8 shrink-0 items-center justify-center rounded-block",
|
|
1174
|
+
className: cn("flex size-8 shrink-0 items-center justify-center rounded-block", flowTone(data.kind)),
|
|
662
1175
|
children: /* @__PURE__ */ jsx(Icon, {
|
|
663
1176
|
className: "size-icon-md",
|
|
664
1177
|
"aria-hidden": true
|
|
@@ -666,42 +1179,117 @@ function FlowBlock({ id, data, selected, isConnectable }) {
|
|
|
666
1179
|
}),
|
|
667
1180
|
/* @__PURE__ */ jsxs("div", {
|
|
668
1181
|
className: "flex min-w-0 flex-1 flex-col",
|
|
1182
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
1183
|
+
"data-flow-title": true,
|
|
1184
|
+
className: "truncate text-sm font-medium",
|
|
1185
|
+
title: name,
|
|
1186
|
+
children: name
|
|
1187
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
1188
|
+
className: "truncate text-xs text-muted-foreground",
|
|
1189
|
+
children: data.active ? "Ativo" : FLOW_TYPE_NAME[data.kind]
|
|
1190
|
+
})]
|
|
1191
|
+
}),
|
|
1192
|
+
actions && /* @__PURE__ */ jsxs("div", {
|
|
1193
|
+
className: "-mt-3 -mr-3 flex shrink-0",
|
|
669
1194
|
children: [
|
|
670
|
-
/* @__PURE__ */ jsx(
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
className:
|
|
676
|
-
|
|
1195
|
+
/* @__PURE__ */ jsx(Button, {
|
|
1196
|
+
variant: "ghost",
|
|
1197
|
+
size: "sm",
|
|
1198
|
+
iconOnly: true,
|
|
1199
|
+
"aria-label": `Executar bloco ${name}`,
|
|
1200
|
+
className: action,
|
|
1201
|
+
icon: /* @__PURE__ */ jsx(Play, {}),
|
|
1202
|
+
onClick: () => actions.play(id)
|
|
677
1203
|
}),
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
1204
|
+
actions.edit && /* @__PURE__ */ jsx(Button, {
|
|
1205
|
+
"data-flow-edit": true,
|
|
1206
|
+
variant: "ghost",
|
|
1207
|
+
size: "sm",
|
|
1208
|
+
iconOnly: true,
|
|
1209
|
+
"aria-label": `Editar bloco ${name}`,
|
|
1210
|
+
className: action,
|
|
1211
|
+
icon: /* @__PURE__ */ jsx(SquarePen, {}),
|
|
1212
|
+
onClick: () => actions.edit?.(id)
|
|
681
1213
|
}),
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
1214
|
+
actions.remove && /* @__PURE__ */ jsx(Button, {
|
|
1215
|
+
variant: "ghost",
|
|
1216
|
+
size: "sm",
|
|
1217
|
+
iconOnly: true,
|
|
1218
|
+
"aria-label": `Excluir bloco ${name}`,
|
|
1219
|
+
className: action,
|
|
1220
|
+
icon: /* @__PURE__ */ jsx(Trash2, {}),
|
|
1221
|
+
onClick: () => actions.remove?.(id)
|
|
685
1222
|
})
|
|
686
1223
|
]
|
|
687
|
-
}),
|
|
688
|
-
data.edit && /* @__PURE__ */ jsx(Button, {
|
|
689
|
-
"data-flow-edit": true,
|
|
690
|
-
variant: "ghost",
|
|
691
|
-
size: "sm",
|
|
692
|
-
iconOnly: true,
|
|
693
|
-
"aria-label": `Editar bloco ${nodeName({
|
|
694
|
-
type: data.kind,
|
|
695
|
-
data
|
|
696
|
-
})}`,
|
|
697
|
-
className: "nodrag nopan -mt-1 -mr-1 min-h-touch min-w-touch shrink-0",
|
|
698
|
-
icon: /* @__PURE__ */ jsx(SquarePen, {})
|
|
699
1224
|
})
|
|
700
1225
|
]
|
|
701
1226
|
}),
|
|
1227
|
+
/* @__PURE__ */ jsx("p", {
|
|
1228
|
+
id: descId,
|
|
1229
|
+
className: "px-3 pb-3 text-xs text-muted-foreground",
|
|
1230
|
+
children: def.description
|
|
1231
|
+
}),
|
|
1232
|
+
summary.length > 0 && /* @__PURE__ */ jsx("dl", {
|
|
1233
|
+
className: "flex flex-col gap-2 border-t p-3",
|
|
1234
|
+
children: summary.map((row) => /* @__PURE__ */ jsxs("div", {
|
|
1235
|
+
className: "flex flex-col gap-1",
|
|
1236
|
+
children: [/* @__PURE__ */ jsx("dt", {
|
|
1237
|
+
className: "text-xs text-muted-foreground",
|
|
1238
|
+
children: row.label
|
|
1239
|
+
}), /* @__PURE__ */ jsx("dd", { children: /* @__PURE__ */ jsx(Value, { text: row.value }) })]
|
|
1240
|
+
}, row.label))
|
|
1241
|
+
}),
|
|
1242
|
+
received && /* @__PURE__ */ jsxs("p", {
|
|
1243
|
+
className: "mx-3 mb-3 flex items-center gap-2 rounded-block bg-success-soft p-2 text-xs text-success-soft-foreground",
|
|
1244
|
+
children: [/* @__PURE__ */ jsx(CheckCircle2, {
|
|
1245
|
+
className: "size-icon-sm shrink-0",
|
|
1246
|
+
"aria-hidden": true
|
|
1247
|
+
}), "Dados de exemplo recebidos"]
|
|
1248
|
+
}),
|
|
1249
|
+
data.kind === "condicao" && /* @__PURE__ */ jsxs("div", {
|
|
1250
|
+
className: "flex flex-col border-t",
|
|
1251
|
+
children: [rules.map((rule) => /* @__PURE__ */ jsxs("div", {
|
|
1252
|
+
"data-flow-output": rule.id,
|
|
1253
|
+
className: "relative flex min-h-touch items-center justify-between gap-2 border-b px-3 pr-4",
|
|
1254
|
+
children: [
|
|
1255
|
+
/* @__PURE__ */ jsx("span", {
|
|
1256
|
+
className: "text-xs font-medium",
|
|
1257
|
+
children: branchName(shape, rule.id)
|
|
1258
|
+
}),
|
|
1259
|
+
/* @__PURE__ */ jsx("span", {
|
|
1260
|
+
className: "min-w-0 truncate text-xs text-muted-foreground",
|
|
1261
|
+
title: ruleSummary(rule),
|
|
1262
|
+
children: ruleSummary(rule)
|
|
1263
|
+
}),
|
|
1264
|
+
/* @__PURE__ */ jsx(Port, {
|
|
1265
|
+
type: "source",
|
|
1266
|
+
id: rule.id,
|
|
1267
|
+
isConnectable
|
|
1268
|
+
})
|
|
1269
|
+
]
|
|
1270
|
+
}, rule.id)), /* @__PURE__ */ jsxs("div", {
|
|
1271
|
+
"data-flow-output": "senao",
|
|
1272
|
+
className: "relative flex min-h-touch items-center justify-between gap-2 px-3 pr-4",
|
|
1273
|
+
children: [
|
|
1274
|
+
/* @__PURE__ */ jsx("span", {
|
|
1275
|
+
className: "text-xs font-medium",
|
|
1276
|
+
children: "Senão"
|
|
1277
|
+
}),
|
|
1278
|
+
/* @__PURE__ */ jsx("span", {
|
|
1279
|
+
className: "min-w-0 truncate text-xs text-muted-foreground",
|
|
1280
|
+
children: "Nenhuma regra bateu"
|
|
1281
|
+
}),
|
|
1282
|
+
/* @__PURE__ */ jsx(Port, {
|
|
1283
|
+
type: "source",
|
|
1284
|
+
id: "senao",
|
|
1285
|
+
isConnectable
|
|
1286
|
+
})
|
|
1287
|
+
]
|
|
1288
|
+
})]
|
|
1289
|
+
}),
|
|
702
1290
|
invalid && /* @__PURE__ */ jsxs("p", {
|
|
703
1291
|
id: issueId,
|
|
704
|
-
className: "mt-
|
|
1292
|
+
className: "m-3 mt-0 flex flex-wrap gap-x-1 rounded-block bg-destructive-soft p-2 text-xs text-destructive-soft-foreground",
|
|
705
1293
|
children: [
|
|
706
1294
|
/* @__PURE__ */ jsx(TriangleAlert, {
|
|
707
1295
|
className: "size-icon-sm shrink-0",
|
|
@@ -712,42 +1300,119 @@ function FlowBlock({ id, data, selected, isConnectable }) {
|
|
|
712
1300
|
data.issues.join(" ")
|
|
713
1301
|
]
|
|
714
1302
|
}),
|
|
715
|
-
data.kind
|
|
1303
|
+
data.kind !== "condicao" && /* @__PURE__ */ jsx(Port, {
|
|
1304
|
+
type: "source",
|
|
1305
|
+
isConnectable
|
|
1306
|
+
})
|
|
1307
|
+
]
|
|
1308
|
+
});
|
|
1309
|
+
}
|
|
1310
|
+
/** Ponto da curva de Bézier de uma ligação (t entre 0 e 1), com os mesmos controles da biblioteca. */
|
|
1311
|
+
function bezierAt(sx, sy, tx, ty, t) {
|
|
1312
|
+
const d = tx - sx;
|
|
1313
|
+
const offset = d >= 0 ? .5 * d : 6.25 * Math.sqrt(-d);
|
|
1314
|
+
const mt = 1 - t;
|
|
1315
|
+
return {
|
|
1316
|
+
x: mt ** 3 * sx + 3 * mt ** 2 * t * (sx + offset) + 3 * mt * t ** 2 * (tx - offset) + t ** 3 * tx,
|
|
1317
|
+
y: mt ** 3 * sy + 3 * mt ** 2 * t * sy + 3 * mt * t ** 2 * ty + t ** 3 * ty
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1320
|
+
function FlowLink({ id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, markerEnd, selected, data }) {
|
|
1321
|
+
const actions = useContext(FlowActionsContext);
|
|
1322
|
+
const [confirming, setConfirming] = useState(false);
|
|
1323
|
+
const noRef = useRef(null);
|
|
1324
|
+
useEffect(() => {
|
|
1325
|
+
if (confirming) noRef.current?.focus();
|
|
1326
|
+
}, [confirming]);
|
|
1327
|
+
const [path, midX, midY] = getBezierPath({
|
|
1328
|
+
sourceX,
|
|
1329
|
+
sourceY,
|
|
1330
|
+
sourcePosition,
|
|
1331
|
+
targetX,
|
|
1332
|
+
targetY,
|
|
1333
|
+
targetPosition
|
|
1334
|
+
});
|
|
1335
|
+
const start = bezierAt(sourceX, sourceY, targetX, targetY, .18);
|
|
1336
|
+
const showTrash = Boolean(selected && actions?.removeEdge);
|
|
1337
|
+
const names = `${data?.from ?? ""} para ${data?.to ?? ""}`;
|
|
1338
|
+
const label = data?.label;
|
|
1339
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(BaseEdge, {
|
|
1340
|
+
id,
|
|
1341
|
+
path,
|
|
1342
|
+
markerEnd,
|
|
1343
|
+
interactionWidth: 24
|
|
1344
|
+
}), /* @__PURE__ */ jsxs(EdgeLabelRenderer, { children: [label && /* @__PURE__ */ jsx("div", {
|
|
1345
|
+
className: "pointer-events-none absolute top-0 left-0 translate-x-(--flow-x) translate-y-(--flow-y)",
|
|
1346
|
+
style: {
|
|
1347
|
+
"--flow-x": `${start.x}px`,
|
|
1348
|
+
"--flow-y": `${start.y}px`
|
|
1349
|
+
},
|
|
1350
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
1351
|
+
"data-flow-edge-label": true,
|
|
1352
|
+
className: "block -translate-x-1/2 -translate-y-1/2 rounded-item border bg-card px-2 py-1 text-xs font-medium text-card-foreground",
|
|
1353
|
+
children: label
|
|
1354
|
+
})
|
|
1355
|
+
}), showTrash && actions?.removeEdge && /* @__PURE__ */ jsx("div", {
|
|
1356
|
+
className: "pointer-events-none absolute top-0 left-0 z-50 translate-x-(--flow-x) translate-y-(--flow-y)",
|
|
1357
|
+
style: {
|
|
1358
|
+
"--flow-x": `${midX}px`,
|
|
1359
|
+
"--flow-y": `${midY}px`
|
|
1360
|
+
},
|
|
1361
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
1362
|
+
"data-flow-edge-actions": true,
|
|
1363
|
+
role: "presentation",
|
|
1364
|
+
className: "nodrag nopan pointer-events-auto flex -translate-x-1/2 -translate-y-1/2 items-center gap-2 rounded-control border bg-card p-1 shadow-md",
|
|
1365
|
+
onKeyDown: (e) => {
|
|
1366
|
+
if (e.key === "Escape" && confirming) {
|
|
1367
|
+
e.stopPropagation();
|
|
1368
|
+
setConfirming(false);
|
|
1369
|
+
}
|
|
1370
|
+
},
|
|
1371
|
+
children: confirming ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
716
1372
|
/* @__PURE__ */ jsx("span", {
|
|
717
|
-
|
|
1373
|
+
role: "alertdialog",
|
|
1374
|
+
"aria-label": "Confirmar exclusão da ligação",
|
|
1375
|
+
className: "px-2 text-xs font-medium",
|
|
1376
|
+
children: "Excluir ligação?"
|
|
1377
|
+
}),
|
|
1378
|
+
/* @__PURE__ */ jsx(Button, {
|
|
1379
|
+
size: "sm",
|
|
1380
|
+
variant: "destructive",
|
|
1381
|
+
className: "min-h-touch",
|
|
1382
|
+
onClick: () => {
|
|
1383
|
+
setConfirming(false);
|
|
1384
|
+
actions.removeEdge?.(id);
|
|
1385
|
+
},
|
|
718
1386
|
children: "Sim"
|
|
719
1387
|
}),
|
|
720
|
-
/* @__PURE__ */ jsx(
|
|
721
|
-
|
|
1388
|
+
/* @__PURE__ */ jsx(Button, {
|
|
1389
|
+
size: "sm",
|
|
1390
|
+
variant: "outline",
|
|
1391
|
+
ref: noRef,
|
|
1392
|
+
className: "min-h-touch",
|
|
1393
|
+
onClick: (e) => {
|
|
1394
|
+
setConfirming(false);
|
|
1395
|
+
e.currentTarget.closest(".flow-surface")?.focus();
|
|
1396
|
+
},
|
|
722
1397
|
children: "Não"
|
|
723
|
-
}),
|
|
724
|
-
/* @__PURE__ */ jsx(Handle, {
|
|
725
|
-
id: "sim",
|
|
726
|
-
type: "source",
|
|
727
|
-
position: Position.Right,
|
|
728
|
-
className: handle,
|
|
729
|
-
isConnectable
|
|
730
|
-
}),
|
|
731
|
-
/* @__PURE__ */ jsx(Handle, {
|
|
732
|
-
id: "nao",
|
|
733
|
-
type: "source",
|
|
734
|
-
position: Position.Bottom,
|
|
735
|
-
className: handle,
|
|
736
|
-
isConnectable
|
|
737
1398
|
})
|
|
738
|
-
] }),
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
1399
|
+
] }) : /* @__PURE__ */ jsx(Button, {
|
|
1400
|
+
size: "sm",
|
|
1401
|
+
variant: "ghost",
|
|
1402
|
+
iconOnly: true,
|
|
1403
|
+
"aria-label": `Excluir ligação de ${names}`,
|
|
1404
|
+
className: "min-h-touch min-w-touch",
|
|
1405
|
+
icon: /* @__PURE__ */ jsx(Trash2, {}),
|
|
1406
|
+
onClick: () => setConfirming(true)
|
|
744
1407
|
})
|
|
745
|
-
|
|
746
|
-
});
|
|
1408
|
+
})
|
|
1409
|
+
})] })] });
|
|
747
1410
|
}
|
|
748
1411
|
var nodeTypes = { bloco: FlowBlock };
|
|
1412
|
+
var edgeTypes = { ligacao: FlowLink };
|
|
749
1413
|
var defaultEdgeOptions = {
|
|
750
|
-
type: "
|
|
1414
|
+
type: "ligacao",
|
|
1415
|
+
animated: true,
|
|
751
1416
|
markerEnd: { type: MarkerType.ArrowClosed }
|
|
752
1417
|
};
|
|
753
1418
|
var fitViewOptions = {
|
|
@@ -796,47 +1461,78 @@ function FlowZoomControls() {
|
|
|
796
1461
|
]
|
|
797
1462
|
});
|
|
798
1463
|
}
|
|
799
|
-
/**
|
|
1464
|
+
/** Zoom mínimo do enquadramento inicial: abaixo disso os cards e as alças ficam ilegíveis e pequenos demais. */
|
|
1465
|
+
var MIN_FIT_ZOOM = .5;
|
|
1466
|
+
/**
|
|
1467
|
+
* Enquadramento inicial e quando a largura do canvas muda. Cabe tudo se o zoom ficar em 50 % ou
|
|
1468
|
+
* mais; senão abre em 50 % começando pela esquerda (pelo gatilho) e a pessoa navega. O botão
|
|
1469
|
+
* "Enquadrar" continua mostrando o fluxo inteiro, sem piso de zoom.
|
|
1470
|
+
*/
|
|
800
1471
|
function FlowAutoFit() {
|
|
801
|
-
const {
|
|
1472
|
+
const { getNodes, setViewport } = useReactFlow();
|
|
802
1473
|
const width = useStore((st) => st.width);
|
|
1474
|
+
const height = useStore((st) => st.height);
|
|
1475
|
+
const ready = useStore((st) => st.nodeLookup.size > 0 && [...st.nodeLookup.values()].every((n) => (n.measured.width ?? 0) > 0));
|
|
803
1476
|
const before = useRef(0);
|
|
1477
|
+
const [attempt, setAttempt] = useState(0);
|
|
804
1478
|
useEffect(() => {
|
|
805
|
-
if (
|
|
806
|
-
|
|
807
|
-
|
|
1479
|
+
if (!ready || width <= 0 || height <= 0 || width === before.current) return;
|
|
1480
|
+
const nodes = getNodes();
|
|
1481
|
+
if (nodes.length === 0) return;
|
|
1482
|
+
const b = getNodesBounds(nodes);
|
|
1483
|
+
const fit = Math.min(1, (width - 32) / b.width, (height - 128) / b.height);
|
|
1484
|
+
const zoom = Math.max(fit, MIN_FIT_ZOOM);
|
|
1485
|
+
const x = zoom === fit ? (width - b.width * zoom) / 2 - b.x * zoom : 16 - b.x * zoom;
|
|
1486
|
+
const y = Math.max((height - b.height * zoom) / 2, 64) - b.y * zoom;
|
|
1487
|
+
setViewport({
|
|
1488
|
+
x,
|
|
1489
|
+
y,
|
|
1490
|
+
zoom
|
|
1491
|
+
}).then((ok) => {
|
|
1492
|
+
if (ok) before.current = width;
|
|
1493
|
+
else if (attempt < 20) setTimeout(() => setAttempt((a) => a + 1), 50);
|
|
1494
|
+
});
|
|
1495
|
+
}, [
|
|
1496
|
+
ready,
|
|
1497
|
+
width,
|
|
1498
|
+
height,
|
|
1499
|
+
attempt,
|
|
1500
|
+
getNodes,
|
|
1501
|
+
setViewport
|
|
1502
|
+
]);
|
|
808
1503
|
return null;
|
|
809
1504
|
}
|
|
810
1505
|
/**
|
|
811
1506
|
* O canvas: contêiner com a altura que sobra na tela (`h-board`), tema pelos tokens e a
|
|
812
1507
|
* biblioteca por dentro. A rolagem horizontal é do canvas (`data-allow-overflow`), nunca da página.
|
|
813
1508
|
*/
|
|
814
|
-
function FlowSurface({ className, children, ...props }) {
|
|
1509
|
+
function FlowSurface({ className, children, fill = true, ...props }) {
|
|
815
1510
|
const ref = useFillHeight();
|
|
816
1511
|
return /* @__PURE__ */ jsx("div", {
|
|
817
|
-
ref,
|
|
1512
|
+
ref: fill ? ref : void 0,
|
|
1513
|
+
tabIndex: -1,
|
|
818
1514
|
"data-allow-overflow": true,
|
|
819
|
-
className: cn("flow-surface relative
|
|
1515
|
+
className: cn("flow-surface relative w-full min-w-0 overflow-hidden outline-none", fill ? "h-board min-h-viewer rounded-surface border" : "h-full", className),
|
|
820
1516
|
style: {
|
|
821
1517
|
"--xy-edge-stroke": "var(--rendra-muted-foreground)",
|
|
822
1518
|
"--xy-edge-stroke-selected": "var(--rendra-primary)",
|
|
823
1519
|
"--xy-edge-stroke-width": "2",
|
|
824
1520
|
"--xy-connectionline-stroke": "var(--rendra-primary)",
|
|
825
1521
|
"--xy-connectionline-stroke-width": "2",
|
|
826
|
-
"--xy-handle-background-color": "var(--rendra-primary)",
|
|
1522
|
+
"--xy-handle-background-color": "var(--rendra-primary-text)",
|
|
827
1523
|
"--xy-attribution-background-color": "var(--rendra-card)",
|
|
828
1524
|
"--xy-background-color": "var(--rendra-muted)"
|
|
829
1525
|
},
|
|
830
1526
|
children: /* @__PURE__ */ jsxs(ReactFlow, {
|
|
831
1527
|
className: "flow-canvas",
|
|
832
1528
|
nodeTypes,
|
|
1529
|
+
edgeTypes,
|
|
833
1530
|
defaultEdgeOptions,
|
|
834
1531
|
defaultMarkerColor: "var(--rendra-muted-foreground)",
|
|
835
|
-
connectionLineType: ConnectionLineType.
|
|
1532
|
+
connectionLineType: ConnectionLineType.Bezier,
|
|
1533
|
+
connectionRadius: 12,
|
|
836
1534
|
ariaLabelConfig: FLOW_ARIA,
|
|
837
|
-
|
|
838
|
-
fitViewOptions,
|
|
839
|
-
minZoom: .25,
|
|
1535
|
+
minZoom: .1,
|
|
840
1536
|
maxZoom: 2,
|
|
841
1537
|
zoomOnDoubleClick: false,
|
|
842
1538
|
zoomOnScroll: false,
|
|
@@ -855,17 +1551,16 @@ function FlowSurface({ className, children, ...props }) {
|
|
|
855
1551
|
* não apaga. Para montar e editar, use o FlowEditor.
|
|
856
1552
|
*/
|
|
857
1553
|
function FlowViewer({ value, activeId = null, issues, className }) {
|
|
858
|
-
const
|
|
1554
|
+
const flow = useMemo(() => migrateFlow(value), [value]);
|
|
1555
|
+
const nodes = useMemo(() => toRfNodes(flow, {
|
|
859
1556
|
activeId,
|
|
860
|
-
issues
|
|
861
|
-
handles: "sm",
|
|
862
|
-
edit: false
|
|
1557
|
+
issues
|
|
863
1558
|
}), [
|
|
864
|
-
|
|
1559
|
+
flow,
|
|
865
1560
|
activeId,
|
|
866
1561
|
issues
|
|
867
1562
|
]);
|
|
868
|
-
const edges = useMemo(() => toRfEdges(
|
|
1563
|
+
const edges = useMemo(() => toRfEdges(flow), [flow]);
|
|
869
1564
|
return /* @__PURE__ */ jsx("div", {
|
|
870
1565
|
"data-rendra": "FLX-002",
|
|
871
1566
|
className: cn("flow-viewer min-w-0", className),
|
|
@@ -881,66 +1576,818 @@ function FlowViewer({ value, activeId = null, issues, className }) {
|
|
|
881
1576
|
});
|
|
882
1577
|
}
|
|
883
1578
|
//#endregion
|
|
884
|
-
//#region src/components/ui/flow-
|
|
885
|
-
var
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
1579
|
+
//#region src/components/ui/flow-sidebar.tsx
|
|
1580
|
+
var normalize = (s) => s.normalize("NFD").replace(/\p{M}/gu, "").toLowerCase();
|
|
1581
|
+
function FlowBlockList({ onAdd, onDragStart, suppressClick }) {
|
|
1582
|
+
const [query, setQuery] = useState("");
|
|
1583
|
+
const q = normalize(query.trim());
|
|
1584
|
+
const groups = FLOW_BLOCK_GROUPS.map((g) => ({
|
|
1585
|
+
...g,
|
|
1586
|
+
types: g.types.filter((t) => {
|
|
1587
|
+
const def = FLOW_BLOCKS[t];
|
|
1588
|
+
return !def.hidden && (q === "" || normalize(`${def.name} ${def.hint}`).includes(q));
|
|
1589
|
+
})
|
|
1590
|
+
})).filter((g) => g.types.length > 0);
|
|
1591
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1592
|
+
className: "flex flex-col gap-4",
|
|
1593
|
+
children: [
|
|
1594
|
+
/* @__PURE__ */ jsx(Input, {
|
|
1595
|
+
icon: /* @__PURE__ */ jsx(Search, {}),
|
|
1596
|
+
value: query,
|
|
1597
|
+
onChange: setQuery,
|
|
1598
|
+
placeholder: "Buscar gatilho ou ação",
|
|
1599
|
+
"aria-label": "Buscar bloco",
|
|
1600
|
+
clearable: true
|
|
1601
|
+
}),
|
|
1602
|
+
groups.length === 0 && /* @__PURE__ */ jsx(EmptyState, {
|
|
1603
|
+
size: "compact",
|
|
1604
|
+
title: "Nenhum bloco encontrado",
|
|
1605
|
+
description: "Nenhum bloco tem esse nome."
|
|
1606
|
+
}),
|
|
1607
|
+
groups.map((g) => /* @__PURE__ */ jsxs("section", {
|
|
1608
|
+
role: "group",
|
|
1609
|
+
"aria-label": g.name,
|
|
1610
|
+
className: "flex flex-col gap-2",
|
|
1611
|
+
children: [/* @__PURE__ */ jsx("h3", {
|
|
1612
|
+
className: "text-xs font-medium text-muted-foreground",
|
|
1613
|
+
children: g.name
|
|
1614
|
+
}), g.types.map((type) => {
|
|
1615
|
+
const Icon = FLOW_ICON[type];
|
|
1616
|
+
return /* @__PURE__ */ jsx(Button, {
|
|
1617
|
+
variant: "outline",
|
|
1618
|
+
size: "sm",
|
|
1619
|
+
"data-flow-block": type,
|
|
1620
|
+
className: cn("w-full justify-start", onDragStart && "cursor-grab touch-none"),
|
|
1621
|
+
icon: /* @__PURE__ */ jsx("span", {
|
|
1622
|
+
className: cn("flex size-6 items-center justify-center rounded-block", flowTone(type)),
|
|
1623
|
+
children: /* @__PURE__ */ jsx(Icon, {
|
|
1624
|
+
className: "size-icon-sm",
|
|
1625
|
+
"aria-hidden": true
|
|
1626
|
+
})
|
|
1627
|
+
}),
|
|
1628
|
+
onPointerDown: onDragStart ? (e) => onDragStart(e, type) : void 0,
|
|
1629
|
+
onClick: () => {
|
|
1630
|
+
if (suppressClick?.current) return;
|
|
1631
|
+
onAdd(type);
|
|
1632
|
+
},
|
|
1633
|
+
children: FLOW_BLOCKS[type].name
|
|
1634
|
+
}, type);
|
|
1635
|
+
})]
|
|
1636
|
+
}, g.id))
|
|
1637
|
+
]
|
|
1638
|
+
});
|
|
1639
|
+
}
|
|
1640
|
+
/** Cabeçalho comum dos painéis da barra lateral: título e o botão de fechar. */
|
|
1641
|
+
function FlowPanelShell({ title, onClose, closeLabel = "Fechar painel", children, className }) {
|
|
1642
|
+
const id = useId();
|
|
1643
|
+
return /* @__PURE__ */ jsxs("section", {
|
|
1644
|
+
"aria-labelledby": id,
|
|
1645
|
+
className: cn("flex flex-col gap-4", className),
|
|
1646
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1647
|
+
className: "flex items-center justify-between gap-2",
|
|
1648
|
+
children: [/* @__PURE__ */ jsx("h2", {
|
|
1649
|
+
id,
|
|
1650
|
+
className: "text-sm font-semibold",
|
|
1651
|
+
children: title
|
|
1652
|
+
}), onClose && /* @__PURE__ */ jsx(Button, {
|
|
1653
|
+
variant: "ghost",
|
|
1654
|
+
size: "sm",
|
|
1655
|
+
iconOnly: true,
|
|
1656
|
+
"aria-label": closeLabel,
|
|
1657
|
+
className: "min-h-touch min-w-touch",
|
|
1658
|
+
icon: /* @__PURE__ */ jsx(X, {}),
|
|
1659
|
+
onClick: onClose
|
|
1660
|
+
})]
|
|
1661
|
+
}), children]
|
|
915
1662
|
});
|
|
916
1663
|
}
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
1664
|
+
//#endregion
|
|
1665
|
+
//#region src/lib/flow-edit.ts
|
|
1666
|
+
/** Acrescenta um bloco com id único e determinístico (`tipo-N`) e a configuração padrão do tipo. */
|
|
1667
|
+
function addNode(input, type, position, options = {}) {
|
|
1668
|
+
const flow = migrateFlow(input);
|
|
1669
|
+
const taken = /* @__PURE__ */ new Set([...flow.nodes.map((n) => n.id), ...options.reservedIds ?? []]);
|
|
1670
|
+
let n = 1;
|
|
1671
|
+
while (taken.has(`${type}-${n}`)) n++;
|
|
1672
|
+
const id = `${type}-${n}`;
|
|
1673
|
+
const config = FLOW_BLOCKS[type].defaultConfig();
|
|
1674
|
+
const node = {
|
|
1675
|
+
id,
|
|
1676
|
+
type,
|
|
1677
|
+
position: {
|
|
1678
|
+
x: position.x,
|
|
1679
|
+
y: position.y
|
|
1680
|
+
},
|
|
1681
|
+
data: Object.keys(config).length > 0 ? {
|
|
1682
|
+
label: options.label ?? FLOW_TYPE_NAME[type],
|
|
1683
|
+
config
|
|
1684
|
+
} : { label: options.label ?? FLOW_TYPE_NAME[type] }
|
|
1685
|
+
};
|
|
921
1686
|
return {
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
nao: node.type === "condicao" ? target("nao") : NONE
|
|
1687
|
+
flow: {
|
|
1688
|
+
...flow,
|
|
1689
|
+
nodes: [...flow.nodes, node]
|
|
1690
|
+
},
|
|
1691
|
+
id
|
|
928
1692
|
};
|
|
929
1693
|
}
|
|
930
|
-
var
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
const
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
1694
|
+
var edgeId = (source, target, branch) => branch ? `${source}:${branch}->${target}` : `${source}->${target}`;
|
|
1695
|
+
/** Blocos que `id` alcança e que nenhum outro caminho do gatilho alcança sem passar por ele. */
|
|
1696
|
+
function exclusiveDescendants(flow, id) {
|
|
1697
|
+
const reach = (starts, skip) => {
|
|
1698
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1699
|
+
const stack = [...starts];
|
|
1700
|
+
while (stack.length) {
|
|
1701
|
+
const cur = stack.pop();
|
|
1702
|
+
if (seen.has(cur) || cur === skip) continue;
|
|
1703
|
+
seen.add(cur);
|
|
1704
|
+
for (const e of flow.edges) if (e.source === cur) stack.push(e.target);
|
|
1705
|
+
}
|
|
1706
|
+
return seen;
|
|
1707
|
+
};
|
|
1708
|
+
const below = reach([id]);
|
|
1709
|
+
below.delete(id);
|
|
1710
|
+
const alive = reach(flow.nodes.filter((n) => isTrigger(n.type) && n.id !== id).map((n) => n.id), id);
|
|
1711
|
+
return flow.nodes.filter((n) => below.has(n.id) && !alive.has(n.id)).map((n) => n.id);
|
|
1712
|
+
}
|
|
1713
|
+
/**
|
|
1714
|
+
* Apaga o bloco. `so-este`: religa o anterior ao posterior quando o bloco tem uma entrada e uma
|
|
1715
|
+
* saída e não é condição. `daqui-para-frente`: apaga também tudo o que só existe por causa dele.
|
|
1716
|
+
*/
|
|
1717
|
+
function removeNode(input, id, mode = "so-este") {
|
|
1718
|
+
const flow = migrateFlow(input);
|
|
1719
|
+
const node = flow.nodes.find((n) => n.id === id);
|
|
1720
|
+
if (!node) return flow;
|
|
1721
|
+
const gone = /* @__PURE__ */ new Set([id, ...mode === "daqui-para-frente" ? exclusiveDescendants(flow, id) : []]);
|
|
1722
|
+
let edges = flow.edges.filter((e) => !gone.has(e.source) && !gone.has(e.target));
|
|
1723
|
+
if (mode === "so-este" && node.type !== "condicao") {
|
|
1724
|
+
const into = flow.edges.filter((e) => e.target === id);
|
|
1725
|
+
const from = flow.edges.filter((e) => e.source === id);
|
|
1726
|
+
if (into.length === 1 && from.length === 1) {
|
|
1727
|
+
const a = into[0];
|
|
1728
|
+
const b = from[0];
|
|
1729
|
+
const bridged = {
|
|
1730
|
+
id: edgeId(a.source, b.target, a.branch),
|
|
1731
|
+
source: a.source,
|
|
1732
|
+
target: b.target,
|
|
1733
|
+
...a.branch ? { branch: a.branch } : {}
|
|
1734
|
+
};
|
|
1735
|
+
if (a.source !== b.target && !edges.some((e) => e.id === bridged.id)) edges = [...edges, bridged];
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
return {
|
|
1739
|
+
...flow,
|
|
1740
|
+
nodes: flow.nodes.filter((n) => !gone.has(n.id)),
|
|
1741
|
+
edges
|
|
1742
|
+
};
|
|
1743
|
+
}
|
|
1744
|
+
function removeEdge(flow, id) {
|
|
1745
|
+
return {
|
|
1746
|
+
...flow,
|
|
1747
|
+
edges: flow.edges.filter((e) => e.id !== id)
|
|
1748
|
+
};
|
|
1749
|
+
}
|
|
1750
|
+
/** Tira a ligação da saída `branch` de `source` (a única saída, nos blocos comuns). */
|
|
1751
|
+
function clearOutput(flow, source, branch) {
|
|
1752
|
+
return {
|
|
1753
|
+
...flow,
|
|
1754
|
+
edges: flow.edges.filter((e) => !(e.source === source && (e.branch ?? void 0) === branch))
|
|
1755
|
+
};
|
|
1756
|
+
}
|
|
1757
|
+
function moveNode(flow, id, position) {
|
|
1758
|
+
return {
|
|
1759
|
+
...flow,
|
|
1760
|
+
nodes: flow.nodes.map((n) => n.id === id ? {
|
|
1761
|
+
...n,
|
|
1762
|
+
position: {
|
|
1763
|
+
x: position.x,
|
|
1764
|
+
y: position.y
|
|
1765
|
+
}
|
|
1766
|
+
} : n)
|
|
1767
|
+
};
|
|
1768
|
+
}
|
|
1769
|
+
/** Troca o rótulo e/ou o config do bloco (o config informado substitui o anterior). */
|
|
1770
|
+
function updateNodeData(flow, id, patch) {
|
|
1771
|
+
return {
|
|
1772
|
+
...flow,
|
|
1773
|
+
nodes: flow.nodes.map((n) => {
|
|
1774
|
+
if (n.id !== id) return n;
|
|
1775
|
+
const label = patch.label ?? n.data.label;
|
|
1776
|
+
const config = patch.config ?? n.data.config;
|
|
1777
|
+
return {
|
|
1778
|
+
...n,
|
|
1779
|
+
data: config ? {
|
|
1780
|
+
label,
|
|
1781
|
+
config
|
|
1782
|
+
} : { label }
|
|
1783
|
+
};
|
|
1784
|
+
})
|
|
1785
|
+
};
|
|
1786
|
+
}
|
|
1787
|
+
/**
|
|
1788
|
+
* Liga dois blocos. A segunda ligação numa saída substitui a primeira e devolve a trocada em
|
|
1789
|
+
* `replaced`, para o editor avisar. Recusa, com o motivo, o que o fluxo nunca aceita.
|
|
1790
|
+
*/
|
|
1791
|
+
function connectNodes(flow, source, target, branch) {
|
|
1792
|
+
const from = flow.nodes.find((n) => n.id === source);
|
|
1793
|
+
const to = flow.nodes.find((n) => n.id === target);
|
|
1794
|
+
if (!from || !to) return {
|
|
1795
|
+
ok: false,
|
|
1796
|
+
reason: "Esse bloco não existe mais."
|
|
1797
|
+
};
|
|
1798
|
+
if (source === target) return {
|
|
1799
|
+
ok: false,
|
|
1800
|
+
reason: "Um bloco não pode ligar a ele mesmo."
|
|
1801
|
+
};
|
|
1802
|
+
if (isTrigger(to.type)) return {
|
|
1803
|
+
ok: false,
|
|
1804
|
+
reason: "O gatilho começa o fluxo e não recebe ligação."
|
|
1805
|
+
};
|
|
1806
|
+
if (from.type === "condicao") {
|
|
1807
|
+
if (!branch) return {
|
|
1808
|
+
ok: false,
|
|
1809
|
+
reason: "Escolha a saída da condição: uma regra ou \"Senão\"."
|
|
1810
|
+
};
|
|
1811
|
+
if (!outputIds(from).includes(branch)) return {
|
|
1812
|
+
ok: false,
|
|
1813
|
+
reason: "Essa saída não existe mais na condição."
|
|
1814
|
+
};
|
|
1815
|
+
} else if (branch) return {
|
|
1816
|
+
ok: false,
|
|
1817
|
+
reason: "Só a condição tem saídas por regra."
|
|
1818
|
+
};
|
|
1819
|
+
const id = edgeId(source, target, branch);
|
|
1820
|
+
if (flow.edges.some((e) => e.id === id)) return {
|
|
1821
|
+
ok: false,
|
|
1822
|
+
reason: "Essa ligação já existe."
|
|
1823
|
+
};
|
|
1824
|
+
const sameOutput = (e) => e.source === source && (e.branch ?? void 0) === branch;
|
|
1825
|
+
const old = flow.edges.find(sameOutput);
|
|
1826
|
+
const edge = branch ? {
|
|
1827
|
+
id,
|
|
1828
|
+
source,
|
|
1829
|
+
target,
|
|
1830
|
+
branch
|
|
1831
|
+
} : {
|
|
1832
|
+
id,
|
|
1833
|
+
source,
|
|
1834
|
+
target
|
|
1835
|
+
};
|
|
1836
|
+
return {
|
|
1837
|
+
ok: true,
|
|
1838
|
+
flow: {
|
|
1839
|
+
...flow,
|
|
1840
|
+
edges: [...flow.edges.filter((e) => !sameOutput(e)), edge]
|
|
1841
|
+
},
|
|
1842
|
+
...old ? { replaced: {
|
|
1843
|
+
source: old.source,
|
|
1844
|
+
target: old.target,
|
|
1845
|
+
...old.branch ? { branch: old.branch } : {}
|
|
1846
|
+
} } : {}
|
|
1847
|
+
};
|
|
1848
|
+
}
|
|
1849
|
+
/** Blocos que `id` pode ter como próximo: nunca ele mesmo nem um gatilho. */
|
|
1850
|
+
function nextCandidates(flow, id) {
|
|
1851
|
+
if (!flow.nodes.some((n) => n.id === id)) return [];
|
|
1852
|
+
return flow.nodes.filter((n) => n.id !== id && !isTrigger(n.type));
|
|
1853
|
+
}
|
|
1854
|
+
/** Onde o bloco está ligado: quem vem antes, quem vem depois e quantos cairiam junto. */
|
|
1855
|
+
function describeNodeLinks(flow, id) {
|
|
1856
|
+
const byId = new Map(flow.nodes.map((n) => [n.id, n]));
|
|
1857
|
+
const self = byId.get(id);
|
|
1858
|
+
const anteriores = [];
|
|
1859
|
+
const posteriores = [];
|
|
1860
|
+
for (const e of flow.edges) {
|
|
1861
|
+
if (e.target === id) {
|
|
1862
|
+
const node = byId.get(e.source);
|
|
1863
|
+
if (node) anteriores.push({
|
|
1864
|
+
node,
|
|
1865
|
+
...e.branch ? { saida: branchName(node, e.branch) } : {}
|
|
1866
|
+
});
|
|
1867
|
+
}
|
|
1868
|
+
if (e.source === id && self) {
|
|
1869
|
+
const node = byId.get(e.target);
|
|
1870
|
+
if (node) posteriores.push({
|
|
1871
|
+
node,
|
|
1872
|
+
...e.branch ? { saida: branchName(self, e.branch) } : {}
|
|
1873
|
+
});
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
return {
|
|
1877
|
+
anteriores,
|
|
1878
|
+
posteriores,
|
|
1879
|
+
descendentes: exclusiveDescendants(flow, id).length
|
|
1880
|
+
};
|
|
1881
|
+
}
|
|
1882
|
+
/**
|
|
1883
|
+
* Primeira posição livre a partir de `from`: desce em passos de `FLOW_GAP_Y` e, se a coluna
|
|
1884
|
+
* estiver cheia, passa à coluna da direita. Usa o tamanho medido dos blocos quando há, senão
|
|
1885
|
+
* a estimativa. Determinística: o mesmo fluxo e o mesmo ponto dão sempre a mesma posição.
|
|
1886
|
+
*/
|
|
1887
|
+
function findFreePosition(flow, size, from, measured = {}) {
|
|
1888
|
+
const rects = flow.nodes.map((n) => ({
|
|
1889
|
+
x: n.position.x,
|
|
1890
|
+
y: n.position.y,
|
|
1891
|
+
w: measured[n.id]?.w ?? 336,
|
|
1892
|
+
h: measured[n.id]?.h ?? estimateNodeHeight(n)
|
|
1893
|
+
}));
|
|
1894
|
+
const gap = 16;
|
|
1895
|
+
const free = (x, y) => rects.every((r) => x + size.w + gap <= r.x || r.x + r.w + gap <= x || y + size.h + gap <= r.y || r.y + r.h + gap <= y);
|
|
1896
|
+
for (let col = 0; col < 40; col++) {
|
|
1897
|
+
const x = from.x + col * (size.w + 96);
|
|
1898
|
+
for (let row = 0; row < 60; row++) {
|
|
1899
|
+
const y = from.y + row * 32;
|
|
1900
|
+
if (free(x, y)) return {
|
|
1901
|
+
x,
|
|
1902
|
+
y
|
|
1903
|
+
};
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
return {
|
|
1907
|
+
x: from.x,
|
|
1908
|
+
y: from.y
|
|
1909
|
+
};
|
|
1910
|
+
}
|
|
1911
|
+
/** Aviso de troca: "A ligação anterior de "X" para "Y" foi trocada." */
|
|
1912
|
+
function replacedMessage(flow, replaced) {
|
|
1913
|
+
const name = (id) => {
|
|
1914
|
+
const n = flow.nodes.find((x) => x.id === id);
|
|
1915
|
+
return n ? nodeName(n) : id;
|
|
1916
|
+
};
|
|
1917
|
+
return `A ligação anterior de "${name(replaced.source)}" para "${name(replaced.target)}" foi trocada.`;
|
|
1918
|
+
}
|
|
1919
|
+
//#endregion
|
|
1920
|
+
//#region src/components/ui/flow-config-panels.tsx
|
|
1921
|
+
var list = asList;
|
|
1922
|
+
var NONE = "__nenhum";
|
|
1923
|
+
function VariableInsert({ variables, label, onInsert }) {
|
|
1924
|
+
return /* @__PURE__ */ jsx(Select, {
|
|
1925
|
+
label: `Inserir variável em ${label}`,
|
|
1926
|
+
placeholder: "Inserir variável",
|
|
1927
|
+
disabled: variables.length === 0,
|
|
1928
|
+
value: null,
|
|
1929
|
+
options: variables.map((v) => ({
|
|
1930
|
+
value: v.id,
|
|
1931
|
+
label: v.id,
|
|
1932
|
+
description: v.label
|
|
1933
|
+
})),
|
|
1934
|
+
onChange: (v) => v && onInsert(`{{${v}}}`)
|
|
1935
|
+
});
|
|
1936
|
+
}
|
|
1937
|
+
/** Campo de texto que aceita variáveis `{{x}}`: o texto e o seletor que acrescenta uma no fim. */
|
|
1938
|
+
function TemplateField({ label, value, onChange, variables, multiline, help }) {
|
|
1939
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1940
|
+
className: "flex flex-col gap-2",
|
|
1941
|
+
children: [/* @__PURE__ */ jsx(Field, {
|
|
1942
|
+
label,
|
|
1943
|
+
span: "full",
|
|
1944
|
+
help,
|
|
1945
|
+
children: multiline ? /* @__PURE__ */ jsx(Textarea, {
|
|
1946
|
+
rows: 3,
|
|
1947
|
+
value,
|
|
1948
|
+
onChange
|
|
1949
|
+
}) : /* @__PURE__ */ jsx(Input, {
|
|
1950
|
+
value,
|
|
1951
|
+
onChange
|
|
1952
|
+
})
|
|
1953
|
+
}), /* @__PURE__ */ jsx(VariableInsert, {
|
|
1954
|
+
variables,
|
|
1955
|
+
label,
|
|
1956
|
+
onInsert: (token) => onChange(value + token)
|
|
1957
|
+
})]
|
|
1958
|
+
});
|
|
1959
|
+
}
|
|
1960
|
+
/** Escolha de uma variável do fluxo para o corpo do webhook: guarda `{{id}}`. */
|
|
1961
|
+
function VariableSelect({ label, value, variables, onChange }) {
|
|
1962
|
+
const current = /^\{\{\s*([\w.]+)\s*\}\}$/.exec(value)?.[1] ?? null;
|
|
1963
|
+
const options = variables.map((v) => ({
|
|
1964
|
+
value: v.id,
|
|
1965
|
+
label: v.id,
|
|
1966
|
+
description: v.label
|
|
1967
|
+
}));
|
|
1968
|
+
if (current && !variables.some((v) => v.id === current)) options.push({
|
|
1969
|
+
value: current,
|
|
1970
|
+
label: current
|
|
1971
|
+
});
|
|
1972
|
+
return /* @__PURE__ */ jsx(Select, {
|
|
1973
|
+
label,
|
|
1974
|
+
placeholder: "Variável",
|
|
1975
|
+
value: current,
|
|
1976
|
+
options,
|
|
1977
|
+
clearable: true,
|
|
1978
|
+
onChange: (v) => onChange(v ? `{{${v}}}` : "")
|
|
1979
|
+
});
|
|
1980
|
+
}
|
|
1981
|
+
function ConditionFields({ flow, node, variables, onApply }) {
|
|
1982
|
+
const rules = rulesOf(node);
|
|
1983
|
+
const fieldOptions = (rule) => {
|
|
1984
|
+
const opts = variables.map((v) => ({
|
|
1985
|
+
value: v.id,
|
|
1986
|
+
label: v.id,
|
|
1987
|
+
description: v.label
|
|
1988
|
+
}));
|
|
1989
|
+
if (rule.field && !opts.some((o) => o.value === rule.field)) opts.push({
|
|
1990
|
+
value: rule.field,
|
|
1991
|
+
label: rule.field,
|
|
1992
|
+
description: ""
|
|
1993
|
+
});
|
|
1994
|
+
return opts;
|
|
1995
|
+
};
|
|
1996
|
+
const save = (next, base = flow) => onApply(updateNodeData(base, node.id, { config: {
|
|
1997
|
+
...node.data.config,
|
|
1998
|
+
rules: next
|
|
1999
|
+
} }));
|
|
2000
|
+
const patch = (i, p) => save(rules.map((r, k) => k === i ? {
|
|
2001
|
+
...r,
|
|
2002
|
+
...p
|
|
2003
|
+
} : r));
|
|
2004
|
+
const add = () => save([...rules, {
|
|
2005
|
+
id: nextRuleId(rules),
|
|
2006
|
+
field: "",
|
|
2007
|
+
operator: "igual",
|
|
2008
|
+
value: ""
|
|
2009
|
+
}]);
|
|
2010
|
+
const remove = (i) => {
|
|
2011
|
+
const rule = rules[i];
|
|
2012
|
+
let next = flow;
|
|
2013
|
+
for (const e of flow.edges) if (e.source === node.id && e.branch === rule.id) next = removeEdge(next, e.id);
|
|
2014
|
+
save(rules.filter((_, k) => k !== i), next);
|
|
2015
|
+
};
|
|
2016
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2017
|
+
className: "flex flex-col gap-4",
|
|
2018
|
+
children: [
|
|
2019
|
+
rules.map((rule, i) => /* @__PURE__ */ jsxs("div", {
|
|
2020
|
+
role: "group",
|
|
2021
|
+
"aria-label": branchName(node, rule.id),
|
|
2022
|
+
className: "flex flex-col gap-2 rounded-block border p-3",
|
|
2023
|
+
children: [
|
|
2024
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2025
|
+
className: "flex items-center justify-between gap-2",
|
|
2026
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
2027
|
+
className: "text-sm font-medium",
|
|
2028
|
+
children: branchName(node, rule.id)
|
|
2029
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
2030
|
+
variant: "ghost",
|
|
2031
|
+
size: "sm",
|
|
2032
|
+
iconOnly: true,
|
|
2033
|
+
"aria-label": `Excluir ${branchName(node, rule.id)}`,
|
|
2034
|
+
className: "min-h-touch min-w-touch",
|
|
2035
|
+
icon: /* @__PURE__ */ jsx(Trash2, {}),
|
|
2036
|
+
onClick: () => remove(i)
|
|
2037
|
+
})]
|
|
2038
|
+
}),
|
|
2039
|
+
/* @__PURE__ */ jsx(Select, {
|
|
2040
|
+
label: `Campo da regra ${i + 1}`,
|
|
2041
|
+
placeholder: "Campo",
|
|
2042
|
+
options: fieldOptions(rule),
|
|
2043
|
+
value: rule.field || null,
|
|
2044
|
+
onChange: (v) => patch(i, { field: v ?? "" })
|
|
2045
|
+
}),
|
|
2046
|
+
/* @__PURE__ */ jsx(Select, {
|
|
2047
|
+
label: `Operador da regra ${i + 1}`,
|
|
2048
|
+
options: FLOW_RULE_OPERATORS.map((o) => ({
|
|
2049
|
+
value: o.value,
|
|
2050
|
+
label: o.label
|
|
2051
|
+
})),
|
|
2052
|
+
value: rule.operator,
|
|
2053
|
+
onChange: (v) => {
|
|
2054
|
+
const operator = v ?? "igual";
|
|
2055
|
+
patch(i, {
|
|
2056
|
+
operator,
|
|
2057
|
+
...operatorHasValue(operator) ? {} : { value: "" }
|
|
2058
|
+
});
|
|
2059
|
+
}
|
|
2060
|
+
}),
|
|
2061
|
+
operatorHasValue(rule.operator) && /* @__PURE__ */ jsx(Input, {
|
|
2062
|
+
"aria-label": `Valor da regra ${i + 1}`,
|
|
2063
|
+
placeholder: "Valor",
|
|
2064
|
+
value: rule.value,
|
|
2065
|
+
onChange: (v) => patch(i, { value: v })
|
|
2066
|
+
})
|
|
2067
|
+
]
|
|
2068
|
+
}, rule.id)),
|
|
2069
|
+
rules.length === 0 && /* @__PURE__ */ jsx(EmptyState, {
|
|
2070
|
+
size: "compact",
|
|
2071
|
+
title: "Nenhuma regra",
|
|
2072
|
+
description: "A condição ainda não tem regras."
|
|
2073
|
+
}),
|
|
2074
|
+
/* @__PURE__ */ jsx(Button, {
|
|
2075
|
+
variant: "outline",
|
|
2076
|
+
size: "sm",
|
|
2077
|
+
icon: /* @__PURE__ */ jsx(Plus, {}),
|
|
2078
|
+
onClick: add,
|
|
2079
|
+
children: "Adicionar regra"
|
|
2080
|
+
}),
|
|
2081
|
+
/* @__PURE__ */ jsxs("p", {
|
|
2082
|
+
className: "rounded-block bg-muted p-3 text-sm",
|
|
2083
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
2084
|
+
className: "font-medium",
|
|
2085
|
+
children: "Senão"
|
|
2086
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
2087
|
+
className: "text-muted-foreground",
|
|
2088
|
+
children: " segue quando nenhuma regra bater."
|
|
2089
|
+
})]
|
|
2090
|
+
})
|
|
2091
|
+
]
|
|
2092
|
+
});
|
|
2093
|
+
}
|
|
2094
|
+
function CodeBlock({ children }) {
|
|
2095
|
+
return /* @__PURE__ */ jsx("pre", {
|
|
2096
|
+
className: "rounded-block bg-muted p-3 text-xs break-words whitespace-pre-wrap text-foreground",
|
|
2097
|
+
children
|
|
2098
|
+
});
|
|
2099
|
+
}
|
|
2100
|
+
var ENDPOINT_ORIGIN = "https://app.exemplo.com";
|
|
2101
|
+
function WebhookInFields({ cfg, set }) {
|
|
2102
|
+
const received = cfg.received === true;
|
|
2103
|
+
const mapping = list(cfg.mapping);
|
|
2104
|
+
const sample = flattenPayload(FLOW_SAMPLE_PAYLOAD);
|
|
2105
|
+
const mapOf = (path) => mapping.find((m) => str(m.from) === path);
|
|
2106
|
+
const setMap = (path, to) => set({ mapping: [...mapping.filter((m) => str(m.from) !== path), ...to ? [{
|
|
2107
|
+
from: path,
|
|
2108
|
+
to
|
|
2109
|
+
}] : []] });
|
|
2110
|
+
const copy = async () => {
|
|
2111
|
+
try {
|
|
2112
|
+
await navigator.clipboard.writeText(`${ENDPOINT_ORIGIN}${str(cfg.path)}`);
|
|
2113
|
+
toast.success("Endereço copiado");
|
|
2114
|
+
} catch {
|
|
2115
|
+
toast.error("Não foi possível copiar o endereço");
|
|
2116
|
+
}
|
|
2117
|
+
};
|
|
2118
|
+
return /* @__PURE__ */ jsx(Tabs, {
|
|
2119
|
+
variant: "pill",
|
|
2120
|
+
"aria-label": "Webhook de entrada",
|
|
2121
|
+
items: [{
|
|
2122
|
+
value: "mapeamento",
|
|
2123
|
+
label: "Mapeamento",
|
|
2124
|
+
content: /* @__PURE__ */ jsxs("div", {
|
|
2125
|
+
className: "flex flex-col gap-4 pt-4",
|
|
2126
|
+
children: [
|
|
2127
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2128
|
+
className: "flex items-center gap-2",
|
|
2129
|
+
children: [/* @__PURE__ */ jsx(Button, {
|
|
2130
|
+
variant: "outline",
|
|
2131
|
+
size: "sm",
|
|
2132
|
+
onClick: () => set({ received: true }),
|
|
2133
|
+
children: "Simular chegada"
|
|
2134
|
+
}), /* @__PURE__ */ jsx(InfoHint, {
|
|
2135
|
+
title: "Mapeamento de campos",
|
|
2136
|
+
children: "Simule a chegada de dados de exemplo e ligue cada campo recebido a um campo do sistema. Os campos mapeados viram variáveis para os blocos seguintes."
|
|
2137
|
+
})]
|
|
2138
|
+
}),
|
|
2139
|
+
!received && /* @__PURE__ */ jsx(EmptyState, {
|
|
2140
|
+
size: "compact",
|
|
2141
|
+
title: "Nenhum dado recebido",
|
|
2142
|
+
description: "Os campos aparecem depois da chegada simulada."
|
|
2143
|
+
}),
|
|
2144
|
+
received && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(CodeBlock, { children: JSON.stringify(FLOW_SAMPLE_PAYLOAD, null, 2) }), /* @__PURE__ */ jsx("ul", {
|
|
2145
|
+
"aria-label": "Campos recebidos",
|
|
2146
|
+
className: "flex flex-col gap-3",
|
|
2147
|
+
children: sample.map((f) => /* @__PURE__ */ jsxs("li", {
|
|
2148
|
+
className: "flex flex-col gap-1",
|
|
2149
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
2150
|
+
className: "flex items-center justify-between gap-2 text-xs",
|
|
2151
|
+
children: [/* @__PURE__ */ jsx("code", {
|
|
2152
|
+
className: "font-medium",
|
|
2153
|
+
children: f.path
|
|
2154
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
2155
|
+
className: "truncate text-muted-foreground",
|
|
2156
|
+
children: f.value
|
|
2157
|
+
})]
|
|
2158
|
+
}), /* @__PURE__ */ jsx(Select, {
|
|
2159
|
+
label: `Campo do sistema para ${f.path}`,
|
|
2160
|
+
placeholder: "Não mapear",
|
|
2161
|
+
searchable: true,
|
|
2162
|
+
clearable: true,
|
|
2163
|
+
options: FLOW_SYSTEM_FIELDS.map((o) => ({
|
|
2164
|
+
value: o.value,
|
|
2165
|
+
label: o.label
|
|
2166
|
+
})),
|
|
2167
|
+
value: str(mapOf(f.path)?.to) || null,
|
|
2168
|
+
onChange: (v) => setMap(f.path, v)
|
|
2169
|
+
})]
|
|
2170
|
+
}, f.path))
|
|
2171
|
+
})] })
|
|
2172
|
+
]
|
|
2173
|
+
})
|
|
2174
|
+
}, {
|
|
2175
|
+
value: "configuracoes",
|
|
2176
|
+
label: "Configurações",
|
|
2177
|
+
content: /* @__PURE__ */ jsxs("div", {
|
|
2178
|
+
className: "flex flex-col gap-4 pt-4",
|
|
2179
|
+
children: [
|
|
2180
|
+
/* @__PURE__ */ jsx(Field, {
|
|
2181
|
+
label: "Método",
|
|
2182
|
+
span: "full",
|
|
2183
|
+
children: /* @__PURE__ */ jsx(Select, {
|
|
2184
|
+
label: "Método",
|
|
2185
|
+
options: ["POST", "PUT"].map((m) => ({
|
|
2186
|
+
value: m,
|
|
2187
|
+
label: m
|
|
2188
|
+
})),
|
|
2189
|
+
value: str(cfg.method) || "POST",
|
|
2190
|
+
onChange: (v) => set({ method: v ?? "POST" })
|
|
2191
|
+
})
|
|
2192
|
+
}),
|
|
2193
|
+
/* @__PURE__ */ jsx(Field, {
|
|
2194
|
+
label: "Endereço",
|
|
2195
|
+
span: "full",
|
|
2196
|
+
required: true,
|
|
2197
|
+
children: /* @__PURE__ */ jsx(Input, {
|
|
2198
|
+
value: str(cfg.path),
|
|
2199
|
+
onChange: (v) => set({ path: v })
|
|
2200
|
+
})
|
|
2201
|
+
}),
|
|
2202
|
+
/* @__PURE__ */ jsx(Button, {
|
|
2203
|
+
variant: "outline",
|
|
2204
|
+
size: "sm",
|
|
2205
|
+
icon: /* @__PURE__ */ jsx(Copy, {}),
|
|
2206
|
+
onClick: () => void copy(),
|
|
2207
|
+
children: "Copiar endereço"
|
|
2208
|
+
}),
|
|
2209
|
+
/* @__PURE__ */ jsx(Field, {
|
|
2210
|
+
label: "Código de resposta",
|
|
2211
|
+
span: "full",
|
|
2212
|
+
children: /* @__PURE__ */ jsx(Select, {
|
|
2213
|
+
label: "Código de resposta",
|
|
2214
|
+
options: [
|
|
2215
|
+
{
|
|
2216
|
+
value: "200",
|
|
2217
|
+
label: "200 OK"
|
|
2218
|
+
},
|
|
2219
|
+
{
|
|
2220
|
+
value: "202",
|
|
2221
|
+
label: "202 Accepted"
|
|
2222
|
+
},
|
|
2223
|
+
{
|
|
2224
|
+
value: "204",
|
|
2225
|
+
label: "204 No Content"
|
|
2226
|
+
}
|
|
2227
|
+
],
|
|
2228
|
+
value: String(cfg.responseCode ?? 200),
|
|
2229
|
+
onChange: (v) => set({ responseCode: Number(v ?? 200) })
|
|
2230
|
+
})
|
|
2231
|
+
})
|
|
2232
|
+
]
|
|
2233
|
+
})
|
|
2234
|
+
}]
|
|
2235
|
+
});
|
|
2236
|
+
}
|
|
2237
|
+
function WebhookOutFields({ flow, cfg, variables, set }) {
|
|
2238
|
+
const [simulated, setSimulated] = useState(false);
|
|
2239
|
+
const body = list(cfg.body);
|
|
2240
|
+
const headers = list(cfg.headers);
|
|
2241
|
+
const patchBody = (i, p) => set({ body: body.map((b, k) => k === i ? {
|
|
2242
|
+
...b,
|
|
2243
|
+
...p
|
|
2244
|
+
} : b) });
|
|
2245
|
+
const patchHeader = (i, p) => set({ headers: headers.map((h, k) => k === i ? {
|
|
2246
|
+
...h,
|
|
2247
|
+
...p
|
|
2248
|
+
} : h) });
|
|
2249
|
+
const request = buildWebhookRequest(cfg, flowSampleData(flow));
|
|
2250
|
+
return /* @__PURE__ */ jsx(Tabs, {
|
|
2251
|
+
variant: "pill",
|
|
2252
|
+
"aria-label": "Webhook de saída",
|
|
2253
|
+
items: [{
|
|
2254
|
+
value: "corpo",
|
|
2255
|
+
label: "Corpo",
|
|
2256
|
+
content: /* @__PURE__ */ jsxs("div", {
|
|
2257
|
+
className: "flex flex-col gap-4 pt-4",
|
|
2258
|
+
children: [
|
|
2259
|
+
body.map((b, i) => /* @__PURE__ */ jsxs("div", {
|
|
2260
|
+
role: "group",
|
|
2261
|
+
"aria-label": `Campo enviado ${i + 1}`,
|
|
2262
|
+
className: "flex flex-col gap-2 rounded-block border p-3",
|
|
2263
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
2264
|
+
className: "flex items-center gap-2",
|
|
2265
|
+
children: [/* @__PURE__ */ jsx(Input, {
|
|
2266
|
+
"aria-label": `Nome do campo ${i + 1}`,
|
|
2267
|
+
placeholder: "Nome do campo",
|
|
2268
|
+
value: str(b.key),
|
|
2269
|
+
onChange: (v) => patchBody(i, { key: v })
|
|
2270
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
2271
|
+
variant: "ghost",
|
|
2272
|
+
size: "sm",
|
|
2273
|
+
iconOnly: true,
|
|
2274
|
+
"aria-label": `Excluir campo ${i + 1}`,
|
|
2275
|
+
className: "min-h-touch min-w-touch shrink-0",
|
|
2276
|
+
icon: /* @__PURE__ */ jsx(Trash2, {}),
|
|
2277
|
+
onClick: () => set({ body: body.filter((_, k) => k !== i) })
|
|
2278
|
+
})]
|
|
2279
|
+
}), /* @__PURE__ */ jsx(VariableSelect, {
|
|
2280
|
+
label: `Variável do campo ${i + 1}`,
|
|
2281
|
+
value: str(b.value),
|
|
2282
|
+
variables,
|
|
2283
|
+
onChange: (token) => patchBody(i, { value: token })
|
|
2284
|
+
})]
|
|
2285
|
+
}, i)),
|
|
2286
|
+
/* @__PURE__ */ jsx(Button, {
|
|
2287
|
+
variant: "outline",
|
|
2288
|
+
size: "sm",
|
|
2289
|
+
icon: /* @__PURE__ */ jsx(Plus, {}),
|
|
2290
|
+
onClick: () => set({ body: [...body, {
|
|
2291
|
+
key: "",
|
|
2292
|
+
value: ""
|
|
2293
|
+
}] }),
|
|
2294
|
+
children: "Adicionar campo"
|
|
2295
|
+
}),
|
|
2296
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2297
|
+
className: "flex items-center gap-2",
|
|
2298
|
+
children: [/* @__PURE__ */ jsx(Button, {
|
|
2299
|
+
variant: "outline",
|
|
2300
|
+
size: "sm",
|
|
2301
|
+
onClick: () => setSimulated(true),
|
|
2302
|
+
children: "Simular envio"
|
|
2303
|
+
}), /* @__PURE__ */ jsx(InfoHint, {
|
|
2304
|
+
title: "Simulação do envio",
|
|
2305
|
+
children: "Nada é enviado de verdade: a requisição é montada com os dados de exemplo e a resposta é uma amostra."
|
|
2306
|
+
})]
|
|
2307
|
+
}),
|
|
2308
|
+
simulated && /* @__PURE__ */ jsxs("div", {
|
|
2309
|
+
role: "region",
|
|
2310
|
+
"aria-label": "Simulação do envio",
|
|
2311
|
+
className: "flex flex-col gap-2",
|
|
2312
|
+
children: [/* @__PURE__ */ jsx(CodeBlock, { children: `${request.method} ${request.url}\n${JSON.stringify(request.body, null, 2)}` }), /* @__PURE__ */ jsx(CodeBlock, { children: "200 OK { \"id\": \"crm-88\" }" })]
|
|
2313
|
+
})
|
|
2314
|
+
]
|
|
2315
|
+
})
|
|
2316
|
+
}, {
|
|
2317
|
+
value: "configuracoes",
|
|
2318
|
+
label: "Configurações",
|
|
2319
|
+
content: /* @__PURE__ */ jsxs("div", {
|
|
2320
|
+
className: "flex flex-col gap-4 pt-4",
|
|
2321
|
+
children: [
|
|
2322
|
+
/* @__PURE__ */ jsx(Field, {
|
|
2323
|
+
label: "Método",
|
|
2324
|
+
span: "full",
|
|
2325
|
+
children: /* @__PURE__ */ jsx(Select, {
|
|
2326
|
+
label: "Método",
|
|
2327
|
+
options: [
|
|
2328
|
+
"POST",
|
|
2329
|
+
"PUT",
|
|
2330
|
+
"PATCH"
|
|
2331
|
+
].map((m) => ({
|
|
2332
|
+
value: m,
|
|
2333
|
+
label: m
|
|
2334
|
+
})),
|
|
2335
|
+
value: str(cfg.method) || "POST",
|
|
2336
|
+
onChange: (v) => set({ method: v ?? "POST" })
|
|
2337
|
+
})
|
|
2338
|
+
}),
|
|
2339
|
+
/* @__PURE__ */ jsx(Field, {
|
|
2340
|
+
label: "Endereço",
|
|
2341
|
+
span: "full",
|
|
2342
|
+
required: true,
|
|
2343
|
+
children: /* @__PURE__ */ jsx(Input, {
|
|
2344
|
+
value: str(cfg.url),
|
|
2345
|
+
onChange: (v) => set({ url: v })
|
|
2346
|
+
})
|
|
2347
|
+
}),
|
|
2348
|
+
headers.map((h, i) => /* @__PURE__ */ jsxs("div", {
|
|
2349
|
+
role: "group",
|
|
2350
|
+
"aria-label": `Cabeçalho ${i + 1}`,
|
|
2351
|
+
className: "flex flex-col gap-2 rounded-block border p-3",
|
|
2352
|
+
children: [/* @__PURE__ */ jsx(Input, {
|
|
2353
|
+
"aria-label": `Nome do cabeçalho ${i + 1}`,
|
|
2354
|
+
placeholder: "Nome",
|
|
2355
|
+
value: str(h.name),
|
|
2356
|
+
onChange: (v) => patchHeader(i, { name: v })
|
|
2357
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
2358
|
+
className: "flex items-center gap-2",
|
|
2359
|
+
children: [/* @__PURE__ */ jsx(Input, {
|
|
2360
|
+
"aria-label": `Valor do cabeçalho ${i + 1}`,
|
|
2361
|
+
placeholder: "Valor",
|
|
2362
|
+
value: str(h.value),
|
|
2363
|
+
onChange: (v) => patchHeader(i, { value: v })
|
|
2364
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
2365
|
+
variant: "ghost",
|
|
2366
|
+
size: "sm",
|
|
2367
|
+
iconOnly: true,
|
|
2368
|
+
"aria-label": `Excluir cabeçalho ${i + 1}`,
|
|
2369
|
+
className: "min-h-touch min-w-touch shrink-0",
|
|
2370
|
+
icon: /* @__PURE__ */ jsx(Trash2, {}),
|
|
2371
|
+
onClick: () => set({ headers: headers.filter((_, k) => k !== i) })
|
|
2372
|
+
})]
|
|
2373
|
+
})]
|
|
2374
|
+
}, i)),
|
|
2375
|
+
/* @__PURE__ */ jsx(Button, {
|
|
2376
|
+
variant: "outline",
|
|
2377
|
+
size: "sm",
|
|
2378
|
+
icon: /* @__PURE__ */ jsx(Plus, {}),
|
|
2379
|
+
onClick: () => set({ headers: [...headers, {
|
|
2380
|
+
name: "",
|
|
2381
|
+
value: ""
|
|
2382
|
+
}] }),
|
|
2383
|
+
children: "Adicionar cabeçalho"
|
|
2384
|
+
})
|
|
2385
|
+
]
|
|
2386
|
+
})
|
|
2387
|
+
}]
|
|
943
2388
|
});
|
|
2389
|
+
}
|
|
2390
|
+
function LinkFields({ flow, node, onApply }) {
|
|
944
2391
|
const options = [{
|
|
945
2392
|
value: NONE,
|
|
946
2393
|
label: "Nenhum"
|
|
@@ -948,240 +2395,644 @@ function BlockDrawer({ flow, nodeId, open, onOpenChange, onApply, onDelete }) {
|
|
|
948
2395
|
value: n.id,
|
|
949
2396
|
label: `${FLOW_TYPE_NAME[n.type]}: ${nodeName(n)}`
|
|
950
2397
|
}))];
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
label:
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
options,
|
|
972
|
-
value: draft[key],
|
|
973
|
-
onChange: (v) => set({ [key]: v ?? NONE })
|
|
2398
|
+
return /* @__PURE__ */ jsx("div", {
|
|
2399
|
+
className: "flex flex-col gap-3",
|
|
2400
|
+
children: outputIds(node).map((out) => {
|
|
2401
|
+
const label = out ? `Próximo bloco: ${branchName(node, out)}` : "Próximo bloco";
|
|
2402
|
+
const current = flow.edges.find((e) => e.source === node.id && (e.branch ?? void 0) === out);
|
|
2403
|
+
return /* @__PURE__ */ jsx(Field, {
|
|
2404
|
+
label,
|
|
2405
|
+
span: "full",
|
|
2406
|
+
children: /* @__PURE__ */ jsx(Select, {
|
|
2407
|
+
label,
|
|
2408
|
+
options,
|
|
2409
|
+
value: current?.target ?? NONE,
|
|
2410
|
+
onChange: (v) => {
|
|
2411
|
+
if (!v || v === NONE) return onApply(clearOutput(flow, node.id, out));
|
|
2412
|
+
const r = connectNodes(flow, node.id, v, out);
|
|
2413
|
+
if (!r.ok) return onApply(flow, r.reason);
|
|
2414
|
+
onApply(r.flow, r.replaced ? replacedMessage(flow, r.replaced) : void 0);
|
|
2415
|
+
}
|
|
2416
|
+
})
|
|
2417
|
+
}, out ?? "unica");
|
|
974
2418
|
})
|
|
975
2419
|
});
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
2420
|
+
}
|
|
2421
|
+
function BlockConfigPanel({ flow, nodeId, onApply, onClose }) {
|
|
2422
|
+
const uid = useId();
|
|
2423
|
+
const node = flow.nodes.find((n) => n.id === nodeId);
|
|
2424
|
+
if (!node) return null;
|
|
2425
|
+
const cfg = node.data.config ?? {};
|
|
2426
|
+
const variables = flowVariables(flow);
|
|
2427
|
+
const set = (patch) => onApply(updateNodeData(flow, node.id, { config: {
|
|
2428
|
+
...cfg,
|
|
2429
|
+
...patch
|
|
2430
|
+
} }));
|
|
2431
|
+
const ctx = {
|
|
2432
|
+
flow,
|
|
2433
|
+
node,
|
|
2434
|
+
cfg,
|
|
2435
|
+
variables,
|
|
2436
|
+
set
|
|
2437
|
+
};
|
|
2438
|
+
const t = (key, label, opts = {}) => /* @__PURE__ */ jsx(TemplateField, {
|
|
2439
|
+
label,
|
|
2440
|
+
multiline: opts.multiline,
|
|
2441
|
+
value: str(cfg[key]),
|
|
2442
|
+
variables,
|
|
2443
|
+
onChange: (v) => set({ [key]: v })
|
|
2444
|
+
}, `${uid}-${key}`);
|
|
2445
|
+
let fields = null;
|
|
2446
|
+
switch (node.type) {
|
|
2447
|
+
case "webhook-entrada":
|
|
2448
|
+
fields = /* @__PURE__ */ jsx(WebhookInFields, { ...ctx });
|
|
2449
|
+
break;
|
|
2450
|
+
case "webhook-saida":
|
|
2451
|
+
fields = /* @__PURE__ */ jsx(WebhookOutFields, { ...ctx });
|
|
2452
|
+
break;
|
|
2453
|
+
case "agendamento":
|
|
2454
|
+
fields = /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Field, {
|
|
2455
|
+
label: "Frequência",
|
|
2456
|
+
span: "full",
|
|
2457
|
+
children: /* @__PURE__ */ jsx(Select, {
|
|
2458
|
+
label: "Frequência",
|
|
2459
|
+
options: [
|
|
2460
|
+
{
|
|
2461
|
+
value: "diario",
|
|
2462
|
+
label: "Diário"
|
|
2463
|
+
},
|
|
2464
|
+
{
|
|
2465
|
+
value: "semanal",
|
|
2466
|
+
label: "Semanal"
|
|
2467
|
+
},
|
|
2468
|
+
{
|
|
2469
|
+
value: "mensal",
|
|
2470
|
+
label: "Mensal"
|
|
2471
|
+
}
|
|
2472
|
+
],
|
|
2473
|
+
value: str(cfg.frequency) || "diario",
|
|
2474
|
+
onChange: (v) => set({ frequency: v ?? "diario" })
|
|
2475
|
+
})
|
|
2476
|
+
}), /* @__PURE__ */ jsx(Field, {
|
|
2477
|
+
label: "Horário",
|
|
2478
|
+
span: "full",
|
|
2479
|
+
required: true,
|
|
2480
|
+
children: /* @__PURE__ */ jsx(Input, {
|
|
2481
|
+
type: "time",
|
|
2482
|
+
value: str(cfg.time),
|
|
2483
|
+
onChange: (v) => set({ time: v })
|
|
2484
|
+
})
|
|
2485
|
+
})] });
|
|
2486
|
+
break;
|
|
2487
|
+
case "registro-criado":
|
|
2488
|
+
fields = /* @__PURE__ */ jsx(RadioGroup, {
|
|
2489
|
+
"aria-label": "Registro",
|
|
2490
|
+
options: [
|
|
2491
|
+
{
|
|
2492
|
+
value: "cliente",
|
|
2493
|
+
label: "Cliente"
|
|
2494
|
+
},
|
|
2495
|
+
{
|
|
2496
|
+
value: "pedido",
|
|
2497
|
+
label: "Pedido"
|
|
2498
|
+
},
|
|
2499
|
+
{
|
|
2500
|
+
value: "negocio",
|
|
2501
|
+
label: "Negócio"
|
|
2502
|
+
}
|
|
2503
|
+
],
|
|
2504
|
+
value: str(cfg.record) || "cliente",
|
|
2505
|
+
onChange: (v) => set({ record: v })
|
|
2506
|
+
});
|
|
2507
|
+
break;
|
|
2508
|
+
case "whatsapp":
|
|
2509
|
+
fields = /* @__PURE__ */ jsxs(Fragment$1, { children: [t("to", "Para"), t("message", "Mensagem", { multiline: true })] });
|
|
2510
|
+
break;
|
|
2511
|
+
case "email":
|
|
2512
|
+
fields = /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
2513
|
+
t("to", "Para"),
|
|
2514
|
+
t("subject", "Assunto"),
|
|
2515
|
+
t("message", "Mensagem", { multiline: true })
|
|
2516
|
+
] });
|
|
2517
|
+
break;
|
|
2518
|
+
case "tarefa":
|
|
2519
|
+
fields = /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
2520
|
+
t("title", "Título"),
|
|
1002
2521
|
/* @__PURE__ */ jsx(Field, {
|
|
1003
|
-
label: "
|
|
2522
|
+
label: "Responsável",
|
|
1004
2523
|
span: "full",
|
|
1005
|
-
error: draft.label.trim() === "" ? "Informe um rótulo." : void 0,
|
|
1006
2524
|
children: /* @__PURE__ */ jsx(Input, {
|
|
1007
|
-
value:
|
|
1008
|
-
onChange: (v) => set({
|
|
2525
|
+
value: str(cfg.assignee),
|
|
2526
|
+
onChange: (v) => set({ assignee: v })
|
|
1009
2527
|
})
|
|
1010
2528
|
}),
|
|
1011
|
-
|
|
1012
|
-
label: "
|
|
2529
|
+
/* @__PURE__ */ jsx(Field, {
|
|
2530
|
+
label: "Prazo em dias",
|
|
1013
2531
|
span: "full",
|
|
1014
|
-
error: durationError,
|
|
1015
2532
|
children: /* @__PURE__ */ jsx(Input, {
|
|
1016
|
-
value: draft.duration,
|
|
1017
2533
|
inputMode: "numeric",
|
|
1018
|
-
|
|
2534
|
+
value: String(cfg.dueDays ?? ""),
|
|
2535
|
+
onChange: (v) => set({ dueDays: Number(v.replace(/\D/g, "")) || 0 })
|
|
1019
2536
|
})
|
|
1020
|
-
})
|
|
2537
|
+
})
|
|
2538
|
+
] });
|
|
2539
|
+
break;
|
|
2540
|
+
case "espera":
|
|
2541
|
+
fields = /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Field, {
|
|
2542
|
+
label: "Duração",
|
|
2543
|
+
span: "full",
|
|
2544
|
+
error: typeof cfg.duration === "number" && cfg.duration > 0 ? void 0 : "Informe um número maior que zero.",
|
|
2545
|
+
children: /* @__PURE__ */ jsx(Input, {
|
|
2546
|
+
inputMode: "numeric",
|
|
2547
|
+
value: cfg.duration === void 0 ? "" : String(cfg.duration),
|
|
2548
|
+
onChange: (v) => set({ duration: Number(v.replace(/\D/g, "")) || 0 })
|
|
2549
|
+
})
|
|
2550
|
+
}), /* @__PURE__ */ jsx(Field, {
|
|
2551
|
+
label: "Unidade",
|
|
2552
|
+
span: "full",
|
|
2553
|
+
children: /* @__PURE__ */ jsx(Select, {
|
|
1021
2554
|
label: "Unidade",
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
label:
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
2555
|
+
options: FLOW_WAIT_UNITS.map((u) => ({
|
|
2556
|
+
value: u,
|
|
2557
|
+
label: u
|
|
2558
|
+
})),
|
|
2559
|
+
value: str(cfg.unit) || "minutos",
|
|
2560
|
+
onChange: (v) => set({ unit: v ?? "minutos" })
|
|
2561
|
+
})
|
|
2562
|
+
})] });
|
|
2563
|
+
break;
|
|
2564
|
+
case "condicao": fields = /* @__PURE__ */ jsx(ConditionFields, {
|
|
2565
|
+
...ctx,
|
|
2566
|
+
onApply
|
|
2567
|
+
});
|
|
2568
|
+
}
|
|
2569
|
+
return /* @__PURE__ */ jsxs(FlowPanelShell, {
|
|
2570
|
+
title: FLOW_BLOCKS[node.type].name,
|
|
2571
|
+
onClose,
|
|
2572
|
+
closeLabel: "Fechar configuração",
|
|
2573
|
+
children: [
|
|
2574
|
+
/* @__PURE__ */ jsx(Field, {
|
|
2575
|
+
label: "Nome do bloco",
|
|
2576
|
+
span: "full",
|
|
2577
|
+
children: /* @__PURE__ */ jsx(Input, {
|
|
2578
|
+
value: node.data.label,
|
|
2579
|
+
onChange: (v) => onApply(updateNodeData(flow, node.id, { label: v }))
|
|
2580
|
+
})
|
|
2581
|
+
}),
|
|
2582
|
+
fields,
|
|
2583
|
+
/* @__PURE__ */ jsxs("section", {
|
|
2584
|
+
"aria-label": "Ligações",
|
|
2585
|
+
className: "flex flex-col gap-3 border-t pt-4",
|
|
2586
|
+
children: [/* @__PURE__ */ jsx("h3", {
|
|
2587
|
+
className: "text-xs font-medium text-muted-foreground",
|
|
2588
|
+
children: "Ligações"
|
|
2589
|
+
}), /* @__PURE__ */ jsx(LinkFields, {
|
|
2590
|
+
flow,
|
|
2591
|
+
node,
|
|
2592
|
+
onApply
|
|
2593
|
+
})]
|
|
2594
|
+
})
|
|
2595
|
+
]
|
|
2596
|
+
});
|
|
2597
|
+
}
|
|
2598
|
+
//#endregion
|
|
2599
|
+
//#region src/lib/flow-sim.ts
|
|
2600
|
+
function resetSimulation() {
|
|
2601
|
+
return {
|
|
2602
|
+
status: "idle",
|
|
2603
|
+
steps: [],
|
|
2604
|
+
currentId: null,
|
|
2605
|
+
issues: []
|
|
2606
|
+
};
|
|
2607
|
+
}
|
|
2608
|
+
/** Começa no gatilho. Fluxo inválido não inicia: devolve `error` com os problemas. */
|
|
2609
|
+
function startSimulation(input) {
|
|
2610
|
+
const flow = migrateFlow(input);
|
|
2611
|
+
const issues = validateFlow(flow);
|
|
2612
|
+
if (issues.length > 0) return {
|
|
2613
|
+
status: "error",
|
|
2614
|
+
steps: [],
|
|
2615
|
+
currentId: null,
|
|
2616
|
+
issues
|
|
2617
|
+
};
|
|
2618
|
+
return {
|
|
2619
|
+
status: "running",
|
|
2620
|
+
steps: [],
|
|
2621
|
+
currentId: flow.nodes.find((n) => isTrigger(n.type)).id,
|
|
2622
|
+
issues: []
|
|
2623
|
+
};
|
|
2624
|
+
}
|
|
2625
|
+
/** Avalia uma regra contra os dados de exemplo; campo que não existe conta como vazio. */
|
|
2626
|
+
function evaluateRule(rule, data) {
|
|
2627
|
+
const actual = data[rule.field] ?? "";
|
|
2628
|
+
const wanted = rule.value;
|
|
2629
|
+
switch (rule.operator) {
|
|
2630
|
+
case "igual": return actual === wanted;
|
|
2631
|
+
case "diferente": return actual !== wanted;
|
|
2632
|
+
case "maior":
|
|
2633
|
+
case "menor": {
|
|
2634
|
+
const a = Number(actual);
|
|
2635
|
+
const b = Number(wanted);
|
|
2636
|
+
if (actual.trim() === "" || wanted.trim() === "" || Number.isNaN(a) || Number.isNaN(b)) return false;
|
|
2637
|
+
return rule.operator === "maior" ? a > b : a < b;
|
|
2638
|
+
}
|
|
2639
|
+
case "contem": return actual.includes(wanted);
|
|
2640
|
+
case "vazio": return actual === "";
|
|
2641
|
+
case "nao-vazio": return actual !== "";
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
var FREQUENCY = {
|
|
2645
|
+
diario: "todo dia",
|
|
2646
|
+
semanal: "toda semana",
|
|
2647
|
+
mensal: "todo mês"
|
|
2648
|
+
};
|
|
2649
|
+
/** A regra que bate primeiro (ou `null`, e o caminho segue por "Senão"). */
|
|
2650
|
+
function matchedRule(node, data) {
|
|
2651
|
+
const rules = rulesOf(node);
|
|
2652
|
+
const index = rules.findIndex((r) => evaluateRule(r, data));
|
|
2653
|
+
return {
|
|
2654
|
+
rule: index < 0 ? null : rules[index],
|
|
2655
|
+
index
|
|
2656
|
+
};
|
|
2657
|
+
}
|
|
2658
|
+
function conditionDetail(node, data) {
|
|
2659
|
+
const { rule, index } = matchedRule(node, data);
|
|
2660
|
+
return rule ? `Regra ${index + 1} bateu: ${ruleSummary(rule)}, seguiu por ela` : "Nenhuma regra bateu, seguiu por Senão";
|
|
2661
|
+
}
|
|
2662
|
+
function blockDetail(node, data) {
|
|
2663
|
+
const c = node.data.config;
|
|
2664
|
+
switch (node.type) {
|
|
2665
|
+
case "webhook-entrada": return `Receberia ${str(c?.method) || "POST"} em ${str(c?.path)}`;
|
|
2666
|
+
case "agendamento": return `Iniciaria ${FREQUENCY[str(c?.frequency)] ?? ""} às ${str(c?.time)}`.replace(" ", " ");
|
|
2667
|
+
case "registro-criado": return `Iniciaria quando um ${(RECORD_LABEL[str(c?.record)] ?? "registro").toLowerCase()} fosse criado`;
|
|
2668
|
+
case "whatsapp": return `Enviaria WhatsApp para ${renderTemplate(str(c?.to), data)}: "${renderTemplate(str(c?.message), data)}"`;
|
|
2669
|
+
case "email": return `Enviaria e-mail para ${renderTemplate(str(c?.to), data)}, assunto "${renderTemplate(str(c?.subject), data)}": "${renderTemplate(str(c?.message), data)}"`;
|
|
2670
|
+
case "espera": return describeWait(c);
|
|
2671
|
+
case "webhook-saida": {
|
|
2672
|
+
const r = buildWebhookRequest(c, data);
|
|
2673
|
+
return `Enviaria ${r.method} ${r.url} com ${JSON.stringify(r.body)}`;
|
|
2674
|
+
}
|
|
2675
|
+
case "tarefa": {
|
|
2676
|
+
const days = typeof c?.dueDays === "number" ? c.dueDays : 0;
|
|
2677
|
+
return `Criaria a tarefa "${renderTemplate(str(c?.title), data)}" para ${str(c?.assignee) || "ninguém"} com prazo de ${days} ${days === 1 ? "dia" : "dias"}`;
|
|
2678
|
+
}
|
|
2679
|
+
case "condicao": return conditionDetail(node, data);
|
|
2680
|
+
default: return "Executaria o bloco";
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2683
|
+
var stepOf = (node, detail) => ({
|
|
2684
|
+
nodeId: node.id,
|
|
2685
|
+
type: node.type,
|
|
2686
|
+
label: nodeName(node),
|
|
2687
|
+
status: "succeeded",
|
|
2688
|
+
detail
|
|
2689
|
+
});
|
|
2690
|
+
/** Roda só um bloco, com os mesmos dados de exemplo, e devolve o passo (o play do card). */
|
|
2691
|
+
function simulateBlock(input, nodeId) {
|
|
2692
|
+
const flow = migrateFlow(input);
|
|
2693
|
+
const node = flow.nodes.find((n) => n.id === nodeId);
|
|
2694
|
+
return node ? stepOf(node, blockDetail(node, flowSampleData(flow))) : null;
|
|
2695
|
+
}
|
|
2696
|
+
/** Executa o bloco ativo (um passo) e passa ao próximo. A condição escolhe sozinha a saída. */
|
|
2697
|
+
function advanceSimulation(input, state) {
|
|
2698
|
+
if (state.status !== "running" || !state.currentId) return state;
|
|
2699
|
+
const flow = migrateFlow(input);
|
|
2700
|
+
const node = flow.nodes.find((n) => n.id === state.currentId);
|
|
2701
|
+
const fail = () => ({
|
|
2702
|
+
...state,
|
|
2703
|
+
status: "error",
|
|
2704
|
+
currentId: null,
|
|
2705
|
+
issues: [{
|
|
2706
|
+
code: "fluxo-mudou",
|
|
2707
|
+
message: "O fluxo mudou durante o teste. Reinicie para testar de novo."
|
|
2708
|
+
}]
|
|
2709
|
+
});
|
|
2710
|
+
if (!node) return fail();
|
|
2711
|
+
const data = flowSampleData(flow);
|
|
2712
|
+
let output;
|
|
2713
|
+
if (node.type === "condicao") output = matchedRule(node, data).rule?.id ?? "senao";
|
|
2714
|
+
const edge = flow.edges.find((e) => e.source === node.id && (e.branch ?? void 0) === output);
|
|
2715
|
+
const target = edge ? flow.nodes.find((n) => n.id === edge.target) : void 0;
|
|
2716
|
+
if (edge && !target) return fail();
|
|
2717
|
+
const detail = blockDetail(node, data);
|
|
2718
|
+
const step = stepOf(node, target ? detail : `${detail}. Caminho encerrado`);
|
|
2719
|
+
const steps = [...state.steps, step];
|
|
2720
|
+
if (!target) return {
|
|
2721
|
+
status: "finished",
|
|
2722
|
+
steps,
|
|
2723
|
+
currentId: null,
|
|
2724
|
+
issues: []
|
|
2725
|
+
};
|
|
2726
|
+
return {
|
|
2727
|
+
status: "running",
|
|
2728
|
+
steps,
|
|
2729
|
+
currentId: target.id,
|
|
2730
|
+
issues: []
|
|
2731
|
+
};
|
|
2732
|
+
}
|
|
2733
|
+
//#endregion
|
|
2734
|
+
//#region src/components/ui/flow-simulation-panel.tsx
|
|
2735
|
+
function IssueList({ flow, issues, onViewBlock }) {
|
|
2736
|
+
return /* @__PURE__ */ jsx("ul", {
|
|
2737
|
+
className: "flex flex-col gap-2",
|
|
2738
|
+
children: issues.map((i, k) => {
|
|
2739
|
+
const node = flow.nodes.find((n) => n.id === i.nodeId);
|
|
2740
|
+
return /* @__PURE__ */ jsxs("li", {
|
|
2741
|
+
className: "flex flex-wrap items-center justify-between gap-2 rounded-control border bg-card px-3 py-2 text-sm",
|
|
2742
|
+
children: [/* @__PURE__ */ jsx("span", { children: i.message }), i.nodeId && /* @__PURE__ */ jsx(Button, {
|
|
2743
|
+
size: "sm",
|
|
2744
|
+
variant: "outline",
|
|
2745
|
+
"aria-label": `Ver bloco ${node ? nodeName(node) : i.nodeId}`,
|
|
2746
|
+
onClick: () => onViewBlock(i.nodeId),
|
|
2747
|
+
children: "Ver bloco"
|
|
2748
|
+
})]
|
|
2749
|
+
}, `${i.code}-${i.nodeId ?? i.edgeId ?? k}-${k}`);
|
|
1036
2750
|
})
|
|
1037
|
-
})
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
title: "Descartar alterações?",
|
|
1042
|
-
description: "As informações preenchidas neste painel serão perdidas.",
|
|
1043
|
-
confirmLabel: "Descartar",
|
|
1044
|
-
cancelLabel: "Continuar editando",
|
|
1045
|
-
onConfirm: () => onOpenChange(false)
|
|
1046
|
-
})] });
|
|
1047
|
-
}
|
|
1048
|
-
function SimulationPanel({ flow, sim, onSim, onClose }) {
|
|
1049
|
-
const waiting = sim.status === "waiting-choice";
|
|
1050
|
-
const current = flow.nodes.find((n) => n.id === sim.currentId);
|
|
1051
|
-
const events = sim.steps.map((s, i) => ({
|
|
2751
|
+
});
|
|
2752
|
+
}
|
|
2753
|
+
function FlowSimulationPanel({ flow, sim, blockStep, onSim, onClose, onViewBlock }) {
|
|
2754
|
+
const events = (sim.steps.length === 0 && blockStep ? [blockStep] : sim.steps).map((s, i) => ({
|
|
1052
2755
|
id: `${i}-${s.nodeId}`,
|
|
1053
2756
|
title: s.label,
|
|
1054
2757
|
description: s.detail,
|
|
1055
|
-
date: `Passo ${i + 1}`,
|
|
2758
|
+
date: blockStep && sim.steps.length === 0 ? "Só este bloco" : `Passo ${i + 1}`,
|
|
1056
2759
|
status: s.status
|
|
1057
2760
|
}));
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
2761
|
+
return /* @__PURE__ */ jsxs(FlowPanelShell, {
|
|
2762
|
+
title: "Teste do fluxo",
|
|
2763
|
+
onClose,
|
|
2764
|
+
closeLabel: "Fechar teste",
|
|
2765
|
+
children: [
|
|
2766
|
+
/* @__PURE__ */ jsx(ActionBar, {
|
|
2767
|
+
primary: {
|
|
2768
|
+
label: "Avançar",
|
|
2769
|
+
disabled: sim.status !== "running",
|
|
2770
|
+
onClick: () => onSim(advanceSimulation(flow, sim))
|
|
2771
|
+
},
|
|
2772
|
+
cancel: {
|
|
2773
|
+
label: "Reiniciar",
|
|
2774
|
+
onClick: () => onSim(startSimulation(flow))
|
|
2775
|
+
}
|
|
2776
|
+
}),
|
|
2777
|
+
sim.status === "error" && /* @__PURE__ */ jsxs("div", {
|
|
2778
|
+
className: "flex flex-col gap-3",
|
|
2779
|
+
children: [/* @__PURE__ */ jsx(Alert, {
|
|
2780
|
+
type: "error",
|
|
2781
|
+
title: "O fluxo precisa de ajustes antes do teste"
|
|
2782
|
+
}), /* @__PURE__ */ jsx(IssueList, {
|
|
2783
|
+
flow,
|
|
2784
|
+
issues: sim.issues,
|
|
2785
|
+
onViewBlock
|
|
2786
|
+
})]
|
|
2787
|
+
}),
|
|
2788
|
+
sim.status === "finished" && /* @__PURE__ */ jsxs("p", {
|
|
2789
|
+
role: "status",
|
|
2790
|
+
className: "flex items-center gap-2 text-sm font-medium text-success-soft-foreground",
|
|
2791
|
+
children: [/* @__PURE__ */ jsx(CheckCircle2, {
|
|
2792
|
+
className: "size-icon-md",
|
|
2793
|
+
"aria-hidden": true
|
|
2794
|
+
}), "Fluxo concluído"]
|
|
1071
2795
|
}),
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
2796
|
+
events.length > 0 && /* @__PURE__ */ jsx(Timeline, { events }),
|
|
2797
|
+
events.length === 0 && sim.status !== "error" && /* @__PURE__ */ jsx("p", {
|
|
2798
|
+
className: "text-sm text-muted-foreground",
|
|
2799
|
+
children: "Nenhum passo executado ainda."
|
|
2800
|
+
})
|
|
2801
|
+
]
|
|
2802
|
+
});
|
|
2803
|
+
}
|
|
2804
|
+
function FlowValidationPanel({ flow, issues, onClose, onViewBlock }) {
|
|
2805
|
+
return /* @__PURE__ */ jsx(FlowPanelShell, {
|
|
2806
|
+
title: "Validação do fluxo",
|
|
2807
|
+
onClose,
|
|
2808
|
+
closeLabel: "Fechar validação",
|
|
2809
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
2810
|
+
"aria-live": "polite",
|
|
2811
|
+
className: "flex flex-col gap-3",
|
|
2812
|
+
children: issues.length === 0 ? /* @__PURE__ */ jsx(Alert, {
|
|
2813
|
+
type: "success",
|
|
2814
|
+
title: "Fluxo válido"
|
|
2815
|
+
}) : /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Alert, {
|
|
2816
|
+
type: "error",
|
|
2817
|
+
title: issues.length === 1 ? "1 problema encontrado" : `${issues.length} problemas encontrados`
|
|
2818
|
+
}), /* @__PURE__ */ jsx(IssueList, {
|
|
2819
|
+
flow,
|
|
2820
|
+
issues,
|
|
2821
|
+
onViewBlock
|
|
2822
|
+
})] })
|
|
2823
|
+
})
|
|
2824
|
+
});
|
|
2825
|
+
}
|
|
2826
|
+
//#endregion
|
|
2827
|
+
//#region src/components/ui/flow-editor.tsx
|
|
2828
|
+
var DRAG_START = 4;
|
|
2829
|
+
var NO_BLOCK = "Exclua um bloco por vez.";
|
|
2830
|
+
/** Reaproveita os objetos de nó que não mudaram (a biblioteca guarda medida e seleção neles). */
|
|
2831
|
+
function mergeNodes(prev, next) {
|
|
2832
|
+
const old = new Map(prev.map((n) => [n.id, n]));
|
|
2833
|
+
return next.map((n) => {
|
|
2834
|
+
const o = old.get(n.id);
|
|
2835
|
+
if (!o) return n;
|
|
2836
|
+
const a = o.data;
|
|
2837
|
+
const b = n.data;
|
|
2838
|
+
if (!o.dragging && o.position.x === n.position.x && o.position.y === n.position.y && o.ariaLabel === n.ariaLabel && a.label === b.label && a.kind === b.kind && a.config === b.config && a.active === b.active && a.issues.length === b.issues.length && a.issues.every((m, i) => m === b.issues[i])) return o;
|
|
2839
|
+
return {
|
|
2840
|
+
...o,
|
|
2841
|
+
position: o.dragging ? o.position : n.position,
|
|
2842
|
+
ariaLabel: n.ariaLabel,
|
|
2843
|
+
data: b
|
|
2844
|
+
};
|
|
2845
|
+
});
|
|
2846
|
+
}
|
|
2847
|
+
function mergeEdges(prev, next) {
|
|
2848
|
+
const old = new Map(prev.map((e) => [e.id, e]));
|
|
2849
|
+
return next.map((e) => {
|
|
2850
|
+
const o = old.get(e.id);
|
|
2851
|
+
if (!o) return e;
|
|
2852
|
+
return o.source === e.source && o.target === e.target && o.sourceHandle === e.sourceHandle && o.ariaLabel === e.ariaLabel && o.data?.label === e.data?.label ? o : {
|
|
2853
|
+
...o,
|
|
2854
|
+
...e
|
|
2855
|
+
};
|
|
2856
|
+
});
|
|
2857
|
+
}
|
|
2858
|
+
/**
|
|
2859
|
+
* Excluir bloco: mostra o bloco, onde ele está ligado e duas opções (só este, ou este e tudo o
|
|
2860
|
+
* que vem depois), com dupla verificação: a caixa CONFIRMO na primeira, a palavra digitada na
|
|
2861
|
+
* segunda. O Modal fica como está: sem a verificação, o botão avisa e o modal continua aberto.
|
|
2862
|
+
*/
|
|
2863
|
+
function DeleteBlockModal({ flow, nodeId, onClose, onConfirm }) {
|
|
2864
|
+
const node = flow.nodes.find((n) => n.id === nodeId);
|
|
2865
|
+
const [mode, setMode] = useState("so-este");
|
|
2866
|
+
const [checked, setChecked] = useState(false);
|
|
2867
|
+
const [typed, setTyped] = useState("");
|
|
2868
|
+
const [error, setError] = useState(false);
|
|
2869
|
+
const keepOpen = useRef(false);
|
|
2870
|
+
if (!node) return null;
|
|
2871
|
+
const links = describeNodeLinks(flow, node.id);
|
|
2872
|
+
const total = links.descendentes + 1;
|
|
2873
|
+
const verified = mode === "so-este" ? checked : typed.trim().toLowerCase() === "confirmo";
|
|
2874
|
+
const list = (items, empty) => items.length === 0 ? /* @__PURE__ */ jsx("span", {
|
|
2875
|
+
className: "text-muted-foreground",
|
|
2876
|
+
children: empty
|
|
2877
|
+
}) : /* @__PURE__ */ jsx("ul", {
|
|
2878
|
+
className: "flex flex-col gap-1",
|
|
2879
|
+
children: items.map((x) => /* @__PURE__ */ jsxs("li", { children: [nodeName(x.node), x.saida ? /* @__PURE__ */ jsxs("span", {
|
|
2880
|
+
className: "text-muted-foreground",
|
|
1075
2881
|
children: [
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
2882
|
+
" (saída ",
|
|
2883
|
+
x.saida,
|
|
2884
|
+
")"
|
|
2885
|
+
]
|
|
2886
|
+
}) : null] }, x.node.id))
|
|
2887
|
+
});
|
|
2888
|
+
return /* @__PURE__ */ jsx(Modal, {
|
|
2889
|
+
open: true,
|
|
2890
|
+
size: "md",
|
|
2891
|
+
type: "destructive",
|
|
2892
|
+
title: "Excluir este bloco?",
|
|
2893
|
+
description: nodeName(node) === FLOW_TYPE_NAME[node.type] ? nodeName(node) : `${FLOW_TYPE_NAME[node.type]}: ${nodeName(node)}`,
|
|
2894
|
+
confirmLabel: "Excluir",
|
|
2895
|
+
onOpenChange: (open) => {
|
|
2896
|
+
if (open) return;
|
|
2897
|
+
if (keepOpen.current) {
|
|
2898
|
+
keepOpen.current = false;
|
|
2899
|
+
return;
|
|
2900
|
+
}
|
|
2901
|
+
onClose();
|
|
2902
|
+
},
|
|
2903
|
+
onConfirm: () => {
|
|
2904
|
+
if (!verified) {
|
|
2905
|
+
keepOpen.current = true;
|
|
2906
|
+
setError(true);
|
|
2907
|
+
return;
|
|
2908
|
+
}
|
|
2909
|
+
onConfirm(mode);
|
|
2910
|
+
},
|
|
2911
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
2912
|
+
className: "flex w-full flex-col gap-4 text-left text-sm",
|
|
2913
|
+
children: [
|
|
2914
|
+
/* @__PURE__ */ jsx("p", {
|
|
2915
|
+
className: "text-muted-foreground",
|
|
2916
|
+
children: FLOW_BLOCKS[node.type].description
|
|
1086
2917
|
}),
|
|
1087
|
-
|
|
1088
|
-
className: "flex flex-col gap-
|
|
1089
|
-
children: [/* @__PURE__ */ jsx(
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
}), /* @__PURE__ */ jsx("
|
|
1093
|
-
className: "
|
|
1094
|
-
children:
|
|
1095
|
-
})]
|
|
2918
|
+
/* @__PURE__ */ jsxs("dl", {
|
|
2919
|
+
className: "flex flex-col gap-2",
|
|
2920
|
+
children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", {
|
|
2921
|
+
className: "text-xs text-muted-foreground",
|
|
2922
|
+
children: "Ligado depois de"
|
|
2923
|
+
}), /* @__PURE__ */ jsx("dd", { children: list(links.anteriores, "Nenhum bloco antes") })] }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", {
|
|
2924
|
+
className: "text-xs text-muted-foreground",
|
|
2925
|
+
children: "Ligado antes de"
|
|
2926
|
+
}), /* @__PURE__ */ jsx("dd", { children: list(links.posteriores, "Nenhum bloco depois") })] })]
|
|
1096
2927
|
}),
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
variant: "outline",
|
|
1112
|
-
onClick: () => choose("nao"),
|
|
1113
|
-
children: "Não"
|
|
1114
|
-
})]
|
|
1115
|
-
})]
|
|
2928
|
+
/* @__PURE__ */ jsx(RadioGroup, {
|
|
2929
|
+
"aria-label": "O que excluir",
|
|
2930
|
+
value: mode,
|
|
2931
|
+
onChange: (v) => {
|
|
2932
|
+
setMode(v);
|
|
2933
|
+
setError(false);
|
|
2934
|
+
},
|
|
2935
|
+
options: [{
|
|
2936
|
+
value: "so-este",
|
|
2937
|
+
label: "Excluir só este bloco"
|
|
2938
|
+
}, {
|
|
2939
|
+
value: "daqui-para-frente",
|
|
2940
|
+
label: `Excluir este bloco e tudo o que vem depois dele (${total} ${total === 1 ? "bloco" : "blocos"})`
|
|
2941
|
+
}]
|
|
1116
2942
|
}),
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
2943
|
+
mode === "so-este" ? /* @__PURE__ */ jsx(Checkbox, {
|
|
2944
|
+
checked,
|
|
2945
|
+
onCheckedChange: (v) => {
|
|
2946
|
+
setChecked(v === true);
|
|
2947
|
+
setError(false);
|
|
2948
|
+
},
|
|
2949
|
+
label: "CONFIRMO"
|
|
2950
|
+
}) : /* @__PURE__ */ jsx(Field, {
|
|
2951
|
+
label: "Digite CONFIRMO para excluir",
|
|
2952
|
+
span: "full",
|
|
2953
|
+
children: /* @__PURE__ */ jsx(Input, {
|
|
2954
|
+
value: typed,
|
|
2955
|
+
onChange: (v) => {
|
|
2956
|
+
setTyped(v);
|
|
2957
|
+
setError(false);
|
|
2958
|
+
}
|
|
2959
|
+
})
|
|
1124
2960
|
}),
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
className: "
|
|
1128
|
-
children: "
|
|
2961
|
+
error && /* @__PURE__ */ jsx("p", {
|
|
2962
|
+
role: "alert",
|
|
2963
|
+
className: "font-medium text-destructive",
|
|
2964
|
+
children: mode === "so-este" ? "Marque a caixa CONFIRMO para excluir." : "Digite CONFIRMO para excluir."
|
|
1129
2965
|
})
|
|
1130
2966
|
]
|
|
1131
|
-
})
|
|
2967
|
+
})
|
|
1132
2968
|
});
|
|
1133
2969
|
}
|
|
1134
|
-
function FlowEditorInner({ value, onChange, readOnly = false, className }) {
|
|
2970
|
+
function FlowEditorInner({ value, onChange, readOnly = false, toolbar = true, testOpen: testOpenProp, onTestOpenChange, className }) {
|
|
1135
2971
|
const { isMobile } = useBreakpoint();
|
|
1136
2972
|
const canEdit = !readOnly;
|
|
1137
2973
|
const canBuild = canEdit && !isMobile;
|
|
1138
2974
|
const { screenToFlowPosition, fitView } = useReactFlow();
|
|
2975
|
+
const updateNodeInternals = useUpdateNodeInternals();
|
|
2976
|
+
const rootRef = useFillHeight();
|
|
1139
2977
|
const canvasRef = useRef(null);
|
|
1140
2978
|
const noticeId = useId();
|
|
1141
|
-
const
|
|
1142
|
-
|
|
2979
|
+
const flow = useMemo(() => migrateFlow(value), [value]);
|
|
2980
|
+
const flowRef = useRef(flow);
|
|
2981
|
+
flowRef.current = flow;
|
|
1143
2982
|
const everUsed = useRef(/* @__PURE__ */ new Set());
|
|
1144
|
-
|
|
1145
|
-
const [
|
|
2983
|
+
flow.nodes.forEach((n) => everUsed.current.add(n.id));
|
|
2984
|
+
const [view, setView] = useState("blocks");
|
|
2985
|
+
const [configId, setConfigId] = useState(null);
|
|
2986
|
+
const [testState, setTestState] = useState(false);
|
|
2987
|
+
const testOpen = testOpenProp ?? testState;
|
|
1146
2988
|
const [sim, setSim] = useState(resetSimulation);
|
|
2989
|
+
const [blockStep, setBlockStep] = useState(null);
|
|
2990
|
+
const blockStepRef = useRef(null);
|
|
2991
|
+
blockStepRef.current = blockStep;
|
|
2992
|
+
const [validated, setValidated] = useState(false);
|
|
1147
2993
|
const [notice, setNotice] = useState(null);
|
|
1148
|
-
const [drawer, setDrawer] = useState(null);
|
|
1149
2994
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
1150
|
-
const drawerOpenRef = useRef(false);
|
|
1151
|
-
drawerOpenRef.current = drawerOpen;
|
|
1152
2995
|
const [deleting, setDeleting] = useState(null);
|
|
2996
|
+
const deletingRef = useRef(null);
|
|
2997
|
+
deletingRef.current = deleting;
|
|
1153
2998
|
const [drag, setDrag] = useState(null);
|
|
1154
2999
|
const suppressClick = useRef(false);
|
|
1155
|
-
const issues = useMemo(() => validateFlow(
|
|
3000
|
+
const issues = useMemo(() => validateFlow(flow), [flow]);
|
|
1156
3001
|
const shownIssues = validated ? issues : void 0;
|
|
1157
|
-
const activeId = sim.status === "running"
|
|
1158
|
-
const
|
|
3002
|
+
const activeId = sim.status === "running" ? sim.currentId : null;
|
|
3003
|
+
const configNode = configId ? flow.nodes.find((n) => n.id === configId) : void 0;
|
|
3004
|
+
const setTestOpen = useCallback((open) => {
|
|
3005
|
+
setTestState(open);
|
|
3006
|
+
onTestOpenChange?.(open);
|
|
3007
|
+
}, [onTestOpenChange]);
|
|
3008
|
+
const nextNodes = useMemo(() => toRfNodes(flow, {
|
|
1159
3009
|
activeId,
|
|
1160
|
-
issues: shownIssues
|
|
1161
|
-
handles: canBuild ? "lg" : "sm",
|
|
1162
|
-
edit: canBuild
|
|
3010
|
+
issues: shownIssues
|
|
1163
3011
|
}), [
|
|
1164
|
-
|
|
3012
|
+
flow,
|
|
1165
3013
|
activeId,
|
|
1166
|
-
shownIssues
|
|
1167
|
-
canBuild
|
|
3014
|
+
shownIssues
|
|
1168
3015
|
]);
|
|
1169
|
-
const nextEdges = useMemo(() => toRfEdges(
|
|
3016
|
+
const nextEdges = useMemo(() => toRfEdges(flow), [flow]);
|
|
1170
3017
|
const [rfNodes, setRfNodes] = useState(nextNodes);
|
|
1171
3018
|
const [rfEdges, setRfEdges] = useState(nextEdges);
|
|
1172
3019
|
useEffect(() => setRfNodes((prev) => mergeNodes(prev, nextNodes)), [nextNodes]);
|
|
1173
3020
|
useEffect(() => setRfEdges((prev) => mergeEdges(prev, nextEdges)), [nextEdges]);
|
|
1174
|
-
const
|
|
1175
|
-
|
|
1176
|
-
|
|
3021
|
+
const ruleKey = flow.nodes.filter((n) => n.type === "condicao").map((n) => `${n.id}:${rulesOf(n).map((r) => r.id).join(",")}`).join("|");
|
|
3022
|
+
useEffect(() => {
|
|
3023
|
+
for (const part of ruleKey.split("|")) if (part) updateNodeInternals(part.split(":")[0]);
|
|
3024
|
+
}, [ruleKey, updateNodeInternals]);
|
|
3025
|
+
const emit = useCallback((next, message) => {
|
|
3026
|
+
setNotice(message ?? null);
|
|
3027
|
+
onChange(next);
|
|
1177
3028
|
}, [onChange]);
|
|
1178
3029
|
const commitMoves = useCallback((moves) => {
|
|
1179
|
-
let
|
|
3030
|
+
let next = flowRef.current;
|
|
1180
3031
|
for (const m of moves) {
|
|
1181
|
-
const cur =
|
|
1182
|
-
if (cur && (cur.position.x !== m.position.x || cur.position.y !== m.position.y))
|
|
3032
|
+
const cur = next.nodes.find((n) => n.id === m.id);
|
|
3033
|
+
if (cur && (cur.position.x !== m.position.x || cur.position.y !== m.position.y)) next = moveNode(next, m.id, m.position);
|
|
1183
3034
|
}
|
|
1184
|
-
if (
|
|
3035
|
+
if (next !== flowRef.current) emit(next);
|
|
1185
3036
|
}, [emit]);
|
|
1186
3037
|
const onNodesChange = useCallback((changes) => {
|
|
1187
3038
|
setRfNodes((nds) => applyNodeChanges(changes.filter((c) => c.type !== "remove"), nds));
|
|
@@ -1192,38 +3043,88 @@ function FlowEditorInner({ value, onChange, readOnly = false, className }) {
|
|
|
1192
3043
|
if (moves.length > 0 && canBuild) commitMoves(moves);
|
|
1193
3044
|
}, [canBuild, commitMoves]);
|
|
1194
3045
|
const onEdgesChange = useCallback((changes) => setRfEdges((eds) => applyEdgeChanges(changes.filter((c) => c.type !== "remove"), eds)), []);
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
const
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
else setNotice(r.reason);
|
|
3046
|
+
/** Liga e avisa: o motivo da recusa, ou a ligação trocada. */
|
|
3047
|
+
const connect = useCallback((source, target, branch) => {
|
|
3048
|
+
const r = connectNodes(flowRef.current, source, target, branch);
|
|
3049
|
+
if (!r.ok) setNotice(r.reason);
|
|
3050
|
+
else emit(r.flow, r.replaced ? replacedMessage(flowRef.current, r.replaced) : void 0);
|
|
1201
3051
|
}, [emit]);
|
|
3052
|
+
const onConnect = useCallback((c) => {
|
|
3053
|
+
if (c.source && c.target) connect(c.source, c.target, c.sourceHandle ?? void 0);
|
|
3054
|
+
}, [connect]);
|
|
3055
|
+
/** Soltar em qualquer ponto do bloco liga; fora de qualquer bloco, nada acontece. */
|
|
3056
|
+
const onConnectEnd = useCallback((event, state) => {
|
|
3057
|
+
if (state.isValid || !state.fromNode || state.fromHandle?.type !== "source") return;
|
|
3058
|
+
let to = state.toNode?.id;
|
|
3059
|
+
if (!to && typeof document.elementFromPoint === "function") {
|
|
3060
|
+
const p = "changedTouches" in event ? event.changedTouches[0] : event;
|
|
3061
|
+
if (p) to = document.elementFromPoint(p.clientX, p.clientY)?.closest(".react-flow__node")?.getAttribute("data-id") ?? void 0;
|
|
3062
|
+
}
|
|
3063
|
+
if (!to) return;
|
|
3064
|
+
connect(state.fromNode.id, to, state.fromHandle.id ?? void 0);
|
|
3065
|
+
}, [connect]);
|
|
3066
|
+
const showDrawer = () => {
|
|
3067
|
+
if (isMobile) setDrawerOpen(true);
|
|
3068
|
+
};
|
|
3069
|
+
const openBlocks = () => {
|
|
3070
|
+
setView("blocks");
|
|
3071
|
+
setTestOpen(false);
|
|
3072
|
+
showDrawer();
|
|
3073
|
+
};
|
|
3074
|
+
const openConfig = useCallback((id) => {
|
|
3075
|
+
if (!canEdit) return;
|
|
3076
|
+
setConfigId(id);
|
|
3077
|
+
setView("config");
|
|
3078
|
+
setTestOpen(false);
|
|
3079
|
+
if (isMobile) setDrawerOpen(true);
|
|
3080
|
+
}, [
|
|
3081
|
+
canEdit,
|
|
3082
|
+
isMobile,
|
|
3083
|
+
setTestOpen
|
|
3084
|
+
]);
|
|
1202
3085
|
const addBlock = useCallback((type, at) => {
|
|
1203
|
-
const
|
|
1204
|
-
if (type
|
|
3086
|
+
const cur = flowRef.current;
|
|
3087
|
+
if (isTrigger(type) && cur.nodes.some((n) => isTrigger(n.type))) {
|
|
1205
3088
|
setNotice("Já existe um gatilho neste fluxo.");
|
|
1206
3089
|
return;
|
|
1207
3090
|
}
|
|
1208
|
-
|
|
1209
|
-
|
|
3091
|
+
const size = {
|
|
3092
|
+
w: 336,
|
|
3093
|
+
h: estimateNodeHeight({
|
|
3094
|
+
type,
|
|
3095
|
+
data: { label: "" }
|
|
3096
|
+
})
|
|
3097
|
+
};
|
|
3098
|
+
let from = at;
|
|
3099
|
+
if (!from) {
|
|
1210
3100
|
const rect = canvasRef.current?.getBoundingClientRect();
|
|
1211
3101
|
const center = screenToFlowPosition({
|
|
1212
3102
|
x: (rect?.left ?? 0) + (rect?.width ?? 0) / 2,
|
|
1213
3103
|
y: (rect?.top ?? 0) + (rect?.height ?? 0) / 2
|
|
1214
3104
|
});
|
|
1215
|
-
|
|
1216
|
-
x: center.x -
|
|
1217
|
-
y: center.y -
|
|
1218
|
-
};
|
|
1219
|
-
while (flow.nodes.some((n) => Math.abs(n.position.x - pos.x) < 24 && Math.abs(n.position.y - pos.y) < 24)) pos = {
|
|
1220
|
-
x: pos.x + 24,
|
|
1221
|
-
y: pos.y + 24
|
|
3105
|
+
from = {
|
|
3106
|
+
x: center.x - size.w / 2,
|
|
3107
|
+
y: center.y - size.h / 2
|
|
1222
3108
|
};
|
|
1223
3109
|
}
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
3110
|
+
const measured = Object.fromEntries(rfNodes.flatMap((n) => n.measured?.width && n.measured.height ? [[n.id, {
|
|
3111
|
+
w: n.measured.width,
|
|
3112
|
+
h: n.measured.height
|
|
3113
|
+
}]] : []));
|
|
3114
|
+
const added = addNode(cur, type, findFreePosition(cur, size, from, measured), { reservedIds: everUsed.current });
|
|
3115
|
+
emit(added.flow);
|
|
3116
|
+
setConfigId(added.id);
|
|
3117
|
+
setView("config");
|
|
3118
|
+
setTestOpen(false);
|
|
3119
|
+
if (isMobile) setDrawerOpen(true);
|
|
3120
|
+
}, [
|
|
3121
|
+
emit,
|
|
3122
|
+
isMobile,
|
|
3123
|
+
rfNodes,
|
|
3124
|
+
screenToFlowPosition,
|
|
3125
|
+
setTestOpen
|
|
3126
|
+
]);
|
|
3127
|
+
/** Arrastar um bloco da barra até o canvas, por pointer events (funciona com mouse e toque). */
|
|
1227
3128
|
const startPaletteDrag = (e, type) => {
|
|
1228
3129
|
if (e.pointerType === "mouse" && e.button !== 0) return;
|
|
1229
3130
|
const origin = {
|
|
@@ -1263,8 +3164,11 @@ function FlowEditorInner({ value, onChange, readOnly = false, className }) {
|
|
|
1263
3164
|
y: ev.clientY
|
|
1264
3165
|
});
|
|
1265
3166
|
addBlock(type, {
|
|
1266
|
-
x: p.x -
|
|
1267
|
-
y: p.y -
|
|
3167
|
+
x: p.x - 168,
|
|
3168
|
+
y: p.y - estimateNodeHeight({
|
|
3169
|
+
type,
|
|
3170
|
+
data: { label: "" }
|
|
3171
|
+
}) / 2
|
|
1268
3172
|
});
|
|
1269
3173
|
}
|
|
1270
3174
|
};
|
|
@@ -1283,14 +3187,6 @@ function FlowEditorInner({ value, onChange, readOnly = false, className }) {
|
|
|
1283
3187
|
window.addEventListener("pointercancel", cancel);
|
|
1284
3188
|
window.addEventListener("keydown", key);
|
|
1285
3189
|
};
|
|
1286
|
-
const openDrawer = (id) => {
|
|
1287
|
-
if (!canEdit) return;
|
|
1288
|
-
setDrawer((d) => ({
|
|
1289
|
-
id,
|
|
1290
|
-
session: (d?.session ?? 0) + 1
|
|
1291
|
-
}));
|
|
1292
|
-
setDrawerOpen(true);
|
|
1293
|
-
};
|
|
1294
3190
|
const viewBlock = (id) => {
|
|
1295
3191
|
setRfNodes((nds) => nds.map((n) => n.selected === (n.id === id) ? n : {
|
|
1296
3192
|
...n,
|
|
@@ -1302,178 +3198,232 @@ function FlowEditorInner({ value, onChange, readOnly = false, className }) {
|
|
|
1302
3198
|
maxZoom: 1
|
|
1303
3199
|
});
|
|
1304
3200
|
};
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
3201
|
+
useEffect(() => {
|
|
3202
|
+
if (testOpen && !blockStepRef.current) setSim((s) => s.status === "idle" ? startSimulation(flowRef.current) : s);
|
|
3203
|
+
if (testOpen && isMobile) setDrawerOpen(true);
|
|
3204
|
+
if (!testOpen) {
|
|
3205
|
+
setSim(resetSimulation());
|
|
3206
|
+
setBlockStep(null);
|
|
3207
|
+
}
|
|
3208
|
+
}, [testOpen, isMobile]);
|
|
3209
|
+
const openTest = () => {
|
|
3210
|
+
setTestOpen(true);
|
|
3211
|
+
showDrawer();
|
|
3212
|
+
};
|
|
3213
|
+
const playBlock = useCallback((id) => {
|
|
3214
|
+
setSim(resetSimulation());
|
|
3215
|
+
const step = simulateBlock(flowRef.current, id);
|
|
3216
|
+
setBlockStep(step);
|
|
3217
|
+
blockStepRef.current = step;
|
|
3218
|
+
setTestOpen(true);
|
|
3219
|
+
if (isMobile) setDrawerOpen(true);
|
|
3220
|
+
}, [isMobile, setTestOpen]);
|
|
3221
|
+
const focusBoard = () => canvasRef.current?.querySelector(".flow-surface")?.focus();
|
|
3222
|
+
const actions = useMemo(() => ({
|
|
3223
|
+
play: playBlock,
|
|
3224
|
+
...canEdit ? {
|
|
3225
|
+
edit: openConfig,
|
|
3226
|
+
remove: (id) => setDeleting(id)
|
|
3227
|
+
} : {},
|
|
3228
|
+
...canBuild ? { removeEdge: (id) => {
|
|
3229
|
+
emit(removeEdge(flowRef.current, id));
|
|
3230
|
+
focusBoard();
|
|
3231
|
+
} } : {}
|
|
3232
|
+
}), [
|
|
3233
|
+
canEdit,
|
|
3234
|
+
canBuild,
|
|
3235
|
+
playBlock,
|
|
3236
|
+
openConfig,
|
|
3237
|
+
emit
|
|
3238
|
+
]);
|
|
3239
|
+
const closeSidebarPanel = () => {
|
|
3240
|
+
setView("blocks");
|
|
3241
|
+
setValidated(false);
|
|
3242
|
+
setDrawerOpen(false);
|
|
3243
|
+
};
|
|
3244
|
+
const panel = testOpen ? /* @__PURE__ */ jsx(FlowSimulationPanel, {
|
|
3245
|
+
flow,
|
|
3246
|
+
sim,
|
|
3247
|
+
blockStep,
|
|
3248
|
+
onSim: (s) => {
|
|
3249
|
+
setBlockStep(null);
|
|
3250
|
+
setSim(s);
|
|
3251
|
+
},
|
|
3252
|
+
onClose: isMobile ? void 0 : () => {
|
|
3253
|
+
setTestOpen(false);
|
|
3254
|
+
setDrawerOpen(false);
|
|
3255
|
+
},
|
|
3256
|
+
onViewBlock: viewBlock
|
|
3257
|
+
}) : view === "validate" ? /* @__PURE__ */ jsx(FlowValidationPanel, {
|
|
3258
|
+
flow,
|
|
3259
|
+
issues,
|
|
3260
|
+
onClose: isMobile ? void 0 : closeSidebarPanel,
|
|
3261
|
+
onViewBlock: viewBlock
|
|
3262
|
+
}) : view === "config" && configNode ? /* @__PURE__ */ jsx(BlockConfigPanel, {
|
|
3263
|
+
flow,
|
|
3264
|
+
nodeId: configNode.id,
|
|
3265
|
+
onApply: (next, message) => emit(next, message),
|
|
3266
|
+
onClose: isMobile ? void 0 : closeSidebarPanel
|
|
3267
|
+
}, configNode.id) : canEdit ? /* @__PURE__ */ jsx(FlowPanelShell, {
|
|
3268
|
+
title: "Adicionar bloco",
|
|
3269
|
+
children: /* @__PURE__ */ jsx(FlowBlockList, {
|
|
3270
|
+
onAdd: (type) => addBlock(type),
|
|
3271
|
+
onDragStart: canBuild ? startPaletteDrag : void 0,
|
|
3272
|
+
suppressClick
|
|
3273
|
+
})
|
|
3274
|
+
}) : null;
|
|
3275
|
+
const sidebarBody = panel && /* @__PURE__ */ jsx("div", {
|
|
3276
|
+
className: "nokey",
|
|
3277
|
+
children: panel
|
|
3278
|
+
});
|
|
3279
|
+
const showAside = !isMobile && sidebarBody !== null;
|
|
3280
|
+
return /* @__PURE__ */ jsx("div", {
|
|
3281
|
+
className: "@container min-w-0",
|
|
3282
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
3283
|
+
ref: rootRef,
|
|
3284
|
+
"data-rendra": "FLX-001",
|
|
3285
|
+
className: cn("flow-editor flex min-w-0 flex-col overflow-hidden rounded-surface border bg-card @5xl:h-board @5xl:min-h-viewer @5xl:flex-row", className),
|
|
3286
|
+
children: [
|
|
3287
|
+
/* @__PURE__ */ jsxs("div", {
|
|
3288
|
+
ref: canvasRef,
|
|
3289
|
+
className: cn("relative h-board min-w-0 md:h-viewer @5xl:h-full @5xl:flex-1", drag?.over && "ring-2 ring-primary ring-inset"),
|
|
3290
|
+
children: [
|
|
3291
|
+
/* @__PURE__ */ jsx(FlowActionsContext.Provider, {
|
|
3292
|
+
value: actions,
|
|
3293
|
+
children: /* @__PURE__ */ jsx(FlowSurface, {
|
|
3294
|
+
fill: false,
|
|
3295
|
+
nodes: rfNodes,
|
|
3296
|
+
edges: rfEdges,
|
|
3297
|
+
onNodesChange,
|
|
3298
|
+
onEdgesChange,
|
|
3299
|
+
onConnect,
|
|
3300
|
+
onConnectEnd,
|
|
3301
|
+
nodesDraggable: canBuild,
|
|
3302
|
+
nodesConnectable: canBuild,
|
|
3303
|
+
elementsSelectable: canEdit,
|
|
3304
|
+
deleteKeyCode: canEdit ? ["Delete", "Backspace"] : null,
|
|
3305
|
+
onBeforeDelete: async ({ nodes: gone }) => {
|
|
3306
|
+
if (!canEdit || drawerOpen || deletingRef.current) return false;
|
|
3307
|
+
if (gone.length === 0) return true;
|
|
3308
|
+
if (gone.length === 1) setDeleting(gone[0].id);
|
|
3309
|
+
else setNotice(NO_BLOCK);
|
|
3310
|
+
return false;
|
|
3311
|
+
},
|
|
3312
|
+
onDelete: ({ edges: gone }) => {
|
|
3313
|
+
let next = flowRef.current;
|
|
3314
|
+
for (const e of gone) next = removeEdge(next, e.id);
|
|
3315
|
+
if (next !== flowRef.current) emit(next);
|
|
3316
|
+
},
|
|
3317
|
+
onNodeDragStop: (_, __, nodes) => commitMoves(nodes.map((n) => ({
|
|
3318
|
+
id: n.id,
|
|
3319
|
+
position: n.position
|
|
3320
|
+
}))),
|
|
3321
|
+
onNodeClick: (e, node) => {
|
|
3322
|
+
if (e.target.closest("button")) return;
|
|
3323
|
+
openConfig(node.id);
|
|
3324
|
+
},
|
|
3325
|
+
onPaneClick: () => {
|
|
3326
|
+
if (view === "config" && !isMobile) setView("blocks");
|
|
3327
|
+
},
|
|
3328
|
+
onKeyDown: (e) => {
|
|
3329
|
+
const el = e.target;
|
|
3330
|
+
if (e.key === "Enter" && el.classList?.contains("react-flow__node") && el.dataset.id) {
|
|
3331
|
+
e.preventDefault();
|
|
3332
|
+
openConfig(el.dataset.id);
|
|
1391
3333
|
}
|
|
1392
|
-
}
|
|
1393
|
-
}),
|
|
1394
|
-
/* @__PURE__ */ jsx("div", {
|
|
1395
|
-
role: "status",
|
|
1396
|
-
"aria-live": "polite",
|
|
1397
|
-
id: noticeId,
|
|
1398
|
-
className: cn(!notice && "sr-only"),
|
|
1399
|
-
children: notice && /* @__PURE__ */ jsx(Alert, {
|
|
1400
|
-
type: "warning",
|
|
1401
|
-
title: notice,
|
|
1402
|
-
onDismiss: () => setNotice(null)
|
|
1403
|
-
})
|
|
1404
|
-
}),
|
|
1405
|
-
validated && /* @__PURE__ */ jsx("section", {
|
|
1406
|
-
"aria-label": "Validação do fluxo",
|
|
1407
|
-
"aria-live": "polite",
|
|
1408
|
-
className: "flex flex-col gap-3",
|
|
1409
|
-
children: issues.length === 0 ? /* @__PURE__ */ jsx(Alert, {
|
|
1410
|
-
type: "success",
|
|
1411
|
-
title: "Fluxo válido"
|
|
1412
|
-
}) : /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Alert, {
|
|
1413
|
-
type: "error",
|
|
1414
|
-
title: issues.length === 1 ? "1 problema encontrado" : `${issues.length} problemas encontrados`
|
|
1415
|
-
}), /* @__PURE__ */ jsx("ul", {
|
|
1416
|
-
className: "flex flex-col gap-2",
|
|
1417
|
-
children: issues.map((i, k) => /* @__PURE__ */ jsxs("li", {
|
|
1418
|
-
className: "flex flex-wrap items-center justify-between gap-2 rounded-control border bg-card px-3 py-2 text-sm",
|
|
1419
|
-
children: [/* @__PURE__ */ jsx("span", { children: i.message }), i.nodeId && /* @__PURE__ */ jsx(Button, {
|
|
1420
|
-
size: "sm",
|
|
1421
|
-
variant: "outline",
|
|
1422
|
-
"aria-label": `Ver bloco ${nodeName(value.nodes.find((n) => n.id === i.nodeId) ?? {
|
|
1423
|
-
type: "acao",
|
|
1424
|
-
data: { label: i.nodeId }
|
|
1425
|
-
})}`,
|
|
1426
|
-
onClick: () => viewBlock(i.nodeId),
|
|
1427
|
-
children: "Ver bloco"
|
|
1428
|
-
})]
|
|
1429
|
-
}, `${i.code}-${i.nodeId ?? i.edgeId ?? k}`))
|
|
1430
|
-
})] })
|
|
3334
|
+
}
|
|
1431
3335
|
})
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
}
|
|
1475
|
-
|
|
1476
|
-
|
|
3336
|
+
}),
|
|
3337
|
+
(toolbar || isMobile && canEdit) && /* @__PURE__ */ jsxs("div", {
|
|
3338
|
+
className: "absolute top-2 right-2 z-10 flex gap-2",
|
|
3339
|
+
children: [isMobile && canEdit && /* @__PURE__ */ jsx(Button, {
|
|
3340
|
+
variant: "outline",
|
|
3341
|
+
iconOnly: true,
|
|
3342
|
+
"aria-label": "Blocos",
|
|
3343
|
+
icon: /* @__PURE__ */ jsx(Plus, {}),
|
|
3344
|
+
onClick: openBlocks
|
|
3345
|
+
}), toolbar && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Button, {
|
|
3346
|
+
variant: "outline",
|
|
3347
|
+
iconOnly: isMobile,
|
|
3348
|
+
"aria-label": "Validar",
|
|
3349
|
+
icon: /* @__PURE__ */ jsx(ListChecks, {}),
|
|
3350
|
+
onClick: () => {
|
|
3351
|
+
setValidated(true);
|
|
3352
|
+
setView("validate");
|
|
3353
|
+
setTestOpen(false);
|
|
3354
|
+
showDrawer();
|
|
3355
|
+
},
|
|
3356
|
+
children: isMobile ? null : "Validar"
|
|
3357
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
3358
|
+
iconOnly: isMobile,
|
|
3359
|
+
"aria-label": "Testar fluxo",
|
|
3360
|
+
icon: /* @__PURE__ */ jsx(FlaskConical, {}),
|
|
3361
|
+
onClick: openTest,
|
|
3362
|
+
children: isMobile ? null : "Testar fluxo"
|
|
3363
|
+
})] })]
|
|
3364
|
+
}),
|
|
3365
|
+
/* @__PURE__ */ jsx("div", {
|
|
3366
|
+
role: "status",
|
|
3367
|
+
"aria-live": "polite",
|
|
3368
|
+
id: noticeId,
|
|
3369
|
+
"data-flow-notice": true,
|
|
3370
|
+
className: cn(notice ? "absolute inset-x-2 top-16 z-10" : "sr-only"),
|
|
3371
|
+
children: notice && /* @__PURE__ */ jsx(Alert, {
|
|
3372
|
+
type: "warning",
|
|
3373
|
+
title: notice,
|
|
3374
|
+
onDismiss: () => setNotice(null)
|
|
3375
|
+
})
|
|
3376
|
+
})
|
|
3377
|
+
]
|
|
3378
|
+
}),
|
|
3379
|
+
showAside && /* @__PURE__ */ jsx("aside", {
|
|
3380
|
+
"aria-label": "Painel do fluxo",
|
|
3381
|
+
className: "w-full shrink-0 scrollbar-subtle border-t p-4 @5xl:h-full @5xl:w-flow-panel @5xl:overflow-y-auto @5xl:border-t-0 @5xl:border-l",
|
|
3382
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
3383
|
+
role: "presentation",
|
|
3384
|
+
onKeyDown: (e) => {
|
|
3385
|
+
if (e.key === "Escape" && view === "config" && !testOpen && !e.target.closest("[role=\"combobox\"],[role=\"listbox\"],[role=\"dialog\"],[data-radix-popper-content-wrapper],[cmdk-root]")) setView("blocks");
|
|
3386
|
+
},
|
|
3387
|
+
children: sidebarBody
|
|
3388
|
+
})
|
|
3389
|
+
}),
|
|
3390
|
+
drag && /* @__PURE__ */ jsx("div", {
|
|
3391
|
+
"aria-hidden": true,
|
|
3392
|
+
"data-flow-drag": true,
|
|
3393
|
+
className: "pointer-events-none fixed top-0 left-0 z-50 translate-x-(--flow-x) translate-y-(--flow-y) rounded-control border bg-card px-3 py-2 text-sm shadow-md",
|
|
3394
|
+
style: {
|
|
3395
|
+
"--flow-x": `${drag.x}px`,
|
|
3396
|
+
"--flow-y": `${drag.y}px`
|
|
3397
|
+
},
|
|
3398
|
+
children: FLOW_TYPE_NAME[drag.type]
|
|
3399
|
+
}),
|
|
3400
|
+
isMobile && sidebarBody !== null && /* @__PURE__ */ jsx(Drawer, {
|
|
3401
|
+
open: drawerOpen,
|
|
3402
|
+
onOpenChange: (open) => {
|
|
3403
|
+
setDrawerOpen(open);
|
|
3404
|
+
if (!open) {
|
|
3405
|
+
if (testOpen) setTestOpen(false);
|
|
3406
|
+
setView("blocks");
|
|
3407
|
+
setValidated(false);
|
|
3408
|
+
}
|
|
3409
|
+
},
|
|
3410
|
+
title: "Fluxo de automação",
|
|
3411
|
+
children: sidebarBody
|
|
3412
|
+
}),
|
|
3413
|
+
canEdit && deleting && /* @__PURE__ */ jsx(DeleteBlockModal, {
|
|
3414
|
+
flow,
|
|
3415
|
+
nodeId: deleting,
|
|
3416
|
+
onClose: () => setDeleting(null),
|
|
3417
|
+
onConfirm: (mode) => {
|
|
3418
|
+
emit(removeNode(flowRef.current, deleting, mode));
|
|
3419
|
+
setDeleting(null);
|
|
3420
|
+
setConfigId((id) => id === deleting ? null : id);
|
|
3421
|
+
setView("blocks");
|
|
3422
|
+
setDrawerOpen(false);
|
|
3423
|
+
}
|
|
3424
|
+
}, deleting)
|
|
3425
|
+
]
|
|
3426
|
+
})
|
|
1477
3427
|
});
|
|
1478
3428
|
}
|
|
1479
3429
|
function FlowEditor(props) {
|