@adatechnology/conversations-ui 0.1.0-rc.23 → 0.1.0-rc.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/flows/index.d.ts +128 -2
- package/dist/flows/index.js +1085 -6
- package/dist/index.d.ts +255 -9
- package/dist/index.js +1309 -325
- package/dist/preview/index.d.ts +2 -2
- package/dist/styles.css +117 -0
- package/dist/{types-C6A_9edv.d.ts → types-BfINicc-.d.ts} +36 -1
- package/package.json +2 -2
- package/src/documents/DocumentsWorkspace.tsx +489 -0
- package/src/documents/index.ts +8 -0
- package/src/documents/labels.ts +88 -0
- package/src/flows/FlowNodeCard.tsx +12 -3
- package/src/flows/FlowNodePanel.tsx +15 -2
- package/src/flows/FlowsWorkspace.tsx +1252 -0
- package/src/flows/index.ts +3 -1
- package/src/flows/labels.ts +132 -0
- package/src/hooks/useUrlFilterState.ts +107 -0
- package/src/index.ts +30 -0
- package/src/listing/index.tsx +198 -0
- package/src/providers/types.ts +36 -1
- package/src/settings/MessagesWorkspace.tsx +465 -0
- package/src/settings/WhatsAppTemplatesSettings.tsx +7 -1
- package/src/styles.css +108 -0
- package/src/workspace/ConversationPane.tsx +119 -4
- package/src/workspace/ConversationsWorkspace.tsx +33 -1
- package/src/workspace/labels.ts +5 -0
package/dist/flows/index.js
CHANGED
|
@@ -39,6 +39,7 @@ var DEFAULT_ACTION_KIND_ICON = {
|
|
|
39
39
|
};
|
|
40
40
|
function nodeLabel(node, labels) {
|
|
41
41
|
if (!node) return "\u2014";
|
|
42
|
+
if (node.label) return node.label;
|
|
42
43
|
if (node.type === "action") {
|
|
43
44
|
const actionKindLabel = node.actionKind ? labels.actionKindLabels[node.actionKind] : void 0;
|
|
44
45
|
return node.directMessage || node.fallbackMessage || actionKindLabel || node.actionKind || node.id;
|
|
@@ -88,7 +89,7 @@ function SourceRow({ label, isDefault, handleId }) {
|
|
|
88
89
|
] });
|
|
89
90
|
}
|
|
90
91
|
function FlowNodeCard({ data }) {
|
|
91
|
-
const { node, liveCount, isStart, isSelected, issues, labels, actionKindIcons, onSelect } = data;
|
|
92
|
+
const { node, liveCount, isStart, isSelected, isDetached, issues, labels, actionKindIcons, onSelect } = data;
|
|
92
93
|
const label = nodeLabel(node, labels);
|
|
93
94
|
const iconMap = { ...DEFAULT_ACTION_KIND_ICON, ...actionKindIcons };
|
|
94
95
|
const Icon = node.type === "action" && node.actionKind ? iconMap[node.actionKind] ?? NODE_TYPE_ICON[node.type] : NODE_TYPE_ICON[node.type];
|
|
@@ -98,8 +99,8 @@ function FlowNodeCard({ data }) {
|
|
|
98
99
|
return /* @__PURE__ */ jsxs(
|
|
99
100
|
"div",
|
|
100
101
|
{
|
|
101
|
-
title: label,
|
|
102
|
-
className: `relative rounded-lg border-2 px-3 py-2 w-60 cursor-pointer shadow-sm hover:shadow-md transition-shadow ${NODE_TYPE_COLOR[node.type]} ${isSelected ? "ring-2 ring-blue-500 ring-offset-2 dark:ring-offset-gray-900" : ""}`,
|
|
102
|
+
title: isDetached ? labels.detachedNodeTooltip : label,
|
|
103
|
+
className: `relative rounded-lg border-2 px-3 py-2 w-60 cursor-pointer shadow-sm hover:shadow-md transition-shadow ${NODE_TYPE_COLOR[node.type]} ${isSelected ? "ring-2 ring-blue-500 ring-offset-2 dark:ring-offset-gray-900" : ""} ${isDetached ? "border-dashed !border-amber-400 animate-pulse" : ""}`,
|
|
103
104
|
onClick: () => onSelect(node.id),
|
|
104
105
|
children: [
|
|
105
106
|
/* @__PURE__ */ jsx(Handle, { type: "target", position: Position.Top, id: "target", className: "!bg-gray-400 dark:!bg-gray-500" }),
|
|
@@ -144,6 +145,7 @@ var DEFAULT_FLOW_EDITOR_LABELS = {
|
|
|
144
145
|
condition: "Condi\xE7\xE3o"
|
|
145
146
|
},
|
|
146
147
|
startNodeTooltip: "In\xEDcio do fluxo",
|
|
148
|
+
detachedNodeTooltip: "Sem liga\xE7\xE3o de entrada \u2014 o bot n\xE3o chega neste n\xF3. Puxe um fio de outro card at\xE9 ele.",
|
|
147
149
|
liveCountTooltip: (count) => `${count} conversa(s) ativa(s) aqui agora`,
|
|
148
150
|
edgeFallbackLabel: "outro",
|
|
149
151
|
actionKindLabels: {
|
|
@@ -172,6 +174,9 @@ var DEFAULT_FLOW_EDITOR_LABELS = {
|
|
|
172
174
|
nodePanel: {
|
|
173
175
|
title: "Editar n\xF3",
|
|
174
176
|
contextKey: "Chave (contexto)",
|
|
177
|
+
nodeName: "Nome do n\xF3 (opcional)",
|
|
178
|
+
nodeNamePlaceholder: "Ex.: Enviar tabela de pre\xE7os",
|
|
179
|
+
nodeNameHint: "S\xF3 aparece no editor \u2014 o cliente n\xE3o v\xEA.",
|
|
175
180
|
questionType: "Tipo de resposta",
|
|
176
181
|
question: "Texto da pergunta",
|
|
177
182
|
options: "Op\xE7\xF5es (choice)",
|
|
@@ -219,7 +224,9 @@ var DEFAULT_FLOW_EDITOR_LABELS = {
|
|
|
219
224
|
},
|
|
220
225
|
flowMap: {
|
|
221
226
|
nodeCount: (count) => `${count} n\xF3(s)`,
|
|
222
|
-
openFlow: "Abrir fluxo"
|
|
227
|
+
openFlow: "Abrir fluxo",
|
|
228
|
+
toggleToMap: "Mapa de fluxos",
|
|
229
|
+
toggleToDetail: "Voltar ao editor"
|
|
223
230
|
},
|
|
224
231
|
flowGroup: {
|
|
225
232
|
focus: "Focar neste fluxo",
|
|
@@ -228,6 +235,59 @@ var DEFAULT_FLOW_EDITOR_LABELS = {
|
|
|
228
235
|
crossFlowPortal: {
|
|
229
236
|
tooltip: "Clique para abrir esse fluxo aqui do lado, ligado ao ponto de onde ele \xE9 chamado",
|
|
230
237
|
goesTo: (label) => `\u21AA Vai para: ${label}`
|
|
238
|
+
},
|
|
239
|
+
collectionChain: {
|
|
240
|
+
feeds: (label) => `Alimenta: ${label}`
|
|
241
|
+
},
|
|
242
|
+
validation: {
|
|
243
|
+
title: "Antes de publicar",
|
|
244
|
+
errors: (count) => `${count} erro(s) \u2014 corrija antes de salvar`,
|
|
245
|
+
warnings: (count) => `${count} aviso(s)`,
|
|
246
|
+
noStart: "O fluxo precisa de um n\xF3 inicial v\xE1lido.",
|
|
247
|
+
brokenRef: (from, to) => `N\xF3 "${from}": liga\xE7\xE3o aponta para "${to}", que n\xE3o existe.`,
|
|
248
|
+
choiceWithoutOptions: (id) => `N\xF3 "${id}": escolha sem nenhuma op\xE7\xE3o.`,
|
|
249
|
+
duplicatedOptionId: (id, optionId) => `N\xF3 "${id}": valor de op\xE7\xE3o "${optionId}" duplicado.`,
|
|
250
|
+
optionWithoutTarget: (id, label) => `N\xF3 "${id}": op\xE7\xE3o "${label}" n\xE3o tem destino definido.`,
|
|
251
|
+
tooManyOptions: (id, count) => `N\xF3 "${id}": ${count} op\xE7\xF5es \u2014 o WhatsApp aceita no m\xE1ximo 10 em lista.`,
|
|
252
|
+
buttonTitleTooLong: (id, label) => `N\xF3 "${id}": bot\xE3o "${label}" passa de 20 caracteres.`,
|
|
253
|
+
listTitleTooLong: (id, label) => `N\xF3 "${id}": item de lista "${label}" passa de 24 caracteres.`,
|
|
254
|
+
bodyTooLong: (id) => `N\xF3 "${id}": texto passa de 1024 caracteres.`,
|
|
255
|
+
unreachable: (id) => `N\xF3 "${id}" \xE9 inalcan\xE7\xE1vel a partir do in\xEDcio do fluxo.`,
|
|
256
|
+
deadEndQuestion: (id) => `N\xF3 "${id}": pergunta sem pr\xF3ximo passo definido.`,
|
|
257
|
+
conditionIncomplete: (id) => `N\xF3 "${id}": condi\xE7\xE3o incompleta \u2014 defina vari\xE1vel, operador e valor.`,
|
|
258
|
+
conditionBranchMissing: (id, branch) => `N\xF3 "${id}": ramo "${branch === "true" ? "Verdadeiro" : "Falso"}" sem destino definido.`
|
|
259
|
+
},
|
|
260
|
+
workspace: {
|
|
261
|
+
title: "Fluxos do Bot",
|
|
262
|
+
subtitle: "Blueprint visual dos fluxos de conversa, sincronizado com o que est\xE1 em produ\xE7\xE3o.",
|
|
263
|
+
loading: "Carregando fluxos\u2026",
|
|
264
|
+
loadError: "N\xE3o foi poss\xEDvel carregar os fluxos.",
|
|
265
|
+
saveGraph: "Publicar altera\xE7\xF5es",
|
|
266
|
+
saving: "Publicando\u2026",
|
|
267
|
+
saveSuccess: "Fluxo publicado! O bot j\xE1 est\xE1 usando a vers\xE3o nova.",
|
|
268
|
+
saveError: "N\xE3o foi poss\xEDvel salvar \u2014 verifique se todos os destinos apontam para n\xF3s existentes.",
|
|
269
|
+
organize: "Organizar",
|
|
270
|
+
organizeTooltip: "Reorganiza os n\xF3s automaticamente e salva as novas posi\xE7\xF5es",
|
|
271
|
+
discardChanges: "Desfazer altera\xE7\xF5es",
|
|
272
|
+
discardTooltip: "Devolve os fluxos abertos ao que est\xE1 publicado, descartando o que n\xE3o foi salvo",
|
|
273
|
+
discardConfirm: "Descartar todas as altera\xE7\xF5es n\xE3o publicadas e voltar ao fluxo que est\xE1 no ar?",
|
|
274
|
+
unsavedChangesConfirm: "Voc\xEA tem altera\xE7\xF5es n\xE3o publicadas neste fluxo. Trocar de fluxo agora descarta essas edi\xE7\xF5es. Continuar?"
|
|
275
|
+
},
|
|
276
|
+
flowManager: {
|
|
277
|
+
newFlow: "Novo fluxo",
|
|
278
|
+
createTitle: "Criar novo fluxo",
|
|
279
|
+
key: "Identificador \xFAnico",
|
|
280
|
+
keyHint: "letras min\xFAsculas, n\xFAmeros e _ (ex.: promocoes_semana)",
|
|
281
|
+
keyInvalid: "Use apenas letras min\xFAsculas, n\xFAmeros e _ (2 a 40 caracteres)",
|
|
282
|
+
label: "Nome exibido",
|
|
283
|
+
showInMenu: "Exibir como op\xE7\xE3o no menu principal do bot",
|
|
284
|
+
menuOptionLabel: "Texto da op\xE7\xE3o no menu",
|
|
285
|
+
create: "Criar fluxo",
|
|
286
|
+
creating: "Criando\u2026",
|
|
287
|
+
deleteFlow: "Excluir fluxo",
|
|
288
|
+
deleteConfirm: (label) => `Excluir o fluxo "${label}"? Esta a\xE7\xE3o n\xE3o pode ser desfeita.`,
|
|
289
|
+
createError: "N\xE3o foi poss\xEDvel criar o fluxo.",
|
|
290
|
+
deleteError: "N\xE3o foi poss\xEDvel excluir o fluxo."
|
|
231
291
|
}
|
|
232
292
|
};
|
|
233
293
|
function mergeFlowEditorLabels(override) {
|
|
@@ -246,7 +306,11 @@ function mergeFlowEditorLabels(override) {
|
|
|
246
306
|
palette: { ...DEFAULT_FLOW_EDITOR_LABELS.palette, ...override.palette },
|
|
247
307
|
flowMap: { ...DEFAULT_FLOW_EDITOR_LABELS.flowMap, ...override.flowMap },
|
|
248
308
|
flowGroup: { ...DEFAULT_FLOW_EDITOR_LABELS.flowGroup, ...override.flowGroup },
|
|
249
|
-
crossFlowPortal: { ...DEFAULT_FLOW_EDITOR_LABELS.crossFlowPortal, ...override.crossFlowPortal }
|
|
309
|
+
crossFlowPortal: { ...DEFAULT_FLOW_EDITOR_LABELS.crossFlowPortal, ...override.crossFlowPortal },
|
|
310
|
+
collectionChain: { ...DEFAULT_FLOW_EDITOR_LABELS.collectionChain, ...override.collectionChain },
|
|
311
|
+
validation: { ...DEFAULT_FLOW_EDITOR_LABELS.validation, ...override.validation },
|
|
312
|
+
workspace: { ...DEFAULT_FLOW_EDITOR_LABELS.workspace, ...override.workspace },
|
|
313
|
+
flowManager: { ...DEFAULT_FLOW_EDITOR_LABELS.flowManager, ...override.flowManager }
|
|
250
314
|
};
|
|
251
315
|
}
|
|
252
316
|
|
|
@@ -946,6 +1010,19 @@ function FlowNodePanel({
|
|
|
946
1010
|
isFixedLogic && /* @__PURE__ */ jsx9("p", { className: "text-xs text-purple-700 dark:text-purple-400 bg-purple-50 dark:bg-purple-950/30 rounded-lg p-2", children: labels.nodePanel.fixedLogicNotice }),
|
|
947
1011
|
isAction && /* @__PURE__ */ jsx9("p", { className: "text-xs text-orange-700 dark:text-orange-400 bg-orange-50 dark:bg-orange-950/30 rounded-lg p-2", children: labels.nodePanel.actionNotice }),
|
|
948
1012
|
isCondition && /* @__PURE__ */ jsx9("p", { className: "text-xs text-cyan-700 dark:text-cyan-400 bg-cyan-50 dark:bg-cyan-950/30 rounded-lg p-2", children: labels.nodePanel.conditionNotice }),
|
|
1013
|
+
/* @__PURE__ */ jsxs8("div", { children: [
|
|
1014
|
+
/* @__PURE__ */ jsx9("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.nodeName }),
|
|
1015
|
+
/* @__PURE__ */ jsx9(
|
|
1016
|
+
"input",
|
|
1017
|
+
{
|
|
1018
|
+
value: draft.label ?? "",
|
|
1019
|
+
placeholder: labels.nodePanel.nodeNamePlaceholder,
|
|
1020
|
+
onChange: (event) => setDraft((prev) => ({ ...prev, label: event.target.value || void 0 })),
|
|
1021
|
+
className: `w-full mt-1 ${INPUT_CLASSNAME}`
|
|
1022
|
+
}
|
|
1023
|
+
),
|
|
1024
|
+
/* @__PURE__ */ jsx9("p", { className: "text-[11px] text-gray-400 dark:text-gray-500 mt-1", children: labels.nodePanel.nodeNameHint })
|
|
1025
|
+
] }),
|
|
949
1026
|
draft.contextKey && /* @__PURE__ */ jsxs8("div", { children: [
|
|
950
1027
|
/* @__PURE__ */ jsx9("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.contextKey }),
|
|
951
1028
|
/* @__PURE__ */ jsx9(
|
|
@@ -1032,7 +1109,7 @@ function FlowNodePanel({
|
|
|
1032
1109
|
),
|
|
1033
1110
|
isSendMedia && /* @__PURE__ */ jsxs8("div", { children: [
|
|
1034
1111
|
/* @__PURE__ */ jsx9("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.nodePanel.media }),
|
|
1035
|
-
/* @__PURE__ */ jsx9("div", { className: "mt-1", children: renderMediaPicker?.(node) ?? /* @__PURE__ */ jsx9("p", { className: "text-xs text-gray-400", children: labels.nodePanel.mediaUnavailable }) })
|
|
1112
|
+
/* @__PURE__ */ jsx9("div", { className: "mt-1", children: renderMediaPicker?.(node, graph) ?? /* @__PURE__ */ jsx9("p", { className: "text-xs text-gray-400", children: labels.nodePanel.mediaUnavailable }) })
|
|
1036
1113
|
] }),
|
|
1037
1114
|
isAction && draft.actionKind !== "send_product_list" && !isSendMedia && /* @__PURE__ */ jsx9(
|
|
1038
1115
|
WhatsAppTextField,
|
|
@@ -1151,6 +1228,1007 @@ function FlowNodePanel({
|
|
|
1151
1228
|
] })
|
|
1152
1229
|
] });
|
|
1153
1230
|
}
|
|
1231
|
+
|
|
1232
|
+
// src/flows/FlowsWorkspace.tsx
|
|
1233
|
+
import { useCallback, useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2, useState as useState3 } from "react";
|
|
1234
|
+
import {
|
|
1235
|
+
ReactFlow as ReactFlow2,
|
|
1236
|
+
Background as Background2,
|
|
1237
|
+
Controls as Controls2,
|
|
1238
|
+
MiniMap,
|
|
1239
|
+
MarkerType as MarkerType2,
|
|
1240
|
+
applyNodeChanges
|
|
1241
|
+
} from "@xyflow/react";
|
|
1242
|
+
import "@xyflow/react/dist/style.css";
|
|
1243
|
+
import { Plus as Plus3, Trash2 as Trash22, LayoutGrid, AlertTriangle as AlertTriangle3, AlertCircle as AlertCircle3, Save as Save2, Undo2, Map as MapIcon, Workflow } from "lucide-react";
|
|
1244
|
+
import { Fragment as Fragment2, jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
1245
|
+
var RF_NODE_TYPES = {
|
|
1246
|
+
...flowNodeTypes,
|
|
1247
|
+
...flowPortalNodeTypes,
|
|
1248
|
+
...flowGroupHeaderNodeTypes,
|
|
1249
|
+
...flowGroupFrameNodeTypes
|
|
1250
|
+
};
|
|
1251
|
+
var CHAIN_FRAME_PADDING = 36;
|
|
1252
|
+
var COLUMN_GAP = 360;
|
|
1253
|
+
var ROW_GAP = 40;
|
|
1254
|
+
var NS_SEP = "::";
|
|
1255
|
+
var FLOW_KEY_PATTERN = /^[a-z0-9_]{2,40}$/;
|
|
1256
|
+
var EDGE_COLOR_LINEAR = "#94a3b8";
|
|
1257
|
+
var EDGE_COLOR_BRANCH = "#8b5cf6";
|
|
1258
|
+
var EDGE_COLOR_FALLBACK = "#cbd5e1";
|
|
1259
|
+
var EDGE_COLOR_LIVE = "#3b82f6";
|
|
1260
|
+
var EDGE_COLOR_CROSS_FLOW = "#06b6d4";
|
|
1261
|
+
var BACKGROUND_COLOR_LIGHT2 = "#cbd5e1";
|
|
1262
|
+
var BACKGROUND_COLOR_DARK2 = "#334155";
|
|
1263
|
+
function portalNodeId(sourceId, target) {
|
|
1264
|
+
return `__portal__${sourceId}__${target}`;
|
|
1265
|
+
}
|
|
1266
|
+
function ns(flowKey, nodeId) {
|
|
1267
|
+
return `${flowKey}${NS_SEP}${nodeId}`;
|
|
1268
|
+
}
|
|
1269
|
+
function parseNs(id) {
|
|
1270
|
+
const index = id.indexOf(NS_SEP);
|
|
1271
|
+
return index === -1 ? { flowKey: "", nodeId: id } : { flowKey: id.slice(0, index), nodeId: id.slice(index + NS_SEP.length) };
|
|
1272
|
+
}
|
|
1273
|
+
function autoMergeAll(rootKey, graphsSource) {
|
|
1274
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1275
|
+
const queue = [rootKey];
|
|
1276
|
+
while (queue.length > 0) {
|
|
1277
|
+
const key = queue.shift();
|
|
1278
|
+
if (visited.has(key) || !graphsSource[key]) continue;
|
|
1279
|
+
visited.add(key);
|
|
1280
|
+
for (const target of crossFlowTargetsOf(graphsSource[key])) {
|
|
1281
|
+
if (!visited.has(target) && graphsSource[target]) queue.push(target);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
return [...visited].map((key) => ({ key, offset: { x: 0, y: 0 } }));
|
|
1285
|
+
}
|
|
1286
|
+
function computeMergedLayout(openFlows, workingGraphs, primaryFlowKey) {
|
|
1287
|
+
const openKeys = new Set(openFlows.map((flow) => flow.key));
|
|
1288
|
+
const nodeByNsId = /* @__PURE__ */ new Map();
|
|
1289
|
+
for (const { key } of openFlows) {
|
|
1290
|
+
const graph = workingGraphs[key];
|
|
1291
|
+
if (!graph) continue;
|
|
1292
|
+
for (const node of Object.values(graph.nodes)) nodeByNsId.set(ns(key, node.id), node);
|
|
1293
|
+
}
|
|
1294
|
+
function forwardEdges(flowKey, node) {
|
|
1295
|
+
const result = [];
|
|
1296
|
+
for (const { target } of targetsOf(node)) {
|
|
1297
|
+
if (isCrossFlowTarget(target)) {
|
|
1298
|
+
const targetFlowKey = crossFlowKey(target);
|
|
1299
|
+
const targetGraph = openKeys.has(targetFlowKey) ? workingGraphs[targetFlowKey] : void 0;
|
|
1300
|
+
if (targetGraph) result.push(ns(targetFlowKey, targetGraph.startNodeId));
|
|
1301
|
+
} else if (workingGraphs[flowKey]?.nodes[target]) {
|
|
1302
|
+
result.push(ns(flowKey, target));
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
return result;
|
|
1306
|
+
}
|
|
1307
|
+
const rank = /* @__PURE__ */ new Map();
|
|
1308
|
+
const primaryGraph = workingGraphs[primaryFlowKey];
|
|
1309
|
+
const rootId = primaryGraph ? ns(primaryFlowKey, primaryGraph.startNodeId) : void 0;
|
|
1310
|
+
if (rootId && nodeByNsId.has(rootId)) {
|
|
1311
|
+
rank.set(rootId, 0);
|
|
1312
|
+
const queue = [rootId];
|
|
1313
|
+
while (queue.length > 0) {
|
|
1314
|
+
const id = queue.shift();
|
|
1315
|
+
const node = nodeByNsId.get(id);
|
|
1316
|
+
if (!node) continue;
|
|
1317
|
+
for (const nextId of forwardEdges(parseNs(id).flowKey, node)) {
|
|
1318
|
+
if (!rank.has(nextId)) {
|
|
1319
|
+
rank.set(nextId, rank.get(id) + 1);
|
|
1320
|
+
queue.push(nextId);
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
const strayRank = Math.max(0, ...rank.values()) + 1;
|
|
1326
|
+
for (const nsId of nodeByNsId.keys()) {
|
|
1327
|
+
if (!rank.has(nsId)) rank.set(nsId, strayRank);
|
|
1328
|
+
}
|
|
1329
|
+
const layers = /* @__PURE__ */ new Map();
|
|
1330
|
+
for (const [nsId, nodeRank] of rank) {
|
|
1331
|
+
if (!layers.has(nodeRank)) layers.set(nodeRank, []);
|
|
1332
|
+
layers.get(nodeRank).push(nsId);
|
|
1333
|
+
}
|
|
1334
|
+
const positions = /* @__PURE__ */ new Map();
|
|
1335
|
+
for (const nodeRank of [...layers.keys()].sort((a, b) => a - b)) {
|
|
1336
|
+
let cursorY = 0;
|
|
1337
|
+
for (const nsId of layers.get(nodeRank)) {
|
|
1338
|
+
positions.set(nsId, { x: nodeRank * COLUMN_GAP, y: cursorY });
|
|
1339
|
+
cursorY += estimateNodeHeight(nodeByNsId.get(nsId)) + ROW_GAP;
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
return positions;
|
|
1343
|
+
}
|
|
1344
|
+
function computeLiveCounts(flowKey, rootFlowKey, livePositions) {
|
|
1345
|
+
const counts = {};
|
|
1346
|
+
for (const position of livePositions ?? []) {
|
|
1347
|
+
if (position.flow !== flowKey && flowKey !== rootFlowKey) continue;
|
|
1348
|
+
const nodeId = flowKey === rootFlowKey ? position.menuNodeId : position.nodeId;
|
|
1349
|
+
if (!nodeId) continue;
|
|
1350
|
+
counts[nodeId] = (counts[nodeId] ?? 0) + 1;
|
|
1351
|
+
}
|
|
1352
|
+
return counts;
|
|
1353
|
+
}
|
|
1354
|
+
function buildAllEdges(openFlows, workingGraphs, rootFlowKey, livePositions) {
|
|
1355
|
+
const openKeys = new Set(openFlows.map((flow) => flow.key));
|
|
1356
|
+
const edges = [];
|
|
1357
|
+
for (const { key: flowKey } of openFlows) {
|
|
1358
|
+
const graph = workingGraphs[flowKey];
|
|
1359
|
+
if (!graph) continue;
|
|
1360
|
+
const liveCounts = computeLiveCounts(flowKey, rootFlowKey, livePositions);
|
|
1361
|
+
for (const [id, node] of Object.entries(graph.nodes)) {
|
|
1362
|
+
const isLive = (liveCounts[id] ?? 0) > 0;
|
|
1363
|
+
for (const { target, optionId, isDefault } of targetsOf(node)) {
|
|
1364
|
+
const crossFlow = isCrossFlowTarget(target);
|
|
1365
|
+
let targetFlowKey = flowKey;
|
|
1366
|
+
let rawTargetId = target;
|
|
1367
|
+
if (crossFlow) {
|
|
1368
|
+
const wantedKey = crossFlowKey(target);
|
|
1369
|
+
const targetGraph = openKeys.has(wantedKey) ? workingGraphs[wantedKey] : void 0;
|
|
1370
|
+
if (targetGraph) {
|
|
1371
|
+
targetFlowKey = wantedKey;
|
|
1372
|
+
rawTargetId = targetGraph.startNodeId;
|
|
1373
|
+
} else {
|
|
1374
|
+
rawTargetId = portalNodeId(id, target);
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
const source = ns(flowKey, id);
|
|
1378
|
+
const edgeTarget = ns(targetFlowKey, rawTargetId);
|
|
1379
|
+
if (optionId === void 0 && !isDefault) {
|
|
1380
|
+
const color = crossFlow ? EDGE_COLOR_CROSS_FLOW : isLive ? EDGE_COLOR_LIVE : EDGE_COLOR_LINEAR;
|
|
1381
|
+
edges.push({
|
|
1382
|
+
id: `${source}->${edgeTarget}`,
|
|
1383
|
+
source,
|
|
1384
|
+
target: edgeTarget,
|
|
1385
|
+
type: "bezier",
|
|
1386
|
+
animated: isLive,
|
|
1387
|
+
style: crossFlow ? { stroke: color, strokeWidth: 1.5, strokeDasharray: "3 3" } : { stroke: color, strokeWidth: isLive ? 2.5 : 1.5 },
|
|
1388
|
+
markerEnd: { type: MarkerType2.ArrowClosed, color, width: 18, height: 18 }
|
|
1389
|
+
});
|
|
1390
|
+
} else if (isDefault) {
|
|
1391
|
+
const color = crossFlow ? EDGE_COLOR_CROSS_FLOW : EDGE_COLOR_FALLBACK;
|
|
1392
|
+
edges.push({
|
|
1393
|
+
id: `${source}->${edgeTarget}-default`,
|
|
1394
|
+
source,
|
|
1395
|
+
sourceHandle: "__default",
|
|
1396
|
+
target: edgeTarget,
|
|
1397
|
+
type: "bezier",
|
|
1398
|
+
style: { stroke: color, strokeWidth: 1.5, strokeDasharray: "5 4" },
|
|
1399
|
+
markerEnd: { type: MarkerType2.ArrowClosed, color, width: 16, height: 16 }
|
|
1400
|
+
});
|
|
1401
|
+
} else {
|
|
1402
|
+
const color = crossFlow ? EDGE_COLOR_CROSS_FLOW : isLive ? EDGE_COLOR_LIVE : EDGE_COLOR_BRANCH;
|
|
1403
|
+
edges.push({
|
|
1404
|
+
id: `${source}->${edgeTarget}-${optionId}`,
|
|
1405
|
+
source,
|
|
1406
|
+
sourceHandle: optionId,
|
|
1407
|
+
target: edgeTarget,
|
|
1408
|
+
type: "bezier",
|
|
1409
|
+
animated: isLive,
|
|
1410
|
+
style: crossFlow ? { stroke: color, strokeWidth: 1.75, strokeDasharray: "3 3" } : { stroke: color, strokeWidth: isLive ? 2.5 : 1.75 },
|
|
1411
|
+
markerEnd: { type: MarkerType2.ArrowClosed, color, width: 18, height: 18 }
|
|
1412
|
+
});
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
return edges;
|
|
1418
|
+
}
|
|
1419
|
+
function newNodeFromSpec(spec, existingIds) {
|
|
1420
|
+
if (spec.kind === "question") {
|
|
1421
|
+
const id2 = slugifyNodeId("nova_pergunta", existingIds);
|
|
1422
|
+
return { id: id2, type: "question", questionType: spec.questionType, contextKey: id2, question: "" };
|
|
1423
|
+
}
|
|
1424
|
+
if (spec.kind === "decision") {
|
|
1425
|
+
const id2 = slugifyNodeId("nova_decisao", existingIds);
|
|
1426
|
+
return {
|
|
1427
|
+
id: id2,
|
|
1428
|
+
type: "question",
|
|
1429
|
+
questionType: "choice",
|
|
1430
|
+
contextKey: id2,
|
|
1431
|
+
question: "",
|
|
1432
|
+
options: [
|
|
1433
|
+
["1", "Op\xE7\xE3o 1"],
|
|
1434
|
+
["2", "Op\xE7\xE3o 2"]
|
|
1435
|
+
]
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
1438
|
+
if (spec.kind === "condition") {
|
|
1439
|
+
const id2 = slugifyNodeId("nova_condicao", existingIds);
|
|
1440
|
+
return { id: id2, type: "condition", conditionOperator: ">" };
|
|
1441
|
+
}
|
|
1442
|
+
const id = slugifyNodeId("nova_acao", existingIds);
|
|
1443
|
+
return { id, type: "action", actionKind: spec.actionKind };
|
|
1444
|
+
}
|
|
1445
|
+
function removeNodeAndCleanRefs(nodes, nodeId) {
|
|
1446
|
+
const { [nodeId]: _removed, ...rest } = nodes;
|
|
1447
|
+
return Object.fromEntries(
|
|
1448
|
+
Object.entries(rest).map(([id, node]) => {
|
|
1449
|
+
if (!node.next) return [id, node];
|
|
1450
|
+
if (typeof node.next === "string") {
|
|
1451
|
+
return [id, node.next === nodeId ? { ...node, next: void 0 } : node];
|
|
1452
|
+
}
|
|
1453
|
+
return [
|
|
1454
|
+
id,
|
|
1455
|
+
{
|
|
1456
|
+
...node,
|
|
1457
|
+
next: {
|
|
1458
|
+
byAnswer: Object.fromEntries(
|
|
1459
|
+
Object.entries(node.next.byAnswer).map(([key, value]) => [key, value === nodeId ? "" : value])
|
|
1460
|
+
),
|
|
1461
|
+
default: node.next.default === nodeId ? "" : node.next.default
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
];
|
|
1465
|
+
})
|
|
1466
|
+
);
|
|
1467
|
+
}
|
|
1468
|
+
function extractErrorMessage(error) {
|
|
1469
|
+
if (error instanceof Error) return error.message;
|
|
1470
|
+
return void 0;
|
|
1471
|
+
}
|
|
1472
|
+
var OUTLINE_BUTTON = "inline-flex items-center gap-1.5 rounded-lg border border-gray-200 dark:border-gray-700 px-3 py-1.5 text-xs font-medium text-gray-600 dark:text-gray-300 hover:border-blue-300 disabled:opacity-40 disabled:cursor-not-allowed";
|
|
1473
|
+
var PRIMARY_BUTTON = "inline-flex items-center gap-1.5 rounded-lg bg-blue-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-blue-700 disabled:opacity-40 disabled:cursor-not-allowed";
|
|
1474
|
+
var DIALOG_INPUT = "w-full border border-gray-200 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 rounded-xl px-3 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-400";
|
|
1475
|
+
function FlowsWorkspace({
|
|
1476
|
+
api,
|
|
1477
|
+
rootFlowKey = "menu",
|
|
1478
|
+
labels: labelsOverride,
|
|
1479
|
+
actionOptions,
|
|
1480
|
+
renderMediaPicker,
|
|
1481
|
+
livePollIntervalMs = 5e3,
|
|
1482
|
+
className
|
|
1483
|
+
}) {
|
|
1484
|
+
const labels = useMemo2(() => mergeFlowEditorLabels(labelsOverride), [labelsOverride]);
|
|
1485
|
+
const isDark = useIsDarkTheme();
|
|
1486
|
+
const [graphs, setGraphs] = useState3(void 0);
|
|
1487
|
+
const [loadState, setLoadState] = useState3("loading");
|
|
1488
|
+
const [livePositions, setLivePositions] = useState3(void 0);
|
|
1489
|
+
const [viewMode, setViewMode] = useState3("detail");
|
|
1490
|
+
const [openFlows, setOpenFlows] = useState3([{ key: rootFlowKey, offset: { x: 0, y: 0 } }]);
|
|
1491
|
+
const [hasAutoMerged, setHasAutoMerged] = useState3(false);
|
|
1492
|
+
const [workingGraphs, setWorkingGraphs] = useState3({});
|
|
1493
|
+
const [editingRef, setEditingRef] = useState3(null);
|
|
1494
|
+
const [saveState, setSaveState] = useState3("idle");
|
|
1495
|
+
const [saveErrorMessage, setSaveErrorMessage] = useState3(void 0);
|
|
1496
|
+
const [showCreateDialog, setShowCreateDialog] = useState3(false);
|
|
1497
|
+
const [showDeleteDialog, setShowDeleteDialog] = useState3(false);
|
|
1498
|
+
const [newFlow, setNewFlow] = useState3({ key: "", label: "", showInMenu: false, menuOptionLabel: "" });
|
|
1499
|
+
const [flowMutationState, setFlowMutationState] = useState3({ pending: false });
|
|
1500
|
+
const [rfNodes, setRfNodes] = useState3([]);
|
|
1501
|
+
const [flowInstance, setFlowInstance] = useState3(null);
|
|
1502
|
+
const [pendingFocusNodeId, setPendingFocusNodeId] = useState3(null);
|
|
1503
|
+
const reloadGraphs = useCallback(async () => {
|
|
1504
|
+
try {
|
|
1505
|
+
const loaded = await api.getGraphs();
|
|
1506
|
+
setGraphs(loaded);
|
|
1507
|
+
setLoadState("ready");
|
|
1508
|
+
return loaded;
|
|
1509
|
+
} catch {
|
|
1510
|
+
setLoadState("error");
|
|
1511
|
+
return void 0;
|
|
1512
|
+
}
|
|
1513
|
+
}, [api]);
|
|
1514
|
+
useEffect2(() => {
|
|
1515
|
+
void reloadGraphs();
|
|
1516
|
+
}, [reloadGraphs]);
|
|
1517
|
+
useEffect2(() => {
|
|
1518
|
+
const fetchLive = api.getLivePositions;
|
|
1519
|
+
if (!fetchLive) return;
|
|
1520
|
+
let active = true;
|
|
1521
|
+
async function poll() {
|
|
1522
|
+
try {
|
|
1523
|
+
const positions = await fetchLive();
|
|
1524
|
+
if (active) setLivePositions(positions);
|
|
1525
|
+
} catch {
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
void poll();
|
|
1529
|
+
const timer = setInterval(() => void poll(), livePollIntervalMs);
|
|
1530
|
+
return () => {
|
|
1531
|
+
active = false;
|
|
1532
|
+
clearInterval(timer);
|
|
1533
|
+
};
|
|
1534
|
+
}, [api, livePollIntervalMs]);
|
|
1535
|
+
const primaryFlowKey = openFlows[0]?.key ?? rootFlowKey;
|
|
1536
|
+
const primaryGraph = workingGraphs[primaryFlowKey];
|
|
1537
|
+
useEffect2(() => {
|
|
1538
|
+
if (!graphs || hasAutoMerged) return;
|
|
1539
|
+
setOpenFlows(autoMergeAll(rootFlowKey, graphs));
|
|
1540
|
+
setHasAutoMerged(true);
|
|
1541
|
+
}, [graphs, hasAutoMerged, rootFlowKey]);
|
|
1542
|
+
useEffect2(() => {
|
|
1543
|
+
if (!graphs) return;
|
|
1544
|
+
setWorkingGraphs((prev) => {
|
|
1545
|
+
let changed = false;
|
|
1546
|
+
const next = { ...prev };
|
|
1547
|
+
for (const { key } of openFlows) {
|
|
1548
|
+
if (!next[key] && graphs[key]) {
|
|
1549
|
+
next[key] = graphs[key];
|
|
1550
|
+
changed = true;
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
return changed ? next : prev;
|
|
1554
|
+
});
|
|
1555
|
+
}, [graphs, openFlows]);
|
|
1556
|
+
const isFlowDirty = useCallback(
|
|
1557
|
+
(key) => {
|
|
1558
|
+
const working = workingGraphs[key];
|
|
1559
|
+
const server = graphs?.[key];
|
|
1560
|
+
if (!working || !server) return false;
|
|
1561
|
+
return JSON.stringify(working) !== JSON.stringify(server);
|
|
1562
|
+
},
|
|
1563
|
+
[workingGraphs, graphs]
|
|
1564
|
+
);
|
|
1565
|
+
const issuesByFlow = useMemo2(() => {
|
|
1566
|
+
const map = {};
|
|
1567
|
+
for (const { key } of openFlows) {
|
|
1568
|
+
const graph = workingGraphs[key];
|
|
1569
|
+
if (graph) map[key] = validateGraph(graph, labels.validation);
|
|
1570
|
+
}
|
|
1571
|
+
return map;
|
|
1572
|
+
}, [openFlows, workingGraphs, labels]);
|
|
1573
|
+
const errorCount = Object.values(issuesByFlow).reduce(
|
|
1574
|
+
(sum, list) => sum + list.filter((issue) => issue.severity === "error").length,
|
|
1575
|
+
0
|
|
1576
|
+
);
|
|
1577
|
+
const warningCount = Object.values(issuesByFlow).reduce(
|
|
1578
|
+
(sum, list) => sum + list.filter((issue) => issue.severity === "warning").length,
|
|
1579
|
+
0
|
|
1580
|
+
);
|
|
1581
|
+
const dirtyKeys = openFlows.map((flow) => flow.key).filter(isFlowDirty);
|
|
1582
|
+
const isDirty = dirtyKeys.length > 0;
|
|
1583
|
+
const updateFlow = useCallback((flowKey, updater) => {
|
|
1584
|
+
setWorkingGraphs((prev) => {
|
|
1585
|
+
const graph = prev[flowKey];
|
|
1586
|
+
if (!graph) return prev;
|
|
1587
|
+
return { ...prev, [flowKey]: updater(graph) };
|
|
1588
|
+
});
|
|
1589
|
+
}, []);
|
|
1590
|
+
const focusFlow = useCallback(
|
|
1591
|
+
(key) => {
|
|
1592
|
+
const others = openFlows.filter((flow) => flow.key !== key);
|
|
1593
|
+
if (others.some((flow) => isFlowDirty(flow.key)) && !window.confirm(labels.workspace.unsavedChangesConfirm)) return;
|
|
1594
|
+
setOpenFlows(graphs ? autoMergeAll(key, graphs) : [{ key, offset: { x: 0, y: 0 } }]);
|
|
1595
|
+
if (editingRef && editingRef.flowKey !== key) setEditingRef(null);
|
|
1596
|
+
},
|
|
1597
|
+
[openFlows, isFlowDirty, editingRef, graphs, labels]
|
|
1598
|
+
);
|
|
1599
|
+
const closeFlow = useCallback(
|
|
1600
|
+
(key) => {
|
|
1601
|
+
if (isFlowDirty(key) && !window.confirm(labels.workspace.unsavedChangesConfirm)) return;
|
|
1602
|
+
setOpenFlows((prev) => prev.filter((flow) => flow.key !== key));
|
|
1603
|
+
setWorkingGraphs((prev) => {
|
|
1604
|
+
const { [key]: _removed, ...rest } = prev;
|
|
1605
|
+
return rest;
|
|
1606
|
+
});
|
|
1607
|
+
if (editingRef?.flowKey === key) setEditingRef(null);
|
|
1608
|
+
},
|
|
1609
|
+
[isFlowDirty, editingRef, labels]
|
|
1610
|
+
);
|
|
1611
|
+
const mergeFlow = useCallback(
|
|
1612
|
+
(targetFlowKey, source) => {
|
|
1613
|
+
setOpenFlows((prev) => {
|
|
1614
|
+
if (prev.some((flow) => flow.key === targetFlowKey)) return prev;
|
|
1615
|
+
const sourceOpen = prev.find((flow) => flow.key === source.flowKey);
|
|
1616
|
+
const sourceGraph = workingGraphs[source.flowKey];
|
|
1617
|
+
const sourceLocalPosition = sourceGraph?.nodes[source.nodeId]?.position ?? (sourceGraph ? computeAutoLayout(sourceGraph)[source.nodeId] : void 0) ?? { x: 0, y: 0 };
|
|
1618
|
+
const baseOffset = sourceOpen?.offset ?? { x: 0, y: 0 };
|
|
1619
|
+
return [
|
|
1620
|
+
...prev,
|
|
1621
|
+
{
|
|
1622
|
+
key: targetFlowKey,
|
|
1623
|
+
offset: {
|
|
1624
|
+
x: baseOffset.x + sourceLocalPosition.x + 400,
|
|
1625
|
+
y: baseOffset.y + sourceLocalPosition.y
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
];
|
|
1629
|
+
});
|
|
1630
|
+
},
|
|
1631
|
+
[workingGraphs]
|
|
1632
|
+
);
|
|
1633
|
+
const isMerged = openFlows.length > 1;
|
|
1634
|
+
const mergedPositions = useMemo2(
|
|
1635
|
+
() => isMerged ? computeMergedLayout(openFlows, workingGraphs, primaryFlowKey) : null,
|
|
1636
|
+
[isMerged, openFlows, workingGraphs, primaryFlowKey]
|
|
1637
|
+
);
|
|
1638
|
+
const renderedPositionsRef = useRef2(/* @__PURE__ */ new Map());
|
|
1639
|
+
const derivedNodes = useMemo2(() => {
|
|
1640
|
+
const allNodes = [];
|
|
1641
|
+
for (const { key: flowKey, offset } of openFlows) {
|
|
1642
|
+
let resolvePosition2 = function(nodeId) {
|
|
1643
|
+
if (mergedPositions) {
|
|
1644
|
+
const nsId = ns(flowKey, nodeId);
|
|
1645
|
+
return renderedPositionsRef.current.get(nsId) ?? mergedPositions.get(nsId) ?? { x: 0, y: 0 };
|
|
1646
|
+
}
|
|
1647
|
+
const local = graph.nodes[nodeId]?.position ?? fallbackPositions[nodeId] ?? { x: 0, y: 0 };
|
|
1648
|
+
return { x: local.x + offset.x, y: local.y + offset.y };
|
|
1649
|
+
};
|
|
1650
|
+
var resolvePosition = resolvePosition2;
|
|
1651
|
+
const graph = workingGraphs[flowKey];
|
|
1652
|
+
if (!graph) continue;
|
|
1653
|
+
const fallbackPositions = mergedPositions ? {} : computeAutoLayout(graph);
|
|
1654
|
+
const liveCounts = computeLiveCounts(flowKey, rootFlowKey, livePositions);
|
|
1655
|
+
const flowIssues = issuesByFlow[flowKey] ?? [];
|
|
1656
|
+
const isPrimary = flowKey === primaryFlowKey;
|
|
1657
|
+
const connectedTargets = /* @__PURE__ */ new Set();
|
|
1658
|
+
for (const candidate of Object.values(graph.nodes)) {
|
|
1659
|
+
for (const { target } of targetsOf(candidate)) {
|
|
1660
|
+
if (!isCrossFlowTarget(target)) connectedTargets.add(target);
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
for (const node of Object.values(graph.nodes)) {
|
|
1664
|
+
const position = resolvePosition2(node.id);
|
|
1665
|
+
allNodes.push({
|
|
1666
|
+
id: ns(flowKey, node.id),
|
|
1667
|
+
type: "flowNode",
|
|
1668
|
+
position,
|
|
1669
|
+
draggable: true,
|
|
1670
|
+
data: {
|
|
1671
|
+
node,
|
|
1672
|
+
liveCount: liveCounts[node.id] ?? 0,
|
|
1673
|
+
isStart: node.id === graph.startNodeId,
|
|
1674
|
+
isSelected: editingRef?.flowKey === flowKey && editingRef?.nodeId === node.id,
|
|
1675
|
+
isDetached: node.id !== graph.startNodeId && !connectedTargets.has(node.id),
|
|
1676
|
+
issues: flowIssues,
|
|
1677
|
+
labels,
|
|
1678
|
+
onSelect: (nodeId) => setEditingRef({ flowKey, nodeId })
|
|
1679
|
+
}
|
|
1680
|
+
});
|
|
1681
|
+
const crossFlowTargets = [...new Set(targetsOf(node).map((edge) => edge.target).filter(isCrossFlowTarget))];
|
|
1682
|
+
crossFlowTargets.forEach((target, index) => {
|
|
1683
|
+
const targetFlowKey = crossFlowKey(target);
|
|
1684
|
+
if (openFlows.some((flow) => flow.key === targetFlowKey)) return;
|
|
1685
|
+
allNodes.push({
|
|
1686
|
+
id: ns(flowKey, portalNodeId(node.id, target)),
|
|
1687
|
+
type: "flowPortal",
|
|
1688
|
+
draggable: false,
|
|
1689
|
+
selectable: false,
|
|
1690
|
+
position: { x: position.x + 320, y: position.y + index * 70 },
|
|
1691
|
+
data: {
|
|
1692
|
+
label: graphs?.[targetFlowKey]?.label ?? targetFlowKey,
|
|
1693
|
+
onNavigate: () => mergeFlow(targetFlowKey, { flowKey, nodeId: node.id })
|
|
1694
|
+
}
|
|
1695
|
+
});
|
|
1696
|
+
});
|
|
1697
|
+
}
|
|
1698
|
+
for (const chain of findCollectionChains(graph)) {
|
|
1699
|
+
const chainNodeIds = [...chain.nodeIds, chain.actionNodeId];
|
|
1700
|
+
const positions = chainNodeIds.map((id) => resolvePosition2(id));
|
|
1701
|
+
const minX = Math.min(...positions.map((point) => point.x));
|
|
1702
|
+
const maxX = Math.max(...positions.map((point) => point.x)) + NODE_CARD_WIDTH;
|
|
1703
|
+
const minY = Math.min(...positions.map((point) => point.y));
|
|
1704
|
+
const maxY = Math.max(...positions.map((point) => point.y)) + estimateNodeHeight(graph.nodes[chain.actionNodeId]);
|
|
1705
|
+
allNodes.push({
|
|
1706
|
+
id: ns(flowKey, `__chain__${chain.actionNodeId}`),
|
|
1707
|
+
type: "flowGroupFrame",
|
|
1708
|
+
draggable: false,
|
|
1709
|
+
selectable: false,
|
|
1710
|
+
zIndex: -1,
|
|
1711
|
+
position: { x: minX - CHAIN_FRAME_PADDING, y: minY - CHAIN_FRAME_PADDING - 24 },
|
|
1712
|
+
style: {
|
|
1713
|
+
width: maxX - minX + CHAIN_FRAME_PADDING * 2,
|
|
1714
|
+
height: maxY - minY + CHAIN_FRAME_PADDING * 2 + 24
|
|
1715
|
+
},
|
|
1716
|
+
data: {
|
|
1717
|
+
label: labels.collectionChain.feeds(nodeLabel(graph.nodes[chain.actionNodeId], labels))
|
|
1718
|
+
}
|
|
1719
|
+
});
|
|
1720
|
+
}
|
|
1721
|
+
if (!isPrimary) {
|
|
1722
|
+
const startPosition = resolvePosition2(graph.startNodeId);
|
|
1723
|
+
allNodes.push({
|
|
1724
|
+
id: ns(flowKey, "__group_header__"),
|
|
1725
|
+
type: "flowGroupHeader",
|
|
1726
|
+
draggable: false,
|
|
1727
|
+
selectable: false,
|
|
1728
|
+
position: { x: startPosition.x, y: startPosition.y - 60 },
|
|
1729
|
+
data: {
|
|
1730
|
+
label: graph.label,
|
|
1731
|
+
onFocus: () => focusFlow(flowKey),
|
|
1732
|
+
onClose: () => closeFlow(flowKey)
|
|
1733
|
+
}
|
|
1734
|
+
});
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
return allNodes;
|
|
1738
|
+
}, [
|
|
1739
|
+
openFlows,
|
|
1740
|
+
workingGraphs,
|
|
1741
|
+
mergedPositions,
|
|
1742
|
+
livePositions,
|
|
1743
|
+
issuesByFlow,
|
|
1744
|
+
primaryFlowKey,
|
|
1745
|
+
rootFlowKey,
|
|
1746
|
+
editingRef,
|
|
1747
|
+
graphs,
|
|
1748
|
+
mergeFlow,
|
|
1749
|
+
focusFlow,
|
|
1750
|
+
closeFlow,
|
|
1751
|
+
labels
|
|
1752
|
+
]);
|
|
1753
|
+
const edges = useMemo2(
|
|
1754
|
+
() => buildAllEdges(openFlows, workingGraphs, rootFlowKey, livePositions),
|
|
1755
|
+
[openFlows, workingGraphs, rootFlowKey, livePositions]
|
|
1756
|
+
);
|
|
1757
|
+
useEffect2(() => {
|
|
1758
|
+
setRfNodes(derivedNodes);
|
|
1759
|
+
for (const node of derivedNodes) {
|
|
1760
|
+
if (node.type === "flowNode") renderedPositionsRef.current.set(node.id, node.position);
|
|
1761
|
+
}
|
|
1762
|
+
}, [derivedNodes]);
|
|
1763
|
+
useEffect2(() => {
|
|
1764
|
+
if (!pendingFocusNodeId || !flowInstance) return;
|
|
1765
|
+
const target = rfNodes.find((node) => node.id === pendingFocusNodeId);
|
|
1766
|
+
if (!target) return;
|
|
1767
|
+
flowInstance.setCenter(target.position.x + NODE_CARD_WIDTH / 2, target.position.y, { zoom: 1, duration: 400 });
|
|
1768
|
+
setPendingFocusNodeId(null);
|
|
1769
|
+
}, [pendingFocusNodeId, flowInstance, rfNodes]);
|
|
1770
|
+
const onNodesChange = useCallback((changes) => {
|
|
1771
|
+
setRfNodes((current) => applyNodeChanges(changes, current));
|
|
1772
|
+
}, []);
|
|
1773
|
+
const onNodeDragStop = useCallback(
|
|
1774
|
+
(_event, node) => {
|
|
1775
|
+
renderedPositionsRef.current.set(node.id, node.position);
|
|
1776
|
+
const { flowKey, nodeId } = parseNs(node.id);
|
|
1777
|
+
const openFlow = openFlows.find((flow) => flow.key === flowKey);
|
|
1778
|
+
if (!openFlow) return;
|
|
1779
|
+
const localPosition = { x: node.position.x - openFlow.offset.x, y: node.position.y - openFlow.offset.y };
|
|
1780
|
+
updateFlow(
|
|
1781
|
+
flowKey,
|
|
1782
|
+
(graph) => graph.nodes[nodeId] ? { ...graph, nodes: { ...graph.nodes, [nodeId]: { ...graph.nodes[nodeId], position: localPosition } } } : graph
|
|
1783
|
+
);
|
|
1784
|
+
},
|
|
1785
|
+
[openFlows, updateFlow]
|
|
1786
|
+
);
|
|
1787
|
+
const onConnect = useCallback(
|
|
1788
|
+
(connection) => {
|
|
1789
|
+
const { source, sourceHandle, target } = connection;
|
|
1790
|
+
if (!source || !target || source === target) return;
|
|
1791
|
+
const sourceRef = parseNs(source);
|
|
1792
|
+
const targetRef = parseNs(target);
|
|
1793
|
+
let targetValue;
|
|
1794
|
+
if (targetRef.flowKey === sourceRef.flowKey) {
|
|
1795
|
+
targetValue = targetRef.nodeId;
|
|
1796
|
+
} else {
|
|
1797
|
+
const targetGraph = workingGraphs[targetRef.flowKey];
|
|
1798
|
+
if (!targetGraph || targetGraph.startNodeId !== targetRef.nodeId) return;
|
|
1799
|
+
targetValue = `${CROSS_FLOW_PREFIX}${targetRef.flowKey}`;
|
|
1800
|
+
}
|
|
1801
|
+
updateFlow(sourceRef.flowKey, (graph) => {
|
|
1802
|
+
const node = graph.nodes[sourceRef.nodeId];
|
|
1803
|
+
if (!node) return graph;
|
|
1804
|
+
const handle = sourceHandle ?? "next";
|
|
1805
|
+
const currentByAnswer = typeof node.next === "object" && node.next ? node.next.byAnswer : {};
|
|
1806
|
+
const currentDefault = typeof node.next === "object" && node.next ? node.next.default : "";
|
|
1807
|
+
const updatedNode = handle === "next" ? { ...node, next: targetValue } : handle === "__default" ? { ...node, next: { byAnswer: currentByAnswer, default: targetValue } } : { ...node, next: { byAnswer: { ...currentByAnswer, [handle]: targetValue }, default: currentDefault } };
|
|
1808
|
+
return { ...graph, nodes: { ...graph.nodes, [sourceRef.nodeId]: updatedNode } };
|
|
1809
|
+
});
|
|
1810
|
+
},
|
|
1811
|
+
[workingGraphs, updateFlow]
|
|
1812
|
+
);
|
|
1813
|
+
function handleAddNode(spec) {
|
|
1814
|
+
if (!primaryGraph) return;
|
|
1815
|
+
const newNode = newNodeFromSpec(spec, new Set(Object.keys(primaryGraph.nodes)));
|
|
1816
|
+
const maxY = Math.max(0, ...Object.values(primaryGraph.nodes).map((node) => node.position?.y ?? 0));
|
|
1817
|
+
newNode.position = { x: 0, y: maxY + 170 };
|
|
1818
|
+
updateFlow(primaryFlowKey, (graph) => ({ ...graph, nodes: { ...graph.nodes, [newNode.id]: newNode } }));
|
|
1819
|
+
setEditingRef({ flowKey: primaryFlowKey, nodeId: newNode.id });
|
|
1820
|
+
setPendingFocusNodeId(ns(primaryFlowKey, newNode.id));
|
|
1821
|
+
}
|
|
1822
|
+
function handleNodePanelChange(updated) {
|
|
1823
|
+
if (!editingRef) return;
|
|
1824
|
+
updateFlow(editingRef.flowKey, (graph) => ({ ...graph, nodes: { ...graph.nodes, [updated.id]: updated } }));
|
|
1825
|
+
setEditingRef(null);
|
|
1826
|
+
}
|
|
1827
|
+
function handleNodeDelete(nodeId) {
|
|
1828
|
+
if (!editingRef) return;
|
|
1829
|
+
updateFlow(editingRef.flowKey, (graph) => ({ ...graph, nodes: removeNodeAndCleanRefs(graph.nodes, nodeId) }));
|
|
1830
|
+
setEditingRef(null);
|
|
1831
|
+
}
|
|
1832
|
+
function handleOrganize() {
|
|
1833
|
+
if (!primaryGraph) return;
|
|
1834
|
+
renderedPositionsRef.current.clear();
|
|
1835
|
+
if (isMerged) {
|
|
1836
|
+
const positions2 = computeMergedLayout(openFlows, workingGraphs, primaryFlowKey);
|
|
1837
|
+
for (const { key, offset } of openFlows) {
|
|
1838
|
+
updateFlow(key, (graph) => ({
|
|
1839
|
+
...graph,
|
|
1840
|
+
nodes: Object.fromEntries(
|
|
1841
|
+
Object.entries(graph.nodes).map(([id, node]) => {
|
|
1842
|
+
const position = positions2.get(ns(key, id));
|
|
1843
|
+
return [
|
|
1844
|
+
id,
|
|
1845
|
+
position ? { ...node, position: { x: position.x - offset.x, y: position.y - offset.y } } : node
|
|
1846
|
+
];
|
|
1847
|
+
})
|
|
1848
|
+
)
|
|
1849
|
+
}));
|
|
1850
|
+
}
|
|
1851
|
+
return;
|
|
1852
|
+
}
|
|
1853
|
+
const positions = computeAutoLayout(primaryGraph);
|
|
1854
|
+
updateFlow(primaryFlowKey, (graph) => ({
|
|
1855
|
+
...graph,
|
|
1856
|
+
nodes: Object.fromEntries(
|
|
1857
|
+
Object.entries(graph.nodes).map(([id, node]) => [id, { ...node, position: positions[id] ?? node.position }])
|
|
1858
|
+
)
|
|
1859
|
+
}));
|
|
1860
|
+
}
|
|
1861
|
+
function handleDiscardChanges() {
|
|
1862
|
+
if (!graphs || !isDirty) return;
|
|
1863
|
+
if (!window.confirm(labels.workspace.discardConfirm)) return;
|
|
1864
|
+
setWorkingGraphs((prev) => {
|
|
1865
|
+
const next = { ...prev };
|
|
1866
|
+
for (const key of dirtyKeys) {
|
|
1867
|
+
const published = graphs[key];
|
|
1868
|
+
if (published) next[key] = published;
|
|
1869
|
+
}
|
|
1870
|
+
return next;
|
|
1871
|
+
});
|
|
1872
|
+
renderedPositionsRef.current.clear();
|
|
1873
|
+
setEditingRef(null);
|
|
1874
|
+
}
|
|
1875
|
+
async function handlePublish() {
|
|
1876
|
+
if (dirtyKeys.length === 0 || errorCount > 0) return;
|
|
1877
|
+
setSaveState("saving");
|
|
1878
|
+
setSaveErrorMessage(void 0);
|
|
1879
|
+
try {
|
|
1880
|
+
await Promise.all(dirtyKeys.map((key) => api.saveGraph(key, workingGraphs[key])));
|
|
1881
|
+
await reloadGraphs();
|
|
1882
|
+
setSaveState("success");
|
|
1883
|
+
setTimeout(() => setSaveState("idle"), 3e3);
|
|
1884
|
+
} catch (error) {
|
|
1885
|
+
setSaveState("error");
|
|
1886
|
+
setSaveErrorMessage(extractErrorMessage(error));
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
async function handleCreateFlow() {
|
|
1890
|
+
const createFlow = api.createFlow;
|
|
1891
|
+
if (!createFlow) return;
|
|
1892
|
+
setFlowMutationState({ pending: true });
|
|
1893
|
+
try {
|
|
1894
|
+
await createFlow({
|
|
1895
|
+
key: newFlow.key,
|
|
1896
|
+
label: newFlow.label,
|
|
1897
|
+
showInMenu: newFlow.showInMenu,
|
|
1898
|
+
...newFlow.showInMenu ? { menuOptionLabel: newFlow.menuOptionLabel || newFlow.label } : {}
|
|
1899
|
+
});
|
|
1900
|
+
await reloadGraphs();
|
|
1901
|
+
setOpenFlows([{ key: newFlow.key, offset: { x: 0, y: 0 } }]);
|
|
1902
|
+
setShowCreateDialog(false);
|
|
1903
|
+
setNewFlow({ key: "", label: "", showInMenu: false, menuOptionLabel: "" });
|
|
1904
|
+
setFlowMutationState({ pending: false });
|
|
1905
|
+
} catch (error) {
|
|
1906
|
+
setFlowMutationState({ pending: false, error: extractErrorMessage(error) ?? labels.flowManager.createError });
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
async function handleDeleteFlow() {
|
|
1910
|
+
const deleteFlow = api.deleteFlow;
|
|
1911
|
+
if (!deleteFlow || !primaryGraph) return;
|
|
1912
|
+
setFlowMutationState({ pending: true });
|
|
1913
|
+
try {
|
|
1914
|
+
await deleteFlow(primaryGraph.key);
|
|
1915
|
+
const reloaded = await reloadGraphs();
|
|
1916
|
+
setOpenFlows(reloaded ? autoMergeAll(rootFlowKey, reloaded) : [{ key: rootFlowKey, offset: { x: 0, y: 0 } }]);
|
|
1917
|
+
setShowDeleteDialog(false);
|
|
1918
|
+
setFlowMutationState({ pending: false });
|
|
1919
|
+
} catch (error) {
|
|
1920
|
+
setFlowMutationState({ pending: false, error: extractErrorMessage(error) ?? labels.flowManager.deleteError });
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
const editingGraph = editingRef ? workingGraphs[editingRef.flowKey] : null;
|
|
1924
|
+
const editingNode = editingRef && editingGraph ? editingGraph.nodes[editingRef.nodeId] : null;
|
|
1925
|
+
const otherFlows = useMemo2(
|
|
1926
|
+
() => Object.values(graphs ?? {}).filter((graph) => graph.key !== editingRef?.flowKey).map((graph) => ({ key: graph.key, label: graph.label })),
|
|
1927
|
+
[graphs, editingRef?.flowKey]
|
|
1928
|
+
);
|
|
1929
|
+
const keyIsValid = FLOW_KEY_PATTERN.test(newFlow.key);
|
|
1930
|
+
const canCreateFlow = Boolean(api.createFlow);
|
|
1931
|
+
const canDeleteFlow = Boolean(api.deleteFlow) && primaryFlowKey !== rootFlowKey;
|
|
1932
|
+
return /* @__PURE__ */ jsxs9("div", { className: `space-y-4 h-full flex flex-col ${className ?? ""}`, children: [
|
|
1933
|
+
/* @__PURE__ */ jsxs9("div", { className: "flex items-center justify-between flex-wrap gap-3", children: [
|
|
1934
|
+
/* @__PURE__ */ jsxs9("div", { children: [
|
|
1935
|
+
/* @__PURE__ */ jsx10("h2", { className: "text-2xl font-bold text-gray-900 dark:text-gray-100", children: labels.workspace.title }),
|
|
1936
|
+
/* @__PURE__ */ jsx10("p", { className: "text-gray-500 dark:text-gray-400 text-sm mt-1", children: labels.workspace.subtitle })
|
|
1937
|
+
] }),
|
|
1938
|
+
/* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-3", children: [
|
|
1939
|
+
saveState === "success" && /* @__PURE__ */ jsx10("span", { className: "text-sm text-green-600", children: labels.workspace.saveSuccess }),
|
|
1940
|
+
saveState === "error" && /* @__PURE__ */ jsx10("span", { className: "text-sm text-red-600", children: saveErrorMessage ?? labels.workspace.saveError }),
|
|
1941
|
+
/* @__PURE__ */ jsxs9(
|
|
1942
|
+
"button",
|
|
1943
|
+
{
|
|
1944
|
+
type: "button",
|
|
1945
|
+
onClick: () => setViewMode((mode) => mode === "map" ? "detail" : "map"),
|
|
1946
|
+
className: OUTLINE_BUTTON,
|
|
1947
|
+
children: [
|
|
1948
|
+
viewMode === "map" ? /* @__PURE__ */ jsx10(Workflow, { size: 14, "aria-hidden": "true" }) : /* @__PURE__ */ jsx10(MapIcon, { size: 14, "aria-hidden": "true" }),
|
|
1949
|
+
viewMode === "map" ? labels.flowMap.toggleToDetail : labels.flowMap.toggleToMap
|
|
1950
|
+
]
|
|
1951
|
+
}
|
|
1952
|
+
),
|
|
1953
|
+
viewMode === "detail" && /* @__PURE__ */ jsxs9(Fragment2, { children: [
|
|
1954
|
+
/* @__PURE__ */ jsxs9(
|
|
1955
|
+
"button",
|
|
1956
|
+
{
|
|
1957
|
+
type: "button",
|
|
1958
|
+
onClick: handleOrganize,
|
|
1959
|
+
className: OUTLINE_BUTTON,
|
|
1960
|
+
title: labels.workspace.organizeTooltip,
|
|
1961
|
+
disabled: !primaryGraph,
|
|
1962
|
+
children: [
|
|
1963
|
+
/* @__PURE__ */ jsx10(LayoutGrid, { size: 14, "aria-hidden": "true" }),
|
|
1964
|
+
" ",
|
|
1965
|
+
labels.workspace.organize
|
|
1966
|
+
]
|
|
1967
|
+
}
|
|
1968
|
+
),
|
|
1969
|
+
/* @__PURE__ */ jsxs9(
|
|
1970
|
+
"button",
|
|
1971
|
+
{
|
|
1972
|
+
type: "button",
|
|
1973
|
+
onClick: handleDiscardChanges,
|
|
1974
|
+
className: OUTLINE_BUTTON,
|
|
1975
|
+
title: labels.workspace.discardTooltip,
|
|
1976
|
+
disabled: !isDirty || saveState === "saving",
|
|
1977
|
+
children: [
|
|
1978
|
+
/* @__PURE__ */ jsx10(Undo2, { size: 14, "aria-hidden": "true" }),
|
|
1979
|
+
" ",
|
|
1980
|
+
labels.workspace.discardChanges
|
|
1981
|
+
]
|
|
1982
|
+
}
|
|
1983
|
+
),
|
|
1984
|
+
/* @__PURE__ */ jsxs9(
|
|
1985
|
+
"button",
|
|
1986
|
+
{
|
|
1987
|
+
type: "button",
|
|
1988
|
+
onClick: () => void handlePublish(),
|
|
1989
|
+
className: PRIMARY_BUTTON,
|
|
1990
|
+
disabled: !isDirty || errorCount > 0 || saveState === "saving",
|
|
1991
|
+
children: [
|
|
1992
|
+
/* @__PURE__ */ jsx10(Save2, { size: 14, "aria-hidden": "true" }),
|
|
1993
|
+
saveState === "saving" ? labels.workspace.saving : labels.workspace.saveGraph
|
|
1994
|
+
]
|
|
1995
|
+
}
|
|
1996
|
+
)
|
|
1997
|
+
] })
|
|
1998
|
+
] })
|
|
1999
|
+
] }),
|
|
2000
|
+
viewMode === "detail" && /* @__PURE__ */ jsxs9("div", { className: "flex items-center justify-between gap-2 flex-wrap", children: [
|
|
2001
|
+
/* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-2 flex-wrap", children: [
|
|
2002
|
+
graphs && Object.values(graphs).map((graph) => /* @__PURE__ */ jsx10(
|
|
2003
|
+
"button",
|
|
2004
|
+
{
|
|
2005
|
+
type: "button",
|
|
2006
|
+
onClick: () => focusFlow(graph.key),
|
|
2007
|
+
className: `px-3 py-1.5 rounded-full text-xs font-medium border transition-colors ${primaryFlowKey === graph.key ? "bg-blue-600 text-white border-blue-600" : "bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-300 border-gray-200 dark:border-gray-700 hover:border-blue-300"}`,
|
|
2008
|
+
children: graph.label
|
|
2009
|
+
},
|
|
2010
|
+
graph.key
|
|
2011
|
+
)),
|
|
2012
|
+
canCreateFlow && /* @__PURE__ */ jsxs9(
|
|
2013
|
+
"button",
|
|
2014
|
+
{
|
|
2015
|
+
type: "button",
|
|
2016
|
+
onClick: () => {
|
|
2017
|
+
setFlowMutationState({ pending: false });
|
|
2018
|
+
setShowCreateDialog(true);
|
|
2019
|
+
},
|
|
2020
|
+
className: "inline-flex items-center gap-1 px-3 py-1.5 rounded-full text-xs font-medium border border-dashed border-gray-300 dark:border-gray-600 text-gray-500 dark:text-gray-400 hover:border-blue-400 hover:text-blue-600",
|
|
2021
|
+
children: [
|
|
2022
|
+
/* @__PURE__ */ jsx10(Plus3, { size: 12, "aria-hidden": "true" }),
|
|
2023
|
+
" ",
|
|
2024
|
+
labels.flowManager.newFlow
|
|
2025
|
+
]
|
|
2026
|
+
}
|
|
2027
|
+
)
|
|
2028
|
+
] }),
|
|
2029
|
+
/* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-2", children: [
|
|
2030
|
+
primaryGraph && /* @__PURE__ */ jsx10(
|
|
2031
|
+
FlowPalette,
|
|
2032
|
+
{
|
|
2033
|
+
onAdd: handleAddNode,
|
|
2034
|
+
labels,
|
|
2035
|
+
...actionOptions ? { actionOptions: [...actionOptions] } : {}
|
|
2036
|
+
}
|
|
2037
|
+
),
|
|
2038
|
+
primaryGraph && canDeleteFlow && /* @__PURE__ */ jsxs9(
|
|
2039
|
+
"button",
|
|
2040
|
+
{
|
|
2041
|
+
type: "button",
|
|
2042
|
+
onClick: () => {
|
|
2043
|
+
setFlowMutationState({ pending: false });
|
|
2044
|
+
setShowDeleteDialog(true);
|
|
2045
|
+
},
|
|
2046
|
+
className: "inline-flex items-center gap-1.5 rounded-lg border border-red-200 dark:border-red-900 px-3 py-1.5 text-xs font-medium text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-950/30",
|
|
2047
|
+
children: [
|
|
2048
|
+
/* @__PURE__ */ jsx10(Trash22, { size: 13, "aria-hidden": "true" }),
|
|
2049
|
+
" ",
|
|
2050
|
+
labels.flowManager.deleteFlow
|
|
2051
|
+
]
|
|
2052
|
+
}
|
|
2053
|
+
)
|
|
2054
|
+
] })
|
|
2055
|
+
] }),
|
|
2056
|
+
viewMode === "detail" && (errorCount > 0 || warningCount > 0) && /* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-4 rounded-lg border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/60 px-3 py-2 text-xs", children: [
|
|
2057
|
+
/* @__PURE__ */ jsxs9("span", { className: "font-medium text-gray-600 dark:text-gray-300", children: [
|
|
2058
|
+
labels.validation.title,
|
|
2059
|
+
":"
|
|
2060
|
+
] }),
|
|
2061
|
+
errorCount > 0 && /* @__PURE__ */ jsxs9("span", { className: "flex items-center gap-1 text-red-600 dark:text-red-400 font-medium", children: [
|
|
2062
|
+
/* @__PURE__ */ jsx10(AlertCircle3, { size: 13, "aria-hidden": "true" }),
|
|
2063
|
+
" ",
|
|
2064
|
+
labels.validation.errors(errorCount)
|
|
2065
|
+
] }),
|
|
2066
|
+
warningCount > 0 && /* @__PURE__ */ jsxs9("span", { className: "flex items-center gap-1 text-amber-600 dark:text-amber-400", children: [
|
|
2067
|
+
/* @__PURE__ */ jsx10(AlertTriangle3, { size: 13, "aria-hidden": "true" }),
|
|
2068
|
+
" ",
|
|
2069
|
+
labels.validation.warnings(warningCount)
|
|
2070
|
+
] })
|
|
2071
|
+
] }),
|
|
2072
|
+
/* @__PURE__ */ jsxs9("div", { className: "flex-1 rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden relative", children: [
|
|
2073
|
+
loadState === "loading" && /* @__PURE__ */ jsx10("p", { className: "text-center text-gray-400 py-12", children: labels.workspace.loading }),
|
|
2074
|
+
loadState === "error" && /* @__PURE__ */ jsx10("p", { className: "text-center text-red-500 py-12", children: labels.workspace.loadError }),
|
|
2075
|
+
loadState === "ready" && graphs && viewMode === "map" && /* @__PURE__ */ jsx10(
|
|
2076
|
+
FlowMapCanvas,
|
|
2077
|
+
{
|
|
2078
|
+
graphs,
|
|
2079
|
+
rootKey: rootFlowKey,
|
|
2080
|
+
labels,
|
|
2081
|
+
onOpenFlow: (key) => {
|
|
2082
|
+
focusFlow(key);
|
|
2083
|
+
setViewMode("detail");
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
),
|
|
2087
|
+
loadState === "ready" && viewMode === "detail" && primaryGraph && /* @__PURE__ */ jsxs9(
|
|
2088
|
+
ReactFlow2,
|
|
2089
|
+
{
|
|
2090
|
+
nodes: rfNodes,
|
|
2091
|
+
edges,
|
|
2092
|
+
nodeTypes: RF_NODE_TYPES,
|
|
2093
|
+
onNodesChange,
|
|
2094
|
+
onNodeDragStop,
|
|
2095
|
+
onConnect,
|
|
2096
|
+
onInit: setFlowInstance,
|
|
2097
|
+
fitView: true,
|
|
2098
|
+
proOptions: { hideAttribution: true },
|
|
2099
|
+
colorMode: isDark ? "dark" : "light",
|
|
2100
|
+
children: [
|
|
2101
|
+
/* @__PURE__ */ jsx10(Background2, { color: isDark ? BACKGROUND_COLOR_DARK2 : BACKGROUND_COLOR_LIGHT2 }),
|
|
2102
|
+
/* @__PURE__ */ jsx10(Controls2, {}),
|
|
2103
|
+
/* @__PURE__ */ jsx10(MiniMap, { pannable: true, zoomable: true, className: "!bg-white dark:!bg-gray-800" })
|
|
2104
|
+
]
|
|
2105
|
+
}
|
|
2106
|
+
)
|
|
2107
|
+
] }),
|
|
2108
|
+
editingNode && editingGraph && /* @__PURE__ */ jsx10(
|
|
2109
|
+
FlowNodePanel,
|
|
2110
|
+
{
|
|
2111
|
+
graph: editingGraph,
|
|
2112
|
+
node: editingNode,
|
|
2113
|
+
issues: editingRef ? issuesByFlow[editingRef.flowKey] ?? [] : [],
|
|
2114
|
+
otherFlows,
|
|
2115
|
+
labels,
|
|
2116
|
+
onClose: () => setEditingRef(null),
|
|
2117
|
+
onChange: handleNodePanelChange,
|
|
2118
|
+
onDelete: handleNodeDelete,
|
|
2119
|
+
...renderMediaPicker ? { renderMediaPicker } : {}
|
|
2120
|
+
},
|
|
2121
|
+
`${editingRef?.flowKey}:${editingRef?.nodeId}`
|
|
2122
|
+
),
|
|
2123
|
+
showCreateDialog && canCreateFlow && /* @__PURE__ */ jsxs9(FlowDialog, { title: labels.flowManager.createTitle, onClose: () => setShowCreateDialog(false), children: [
|
|
2124
|
+
/* @__PURE__ */ jsxs9("div", { className: "space-y-3", children: [
|
|
2125
|
+
/* @__PURE__ */ jsxs9("div", { children: [
|
|
2126
|
+
/* @__PURE__ */ jsx10("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.flowManager.label }),
|
|
2127
|
+
/* @__PURE__ */ jsx10(
|
|
2128
|
+
"input",
|
|
2129
|
+
{
|
|
2130
|
+
value: newFlow.label,
|
|
2131
|
+
onChange: (event) => setNewFlow((prev) => ({ ...prev, label: event.target.value })),
|
|
2132
|
+
className: `mt-1 ${DIALOG_INPUT}`
|
|
2133
|
+
}
|
|
2134
|
+
)
|
|
2135
|
+
] }),
|
|
2136
|
+
/* @__PURE__ */ jsxs9("div", { children: [
|
|
2137
|
+
/* @__PURE__ */ jsx10("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.flowManager.key }),
|
|
2138
|
+
/* @__PURE__ */ jsx10(
|
|
2139
|
+
"input",
|
|
2140
|
+
{
|
|
2141
|
+
value: newFlow.key,
|
|
2142
|
+
onChange: (event) => setNewFlow((prev) => ({ ...prev, key: event.target.value.toLowerCase() })),
|
|
2143
|
+
className: `mt-1 ${DIALOG_INPUT}`
|
|
2144
|
+
}
|
|
2145
|
+
),
|
|
2146
|
+
/* @__PURE__ */ jsx10("p", { className: "text-[11px] text-gray-400 mt-1", children: newFlow.key && !keyIsValid ? labels.flowManager.keyInvalid : labels.flowManager.keyHint })
|
|
2147
|
+
] }),
|
|
2148
|
+
/* @__PURE__ */ jsxs9("label", { className: "flex items-center gap-2 text-sm text-gray-700 dark:text-gray-200", children: [
|
|
2149
|
+
/* @__PURE__ */ jsx10(
|
|
2150
|
+
"input",
|
|
2151
|
+
{
|
|
2152
|
+
type: "checkbox",
|
|
2153
|
+
checked: newFlow.showInMenu,
|
|
2154
|
+
onChange: (event) => setNewFlow((prev) => ({ ...prev, showInMenu: event.target.checked }))
|
|
2155
|
+
}
|
|
2156
|
+
),
|
|
2157
|
+
labels.flowManager.showInMenu
|
|
2158
|
+
] }),
|
|
2159
|
+
newFlow.showInMenu && /* @__PURE__ */ jsxs9("div", { children: [
|
|
2160
|
+
/* @__PURE__ */ jsx10("label", { className: "text-xs font-medium text-gray-500 dark:text-gray-400", children: labels.flowManager.menuOptionLabel }),
|
|
2161
|
+
/* @__PURE__ */ jsx10(
|
|
2162
|
+
"input",
|
|
2163
|
+
{
|
|
2164
|
+
value: newFlow.menuOptionLabel,
|
|
2165
|
+
onChange: (event) => setNewFlow((prev) => ({ ...prev, menuOptionLabel: event.target.value })),
|
|
2166
|
+
placeholder: newFlow.label,
|
|
2167
|
+
className: `mt-1 ${DIALOG_INPUT}`
|
|
2168
|
+
}
|
|
2169
|
+
)
|
|
2170
|
+
] }),
|
|
2171
|
+
flowMutationState.error && /* @__PURE__ */ jsx10("p", { className: "text-xs text-red-600", children: flowMutationState.error })
|
|
2172
|
+
] }),
|
|
2173
|
+
/* @__PURE__ */ jsxs9("div", { className: "flex justify-end gap-2 mt-4", children: [
|
|
2174
|
+
/* @__PURE__ */ jsx10("button", { type: "button", className: OUTLINE_BUTTON, onClick: () => setShowCreateDialog(false), children: labels.nodePanel.cancel }),
|
|
2175
|
+
/* @__PURE__ */ jsx10(
|
|
2176
|
+
"button",
|
|
2177
|
+
{
|
|
2178
|
+
type: "button",
|
|
2179
|
+
className: PRIMARY_BUTTON,
|
|
2180
|
+
onClick: () => void handleCreateFlow(),
|
|
2181
|
+
disabled: !keyIsValid || !newFlow.label || flowMutationState.pending,
|
|
2182
|
+
children: flowMutationState.pending ? labels.flowManager.creating : labels.flowManager.create
|
|
2183
|
+
}
|
|
2184
|
+
)
|
|
2185
|
+
] })
|
|
2186
|
+
] }),
|
|
2187
|
+
showDeleteDialog && canDeleteFlow && /* @__PURE__ */ jsxs9(FlowDialog, { title: labels.flowManager.deleteFlow, onClose: () => setShowDeleteDialog(false), children: [
|
|
2188
|
+
/* @__PURE__ */ jsx10("p", { className: "text-sm text-gray-600 dark:text-gray-300", children: primaryGraph ? labels.flowManager.deleteConfirm(primaryGraph.label) : "" }),
|
|
2189
|
+
flowMutationState.error && /* @__PURE__ */ jsx10("p", { className: "text-xs text-red-600 mt-2", children: flowMutationState.error }),
|
|
2190
|
+
/* @__PURE__ */ jsxs9("div", { className: "flex justify-end gap-2 mt-4", children: [
|
|
2191
|
+
/* @__PURE__ */ jsx10("button", { type: "button", className: OUTLINE_BUTTON, onClick: () => setShowDeleteDialog(false), children: labels.nodePanel.cancel }),
|
|
2192
|
+
/* @__PURE__ */ jsxs9(
|
|
2193
|
+
"button",
|
|
2194
|
+
{
|
|
2195
|
+
type: "button",
|
|
2196
|
+
className: "inline-flex items-center gap-1.5 rounded-lg bg-red-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-red-700 disabled:opacity-40",
|
|
2197
|
+
onClick: () => void handleDeleteFlow(),
|
|
2198
|
+
disabled: flowMutationState.pending,
|
|
2199
|
+
children: [
|
|
2200
|
+
/* @__PURE__ */ jsx10(Trash22, { size: 13, "aria-hidden": "true" }),
|
|
2201
|
+
" ",
|
|
2202
|
+
labels.flowManager.deleteFlow
|
|
2203
|
+
]
|
|
2204
|
+
}
|
|
2205
|
+
)
|
|
2206
|
+
] })
|
|
2207
|
+
] })
|
|
2208
|
+
] });
|
|
2209
|
+
}
|
|
2210
|
+
function FlowDialog({
|
|
2211
|
+
title,
|
|
2212
|
+
onClose,
|
|
2213
|
+
children
|
|
2214
|
+
}) {
|
|
2215
|
+
return /* @__PURE__ */ jsxs9("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/40 px-4", children: [
|
|
2216
|
+
/* @__PURE__ */ jsx10("div", { className: "absolute inset-0", onClick: onClose, "aria-hidden": "true" }),
|
|
2217
|
+
/* @__PURE__ */ jsxs9(
|
|
2218
|
+
"div",
|
|
2219
|
+
{
|
|
2220
|
+
role: "dialog",
|
|
2221
|
+
"aria-modal": "true",
|
|
2222
|
+
"aria-label": title,
|
|
2223
|
+
className: "relative w-full max-w-md rounded-2xl bg-white dark:bg-gray-800 p-5 shadow-xl",
|
|
2224
|
+
children: [
|
|
2225
|
+
/* @__PURE__ */ jsx10("h3", { className: "text-base font-semibold text-gray-900 dark:text-gray-100 mb-3", children: title }),
|
|
2226
|
+
children
|
|
2227
|
+
]
|
|
2228
|
+
}
|
|
2229
|
+
)
|
|
2230
|
+
] });
|
|
2231
|
+
}
|
|
1154
2232
|
export {
|
|
1155
2233
|
BUILT_IN_ACTION_KINDS,
|
|
1156
2234
|
CONDITION_OPERATORS,
|
|
@@ -1165,6 +2243,7 @@ export {
|
|
|
1165
2243
|
FlowPalette,
|
|
1166
2244
|
FlowPortalNode,
|
|
1167
2245
|
FlowWhatsAppPreview,
|
|
2246
|
+
FlowsWorkspace,
|
|
1168
2247
|
NODE_CARD_WIDTH,
|
|
1169
2248
|
WHATSAPP_LIMITS,
|
|
1170
2249
|
computeAutoLayout,
|