@rendra-ui/web 2.6.0 → 2.8.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 +15 -14
- package/dist/base.css +1 -1
- package/dist/{empty-state-DrQjqxvp.js → brand-feedback-icon-CVux4pNh.js} +4 -64
- package/dist/brand-feedback-icon-CVux4pNh.js.map +1 -0
- package/dist/{button-B6-644xP.js → button-CrSlVIsY.js} +6 -6
- package/dist/{button-B6-644xP.js.map → button-CrSlVIsY.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-TBlI6-gS.js → cn-BlJHpkDs.js} +2 -2
- package/dist/{cn-TBlI6-gS.js.map → cn-BlJHpkDs.js.map} +1 -1
- package/dist/{components-Btk1cUg2.js → components-6RpoJ2Od.js} +20 -4
- package/dist/{components-Btk1cUg2.js.map → components-6RpoJ2Od.js.map} +1 -1
- package/dist/components.css +108 -1
- package/dist/document-viewer.js +4 -4
- package/dist/{tooltip-kgsorOg6.js → dropdown-menu-BUGKtR6k.js} +9 -75
- package/dist/dropdown-menu-BUGKtR6k.js.map +1 -0
- package/dist/empty-state-i4e-LacG.js +67 -0
- package/dist/empty-state-i4e-LacG.js.map +1 -0
- package/dist/{features-max-CC6CorTg.js → features-max-BCIy1Ic-.js} +1 -1
- package/dist/flow.js +1485 -0
- package/dist/flow.js.map +1 -0
- package/dist/index.js +816 -2019
- package/dist/index.js.map +1 -1
- package/dist/{motion-context-BL545cb6.js → motion-context-PJPj6X4g.js} +4 -4
- package/dist/{motion-context-BL545cb6.js.map → motion-context-PJPj6X4g.js.map} +1 -1
- package/dist/{motion-on-DMoTsWrG.js → motion-on-Bocf4Smc.js} +2 -2
- package/dist/{motion-on-DMoTsWrG.js.map → motion-on-Bocf4Smc.js.map} +1 -1
- package/dist/motion.js +2 -2
- package/dist/{rendra-provider-CeCkPafT.js → rendra-provider-Do_qWjJC.js} +3 -3
- package/dist/{rendra-provider-CeCkPafT.js.map → rendra-provider-Do_qWjJC.js.map} +1 -1
- package/dist/rich-text-editor.js +4 -3
- package/dist/rich-text-editor.js.map +1 -1
- package/dist/router-bridge.js +2 -2
- package/dist/timeline-xrMHeV92.js +1803 -0
- package/dist/timeline-xrMHeV92.js.map +1 -0
- package/dist/tokens.css +9 -1
- package/dist/tooltip-to0h6aPe.js +76 -0
- package/dist/tooltip-to0h6aPe.js.map +1 -0
- package/dist/types/components/ui/data-toolbar.d.ts +6 -1
- package/dist/types/components/ui/flow-editor.d.ts +11 -0
- package/dist/types/components/ui/flow-viewer.d.ts +58 -0
- package/dist/types/components/ui/kanban.d.ts +75 -6
- package/dist/types/flow.d.ts +3 -0
- package/dist/types/lib/flow.d.ts +121 -0
- package/dist/{use-breakpoint-CNxDwEpc.js → use-breakpoint-CM-yeDHJ.js} +2 -2
- package/dist/{use-breakpoint-CNxDwEpc.js.map → use-breakpoint-CM-yeDHJ.js.map} +1 -1
- package/dist/{use-motion-presets-DCJPBfNJ.js → use-motion-presets-I4StrsCi.js} +3 -3
- package/dist/{use-motion-presets-DCJPBfNJ.js.map → use-motion-presets-I4StrsCi.js.map} +1 -1
- package/dist/widget-grid.js +2 -2
- package/package.json +8 -1
- package/dist/empty-state-DrQjqxvp.js.map +0 -1
- package/dist/tooltip-kgsorOg6.js.map +0 -1
package/dist/flow.js
ADDED
|
@@ -0,0 +1,1485 @@
|
|
|
1
|
+
/*! Rendra Design System v2.8.0 | MIT | © 2026 Bruno Magalhaes | brunomagalhaes.me */
|
|
2
|
+
import { t as cn } from "./cn-BlJHpkDs.js";
|
|
3
|
+
import { i as useMotionLib } from "./motion-context-PJPj6X4g.js";
|
|
4
|
+
import { t as useMotionPresets } from "./use-motion-presets-I4StrsCi.js";
|
|
5
|
+
import { B as ActionBar, F as CardHeader, I as CardTitle, M as CardContent, N as CardDescription, R as Modal, d as Drawer, i as Field, j as Card, n as useFillHeight, p as Input, r as Select, t as Timeline, z as Alert } from "./timeline-xrMHeV92.js";
|
|
6
|
+
import { t as Button } from "./button-CrSlVIsY.js";
|
|
7
|
+
import { n as useBreakpoint } from "./use-breakpoint-CM-yeDHJ.js";
|
|
8
|
+
import { CheckCircle2, Clock, Flag, FlaskConical, GitBranch, ListChecks, Maximize2, Play, SquarePen, Trash2, TriangleAlert, X, Zap, ZoomIn, ZoomOut } from "lucide-react";
|
|
9
|
+
import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
10
|
+
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 "@xyflow/react/dist/base.css";
|
|
13
|
+
//#region src/lib/flow.ts
|
|
14
|
+
var FLOW_NODE_TYPES = [
|
|
15
|
+
"gatilho",
|
|
16
|
+
"acao",
|
|
17
|
+
"condicao",
|
|
18
|
+
"espera",
|
|
19
|
+
"fim"
|
|
20
|
+
];
|
|
21
|
+
var FLOW_WAIT_UNITS = [
|
|
22
|
+
"segundos",
|
|
23
|
+
"minutos",
|
|
24
|
+
"horas",
|
|
25
|
+
"dias"
|
|
26
|
+
];
|
|
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
|
+
var WAIT_SINGULAR = {
|
|
40
|
+
segundos: "segundo",
|
|
41
|
+
minutos: "minuto",
|
|
42
|
+
horas: "hora",
|
|
43
|
+
dias: "dia"
|
|
44
|
+
};
|
|
45
|
+
var DEFAULT_WAIT = {
|
|
46
|
+
duration: 5,
|
|
47
|
+
unit: "minutos"
|
|
48
|
+
};
|
|
49
|
+
var isUnit = (v) => typeof v === "string" && FLOW_WAIT_UNITS.includes(v);
|
|
50
|
+
/** A duração do bloco de espera ("5 minutos"), ou `null` se o config não a define. */
|
|
51
|
+
function formatWait(config) {
|
|
52
|
+
const duration = config?.duration;
|
|
53
|
+
const unit = config?.unit;
|
|
54
|
+
if (typeof duration !== "number" || !Number.isFinite(duration) || !isUnit(unit)) return null;
|
|
55
|
+
return `${duration} ${duration === 1 ? WAIT_SINGULAR[unit] : unit}`;
|
|
56
|
+
}
|
|
57
|
+
/** Texto da espera simulada: nunca espera de verdade. */
|
|
58
|
+
function describeWait(config) {
|
|
59
|
+
const wait = formatWait(config);
|
|
60
|
+
return wait ? `Aguardaria ${wait}` : "Aguardaria o tempo definido no bloco";
|
|
61
|
+
}
|
|
62
|
+
/** Nome do bloco para mensagens e rótulos: o rótulo, ou o tipo quando ele está vazio. */
|
|
63
|
+
var nodeName = (n) => n.data.label.trim() || FLOW_TYPE_NAME[n.type];
|
|
64
|
+
/** Ligações que saem de cada bloco, na ordem do fluxo. */
|
|
65
|
+
function outgoing(flow) {
|
|
66
|
+
const map = /* @__PURE__ */ new Map();
|
|
67
|
+
for (const e of flow.edges) map.set(e.source, [...map.get(e.source) ?? [], e]);
|
|
68
|
+
return map;
|
|
69
|
+
}
|
|
70
|
+
/** Problemas do fluxo, em ordem estável: a mesma entrada dá sempre a mesma lista. */
|
|
71
|
+
function validateFlow(flow) {
|
|
72
|
+
const issues = [];
|
|
73
|
+
const byId = new Map(flow.nodes.map((n) => [n.id, n]));
|
|
74
|
+
const out = outgoing(flow);
|
|
75
|
+
const triggers = flow.nodes.filter((n) => n.type === "gatilho");
|
|
76
|
+
if (triggers.length === 0) issues.push({
|
|
77
|
+
code: "sem-gatilho",
|
|
78
|
+
message: "O fluxo precisa de um bloco Gatilho para começar."
|
|
79
|
+
});
|
|
80
|
+
else if (triggers.length > 1) issues.push({
|
|
81
|
+
code: "varios-gatilhos",
|
|
82
|
+
message: `O fluxo tem ${triggers.length} gatilhos, mas deve ter só um.`
|
|
83
|
+
});
|
|
84
|
+
const sound = [];
|
|
85
|
+
for (const e of flow.edges) {
|
|
86
|
+
const from = byId.get(e.source);
|
|
87
|
+
const to = byId.get(e.target);
|
|
88
|
+
if (!from || !to) {
|
|
89
|
+
issues.push({
|
|
90
|
+
code: "ligacao-invalida",
|
|
91
|
+
message: "Há uma ligação que aponta para um bloco que não existe.",
|
|
92
|
+
edgeId: e.id
|
|
93
|
+
});
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (from.type === "condicao" ? !e.branch : e.branch) {
|
|
97
|
+
issues.push({
|
|
98
|
+
code: "ramo-invalido",
|
|
99
|
+
message: from.type === "condicao" ? `Uma saída da condição "${nodeName(from)}" não diz se é "Sim" ou "Não".` : `A ligação que sai de "${nodeName(from)}" não pode ter "Sim" ou "Não": isso só vale para condição.`,
|
|
100
|
+
nodeId: from.id,
|
|
101
|
+
edgeId: e.id
|
|
102
|
+
});
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
sound.push(e);
|
|
106
|
+
}
|
|
107
|
+
for (const n of flow.nodes) {
|
|
108
|
+
const edges = out.get(n.id) ?? [];
|
|
109
|
+
const name = nodeName(n);
|
|
110
|
+
if (n.type === "fim") {
|
|
111
|
+
if (edges.length > 0) issues.push({
|
|
112
|
+
code: "fim-com-saida",
|
|
113
|
+
message: `O bloco Fim "${name}" não pode ter saída.`,
|
|
114
|
+
nodeId: n.id
|
|
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".`,
|
|
122
|
+
nodeId: n.id
|
|
123
|
+
});
|
|
124
|
+
if (nao === 0) issues.push({
|
|
125
|
+
code: "condicao-sem-nao",
|
|
126
|
+
message: `A condição "${name}" precisa de uma saída "Não".`,
|
|
127
|
+
nodeId: n.id
|
|
128
|
+
});
|
|
129
|
+
if (sim > 1 || nao > 1) issues.push({
|
|
130
|
+
code: "condicao-saida-repetida",
|
|
131
|
+
message: `A condição "${name}" tem mais de uma saída para o mesmo ramo.`,
|
|
132
|
+
nodeId: n.id
|
|
133
|
+
});
|
|
134
|
+
} else if (edges.length === 0) issues.push({
|
|
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({
|
|
140
|
+
code: "varias-saidas",
|
|
141
|
+
message: `O bloco "${name}" tem mais de uma saída; só a condição pode ter.`,
|
|
142
|
+
nodeId: n.id
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
const next = /* @__PURE__ */ new Map();
|
|
146
|
+
for (const e of sound) next.set(e.source, [...next.get(e.source) ?? [], e.target]);
|
|
147
|
+
if (triggers.length > 0) {
|
|
148
|
+
const seen = /* @__PURE__ */ new Set();
|
|
149
|
+
const stack = triggers.map((t) => t.id);
|
|
150
|
+
while (stack.length) {
|
|
151
|
+
const id = stack.pop();
|
|
152
|
+
if (seen.has(id)) continue;
|
|
153
|
+
seen.add(id);
|
|
154
|
+
stack.push(...next.get(id) ?? []);
|
|
155
|
+
}
|
|
156
|
+
for (const n of flow.nodes) if (!seen.has(n.id)) issues.push({
|
|
157
|
+
code: "inalcancavel",
|
|
158
|
+
message: `O bloco "${nodeName(n)}" não é alcançado a partir do gatilho.`,
|
|
159
|
+
nodeId: n.id
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
const state = /* @__PURE__ */ new Map();
|
|
163
|
+
const path = [];
|
|
164
|
+
const visit = (id) => {
|
|
165
|
+
state.set(id, 1);
|
|
166
|
+
path.push(id);
|
|
167
|
+
for (const t of next.get(id) ?? []) if (state.get(t) === 1) {
|
|
168
|
+
const loop = [...path.slice(path.indexOf(t)), t].map((x) => `"${nodeName(byId.get(x))}"`);
|
|
169
|
+
issues.push({
|
|
170
|
+
code: "ciclo",
|
|
171
|
+
message: `O fluxo volta para um bloco por onde já passou: ${loop.join(" → ")}.`,
|
|
172
|
+
nodeId: t
|
|
173
|
+
});
|
|
174
|
+
} else if (!state.has(t)) visit(t);
|
|
175
|
+
path.pop();
|
|
176
|
+
state.set(id, 2);
|
|
177
|
+
};
|
|
178
|
+
for (const n of flow.nodes) if (!state.has(n.id)) visit(n.id);
|
|
179
|
+
return issues;
|
|
180
|
+
}
|
|
181
|
+
var sortKeys = (v) => {
|
|
182
|
+
if (Array.isArray(v)) return v.map(sortKeys);
|
|
183
|
+
if (v && typeof v === "object") return Object.fromEntries(Object.keys(v).sort().map((k) => [k, sortKeys(v[k])]));
|
|
184
|
+
return v;
|
|
185
|
+
};
|
|
186
|
+
/** JSON do fluxo, com a ordem das chaves fixa: o mesmo fluxo dá sempre o mesmo texto. */
|
|
187
|
+
function serializeFlow(flow) {
|
|
188
|
+
const nodes = flow.nodes.map((n) => ({
|
|
189
|
+
id: n.id,
|
|
190
|
+
type: n.type,
|
|
191
|
+
position: {
|
|
192
|
+
x: n.position.x,
|
|
193
|
+
y: n.position.y
|
|
194
|
+
},
|
|
195
|
+
data: n.data.config ? {
|
|
196
|
+
label: n.data.label,
|
|
197
|
+
config: sortKeys(n.data.config)
|
|
198
|
+
} : { label: n.data.label }
|
|
199
|
+
}));
|
|
200
|
+
const edges = flow.edges.map((e) => e.branch ? {
|
|
201
|
+
id: e.id,
|
|
202
|
+
source: e.source,
|
|
203
|
+
target: e.target,
|
|
204
|
+
branch: e.branch
|
|
205
|
+
} : {
|
|
206
|
+
id: e.id,
|
|
207
|
+
source: e.source,
|
|
208
|
+
target: e.target
|
|
209
|
+
});
|
|
210
|
+
return JSON.stringify({
|
|
211
|
+
nodes,
|
|
212
|
+
edges
|
|
213
|
+
}, null, 2);
|
|
214
|
+
}
|
|
215
|
+
var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
216
|
+
/** Lê o JSON de um fluxo. Nunca lança; campos que o contrato não conhece são ignorados. */
|
|
217
|
+
function parseFlow(text) {
|
|
218
|
+
let raw;
|
|
219
|
+
try {
|
|
220
|
+
raw = JSON.parse(text);
|
|
221
|
+
} catch {
|
|
222
|
+
return {
|
|
223
|
+
ok: false,
|
|
224
|
+
errors: ["O texto não é um JSON válido."]
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
if (!isObject(raw)) return {
|
|
228
|
+
ok: false,
|
|
229
|
+
errors: ["O JSON do fluxo precisa ser um objeto."]
|
|
230
|
+
};
|
|
231
|
+
if (!Array.isArray(raw.nodes) || !Array.isArray(raw.edges)) return {
|
|
232
|
+
ok: false,
|
|
233
|
+
errors: [`O campo "${!Array.isArray(raw.nodes) ? "nodes" : "edges"}" precisa ser uma lista.`]
|
|
234
|
+
};
|
|
235
|
+
const errors = [];
|
|
236
|
+
const nodes = [];
|
|
237
|
+
const ids = /* @__PURE__ */ new Set();
|
|
238
|
+
raw.nodes.forEach((item, i) => {
|
|
239
|
+
const at = `Bloco ${i + 1}`;
|
|
240
|
+
if (!isObject(item)) return void errors.push(`${at}: precisa ser um objeto.`);
|
|
241
|
+
const { id, type, position, data } = item;
|
|
242
|
+
let ok = true;
|
|
243
|
+
if (typeof id !== "string" || id === "") {
|
|
244
|
+
errors.push(`${at}: falta o "id".`);
|
|
245
|
+
ok = false;
|
|
246
|
+
} else if (ids.has(id)) {
|
|
247
|
+
errors.push(`${at}: o id "${id}" está repetido.`);
|
|
248
|
+
ok = false;
|
|
249
|
+
} else ids.add(id);
|
|
250
|
+
if (!FLOW_NODE_TYPES.includes(type)) {
|
|
251
|
+
errors.push(`${at}: tipo desconhecido (${String(type)}).`);
|
|
252
|
+
ok = false;
|
|
253
|
+
}
|
|
254
|
+
const pos = isObject(position) ? position : null;
|
|
255
|
+
if (!pos || typeof pos.x !== "number" || typeof pos.y !== "number" || !Number.isFinite(pos.x) || !Number.isFinite(pos.y)) {
|
|
256
|
+
errors.push(`${at}: "position" precisa ter "x" e "y" numéricos.`);
|
|
257
|
+
ok = false;
|
|
258
|
+
}
|
|
259
|
+
if (!isObject(data) || typeof data.label !== "string") {
|
|
260
|
+
errors.push(`${at}: "data.label" precisa ser um texto.`);
|
|
261
|
+
ok = false;
|
|
262
|
+
} else if (data.config !== void 0 && !isObject(data.config)) {
|
|
263
|
+
errors.push(`${at}: "data.config" precisa ser um objeto.`);
|
|
264
|
+
ok = false;
|
|
265
|
+
}
|
|
266
|
+
if (ok) {
|
|
267
|
+
const d = data;
|
|
268
|
+
nodes.push({
|
|
269
|
+
id,
|
|
270
|
+
type,
|
|
271
|
+
position: {
|
|
272
|
+
x: pos.x,
|
|
273
|
+
y: pos.y
|
|
274
|
+
},
|
|
275
|
+
data: d.config ? {
|
|
276
|
+
label: d.label,
|
|
277
|
+
config: d.config
|
|
278
|
+
} : { label: d.label }
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
const edges = [];
|
|
283
|
+
const edgeIds = /* @__PURE__ */ new Set();
|
|
284
|
+
raw.edges.forEach((item, i) => {
|
|
285
|
+
const at = `Ligação ${i + 1}`;
|
|
286
|
+
if (!isObject(item)) return void errors.push(`${at}: precisa ser um objeto.`);
|
|
287
|
+
const { id, source, target, branch } = item;
|
|
288
|
+
let ok = true;
|
|
289
|
+
for (const [campo, v] of [
|
|
290
|
+
["id", id],
|
|
291
|
+
["source", source],
|
|
292
|
+
["target", target]
|
|
293
|
+
]) if (typeof v !== "string" || v === "") {
|
|
294
|
+
errors.push(`${at}: falta o campo "${campo}".`);
|
|
295
|
+
ok = false;
|
|
296
|
+
}
|
|
297
|
+
if (ok && edgeIds.has(id)) {
|
|
298
|
+
errors.push(`${at}: o id "${id}" está repetido.`);
|
|
299
|
+
ok = false;
|
|
300
|
+
}
|
|
301
|
+
if (branch !== void 0 && branch !== "sim" && branch !== "nao") {
|
|
302
|
+
errors.push(`${at}: "branch" só pode ser "sim" ou "nao".`);
|
|
303
|
+
ok = false;
|
|
304
|
+
}
|
|
305
|
+
if (ok) {
|
|
306
|
+
edgeIds.add(id);
|
|
307
|
+
edges.push(branch ? {
|
|
308
|
+
id,
|
|
309
|
+
source,
|
|
310
|
+
target,
|
|
311
|
+
branch
|
|
312
|
+
} : {
|
|
313
|
+
id,
|
|
314
|
+
source,
|
|
315
|
+
target
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
return errors.length > 0 ? {
|
|
320
|
+
ok: false,
|
|
321
|
+
errors
|
|
322
|
+
} : {
|
|
323
|
+
ok: true,
|
|
324
|
+
flow: {
|
|
325
|
+
nodes,
|
|
326
|
+
edges
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
/** Acrescenta um bloco com id único e determinístico (`tipo-N`). */
|
|
331
|
+
function addNode(flow, type, position, options = {}) {
|
|
332
|
+
const taken = /* @__PURE__ */ new Set([...flow.nodes.map((n) => n.id), ...options.reservedIds ?? []]);
|
|
333
|
+
let n = 1;
|
|
334
|
+
while (taken.has(`${type}-${n}`)) n++;
|
|
335
|
+
const id = `${type}-${n}`;
|
|
336
|
+
const node = {
|
|
337
|
+
id,
|
|
338
|
+
type,
|
|
339
|
+
position: {
|
|
340
|
+
x: position.x,
|
|
341
|
+
y: position.y
|
|
342
|
+
},
|
|
343
|
+
data: type === "espera" ? {
|
|
344
|
+
label: options.label ?? FLOW_TYPE_NAME[type],
|
|
345
|
+
config: { ...DEFAULT_WAIT }
|
|
346
|
+
} : { label: options.label ?? FLOW_TYPE_NAME[type] }
|
|
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
|
+
};
|
|
363
|
+
}
|
|
364
|
+
function removeEdge(flow, id) {
|
|
365
|
+
return {
|
|
366
|
+
...flow,
|
|
367
|
+
edges: flow.edges.filter((e) => e.id !== id)
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
function moveNode(flow, id, position) {
|
|
371
|
+
return {
|
|
372
|
+
...flow,
|
|
373
|
+
nodes: flow.nodes.map((n) => n.id === id ? {
|
|
374
|
+
...n,
|
|
375
|
+
position: {
|
|
376
|
+
x: position.x,
|
|
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
|
+
}));
|
|
612
|
+
}
|
|
613
|
+
function FlowBlock({ id, data, selected, isConnectable }) {
|
|
614
|
+
const lib = useMotionLib();
|
|
615
|
+
const presets = useMotionPresets();
|
|
616
|
+
const uid = useId();
|
|
617
|
+
const Icon = FLOW_ICON[data.kind];
|
|
618
|
+
const wait = data.kind === "espera" ? formatWait(data.config) : null;
|
|
619
|
+
const invalid = data.issues.length > 0;
|
|
620
|
+
const issueId = `${uid}-problema`;
|
|
621
|
+
const handle = cn("rounded-full border-2 border-card", data.handles === "lg" ? "size-6" : "size-2");
|
|
622
|
+
const ring = "pointer-events-none absolute -inset-1 rounded-block border-2 border-primary";
|
|
623
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
624
|
+
"data-flow-node": true,
|
|
625
|
+
"data-kind": data.kind,
|
|
626
|
+
"data-block-id": id,
|
|
627
|
+
"data-active": data.active ? "true" : void 0,
|
|
628
|
+
"data-invalid": invalid ? "true" : void 0,
|
|
629
|
+
"data-selected": selected ? "true" : void 0,
|
|
630
|
+
"aria-describedby": invalid ? issueId : void 0,
|
|
631
|
+
className: cn("relative w-flow-node rounded-block border bg-card p-3 text-card-foreground shadow-sm", data.kind === "condicao" && "pr-12 pb-8", invalid && "border-destructive", data.active && "border-primary", selected && "ring-2 ring-primary"),
|
|
632
|
+
children: [
|
|
633
|
+
data.active && (lib ? /* @__PURE__ */ jsx(lib.m.span, {
|
|
634
|
+
"aria-hidden": true,
|
|
635
|
+
"data-flow-pulse": true,
|
|
636
|
+
className: ring,
|
|
637
|
+
animate: presets.reduced ? { opacity: 1 } : { opacity: [
|
|
638
|
+
1,
|
|
639
|
+
.3,
|
|
640
|
+
1
|
|
641
|
+
] },
|
|
642
|
+
transition: presets.reduced ? presets.fade.transition : {
|
|
643
|
+
duration: presets.tokens.duration.pulse,
|
|
644
|
+
ease: presets.tokens.ease.inOut,
|
|
645
|
+
repeat: Infinity
|
|
646
|
+
}
|
|
647
|
+
}) : /* @__PURE__ */ jsx("span", {
|
|
648
|
+
"aria-hidden": true,
|
|
649
|
+
className: ring
|
|
650
|
+
})),
|
|
651
|
+
data.kind !== "gatilho" && /* @__PURE__ */ jsx(Handle, {
|
|
652
|
+
type: "target",
|
|
653
|
+
position: Position.Top,
|
|
654
|
+
className: handle,
|
|
655
|
+
isConnectable
|
|
656
|
+
}),
|
|
657
|
+
/* @__PURE__ */ jsxs("div", {
|
|
658
|
+
className: "flex items-start gap-3",
|
|
659
|
+
children: [
|
|
660
|
+
/* @__PURE__ */ jsx("span", {
|
|
661
|
+
className: cn("flex size-8 shrink-0 items-center justify-center rounded-block", TONE[data.kind]),
|
|
662
|
+
children: /* @__PURE__ */ jsx(Icon, {
|
|
663
|
+
className: "size-icon-md",
|
|
664
|
+
"aria-hidden": true
|
|
665
|
+
})
|
|
666
|
+
}),
|
|
667
|
+
/* @__PURE__ */ jsxs("div", {
|
|
668
|
+
className: "flex min-w-0 flex-1 flex-col",
|
|
669
|
+
children: [
|
|
670
|
+
/* @__PURE__ */ jsx("span", {
|
|
671
|
+
className: "text-xs text-muted-foreground",
|
|
672
|
+
children: FLOW_TYPE_NAME[data.kind]
|
|
673
|
+
}),
|
|
674
|
+
/* @__PURE__ */ jsx("span", {
|
|
675
|
+
className: "text-sm font-medium break-words",
|
|
676
|
+
children: data.label
|
|
677
|
+
}),
|
|
678
|
+
wait && /* @__PURE__ */ jsx("span", {
|
|
679
|
+
className: "text-xs text-muted-foreground",
|
|
680
|
+
children: wait
|
|
681
|
+
}),
|
|
682
|
+
data.active && /* @__PURE__ */ jsx("span", {
|
|
683
|
+
className: "text-xs font-medium text-primary-text",
|
|
684
|
+
children: "Ativo"
|
|
685
|
+
})
|
|
686
|
+
]
|
|
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
|
+
})
|
|
700
|
+
]
|
|
701
|
+
}),
|
|
702
|
+
invalid && /* @__PURE__ */ jsxs("p", {
|
|
703
|
+
id: issueId,
|
|
704
|
+
className: "mt-2 flex flex-wrap gap-x-1 rounded-block bg-destructive-soft p-2 text-xs text-destructive-soft-foreground",
|
|
705
|
+
children: [
|
|
706
|
+
/* @__PURE__ */ jsx(TriangleAlert, {
|
|
707
|
+
className: "size-icon-sm shrink-0",
|
|
708
|
+
"aria-hidden": true
|
|
709
|
+
}),
|
|
710
|
+
/* @__PURE__ */ jsx("strong", { children: "Problema" }),
|
|
711
|
+
" ",
|
|
712
|
+
data.issues.join(" ")
|
|
713
|
+
]
|
|
714
|
+
}),
|
|
715
|
+
data.kind === "condicao" && /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
716
|
+
/* @__PURE__ */ jsx("span", {
|
|
717
|
+
className: "absolute inset-y-0 right-4 flex items-center text-xs font-medium",
|
|
718
|
+
children: "Sim"
|
|
719
|
+
}),
|
|
720
|
+
/* @__PURE__ */ jsx("span", {
|
|
721
|
+
className: "absolute inset-x-0 bottom-3 text-center text-xs font-medium",
|
|
722
|
+
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
|
+
})
|
|
738
|
+
] }),
|
|
739
|
+
data.kind !== "condicao" && data.kind !== "fim" && /* @__PURE__ */ jsx(Handle, {
|
|
740
|
+
type: "source",
|
|
741
|
+
position: Position.Bottom,
|
|
742
|
+
className: handle,
|
|
743
|
+
isConnectable
|
|
744
|
+
})
|
|
745
|
+
]
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
var nodeTypes = { bloco: FlowBlock };
|
|
749
|
+
var defaultEdgeOptions = {
|
|
750
|
+
type: "smoothstep",
|
|
751
|
+
markerEnd: { type: MarkerType.ArrowClosed }
|
|
752
|
+
};
|
|
753
|
+
var fitViewOptions = {
|
|
754
|
+
padding: {
|
|
755
|
+
top: "64px",
|
|
756
|
+
right: "16px",
|
|
757
|
+
bottom: "64px",
|
|
758
|
+
left: "16px"
|
|
759
|
+
},
|
|
760
|
+
maxZoom: 1
|
|
761
|
+
};
|
|
762
|
+
/** Aproximar, afastar e enquadrar, com 44 px de toque (os botões da biblioteca têm 26 px). */
|
|
763
|
+
function FlowZoomControls() {
|
|
764
|
+
const { zoomIn, zoomOut, fitView } = useReactFlow();
|
|
765
|
+
const cls = "min-h-touch min-w-touch";
|
|
766
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
767
|
+
className: "absolute top-2 left-2 z-10 flex gap-2",
|
|
768
|
+
children: [
|
|
769
|
+
/* @__PURE__ */ jsx(Button, {
|
|
770
|
+
variant: "outline",
|
|
771
|
+
size: "sm",
|
|
772
|
+
iconOnly: true,
|
|
773
|
+
"aria-label": "Aproximar",
|
|
774
|
+
className: cls,
|
|
775
|
+
icon: /* @__PURE__ */ jsx(ZoomIn, {}),
|
|
776
|
+
onClick: () => void zoomIn()
|
|
777
|
+
}),
|
|
778
|
+
/* @__PURE__ */ jsx(Button, {
|
|
779
|
+
variant: "outline",
|
|
780
|
+
size: "sm",
|
|
781
|
+
iconOnly: true,
|
|
782
|
+
"aria-label": "Afastar",
|
|
783
|
+
className: cls,
|
|
784
|
+
icon: /* @__PURE__ */ jsx(ZoomOut, {}),
|
|
785
|
+
onClick: () => void zoomOut()
|
|
786
|
+
}),
|
|
787
|
+
/* @__PURE__ */ jsx(Button, {
|
|
788
|
+
variant: "outline",
|
|
789
|
+
size: "sm",
|
|
790
|
+
iconOnly: true,
|
|
791
|
+
"aria-label": "Enquadrar",
|
|
792
|
+
className: cls,
|
|
793
|
+
icon: /* @__PURE__ */ jsx(Maximize2, {}),
|
|
794
|
+
onClick: () => void fitView(fitViewOptions)
|
|
795
|
+
})
|
|
796
|
+
]
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
/** Enquadra de novo quando a largura do canvas muda (painel do teste abrindo, janela redimensionada). */
|
|
800
|
+
function FlowAutoFit() {
|
|
801
|
+
const { fitView } = useReactFlow();
|
|
802
|
+
const width = useStore((st) => st.width);
|
|
803
|
+
const before = useRef(0);
|
|
804
|
+
useEffect(() => {
|
|
805
|
+
if (before.current > 0 && width > 0 && width !== before.current) fitView(fitViewOptions);
|
|
806
|
+
if (width > 0) before.current = width;
|
|
807
|
+
}, [width, fitView]);
|
|
808
|
+
return null;
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* O canvas: contêiner com a altura que sobra na tela (`h-board`), tema pelos tokens e a
|
|
812
|
+
* biblioteca por dentro. A rolagem horizontal é do canvas (`data-allow-overflow`), nunca da página.
|
|
813
|
+
*/
|
|
814
|
+
function FlowSurface({ className, children, ...props }) {
|
|
815
|
+
const ref = useFillHeight();
|
|
816
|
+
return /* @__PURE__ */ jsx("div", {
|
|
817
|
+
ref,
|
|
818
|
+
"data-allow-overflow": true,
|
|
819
|
+
className: cn("flow-surface relative h-board min-h-viewer w-full min-w-0 overflow-hidden rounded-surface border", className),
|
|
820
|
+
style: {
|
|
821
|
+
"--xy-edge-stroke": "var(--rendra-muted-foreground)",
|
|
822
|
+
"--xy-edge-stroke-selected": "var(--rendra-primary)",
|
|
823
|
+
"--xy-edge-stroke-width": "2",
|
|
824
|
+
"--xy-connectionline-stroke": "var(--rendra-primary)",
|
|
825
|
+
"--xy-connectionline-stroke-width": "2",
|
|
826
|
+
"--xy-handle-background-color": "var(--rendra-primary)",
|
|
827
|
+
"--xy-attribution-background-color": "var(--rendra-card)",
|
|
828
|
+
"--xy-background-color": "var(--rendra-muted)"
|
|
829
|
+
},
|
|
830
|
+
children: /* @__PURE__ */ jsxs(ReactFlow, {
|
|
831
|
+
className: "flow-canvas",
|
|
832
|
+
nodeTypes,
|
|
833
|
+
defaultEdgeOptions,
|
|
834
|
+
defaultMarkerColor: "var(--rendra-muted-foreground)",
|
|
835
|
+
connectionLineType: ConnectionLineType.SmoothStep,
|
|
836
|
+
ariaLabelConfig: FLOW_ARIA,
|
|
837
|
+
fitView: true,
|
|
838
|
+
fitViewOptions,
|
|
839
|
+
minZoom: .25,
|
|
840
|
+
maxZoom: 2,
|
|
841
|
+
zoomOnDoubleClick: false,
|
|
842
|
+
zoomOnScroll: false,
|
|
843
|
+
preventScrolling: false,
|
|
844
|
+
...props,
|
|
845
|
+
children: [
|
|
846
|
+
/* @__PURE__ */ jsx(FlowZoomControls, {}),
|
|
847
|
+
/* @__PURE__ */ jsx(FlowAutoFit, {}),
|
|
848
|
+
children
|
|
849
|
+
]
|
|
850
|
+
})
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* Fluxo de automação só para ler: aproximar, afastar e mover o canvas. Não arrasta, não liga,
|
|
855
|
+
* não apaga. Para montar e editar, use o FlowEditor.
|
|
856
|
+
*/
|
|
857
|
+
function FlowViewer({ value, activeId = null, issues, className }) {
|
|
858
|
+
const nodes = useMemo(() => toRfNodes(value, {
|
|
859
|
+
activeId,
|
|
860
|
+
issues,
|
|
861
|
+
handles: "sm",
|
|
862
|
+
edit: false
|
|
863
|
+
}), [
|
|
864
|
+
value,
|
|
865
|
+
activeId,
|
|
866
|
+
issues
|
|
867
|
+
]);
|
|
868
|
+
const edges = useMemo(() => toRfEdges(value), [value]);
|
|
869
|
+
return /* @__PURE__ */ jsx("div", {
|
|
870
|
+
"data-rendra": "FLX-002",
|
|
871
|
+
className: cn("flow-viewer min-w-0", className),
|
|
872
|
+
children: /* @__PURE__ */ jsx(FlowSurface, {
|
|
873
|
+
nodes,
|
|
874
|
+
edges,
|
|
875
|
+
nodesDraggable: false,
|
|
876
|
+
nodesConnectable: false,
|
|
877
|
+
elementsSelectable: false,
|
|
878
|
+
edgesFocusable: false,
|
|
879
|
+
deleteKeyCode: null
|
|
880
|
+
})
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
//#endregion
|
|
884
|
+
//#region src/components/ui/flow-editor.tsx
|
|
885
|
+
var NODE_W = 256;
|
|
886
|
+
var NODE_H = 72;
|
|
887
|
+
var DRAG_START = 4;
|
|
888
|
+
var NONE = "__nenhum";
|
|
889
|
+
/** Reaproveita os objetos de nó que não mudaram (a biblioteca guarda medida e seleção neles). */
|
|
890
|
+
function mergeNodes(prev, next) {
|
|
891
|
+
const old = new Map(prev.map((n) => [n.id, n]));
|
|
892
|
+
return next.map((n) => {
|
|
893
|
+
const o = old.get(n.id);
|
|
894
|
+
if (!o) return n;
|
|
895
|
+
const a = o.data;
|
|
896
|
+
const b = n.data;
|
|
897
|
+
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.handles === b.handles && a.edit === b.edit && a.issues.length === b.issues.length && a.issues.every((m, i) => m === b.issues[i])) return o;
|
|
898
|
+
return {
|
|
899
|
+
...o,
|
|
900
|
+
position: o.dragging ? o.position : n.position,
|
|
901
|
+
ariaLabel: n.ariaLabel,
|
|
902
|
+
data: b
|
|
903
|
+
};
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
function mergeEdges(prev, next) {
|
|
907
|
+
const old = new Map(prev.map((e) => [e.id, e]));
|
|
908
|
+
return next.map((e) => {
|
|
909
|
+
const o = old.get(e.id);
|
|
910
|
+
if (!o) return e;
|
|
911
|
+
return o.source === e.source && o.target === e.target && o.sourceHandle === e.sourceHandle && o.ariaLabel === e.ariaLabel ? o : {
|
|
912
|
+
...o,
|
|
913
|
+
...e
|
|
914
|
+
};
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
function draftOf(node, flow) {
|
|
918
|
+
const target = (branch) => flow.edges.find((e) => e.source === node.id && (branch ? e.branch === branch : true))?.target ?? NONE;
|
|
919
|
+
const cfg = node.data.config;
|
|
920
|
+
const unit = FLOW_WAIT_UNITS.find((u) => u === cfg?.unit) ?? "minutos";
|
|
921
|
+
return {
|
|
922
|
+
label: node.data.label,
|
|
923
|
+
duration: typeof cfg?.duration === "number" ? String(cfg.duration) : "",
|
|
924
|
+
unit,
|
|
925
|
+
next: node.type === "condicao" ? NONE : target(),
|
|
926
|
+
sim: node.type === "condicao" ? target("sim") : NONE,
|
|
927
|
+
nao: node.type === "condicao" ? target("nao") : NONE
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
var sameDraft = (a, b) => a.label === b.label && a.duration === b.duration && a.unit === b.unit && a.next === b.next && a.sim === b.sim && a.nao === b.nao;
|
|
931
|
+
function BlockDrawer({ flow, nodeId, open, onOpenChange, onApply, onDelete }) {
|
|
932
|
+
const node = flow.nodes.find((n) => n.id === nodeId);
|
|
933
|
+
const initial = useMemo(() => node ? draftOf(node, flow) : null, [node, flow]);
|
|
934
|
+
const [draft, setDraft] = useState(initial);
|
|
935
|
+
const [discarding, setDiscarding] = useState(false);
|
|
936
|
+
if (!node || !initial || !draft) return null;
|
|
937
|
+
const dirty = !sameDraft(draft, initial);
|
|
938
|
+
const duration = Number(draft.duration);
|
|
939
|
+
const durationError = node.type === "espera" && (draft.duration.trim() === "" || !(duration > 0)) ? "Informe um número maior que zero." : void 0;
|
|
940
|
+
const set = (patch) => setDraft({
|
|
941
|
+
...draft,
|
|
942
|
+
...patch
|
|
943
|
+
});
|
|
944
|
+
const options = [{
|
|
945
|
+
value: NONE,
|
|
946
|
+
label: "Nenhum"
|
|
947
|
+
}, ...nextCandidates(flow, node.id).map((n) => ({
|
|
948
|
+
value: n.id,
|
|
949
|
+
label: `${FLOW_TYPE_NAME[n.type]}: ${nodeName(n)}`
|
|
950
|
+
}))];
|
|
951
|
+
const target = (value) => value && value !== NONE ? value : null;
|
|
952
|
+
const apply = () => {
|
|
953
|
+
let next = updateNodeData(flow, node.id, {
|
|
954
|
+
label: draft.label,
|
|
955
|
+
...node.type === "espera" ? { config: {
|
|
956
|
+
duration,
|
|
957
|
+
unit: draft.unit
|
|
958
|
+
} } : {}
|
|
959
|
+
});
|
|
960
|
+
if (node.type === "condicao") {
|
|
961
|
+
next = setNext(next, node.id, target(draft.sim), "sim");
|
|
962
|
+
next = setNext(next, node.id, target(draft.nao), "nao");
|
|
963
|
+
} else if (node.type !== "fim") next = setNext(next, node.id, target(draft.next));
|
|
964
|
+
onApply(next);
|
|
965
|
+
};
|
|
966
|
+
const nextField = (name, key) => /* @__PURE__ */ jsx(Field, {
|
|
967
|
+
label: name,
|
|
968
|
+
span: "full",
|
|
969
|
+
children: /* @__PURE__ */ jsx(Select, {
|
|
970
|
+
label: name,
|
|
971
|
+
options,
|
|
972
|
+
value: draft[key],
|
|
973
|
+
onChange: (v) => set({ [key]: v ?? NONE })
|
|
974
|
+
})
|
|
975
|
+
});
|
|
976
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Drawer, {
|
|
977
|
+
open,
|
|
978
|
+
onOpenChange,
|
|
979
|
+
title: `Bloco: ${nodeName(node)}`,
|
|
980
|
+
description: FLOW_TYPE_NAME[node.type],
|
|
981
|
+
dirty,
|
|
982
|
+
footer: /* @__PURE__ */ jsx(ActionBar, {
|
|
983
|
+
primary: {
|
|
984
|
+
label: "Aplicar",
|
|
985
|
+
onClick: apply,
|
|
986
|
+
disabled: draft.label.trim() === "" || Boolean(durationError)
|
|
987
|
+
},
|
|
988
|
+
cancel: {
|
|
989
|
+
label: "Cancelar",
|
|
990
|
+
onClick: () => dirty ? setDiscarding(true) : onOpenChange(false)
|
|
991
|
+
},
|
|
992
|
+
secondary: [{
|
|
993
|
+
label: "Excluir bloco",
|
|
994
|
+
icon: /* @__PURE__ */ jsx(Trash2, {}),
|
|
995
|
+
destructive: true,
|
|
996
|
+
onClick: () => onDelete(node.id)
|
|
997
|
+
}]
|
|
998
|
+
}),
|
|
999
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
1000
|
+
className: "flex flex-col gap-4",
|
|
1001
|
+
children: [
|
|
1002
|
+
/* @__PURE__ */ jsx(Field, {
|
|
1003
|
+
label: "Rótulo",
|
|
1004
|
+
span: "full",
|
|
1005
|
+
error: draft.label.trim() === "" ? "Informe um rótulo." : void 0,
|
|
1006
|
+
children: /* @__PURE__ */ jsx(Input, {
|
|
1007
|
+
value: draft.label,
|
|
1008
|
+
onChange: (v) => set({ label: v })
|
|
1009
|
+
})
|
|
1010
|
+
}),
|
|
1011
|
+
node.type === "espera" && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Field, {
|
|
1012
|
+
label: "Duração",
|
|
1013
|
+
span: "full",
|
|
1014
|
+
error: durationError,
|
|
1015
|
+
children: /* @__PURE__ */ jsx(Input, {
|
|
1016
|
+
value: draft.duration,
|
|
1017
|
+
inputMode: "numeric",
|
|
1018
|
+
onChange: (v) => set({ duration: v })
|
|
1019
|
+
})
|
|
1020
|
+
}), /* @__PURE__ */ jsx(Field, {
|
|
1021
|
+
label: "Unidade",
|
|
1022
|
+
span: "full",
|
|
1023
|
+
children: /* @__PURE__ */ jsx(Select, {
|
|
1024
|
+
label: "Unidade",
|
|
1025
|
+
options: FLOW_WAIT_UNITS.map((u) => ({
|
|
1026
|
+
value: u,
|
|
1027
|
+
label: u
|
|
1028
|
+
})),
|
|
1029
|
+
value: draft.unit,
|
|
1030
|
+
onChange: (v) => set({ unit: v ?? "minutos" })
|
|
1031
|
+
})
|
|
1032
|
+
})] }),
|
|
1033
|
+
node.type === "condicao" && /* @__PURE__ */ jsxs(Fragment$1, { children: [nextField("Se sim", "sim"), nextField("Se não", "nao")] }),
|
|
1034
|
+
node.type !== "condicao" && node.type !== "fim" && nextField("Próximo bloco", "next")
|
|
1035
|
+
]
|
|
1036
|
+
})
|
|
1037
|
+
}), /* @__PURE__ */ jsx(Modal, {
|
|
1038
|
+
open: discarding,
|
|
1039
|
+
onOpenChange: setDiscarding,
|
|
1040
|
+
type: "destructive",
|
|
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) => ({
|
|
1052
|
+
id: `${i}-${s.nodeId}`,
|
|
1053
|
+
title: s.label,
|
|
1054
|
+
description: s.detail,
|
|
1055
|
+
date: `Passo ${i + 1}`,
|
|
1056
|
+
status: s.status
|
|
1057
|
+
}));
|
|
1058
|
+
const choose = (choice) => onSim(advanceSimulation(flow, sim, choice));
|
|
1059
|
+
return /* @__PURE__ */ jsxs(Card, {
|
|
1060
|
+
role: "region",
|
|
1061
|
+
"aria-label": "Teste do fluxo",
|
|
1062
|
+
className: "min-w-0 shrink-0 lg:w-popover",
|
|
1063
|
+
children: [/* @__PURE__ */ jsxs(CardHeader, {
|
|
1064
|
+
actions: /* @__PURE__ */ jsx(Button, {
|
|
1065
|
+
size: "sm",
|
|
1066
|
+
variant: "ghost",
|
|
1067
|
+
iconOnly: true,
|
|
1068
|
+
"aria-label": "Fechar teste",
|
|
1069
|
+
icon: /* @__PURE__ */ jsx(X, {}),
|
|
1070
|
+
onClick: onClose
|
|
1071
|
+
}),
|
|
1072
|
+
children: [/* @__PURE__ */ jsx(CardTitle, { children: "Teste do fluxo" }), /* @__PURE__ */ jsx(CardDescription, { children: "Um passo por vez, sem executar nada de verdade." })]
|
|
1073
|
+
}), /* @__PURE__ */ jsxs(CardContent, {
|
|
1074
|
+
className: "flex flex-col gap-4",
|
|
1075
|
+
children: [
|
|
1076
|
+
/* @__PURE__ */ jsx(ActionBar, {
|
|
1077
|
+
primary: {
|
|
1078
|
+
label: "Avançar",
|
|
1079
|
+
disabled: sim.status !== "running",
|
|
1080
|
+
onClick: () => onSim(advanceSimulation(flow, sim))
|
|
1081
|
+
},
|
|
1082
|
+
cancel: {
|
|
1083
|
+
label: "Reiniciar",
|
|
1084
|
+
onClick: () => onSim(startSimulation(flow))
|
|
1085
|
+
}
|
|
1086
|
+
}),
|
|
1087
|
+
sim.status === "error" && /* @__PURE__ */ jsxs("div", {
|
|
1088
|
+
className: "flex flex-col gap-3",
|
|
1089
|
+
children: [/* @__PURE__ */ jsx(Alert, {
|
|
1090
|
+
type: "error",
|
|
1091
|
+
title: "O fluxo precisa de ajustes antes do teste"
|
|
1092
|
+
}), /* @__PURE__ */ jsx("ul", {
|
|
1093
|
+
className: "flex list-disc flex-col gap-1 pl-4 text-sm",
|
|
1094
|
+
children: sim.issues.map((i, k) => /* @__PURE__ */ jsx("li", { children: i.message }, `${i.code}-${k}`))
|
|
1095
|
+
})]
|
|
1096
|
+
}),
|
|
1097
|
+
waiting && current && /* @__PURE__ */ jsxs("div", {
|
|
1098
|
+
role: "group",
|
|
1099
|
+
"aria-label": "Escolha o caminho da condição",
|
|
1100
|
+
className: "flex flex-col gap-3",
|
|
1101
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
1102
|
+
className: "text-sm font-medium",
|
|
1103
|
+
children: nodeName(current)
|
|
1104
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
1105
|
+
className: "grid grid-actions-2 gap-3",
|
|
1106
|
+
children: [/* @__PURE__ */ jsx(Button, {
|
|
1107
|
+
variant: "outline",
|
|
1108
|
+
onClick: () => choose("sim"),
|
|
1109
|
+
children: "Sim"
|
|
1110
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
1111
|
+
variant: "outline",
|
|
1112
|
+
onClick: () => choose("nao"),
|
|
1113
|
+
children: "Não"
|
|
1114
|
+
})]
|
|
1115
|
+
})]
|
|
1116
|
+
}),
|
|
1117
|
+
sim.status === "finished" && /* @__PURE__ */ jsxs("p", {
|
|
1118
|
+
role: "status",
|
|
1119
|
+
className: "flex items-center gap-2 text-sm font-medium text-success-soft-foreground",
|
|
1120
|
+
children: [/* @__PURE__ */ jsx(CheckCircle2, {
|
|
1121
|
+
className: "size-icon-md",
|
|
1122
|
+
"aria-hidden": true
|
|
1123
|
+
}), "Fluxo concluído"]
|
|
1124
|
+
}),
|
|
1125
|
+
events.length > 0 && /* @__PURE__ */ jsx(Timeline, { events }),
|
|
1126
|
+
events.length === 0 && sim.status !== "error" && /* @__PURE__ */ jsx("p", {
|
|
1127
|
+
className: "text-sm text-muted-foreground",
|
|
1128
|
+
children: "Nenhum passo executado ainda."
|
|
1129
|
+
})
|
|
1130
|
+
]
|
|
1131
|
+
})]
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
function FlowEditorInner({ value, onChange, readOnly = false, className }) {
|
|
1135
|
+
const { isMobile } = useBreakpoint();
|
|
1136
|
+
const canEdit = !readOnly;
|
|
1137
|
+
const canBuild = canEdit && !isMobile;
|
|
1138
|
+
const { screenToFlowPosition, fitView } = useReactFlow();
|
|
1139
|
+
const canvasRef = useRef(null);
|
|
1140
|
+
const noticeId = useId();
|
|
1141
|
+
const valueRef = useRef(value);
|
|
1142
|
+
valueRef.current = value;
|
|
1143
|
+
const everUsed = useRef(/* @__PURE__ */ new Set());
|
|
1144
|
+
value.nodes.forEach((n) => everUsed.current.add(n.id));
|
|
1145
|
+
const [validated, setValidated] = useState(false);
|
|
1146
|
+
const [sim, setSim] = useState(resetSimulation);
|
|
1147
|
+
const [notice, setNotice] = useState(null);
|
|
1148
|
+
const [drawer, setDrawer] = useState(null);
|
|
1149
|
+
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
1150
|
+
const drawerOpenRef = useRef(false);
|
|
1151
|
+
drawerOpenRef.current = drawerOpen;
|
|
1152
|
+
const [deleting, setDeleting] = useState(null);
|
|
1153
|
+
const [drag, setDrag] = useState(null);
|
|
1154
|
+
const suppressClick = useRef(false);
|
|
1155
|
+
const issues = useMemo(() => validateFlow(value), [value]);
|
|
1156
|
+
const shownIssues = validated ? issues : void 0;
|
|
1157
|
+
const activeId = sim.status === "running" || sim.status === "waiting-choice" ? sim.currentId : null;
|
|
1158
|
+
const nextNodes = useMemo(() => toRfNodes(value, {
|
|
1159
|
+
activeId,
|
|
1160
|
+
issues: shownIssues,
|
|
1161
|
+
handles: canBuild ? "lg" : "sm",
|
|
1162
|
+
edit: canBuild
|
|
1163
|
+
}), [
|
|
1164
|
+
value,
|
|
1165
|
+
activeId,
|
|
1166
|
+
shownIssues,
|
|
1167
|
+
canBuild
|
|
1168
|
+
]);
|
|
1169
|
+
const nextEdges = useMemo(() => toRfEdges(value), [value]);
|
|
1170
|
+
const [rfNodes, setRfNodes] = useState(nextNodes);
|
|
1171
|
+
const [rfEdges, setRfEdges] = useState(nextEdges);
|
|
1172
|
+
useEffect(() => setRfNodes((prev) => mergeNodes(prev, nextNodes)), [nextNodes]);
|
|
1173
|
+
useEffect(() => setRfEdges((prev) => mergeEdges(prev, nextEdges)), [nextEdges]);
|
|
1174
|
+
const emit = useCallback((flow) => {
|
|
1175
|
+
setNotice(null);
|
|
1176
|
+
onChange(flow);
|
|
1177
|
+
}, [onChange]);
|
|
1178
|
+
const commitMoves = useCallback((moves) => {
|
|
1179
|
+
let flow = valueRef.current;
|
|
1180
|
+
for (const m of moves) {
|
|
1181
|
+
const cur = flow.nodes.find((n) => n.id === m.id);
|
|
1182
|
+
if (cur && (cur.position.x !== m.position.x || cur.position.y !== m.position.y)) flow = moveNode(flow, m.id, m.position);
|
|
1183
|
+
}
|
|
1184
|
+
if (flow !== valueRef.current) emit(flow);
|
|
1185
|
+
}, [emit]);
|
|
1186
|
+
const onNodesChange = useCallback((changes) => {
|
|
1187
|
+
setRfNodes((nds) => applyNodeChanges(changes.filter((c) => c.type !== "remove"), nds));
|
|
1188
|
+
const moves = changes.flatMap((c) => c.type === "position" && c.dragging === false && c.position ? [{
|
|
1189
|
+
id: c.id,
|
|
1190
|
+
position: c.position
|
|
1191
|
+
}] : []);
|
|
1192
|
+
if (moves.length > 0 && canBuild) commitMoves(moves);
|
|
1193
|
+
}, [canBuild, commitMoves]);
|
|
1194
|
+
const onEdgesChange = useCallback((changes) => setRfEdges((eds) => applyEdgeChanges(changes.filter((c) => c.type !== "remove"), eds)), []);
|
|
1195
|
+
const onConnect = useCallback((c) => {
|
|
1196
|
+
if (!c.source || !c.target) return;
|
|
1197
|
+
const branch = c.sourceHandle === "sim" || c.sourceHandle === "nao" ? c.sourceHandle : void 0;
|
|
1198
|
+
const r = connectNodes(valueRef.current, c.source, c.target, branch);
|
|
1199
|
+
if (r.ok) emit(r.flow);
|
|
1200
|
+
else setNotice(r.reason);
|
|
1201
|
+
}, [emit]);
|
|
1202
|
+
const addBlock = useCallback((type, at) => {
|
|
1203
|
+
const flow = valueRef.current;
|
|
1204
|
+
if (type === "gatilho" && flow.nodes.some((n) => n.type === "gatilho")) {
|
|
1205
|
+
setNotice("Já existe um gatilho neste fluxo.");
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
let pos = at;
|
|
1209
|
+
if (!pos) {
|
|
1210
|
+
const rect = canvasRef.current?.getBoundingClientRect();
|
|
1211
|
+
const center = screenToFlowPosition({
|
|
1212
|
+
x: (rect?.left ?? 0) + (rect?.width ?? 0) / 2,
|
|
1213
|
+
y: (rect?.top ?? 0) + (rect?.height ?? 0) / 2
|
|
1214
|
+
});
|
|
1215
|
+
pos = {
|
|
1216
|
+
x: center.x - NODE_W / 2,
|
|
1217
|
+
y: center.y - NODE_H / 2
|
|
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
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
emit(addNode(flow, type, pos, { reservedIds: everUsed.current }).flow);
|
|
1225
|
+
}, [emit, screenToFlowPosition]);
|
|
1226
|
+
/** Arrastar um item da paleta até o canvas, por pointer events (funciona com mouse e toque). */
|
|
1227
|
+
const startPaletteDrag = (e, type) => {
|
|
1228
|
+
if (e.pointerType === "mouse" && e.button !== 0) return;
|
|
1229
|
+
const origin = {
|
|
1230
|
+
x: e.clientX,
|
|
1231
|
+
y: e.clientY
|
|
1232
|
+
};
|
|
1233
|
+
let moved = false;
|
|
1234
|
+
const inside = (x, y) => {
|
|
1235
|
+
const r = canvasRef.current?.getBoundingClientRect();
|
|
1236
|
+
return !!r && x >= r.left && x <= r.right && y >= r.top && y <= r.bottom;
|
|
1237
|
+
};
|
|
1238
|
+
const stop = () => {
|
|
1239
|
+
window.removeEventListener("pointermove", move);
|
|
1240
|
+
window.removeEventListener("pointerup", up);
|
|
1241
|
+
window.removeEventListener("pointercancel", cancel);
|
|
1242
|
+
window.removeEventListener("keydown", key);
|
|
1243
|
+
setDrag(null);
|
|
1244
|
+
};
|
|
1245
|
+
const move = (ev) => {
|
|
1246
|
+
if (!moved && Math.hypot(ev.clientX - origin.x, ev.clientY - origin.y) < DRAG_START) return;
|
|
1247
|
+
moved = true;
|
|
1248
|
+
setDrag({
|
|
1249
|
+
type,
|
|
1250
|
+
x: ev.clientX,
|
|
1251
|
+
y: ev.clientY,
|
|
1252
|
+
over: inside(ev.clientX, ev.clientY)
|
|
1253
|
+
});
|
|
1254
|
+
};
|
|
1255
|
+
const up = (ev) => {
|
|
1256
|
+
stop();
|
|
1257
|
+
if (!moved) return;
|
|
1258
|
+
suppressClick.current = true;
|
|
1259
|
+
setTimeout(() => suppressClick.current = false, 0);
|
|
1260
|
+
if (inside(ev.clientX, ev.clientY)) {
|
|
1261
|
+
const p = screenToFlowPosition({
|
|
1262
|
+
x: ev.clientX,
|
|
1263
|
+
y: ev.clientY
|
|
1264
|
+
});
|
|
1265
|
+
addBlock(type, {
|
|
1266
|
+
x: p.x - NODE_W / 2,
|
|
1267
|
+
y: p.y - NODE_H / 2
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
};
|
|
1271
|
+
const cancel = () => {
|
|
1272
|
+
stop();
|
|
1273
|
+
if (moved) {
|
|
1274
|
+
suppressClick.current = true;
|
|
1275
|
+
setTimeout(() => suppressClick.current = false, 0);
|
|
1276
|
+
}
|
|
1277
|
+
};
|
|
1278
|
+
const key = (ev) => {
|
|
1279
|
+
if (ev.key === "Escape") cancel();
|
|
1280
|
+
};
|
|
1281
|
+
window.addEventListener("pointermove", move);
|
|
1282
|
+
window.addEventListener("pointerup", up);
|
|
1283
|
+
window.addEventListener("pointercancel", cancel);
|
|
1284
|
+
window.addEventListener("keydown", key);
|
|
1285
|
+
};
|
|
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
|
+
const viewBlock = (id) => {
|
|
1295
|
+
setRfNodes((nds) => nds.map((n) => n.selected === (n.id === id) ? n : {
|
|
1296
|
+
...n,
|
|
1297
|
+
selected: n.id === id
|
|
1298
|
+
}));
|
|
1299
|
+
fitView({
|
|
1300
|
+
nodes: [{ id }],
|
|
1301
|
+
padding: .5,
|
|
1302
|
+
maxZoom: 1
|
|
1303
|
+
});
|
|
1304
|
+
};
|
|
1305
|
+
const startTest = () => setSim(startSimulation(value));
|
|
1306
|
+
const closeTest = () => setSim(resetSimulation());
|
|
1307
|
+
const deleteNode = deleting ? value.nodes.find((n) => n.id === deleting) : void 0;
|
|
1308
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1309
|
+
"data-rendra": "FLX-001",
|
|
1310
|
+
className: cn("flow-editor flex min-w-0 flex-col gap-4", className),
|
|
1311
|
+
children: [
|
|
1312
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1313
|
+
className: "flex min-w-0 flex-col gap-4 lg:flex-row lg:items-start",
|
|
1314
|
+
children: [/* @__PURE__ */ jsxs(Card, {
|
|
1315
|
+
className: "min-w-0 flex-1",
|
|
1316
|
+
children: [/* @__PURE__ */ jsxs(CardHeader, {
|
|
1317
|
+
actions: /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Button, {
|
|
1318
|
+
variant: "outline",
|
|
1319
|
+
iconOnly: isMobile,
|
|
1320
|
+
"aria-label": "Validar",
|
|
1321
|
+
icon: /* @__PURE__ */ jsx(ListChecks, {}),
|
|
1322
|
+
onClick: () => setValidated(true),
|
|
1323
|
+
children: isMobile ? null : "Validar"
|
|
1324
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
1325
|
+
iconOnly: isMobile,
|
|
1326
|
+
"aria-label": "Testar fluxo",
|
|
1327
|
+
icon: /* @__PURE__ */ jsx(FlaskConical, {}),
|
|
1328
|
+
onClick: startTest,
|
|
1329
|
+
children: isMobile ? null : "Testar fluxo"
|
|
1330
|
+
})] }),
|
|
1331
|
+
children: [/* @__PURE__ */ jsx(CardTitle, { children: "Fluxo de automação" }), /* @__PURE__ */ jsx(CardDescription, { children: "Blocos, ligações e teste do caminho." })]
|
|
1332
|
+
}), /* @__PURE__ */ jsxs(CardContent, {
|
|
1333
|
+
className: "flex flex-col gap-4",
|
|
1334
|
+
children: [
|
|
1335
|
+
canBuild && /* @__PURE__ */ jsx("div", {
|
|
1336
|
+
role: "group",
|
|
1337
|
+
"aria-label": "Blocos",
|
|
1338
|
+
className: "flex flex-wrap items-center gap-2",
|
|
1339
|
+
children: FLOW_NODE_TYPES.map((type) => {
|
|
1340
|
+
const Icon = FLOW_ICON[type];
|
|
1341
|
+
return /* @__PURE__ */ jsx(Button, {
|
|
1342
|
+
variant: "outline",
|
|
1343
|
+
size: "sm",
|
|
1344
|
+
icon: /* @__PURE__ */ jsx(Icon, {}),
|
|
1345
|
+
className: "cursor-grab touch-none",
|
|
1346
|
+
onPointerDown: (e) => startPaletteDrag(e, type),
|
|
1347
|
+
onClick: () => {
|
|
1348
|
+
if (suppressClick.current) return;
|
|
1349
|
+
addBlock(type);
|
|
1350
|
+
},
|
|
1351
|
+
children: FLOW_TYPE_NAME[type]
|
|
1352
|
+
}, type);
|
|
1353
|
+
})
|
|
1354
|
+
}),
|
|
1355
|
+
/* @__PURE__ */ jsx("div", {
|
|
1356
|
+
ref: canvasRef,
|
|
1357
|
+
className: "min-w-0",
|
|
1358
|
+
children: /* @__PURE__ */ jsx(FlowSurface, {
|
|
1359
|
+
className: cn(drag?.over && "ring-2 ring-primary"),
|
|
1360
|
+
nodes: rfNodes,
|
|
1361
|
+
edges: rfEdges,
|
|
1362
|
+
onNodesChange,
|
|
1363
|
+
onEdgesChange,
|
|
1364
|
+
onConnect,
|
|
1365
|
+
nodesDraggable: canBuild,
|
|
1366
|
+
nodesConnectable: canBuild,
|
|
1367
|
+
elementsSelectable: canEdit,
|
|
1368
|
+
deleteKeyCode: canEdit ? ["Delete", "Backspace"] : null,
|
|
1369
|
+
onBeforeDelete: async () => canEdit && !drawerOpenRef.current,
|
|
1370
|
+
onDelete: ({ nodes, edges }) => {
|
|
1371
|
+
let flow = valueRef.current;
|
|
1372
|
+
for (const e of edges) flow = removeEdge(flow, e.id);
|
|
1373
|
+
for (const n of nodes) flow = removeNode(flow, n.id);
|
|
1374
|
+
if (flow !== valueRef.current) emit(flow);
|
|
1375
|
+
},
|
|
1376
|
+
onNodeDragStop: (_, __, nodes) => commitMoves(nodes.map((n) => ({
|
|
1377
|
+
id: n.id,
|
|
1378
|
+
position: n.position
|
|
1379
|
+
}))),
|
|
1380
|
+
onNodeClick: (e, node) => {
|
|
1381
|
+
const fromEditButton = e.target.closest("[data-flow-edit]");
|
|
1382
|
+
if (isMobile || fromEditButton) openDrawer(node.id);
|
|
1383
|
+
},
|
|
1384
|
+
onNodeDoubleClick: (_, node) => openDrawer(node.id),
|
|
1385
|
+
onKeyDown: (e) => {
|
|
1386
|
+
const el = e.target;
|
|
1387
|
+
if (e.key === "Enter" && el.classList?.contains("react-flow__node") && el.dataset.id) {
|
|
1388
|
+
e.preventDefault();
|
|
1389
|
+
openDrawer(el.dataset.id);
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
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
|
+
})] })
|
|
1431
|
+
})
|
|
1432
|
+
]
|
|
1433
|
+
})]
|
|
1434
|
+
}), sim.status !== "idle" && /* @__PURE__ */ jsx(SimulationPanel, {
|
|
1435
|
+
flow: value,
|
|
1436
|
+
sim,
|
|
1437
|
+
onSim: setSim,
|
|
1438
|
+
onClose: closeTest
|
|
1439
|
+
})]
|
|
1440
|
+
}),
|
|
1441
|
+
drag && /* @__PURE__ */ jsx("div", {
|
|
1442
|
+
"aria-hidden": true,
|
|
1443
|
+
"data-flow-drag": true,
|
|
1444
|
+
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",
|
|
1445
|
+
style: {
|
|
1446
|
+
"--flow-x": `${drag.x}px`,
|
|
1447
|
+
"--flow-y": `${drag.y}px`
|
|
1448
|
+
},
|
|
1449
|
+
children: FLOW_TYPE_NAME[drag.type]
|
|
1450
|
+
}),
|
|
1451
|
+
canEdit && drawer && /* @__PURE__ */ jsx(BlockDrawer, {
|
|
1452
|
+
flow: value,
|
|
1453
|
+
nodeId: drawer.id,
|
|
1454
|
+
open: drawerOpen,
|
|
1455
|
+
onOpenChange: setDrawerOpen,
|
|
1456
|
+
onApply: (flow) => {
|
|
1457
|
+
setDrawerOpen(false);
|
|
1458
|
+
emit(flow);
|
|
1459
|
+
},
|
|
1460
|
+
onDelete: setDeleting
|
|
1461
|
+
}, drawer.session),
|
|
1462
|
+
/* @__PURE__ */ jsx(Modal, {
|
|
1463
|
+
open: Boolean(deleting),
|
|
1464
|
+
onOpenChange: (o) => !o && setDeleting(null),
|
|
1465
|
+
type: "destructive",
|
|
1466
|
+
title: "Excluir este bloco?",
|
|
1467
|
+
description: deleteNode ? `O bloco "${nodeName(deleteNode)}" e as ligações dele serão apagados.` : void 0,
|
|
1468
|
+
confirmLabel: "Excluir",
|
|
1469
|
+
onConfirm: () => {
|
|
1470
|
+
if (!deleting) return;
|
|
1471
|
+
emit(removeNode(valueRef.current, deleting));
|
|
1472
|
+
setDeleting(null);
|
|
1473
|
+
setDrawerOpen(false);
|
|
1474
|
+
}
|
|
1475
|
+
})
|
|
1476
|
+
]
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
function FlowEditor(props) {
|
|
1480
|
+
return /* @__PURE__ */ jsx(ReactFlowProvider, { children: /* @__PURE__ */ jsx(FlowEditorInner, { ...props }) });
|
|
1481
|
+
}
|
|
1482
|
+
//#endregion
|
|
1483
|
+
export { FlowEditor, FlowViewer, parseFlow, serializeFlow, validateFlow };
|
|
1484
|
+
|
|
1485
|
+
//# sourceMappingURL=flow.js.map
|