@umodeapporg/ui 0.24.1 → 0.24.2
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/components/UI/Flow/FlowBlockNode.js +8 -2
- package/dist/components/UI/Flow/FlowBlockNode.js.map +1 -1
- package/dist/components/UI/Flow/FlowCanvas.js +52 -3
- package/dist/components/UI/Flow/FlowCanvas.js.map +1 -1
- package/dist/components/UI/Flow/FlowInspector.js +2 -0
- package/dist/components/UI/Flow/FlowInspector.js.map +1 -1
- package/dist/components/UI/Flow/flowViewport.d.ts +25 -0
- package/dist/components/UI/Flow/flowViewport.js +31 -0
- package/dist/components/UI/Flow/flowViewport.js.map +1 -0
- package/dist/locales/en/ui.json.d.ts +3 -3
- package/dist/locales/en/ui.json.js +1 -1
- package/dist/locales/pt/ui.json.d.ts +3 -3
- package/dist/locales/pt/ui.json.js +1 -1
- package/package.json +1 -1
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect } from "react";
|
|
2
3
|
import { useTranslation } from "react-i18next";
|
|
3
4
|
import ControlledButton from "../../Form/ControlledButton.js";
|
|
4
5
|
import ControlledMenu from "../ControlledMenu.js";
|
|
5
6
|
import { MaterialSymbol } from "../MaterialSymbol/index.js";
|
|
6
|
-
import { Handle, Position } from "@xyflow/react";
|
|
7
|
+
import { useUpdateNodeInternals, Handle, Position } from "@xyflow/react";
|
|
7
8
|
import FlowNodeShell from "./FlowNodeShell.js";
|
|
8
9
|
const FLOW_INPUT_HANDLE_ID = "in";
|
|
9
10
|
const portTop = (index, count) => `${(index + 1) / (count + 1) * 100}%`;
|
|
10
|
-
const FlowBlockNode = ({ data, selected }) => {
|
|
11
|
+
const FlowBlockNode = ({ id, data, selected }) => {
|
|
11
12
|
const { t } = useTranslation("ui");
|
|
13
|
+
const updateNodeInternals = useUpdateNodeInternals();
|
|
12
14
|
const outputs = Array.isArray(data.outputs) ? data.outputs : [];
|
|
15
|
+
const handleSignature = `${data.hasInput ? FLOW_INPUT_HANDLE_ID : ""}|${outputs.join(",")}`;
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
updateNodeInternals(id);
|
|
18
|
+
}, [id, handleSignature, updateNodeInternals]);
|
|
13
19
|
const connectedPorts = Array.isArray(data.connectedPorts) ? data.connectedPorts : [];
|
|
14
20
|
const addableBlocks = Array.isArray(data.addableBlocks) ? data.addableBlocks : [];
|
|
15
21
|
const showPortLabels = !(outputs.length === 1 && outputs[0] === "next");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FlowBlockNode.js","sources":["../../../../src/components/UI/Flow/FlowBlockNode.tsx"],"sourcesContent":["import { ReactNode } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport ControlledButton from '@/components/Form/ControlledButton';\nimport ControlledMenu from '@/components/UI/ControlledMenu';\nimport { MaterialSymbol, type SymbolCodepoints } from '@/components/UI/MaterialSymbol';\nimport { Handle, type Node, type NodeProps, Position } from '@xyflow/react';\n\nimport FlowNodeShell from './FlowNodeShell';\nimport type { TFlowIssueSeverity, TFlowPortId, TFlowRunStepStatus } from './flowTypes';\n\nexport const FLOW_INPUT_HANDLE_ID = 'in';\n\nexport interface IFlowAddableBlock {\n type: string;\n label: string;\n icon: SymbolCodepoints;\n}\n\nexport interface IFlowBlockNodeData extends Record<string, unknown> {\n icon: SymbolCodepoints;\n title: string;\n categoryLabel: string;\n summary?: ReactNode;\n hasInput: boolean;\n outputs: Array<TFlowPortId>;\n /** Ports already wired: their edge carries the label, so the port does not repeat it. */\n connectedPorts: Array<TFlowPortId>;\n portLabels: Record<TFlowPortId, string>;\n /** What a free port can grow into; empty in a read-only canvas. */\n addableBlocks: Array<IFlowAddableBlock>;\n onAddFromPort?: (port: TFlowPortId, type: string) => void;\n disabled?: boolean;\n issueSeverity?: TFlowIssueSeverity;\n issueCount: number;\n runStatus?: TFlowRunStepStatus;\n}\n\nexport type TFlowBlockNode = Node<IFlowBlockNodeData, 'flowBlock'>;\n\nconst portTop = (index: number, count: number): string => `${((index + 1) / (count + 1)) * 100}%`;\n\n/**\n * Input on the left edge, output ports spread along the right edge; a lone `next` port needs no\n * label. A free port shows a \"+\" that adds the next block already wired to it.\n */\nconst FlowBlockNode: React.FC<NodeProps<TFlowBlockNode>> = ({ data, selected }) => {\n const { t } = useTranslation('ui');\n const outputs = Array.isArray(data.outputs) ? data.outputs : [];\n const connectedPorts = Array.isArray(data.connectedPorts) ? data.connectedPorts : [];\n const addableBlocks = Array.isArray(data.addableBlocks) ? data.addableBlocks : [];\n const showPortLabels = !(outputs.length === 1 && outputs[0] === 'next');\n const canAdd = Boolean(data.onAddFromPort) && addableBlocks.length > 0;\n\n return (\n <FlowNodeShell\n icon={data.icon}\n title={data.title}\n categoryLabel={data.categoryLabel}\n summary={data.summary}\n selected={selected}\n disabled={data.disabled}\n issueSeverity={data.issueSeverity}\n issueCount={data.issueCount}\n runStatus={data.runStatus}\n className={showPortLabels ? 'pr-12' : undefined}\n >\n {data.hasInput && (\n <Handle id={FLOW_INPUT_HANDLE_ID} type=\"target\" position={Position.Left} className=\"umode-flow-handle\" />\n )}\n {outputs.map((port, index) => (\n <Handle\n key={port}\n id={port}\n type=\"source\"\n position={Position.Right}\n className=\"umode-flow-handle\"\n data-port={port}\n style={{ top: portTop(index, outputs.length) }}\n >\n {showPortLabels && !connectedPorts.includes(port) && (\n <span className=\"pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 whitespace-nowrap text-xxs font-semibold text-foreground-subtle\">\n {data.portLabels[port]}\n </span>\n )}\n </Handle>\n ))}\n {canAdd &&\n outputs.map((port, index) =>\n connectedPorts.includes(port) ? null : (\n <div\n key={`add|${port}`}\n className=\"nodrag nopan absolute -right-10 -translate-y-1/2\"\n style={{ top: portTop(index, outputs.length) }}\n >\n <ControlledMenu\n placement=\"right-start\"\n triggerTooltip={t('flow.node.addBlock')}\n items={addableBlocks.map((block) => ({\n key: block.type,\n label: block.label,\n icon: block.icon,\n onClick: () => data.onAddFromPort?.(port, block.type),\n }))}\n trigger={\n <ControlledButton\n defaultHtmlButton\n className=\"interactive flex size-6 items-center justify-center rounded-full border border-border-emphasis bg-surface text-foreground-muted shadow-sm hover:border-brand-border hover:bg-brand-soft hover:text-brand-emphasis focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n >\n <MaterialSymbol icon=\"add\" size={16} aria-hidden />\n </ControlledButton>\n }\n />\n </div>\n ),\n )}\n </FlowNodeShell>\n );\n};\n\nexport default FlowBlockNode;\n"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"FlowBlockNode.js","sources":["../../../../src/components/UI/Flow/FlowBlockNode.tsx"],"sourcesContent":["import { ReactNode, useEffect } from 'react';\nimport { useTranslation } from 'react-i18next';\n\nimport ControlledButton from '@/components/Form/ControlledButton';\nimport ControlledMenu from '@/components/UI/ControlledMenu';\nimport { MaterialSymbol, type SymbolCodepoints } from '@/components/UI/MaterialSymbol';\nimport { Handle, type Node, type NodeProps, Position, useUpdateNodeInternals } from '@xyflow/react';\n\nimport FlowNodeShell from './FlowNodeShell';\nimport type { TFlowIssueSeverity, TFlowPortId, TFlowRunStepStatus } from './flowTypes';\n\nexport const FLOW_INPUT_HANDLE_ID = 'in';\n\nexport interface IFlowAddableBlock {\n type: string;\n label: string;\n icon: SymbolCodepoints;\n}\n\nexport interface IFlowBlockNodeData extends Record<string, unknown> {\n icon: SymbolCodepoints;\n title: string;\n categoryLabel: string;\n summary?: ReactNode;\n hasInput: boolean;\n outputs: Array<TFlowPortId>;\n /** Ports already wired: their edge carries the label, so the port does not repeat it. */\n connectedPorts: Array<TFlowPortId>;\n portLabels: Record<TFlowPortId, string>;\n /** What a free port can grow into; empty in a read-only canvas. */\n addableBlocks: Array<IFlowAddableBlock>;\n onAddFromPort?: (port: TFlowPortId, type: string) => void;\n disabled?: boolean;\n issueSeverity?: TFlowIssueSeverity;\n issueCount: number;\n runStatus?: TFlowRunStepStatus;\n}\n\nexport type TFlowBlockNode = Node<IFlowBlockNodeData, 'flowBlock'>;\n\nconst portTop = (index: number, count: number): string => `${((index + 1) / (count + 1)) * 100}%`;\n\n/**\n * Input on the left edge, output ports spread along the right edge; a lone `next` port needs no\n * label. A free port shows a \"+\" that adds the next block already wired to it.\n */\nconst FlowBlockNode: React.FC<NodeProps<TFlowBlockNode>> = ({ id, data, selected }) => {\n const { t } = useTranslation('ui');\n const updateNodeInternals = useUpdateNodeInternals();\n const outputs = Array.isArray(data.outputs) ? data.outputs : [];\n const handleSignature = `${data.hasInput ? FLOW_INPUT_HANDLE_ID : ''}|${outputs.join(',')}`;\n\n // React Flow measures handle positions only when the node is resized. An edge whose handle was\n // not in that measurement is never drawn, so remeasure after mount and whenever the ports change.\n useEffect(() => {\n updateNodeInternals(id);\n }, [id, handleSignature, updateNodeInternals]);\n const connectedPorts = Array.isArray(data.connectedPorts) ? data.connectedPorts : [];\n const addableBlocks = Array.isArray(data.addableBlocks) ? data.addableBlocks : [];\n const showPortLabels = !(outputs.length === 1 && outputs[0] === 'next');\n const canAdd = Boolean(data.onAddFromPort) && addableBlocks.length > 0;\n\n return (\n <FlowNodeShell\n icon={data.icon}\n title={data.title}\n categoryLabel={data.categoryLabel}\n summary={data.summary}\n selected={selected}\n disabled={data.disabled}\n issueSeverity={data.issueSeverity}\n issueCount={data.issueCount}\n runStatus={data.runStatus}\n className={showPortLabels ? 'pr-12' : undefined}\n >\n {data.hasInput && (\n <Handle id={FLOW_INPUT_HANDLE_ID} type=\"target\" position={Position.Left} className=\"umode-flow-handle\" />\n )}\n {outputs.map((port, index) => (\n <Handle\n key={port}\n id={port}\n type=\"source\"\n position={Position.Right}\n className=\"umode-flow-handle\"\n data-port={port}\n style={{ top: portTop(index, outputs.length) }}\n >\n {showPortLabels && !connectedPorts.includes(port) && (\n <span className=\"pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 whitespace-nowrap text-xxs font-semibold text-foreground-subtle\">\n {data.portLabels[port]}\n </span>\n )}\n </Handle>\n ))}\n {canAdd &&\n outputs.map((port, index) =>\n connectedPorts.includes(port) ? null : (\n <div\n key={`add|${port}`}\n className=\"nodrag nopan absolute -right-10 -translate-y-1/2\"\n style={{ top: portTop(index, outputs.length) }}\n >\n <ControlledMenu\n placement=\"right-start\"\n triggerTooltip={t('flow.node.addBlock')}\n items={addableBlocks.map((block) => ({\n key: block.type,\n label: block.label,\n icon: block.icon,\n onClick: () => data.onAddFromPort?.(port, block.type),\n }))}\n trigger={\n <ControlledButton\n defaultHtmlButton\n className=\"interactive flex size-6 items-center justify-center rounded-full border border-border-emphasis bg-surface text-foreground-muted shadow-sm hover:border-brand-border hover:bg-brand-soft hover:text-brand-emphasis focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n >\n <MaterialSymbol icon=\"add\" size={16} aria-hidden />\n </ControlledButton>\n }\n />\n </div>\n ),\n )}\n </FlowNodeShell>\n );\n};\n\nexport default FlowBlockNode;\n"],"names":[],"mappings":";;;;;;;;AAWO,MAAM,uBAAuB;AA6BpC,MAAM,UAAU,CAAC,OAAe,UAA0B,IAAK,QAAQ,MAAM,QAAQ,KAAM,GAAG;AAM9F,MAAM,gBAAqD,CAAC,EAAE,IAAI,MAAM,eAAe;AACrF,QAAM,EAAE,EAAA,IAAM,eAAe,IAAI;AACjC,QAAM,sBAAsB,uBAAA;AAC5B,QAAM,UAAU,MAAM,QAAQ,KAAK,OAAO,IAAI,KAAK,UAAU,CAAA;AAC7D,QAAM,kBAAkB,GAAG,KAAK,WAAW,uBAAuB,EAAE,IAAI,QAAQ,KAAK,GAAG,CAAC;AAIzF,YAAU,MAAM;AACd,wBAAoB,EAAE;AAAA,EACxB,GAAG,CAAC,IAAI,iBAAiB,mBAAmB,CAAC;AAC7C,QAAM,iBAAiB,MAAM,QAAQ,KAAK,cAAc,IAAI,KAAK,iBAAiB,CAAA;AAClF,QAAM,gBAAgB,MAAM,QAAQ,KAAK,aAAa,IAAI,KAAK,gBAAgB,CAAA;AAC/E,QAAM,iBAAiB,EAAE,QAAQ,WAAW,KAAK,QAAQ,CAAC,MAAM;AAChE,QAAM,SAAS,QAAQ,KAAK,aAAa,KAAK,cAAc,SAAS;AAErE,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,MAAM,KAAK;AAAA,MACX,OAAO,KAAK;AAAA,MACZ,eAAe,KAAK;AAAA,MACpB,SAAS,KAAK;AAAA,MACd;AAAA,MACA,UAAU,KAAK;AAAA,MACf,eAAe,KAAK;AAAA,MACpB,YAAY,KAAK;AAAA,MACjB,WAAW,KAAK;AAAA,MAChB,WAAW,iBAAiB,UAAU;AAAA,MAErC,UAAA;AAAA,QAAA,KAAK,YACJ,oBAAC,QAAA,EAAO,IAAI,sBAAsB,MAAK,UAAS,UAAU,SAAS,MAAM,WAAU,oBAAA,CAAoB;AAAA,QAExG,QAAQ,IAAI,CAAC,MAAM,UAClB;AAAA,UAAC;AAAA,UAAA;AAAA,YAEC,IAAI;AAAA,YACJ,MAAK;AAAA,YACL,UAAU,SAAS;AAAA,YACnB,WAAU;AAAA,YACV,aAAW;AAAA,YACX,OAAO,EAAE,KAAK,QAAQ,OAAO,QAAQ,MAAM,EAAA;AAAA,YAE1C,UAAA,kBAAkB,CAAC,eAAe,SAAS,IAAI,KAC9C,oBAAC,QAAA,EAAK,WAAU,iIACb,UAAA,KAAK,WAAW,IAAI,EAAA,CACvB;AAAA,UAAA;AAAA,UAXG;AAAA,QAAA,CAcR;AAAA,QACA,UACC,QAAQ;AAAA,UAAI,CAAC,MAAM,UACjB,eAAe,SAAS,IAAI,IAAI,OAC9B;AAAA,YAAC;AAAA,YAAA;AAAA,cAEC,WAAU;AAAA,cACV,OAAO,EAAE,KAAK,QAAQ,OAAO,QAAQ,MAAM,EAAA;AAAA,cAE3C,UAAA;AAAA,gBAAC;AAAA,gBAAA;AAAA,kBACC,WAAU;AAAA,kBACV,gBAAgB,EAAE,oBAAoB;AAAA,kBACtC,OAAO,cAAc,IAAI,CAAC,WAAW;AAAA,oBACnC,KAAK,MAAM;AAAA,oBACX,OAAO,MAAM;AAAA,oBACb,MAAM,MAAM;AAAA,oBACZ,SAAS,MAAM,KAAK,gBAAgB,MAAM,MAAM,IAAI;AAAA,kBAAA,EACpD;AAAA,kBACF,SACE;AAAA,oBAAC;AAAA,oBAAA;AAAA,sBACC,mBAAiB;AAAA,sBACjB,WAAU;AAAA,sBAEV,8BAAC,gBAAA,EAAe,MAAK,OAAM,MAAM,IAAI,eAAW,KAAA,CAAC;AAAA,oBAAA;AAAA,kBAAA;AAAA,gBACnD;AAAA,cAAA;AAAA,YAEJ;AAAA,YArBK,OAAO,IAAI;AAAA,UAAA;AAAA,QAsBlB;AAAA,MAEJ;AAAA,IAAA;AAAA,EAAA;AAGR;"}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useMemo, useState, useCallback } from "react";
|
|
2
|
+
import { useMemo, useState, useRef, useEffect, useCallback } from "react";
|
|
3
3
|
import { useTranslation } from "react-i18next";
|
|
4
4
|
import { twMerge } from "tailwind-merge";
|
|
5
|
-
import { MarkerType, ReactFlow, Background, BackgroundVariant, MiniMap } from "@xyflow/react";
|
|
5
|
+
import { useReactFlow, useStore, MarkerType, ReactFlow, Background, BackgroundVariant, MiniMap } from "@xyflow/react";
|
|
6
6
|
import FlowBlockNode, { FLOW_INPUT_HANDLE_ID } from "./FlowBlockNode.js";
|
|
7
7
|
import { flowOutputsOf } from "./flowBlockRegistry.js";
|
|
8
8
|
import FlowConnectionEdge from "./FlowConnectionEdge.js";
|
|
9
9
|
import { useFlowBlockRegistry, useFlowEditorStore, useFlowEditor } from "./FlowEditorProvider.js";
|
|
10
10
|
import { canConnect, layoutFlowByLevels } from "./flowGraph.js";
|
|
11
|
+
import { boundsOfRects, viewportToReveal } from "./flowViewport.js";
|
|
11
12
|
import FlowZoomControls from "./FlowZoomControls.js";
|
|
12
13
|
import { useFlowLabels } from "./useFlowLabels.js";
|
|
13
14
|
import "@xyflow/react/dist/base.css";
|
|
@@ -19,6 +20,7 @@ const MINIMAP_SIZE = { width: 160, height: 104 };
|
|
|
19
20
|
const MINIMAP_MIN_NODES = 5;
|
|
20
21
|
const SNAP_GRID = [16, 16];
|
|
21
22
|
const PORTS = ["next", "true", "false", "error"];
|
|
23
|
+
const REVEAL_DURATION_MS = 200;
|
|
22
24
|
const isPortId = (value) => PORTS.includes(value);
|
|
23
25
|
const worstSeverity = (issues) => {
|
|
24
26
|
if (issues.some((issue) => issue.severity === "error")) return "error";
|
|
@@ -46,6 +48,47 @@ const FlowCanvas = ({
|
|
|
46
48
|
const canRedo = useFlowEditor((state) => state.future.length > 0);
|
|
47
49
|
const safeIssues = useMemo(() => Array.isArray(issues) ? issues : [], [issues]);
|
|
48
50
|
const [measuredSizes, setMeasuredSizes] = useState({});
|
|
51
|
+
const { getViewport, setViewport } = useReactFlow();
|
|
52
|
+
const canvasWidth = useStore((state) => state.width);
|
|
53
|
+
const canvasHeight = useStore((state) => state.height);
|
|
54
|
+
const [revealNodeIds, setRevealNodeIds] = useState([]);
|
|
55
|
+
const knownNodeIdsRef = useRef(null);
|
|
56
|
+
const pendingViewportRef = useRef(null);
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
const currentIds = new Set(graph.nodes.map((node) => node.nodeId));
|
|
59
|
+
const knownIds = knownNodeIdsRef.current;
|
|
60
|
+
knownNodeIdsRef.current = currentIds;
|
|
61
|
+
const addedAndSelected = knownIds ? selectedNodeIds.filter((id) => currentIds.has(id) && !knownIds.has(id)) : [];
|
|
62
|
+
if (addedAndSelected.length > 0) {
|
|
63
|
+
setRevealNodeIds(addedAndSelected);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
setRevealNodeIds(
|
|
67
|
+
(current) => current.length > 0 && !current.every((id) => selectedNodeIds.includes(id)) ? [] : current
|
|
68
|
+
);
|
|
69
|
+
}, [graph.nodes, selectedNodeIds]);
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (revealNodeIds.length === 0) return;
|
|
72
|
+
const rects = [];
|
|
73
|
+
for (const nodeId of revealNodeIds) {
|
|
74
|
+
const node = graph.nodes.find((candidate) => candidate.nodeId === nodeId);
|
|
75
|
+
const size = measuredSizes[nodeId];
|
|
76
|
+
if (!node || !size) return;
|
|
77
|
+
rects.push({ ...node.position, ...size });
|
|
78
|
+
}
|
|
79
|
+
const bounds = boundsOfRects(rects);
|
|
80
|
+
if (!bounds) return;
|
|
81
|
+
const pending = pendingViewportRef.current;
|
|
82
|
+
const baseViewport = pending && pending.settlesAt > Date.now() ? pending.viewport : getViewport();
|
|
83
|
+
const target = viewportToReveal(baseViewport, bounds, { width: canvasWidth, height: canvasHeight });
|
|
84
|
+
if (!target) return;
|
|
85
|
+
pendingViewportRef.current = { viewport: target, settlesAt: Date.now() + REVEAL_DURATION_MS };
|
|
86
|
+
void setViewport(target, { duration: REVEAL_DURATION_MS });
|
|
87
|
+
}, [revealNodeIds, graph.nodes, measuredSizes, canvasWidth, canvasHeight, getViewport, setViewport]);
|
|
88
|
+
const stopRevealing = useCallback(() => {
|
|
89
|
+
pendingViewportRef.current = null;
|
|
90
|
+
setRevealNodeIds((current) => current.length > 0 ? [] : current);
|
|
91
|
+
}, []);
|
|
49
92
|
const addableBlocks = useMemo(() => {
|
|
50
93
|
if (readOnly) return [];
|
|
51
94
|
return registry.list.filter((definition) => definition.category !== "trigger").filter(
|
|
@@ -214,7 +257,13 @@ const FlowCanvas = ({
|
|
|
214
257
|
onEdgesChange: handleEdgesChange,
|
|
215
258
|
onConnect: handleConnect,
|
|
216
259
|
isValidConnection,
|
|
217
|
-
onNodeDragStart: () =>
|
|
260
|
+
onNodeDragStart: () => {
|
|
261
|
+
stopRevealing();
|
|
262
|
+
store.getState().recordHistory();
|
|
263
|
+
},
|
|
264
|
+
onMoveStart: (event) => {
|
|
265
|
+
if (event) stopRevealing();
|
|
266
|
+
},
|
|
218
267
|
onPaneClick: () => store.getState().setSelection({ nodeIds: [], edgeIds: [] }),
|
|
219
268
|
deleteKeyCode: null,
|
|
220
269
|
nodesDraggable: !readOnly,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FlowCanvas.js","sources":["../../../../src/components/UI/Flow/FlowCanvas.tsx"],"sourcesContent":["import { KeyboardEvent, useCallback, useMemo, useState } from 'react';\nimport { useTranslation } from 'react-i18next';\nimport { twMerge } from 'tailwind-merge';\n\nimport {\n Background,\n BackgroundVariant,\n type Connection,\n type EdgeChange,\n MarkerType,\n MiniMap,\n type NodeChange,\n type PanelPosition,\n type ProOptions,\n ReactFlow,\n} from '@xyflow/react';\n\nimport FlowBlockNode, { FLOW_INPUT_HANDLE_ID, type IFlowAddableBlock, type TFlowBlockNode } from './FlowBlockNode';\nimport { flowOutputsOf } from './flowBlockRegistry';\nimport FlowConnectionEdge, { type TFlowConnectionEdge } from './FlowConnectionEdge';\nimport { useFlowBlockRegistry, useFlowEditor, useFlowEditorStore } from './FlowEditorProvider';\nimport { canConnect, layoutFlowByLevels } from './flowGraph';\nimport type { IFlowIssue, IFlowPosition, IFlowRunOverlay, TFlowIssueSeverity, TFlowPortId } from './flowTypes';\nimport FlowZoomControls from './FlowZoomControls';\nimport { useFlowLabels } from './useFlowLabels';\n\nimport '@xyflow/react/dist/base.css';\nimport '../../../styles/flow.css';\n\nconst PRO_OPTIONS: ProOptions = { hideAttribution: true };\nconst NODE_TYPES = { flowBlock: FlowBlockNode };\nconst EDGE_TYPES = { flowConnection: FlowConnectionEdge };\nconst MINIMAP_SIZE = { width: 160, height: 104 };\n/** A small flow fits on screen whole, where a minimap only covers blocks. */\nconst MINIMAP_MIN_NODES = 5;\nconst SNAP_GRID: [number, number] = [16, 16];\nconst PORTS: Array<TFlowPortId> = ['next', 'true', 'false', 'error'];\n\nexport interface IFlowCanvasProps {\n issues?: Array<IFlowIssue>;\n runOverlay?: IFlowRunOverlay | null;\n readOnly?: boolean;\n /** Accessible name of the canvas region, from `t()`. Defaults to the library label. */\n ariaLabel?: string;\n className?: string;\n /** Where the zoom and history controls sit. */\n controlsPosition?: PanelPosition;\n /** The dot grid behind the blocks. Blocks still snap to the grid when it is hidden. */\n background?: 'dots' | 'none';\n}\n\nconst isPortId = (value: string | null | undefined): value is TFlowPortId => PORTS.includes(value as TFlowPortId);\n\nconst worstSeverity = (issues: Array<IFlowIssue>): TFlowIssueSeverity | undefined => {\n if (issues.some((issue) => issue.severity === 'error')) return 'error';\n if (issues.some((issue) => issue.severity === 'warning')) return 'warning';\n return undefined;\n};\n\nconst isTypingTarget = (target: EventTarget | null): boolean =>\n target instanceof HTMLElement &&\n (target.isContentEditable || ['INPUT', 'TEXTAREA', 'SELECT'].includes(target.tagName));\n\n/**\n * The editing surface. Renders the graph held by `FlowEditorProvider` and turns every\n * gesture (drag, connect, select, delete, shortcuts) into a store action, so undo\n * and the dirty flag see all of them.\n */\nconst FlowCanvas: React.FC<IFlowCanvasProps> = ({\n issues = [],\n runOverlay,\n readOnly = false,\n ariaLabel,\n className,\n controlsPosition,\n background = 'dots',\n}) => {\n const { t } = useTranslation('ui');\n const labels = useFlowLabels();\n const registry = useFlowBlockRegistry();\n const store = useFlowEditorStore();\n\n const graph = useFlowEditor((state) => state.graph);\n const selectedNodeIds = useFlowEditor((state) => state.selectedNodeIds);\n const selectedEdgeIds = useFlowEditor((state) => state.selectedEdgeIds);\n const canUndo = useFlowEditor((state) => state.past.length > 0);\n const canRedo = useFlowEditor((state) => state.future.length > 0);\n\n const safeIssues = useMemo(() => (Array.isArray(issues) ? issues : []), [issues]);\n // Measured sizes are view state, not part of the flow: kept here so the minimap can draw the blocks.\n const [measuredSizes, setMeasuredSizes] = useState<Record<string, { width: number; height: number }>>({});\n\n const addableBlocks = useMemo<Array<IFlowAddableBlock>>(() => {\n if (readOnly) return [];\n return registry.list\n .filter((definition) => definition.category !== 'trigger')\n .filter(\n (definition) =>\n definition.maxInstances === undefined ||\n graph.nodes.filter((node) => node.type === definition.type).length < definition.maxInstances,\n )\n .map((definition) => ({ type: definition.type, label: definition.label, icon: definition.icon }));\n }, [registry, graph.nodes, readOnly]);\n\n // A block in the middle of an edge needs an output to carry the flow on to the old target.\n const insertableBlocks = useMemo<Array<IFlowAddableBlock>>(\n () =>\n addableBlocks.filter((block) => {\n const definition = registry.get(block.type);\n return definition ? flowOutputsOf(definition, definition.createDefaultConfig()).length > 0 : false;\n }),\n [addableBlocks, registry],\n );\n\n const nodes = useMemo<Array<TFlowBlockNode>>(\n () =>\n graph.nodes.map((node) => {\n const definition = registry.get(node.type);\n const nodeIssues = safeIssues.filter((issue) => issue.nodeId === node.nodeId);\n return {\n id: node.nodeId,\n type: 'flowBlock',\n position: node.position,\n measured: measuredSizes[node.nodeId],\n selected: selectedNodeIds.includes(node.nodeId),\n draggable: !readOnly,\n connectable: !readOnly,\n data: {\n icon: definition?.resolveIcon?.(node.config) ?? definition?.icon ?? 'help',\n title: node.label ?? definition?.resolveLabel?.(node.config) ?? definition?.label ?? node.type,\n categoryLabel: definition ? labels.categories[definition.category] : '',\n summary: definition\n ? definition.renderSummary(node.config)\n : labels.issueMessage({ code: 'unknownBlock', severity: 'error' }),\n hasInput: definition ? definition.category !== 'trigger' : true,\n outputs: flowOutputsOf(definition, node.config),\n connectedPorts: graph.edges.filter((edge) => edge.source === node.nodeId).map((edge) => edge.sourcePort),\n portLabels: labels.ports,\n addableBlocks,\n onAddFromPort: readOnly\n ? undefined\n : (port, type) => store.getState().addNodeFromPort(type, node.nodeId, port),\n disabled: node.disabled,\n issueSeverity: worstSeverity(nodeIssues),\n issueCount: nodeIssues.length,\n runStatus: runOverlay?.steps?.[node.nodeId],\n },\n };\n }),\n [\n graph.nodes,\n graph.edges,\n registry,\n safeIssues,\n selectedNodeIds,\n readOnly,\n labels,\n runOverlay,\n measuredSizes,\n addableBlocks,\n store,\n ],\n );\n\n const edges = useMemo<Array<TFlowConnectionEdge>>(() => {\n const traversed = new Set(runOverlay?.traversedEdgeIds ?? []);\n return graph.edges.map((edge) => ({\n id: edge.edgeId,\n type: 'flowConnection',\n source: edge.source,\n sourceHandle: edge.sourcePort,\n target: edge.target,\n targetHandle: FLOW_INPUT_HANDLE_ID,\n selected: selectedEdgeIds.includes(edge.edgeId),\n markerEnd: { type: MarkerType.ArrowClosed, width: 16, height: 16 },\n data: {\n label: edge.sourcePort === 'next' ? undefined : labels.ports[edge.sourcePort],\n traversed: traversed.has(edge.edgeId),\n issueSeverity: worstSeverity(safeIssues.filter((issue) => issue.edgeId === edge.edgeId)),\n insertableBlocks,\n onInsert: readOnly ? undefined : (type: string) => store.getState().insertNodeOnEdge(type, edge.edgeId),\n },\n }));\n }, [graph.edges, selectedEdgeIds, runOverlay, labels, safeIssues, insertableBlocks, readOnly, store]);\n\n const handleNodesChange = useCallback(\n (changes: Array<NodeChange<TFlowBlockNode>>) => {\n const positions: Record<string, IFlowPosition> = {};\n const sizes: Record<string, { width: number; height: number }> = {};\n let selection: Array<string> | null = null;\n for (const change of changes) {\n if (change.type === 'dimensions' && change.dimensions) sizes[change.id] = change.dimensions;\n if (change.type === 'position' && change.position && !readOnly) positions[change.id] = change.position;\n if (change.type === 'select') {\n const current: Array<string> = selection ?? store.getState().selectedNodeIds;\n selection = change.selected\n ? [...new Set([...current, change.id])]\n : current.filter((id) => id !== change.id);\n }\n }\n if (Object.keys(sizes).length > 0) setMeasuredSizes((current) => ({ ...current, ...sizes }));\n if (Object.keys(positions).length > 0) store.getState().moveNodes(positions);\n if (selection) store.getState().setSelection({ nodeIds: selection });\n },\n [store, readOnly],\n );\n\n const handleEdgesChange = useCallback(\n (changes: Array<EdgeChange<TFlowConnectionEdge>>) => {\n let selection: Array<string> | null = null;\n for (const change of changes) {\n if (change.type !== 'select') continue;\n const current: Array<string> = selection ?? store.getState().selectedEdgeIds;\n selection = change.selected ? [...new Set([...current, change.id])] : current.filter((id) => id !== change.id);\n }\n if (selection) store.getState().setSelection({ edgeIds: selection });\n },\n [store],\n );\n\n const handleConnect = useCallback(\n (connection: Connection) => {\n if (readOnly || !isPortId(connection.sourceHandle)) return;\n store.getState().connect(connection.source, connection.sourceHandle, connection.target);\n },\n [store, readOnly],\n );\n\n const isValidConnection = useCallback(\n (connection: Connection | TFlowConnectionEdge) =>\n isPortId(connection.sourceHandle) &&\n canConnect(store.getState().graph, registry, {\n source: connection.source,\n sourcePort: connection.sourceHandle,\n target: connection.target,\n }),\n [store, registry],\n );\n\n const handleKeyDown = useCallback(\n (event: KeyboardEvent<HTMLDivElement>) => {\n if (readOnly || isTypingTarget(event.target)) return;\n const state = store.getState();\n const withModifier = event.metaKey || event.ctrlKey;\n const key = event.key.toLowerCase();\n\n if (event.key === 'Delete' || event.key === 'Backspace') state.removeSelected();\n else if (withModifier && key === 'z' && event.shiftKey) state.redo();\n else if (withModifier && key === 'z') state.undo();\n else if (withModifier && key === 'y') state.redo();\n else if (withModifier && key === 'c') state.copySelected();\n else if (withModifier && key === 'v') state.paste();\n else if (withModifier && key === 'd') state.duplicateSelected();\n else return;\n event.preventDefault();\n },\n [store, readOnly],\n );\n\n const handleLayout = useCallback(() => {\n const state = store.getState();\n state.recordHistory();\n state.moveNodes(layoutFlowByLevels(state.graph, registry));\n }, [store, registry]);\n\n return (\n <div\n role=\"region\"\n aria-label={ariaLabel ?? t('flow.canvas')}\n tabIndex={0}\n onKeyDown={handleKeyDown}\n className={twMerge(\n 'umode-flow relative h-full min-h-[24rem] w-full rounded-xl bg-canvas focus:outline-none',\n className,\n )}\n >\n <ReactFlow<TFlowBlockNode, TFlowConnectionEdge>\n nodes={nodes}\n edges={edges}\n nodeTypes={NODE_TYPES}\n edgeTypes={EDGE_TYPES}\n onNodesChange={handleNodesChange}\n onEdgesChange={handleEdgesChange}\n onConnect={handleConnect}\n isValidConnection={isValidConnection}\n onNodeDragStart={() => store.getState().recordHistory()}\n onPaneClick={() => store.getState().setSelection({ nodeIds: [], edgeIds: [] })}\n deleteKeyCode={null}\n nodesDraggable={!readOnly}\n nodesConnectable={!readOnly}\n snapToGrid\n snapGrid={SNAP_GRID}\n minZoom={0.25}\n maxZoom={2}\n fitView\n fitViewOptions={{ padding: 0.2, maxZoom: 1 }}\n proOptions={PRO_OPTIONS}\n >\n {background === 'dots' && <Background variant={BackgroundVariant.Dots} gap={16} size={1} />}\n {nodes.length >= MINIMAP_MIN_NODES && (\n <MiniMap pannable zoomable position=\"bottom-right\" style={MINIMAP_SIZE} />\n )}\n <FlowZoomControls\n onUndo={readOnly ? undefined : () => store.getState().undo()}\n onRedo={readOnly ? undefined : () => store.getState().redo()}\n canUndo={canUndo}\n canRedo={canRedo}\n onLayout={readOnly ? undefined : handleLayout}\n position={controlsPosition}\n />\n </ReactFlow>\n </div>\n );\n};\n\nexport default FlowCanvas;\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"FlowCanvas.js","sources":["../../../../src/components/UI/Flow/FlowCanvas.tsx"],"sourcesContent":["import { KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { useTranslation } from 'react-i18next';\nimport { twMerge } from 'tailwind-merge';\n\nimport {\n Background,\n BackgroundVariant,\n type Connection,\n type EdgeChange,\n MarkerType,\n MiniMap,\n type NodeChange,\n type PanelPosition,\n type ProOptions,\n ReactFlow,\n useReactFlow,\n useStore as useReactFlowStore,\n} from '@xyflow/react';\n\nimport FlowBlockNode, { FLOW_INPUT_HANDLE_ID, type IFlowAddableBlock, type TFlowBlockNode } from './FlowBlockNode';\nimport { flowOutputsOf } from './flowBlockRegistry';\nimport FlowConnectionEdge, { type TFlowConnectionEdge } from './FlowConnectionEdge';\nimport { useFlowBlockRegistry, useFlowEditor, useFlowEditorStore } from './FlowEditorProvider';\nimport { canConnect, layoutFlowByLevels } from './flowGraph';\nimport type { IFlowIssue, IFlowPosition, IFlowRunOverlay, TFlowIssueSeverity, TFlowPortId } from './flowTypes';\nimport { boundsOfRects, type IFlowRect, type IFlowViewport, viewportToReveal } from './flowViewport';\nimport FlowZoomControls from './FlowZoomControls';\nimport { useFlowLabels } from './useFlowLabels';\n\nimport '@xyflow/react/dist/base.css';\nimport '../../../styles/flow.css';\n\nconst PRO_OPTIONS: ProOptions = { hideAttribution: true };\nconst NODE_TYPES = { flowBlock: FlowBlockNode };\nconst EDGE_TYPES = { flowConnection: FlowConnectionEdge };\nconst MINIMAP_SIZE = { width: 160, height: 104 };\n/** A small flow fits on screen whole, where a minimap only covers blocks. */\nconst MINIMAP_MIN_NODES = 5;\nconst SNAP_GRID: [number, number] = [16, 16];\nconst PORTS: Array<TFlowPortId> = ['next', 'true', 'false', 'error'];\nconst REVEAL_DURATION_MS = 200;\n\nexport interface IFlowCanvasProps {\n issues?: Array<IFlowIssue>;\n runOverlay?: IFlowRunOverlay | null;\n readOnly?: boolean;\n /** Accessible name of the canvas region, from `t()`. Defaults to the library label. */\n ariaLabel?: string;\n className?: string;\n /** Where the zoom and history controls sit. */\n controlsPosition?: PanelPosition;\n /** The dot grid behind the blocks. Blocks still snap to the grid when it is hidden. */\n background?: 'dots' | 'none';\n}\n\nconst isPortId = (value: string | null | undefined): value is TFlowPortId => PORTS.includes(value as TFlowPortId);\n\nconst worstSeverity = (issues: Array<IFlowIssue>): TFlowIssueSeverity | undefined => {\n if (issues.some((issue) => issue.severity === 'error')) return 'error';\n if (issues.some((issue) => issue.severity === 'warning')) return 'warning';\n return undefined;\n};\n\nconst isTypingTarget = (target: EventTarget | null): boolean =>\n target instanceof HTMLElement &&\n (target.isContentEditable || ['INPUT', 'TEXTAREA', 'SELECT'].includes(target.tagName));\n\n/**\n * The editing surface. Renders the graph held by `FlowEditorProvider` and turns every\n * gesture (drag, connect, select, delete, shortcuts) into a store action, so undo\n * and the dirty flag see all of them.\n */\nconst FlowCanvas: React.FC<IFlowCanvasProps> = ({\n issues = [],\n runOverlay,\n readOnly = false,\n ariaLabel,\n className,\n controlsPosition,\n background = 'dots',\n}) => {\n const { t } = useTranslation('ui');\n const labels = useFlowLabels();\n const registry = useFlowBlockRegistry();\n const store = useFlowEditorStore();\n\n const graph = useFlowEditor((state) => state.graph);\n const selectedNodeIds = useFlowEditor((state) => state.selectedNodeIds);\n const selectedEdgeIds = useFlowEditor((state) => state.selectedEdgeIds);\n const canUndo = useFlowEditor((state) => state.past.length > 0);\n const canRedo = useFlowEditor((state) => state.future.length > 0);\n\n const safeIssues = useMemo(() => (Array.isArray(issues) ? issues : []), [issues]);\n // Measured sizes are view state, not part of the flow: kept here so the minimap can draw the blocks.\n const [measuredSizes, setMeasuredSizes] = useState<Record<string, { width: number; height: number }>>({});\n\n const { getViewport, setViewport } = useReactFlow<TFlowBlockNode, TFlowConnectionEdge>();\n const canvasWidth = useReactFlowStore((state) => state.width);\n const canvasHeight = useReactFlowStore((state) => state.height);\n // Blocks just added and selected: kept in view until the user moves the view or the selection, so\n // the host panel that opens on selection (and narrows the canvas) cannot hide them.\n const [revealNodeIds, setRevealNodeIds] = useState<Array<string>>([]);\n const knownNodeIdsRef = useRef<Set<string> | null>(null);\n const pendingViewportRef = useRef<{ viewport: IFlowViewport; settlesAt: number } | null>(null);\n\n useEffect(() => {\n const currentIds = new Set(graph.nodes.map((node) => node.nodeId));\n const knownIds = knownNodeIdsRef.current;\n knownNodeIdsRef.current = currentIds;\n const addedAndSelected = knownIds ? selectedNodeIds.filter((id) => currentIds.has(id) && !knownIds.has(id)) : [];\n if (addedAndSelected.length > 0) {\n setRevealNodeIds(addedAndSelected);\n return;\n }\n setRevealNodeIds((current) =>\n current.length > 0 && !current.every((id) => selectedNodeIds.includes(id)) ? [] : current,\n );\n }, [graph.nodes, selectedNodeIds]);\n\n useEffect(() => {\n if (revealNodeIds.length === 0) return;\n const rects: Array<IFlowRect> = [];\n for (const nodeId of revealNodeIds) {\n const node = graph.nodes.find((candidate) => candidate.nodeId === nodeId);\n const size = measuredSizes[nodeId];\n if (!node || !size) return;\n rects.push({ ...node.position, ...size });\n }\n const bounds = boundsOfRects(rects);\n if (!bounds) return;\n // A pan still animating reports an in-between viewport; build on where it is heading instead.\n const pending = pendingViewportRef.current;\n const baseViewport = pending && pending.settlesAt > Date.now() ? pending.viewport : getViewport();\n const target = viewportToReveal(baseViewport, bounds, { width: canvasWidth, height: canvasHeight });\n if (!target) return;\n pendingViewportRef.current = { viewport: target, settlesAt: Date.now() + REVEAL_DURATION_MS };\n void setViewport(target, { duration: REVEAL_DURATION_MS });\n }, [revealNodeIds, graph.nodes, measuredSizes, canvasWidth, canvasHeight, getViewport, setViewport]);\n\n const stopRevealing = useCallback(() => {\n pendingViewportRef.current = null;\n setRevealNodeIds((current) => (current.length > 0 ? [] : current));\n }, []);\n\n const addableBlocks = useMemo<Array<IFlowAddableBlock>>(() => {\n if (readOnly) return [];\n return registry.list\n .filter((definition) => definition.category !== 'trigger')\n .filter(\n (definition) =>\n definition.maxInstances === undefined ||\n graph.nodes.filter((node) => node.type === definition.type).length < definition.maxInstances,\n )\n .map((definition) => ({ type: definition.type, label: definition.label, icon: definition.icon }));\n }, [registry, graph.nodes, readOnly]);\n\n // A block in the middle of an edge needs an output to carry the flow on to the old target.\n const insertableBlocks = useMemo<Array<IFlowAddableBlock>>(\n () =>\n addableBlocks.filter((block) => {\n const definition = registry.get(block.type);\n return definition ? flowOutputsOf(definition, definition.createDefaultConfig()).length > 0 : false;\n }),\n [addableBlocks, registry],\n );\n\n const nodes = useMemo<Array<TFlowBlockNode>>(\n () =>\n graph.nodes.map((node) => {\n const definition = registry.get(node.type);\n const nodeIssues = safeIssues.filter((issue) => issue.nodeId === node.nodeId);\n return {\n id: node.nodeId,\n type: 'flowBlock',\n position: node.position,\n measured: measuredSizes[node.nodeId],\n selected: selectedNodeIds.includes(node.nodeId),\n draggable: !readOnly,\n connectable: !readOnly,\n data: {\n icon: definition?.resolveIcon?.(node.config) ?? definition?.icon ?? 'help',\n title: node.label ?? definition?.resolveLabel?.(node.config) ?? definition?.label ?? node.type,\n categoryLabel: definition ? labels.categories[definition.category] : '',\n summary: definition\n ? definition.renderSummary(node.config)\n : labels.issueMessage({ code: 'unknownBlock', severity: 'error' }),\n hasInput: definition ? definition.category !== 'trigger' : true,\n outputs: flowOutputsOf(definition, node.config),\n connectedPorts: graph.edges.filter((edge) => edge.source === node.nodeId).map((edge) => edge.sourcePort),\n portLabels: labels.ports,\n addableBlocks,\n onAddFromPort: readOnly\n ? undefined\n : (port, type) => store.getState().addNodeFromPort(type, node.nodeId, port),\n disabled: node.disabled,\n issueSeverity: worstSeverity(nodeIssues),\n issueCount: nodeIssues.length,\n runStatus: runOverlay?.steps?.[node.nodeId],\n },\n };\n }),\n [\n graph.nodes,\n graph.edges,\n registry,\n safeIssues,\n selectedNodeIds,\n readOnly,\n labels,\n runOverlay,\n measuredSizes,\n addableBlocks,\n store,\n ],\n );\n\n const edges = useMemo<Array<TFlowConnectionEdge>>(() => {\n const traversed = new Set(runOverlay?.traversedEdgeIds ?? []);\n return graph.edges.map((edge) => ({\n id: edge.edgeId,\n type: 'flowConnection',\n source: edge.source,\n sourceHandle: edge.sourcePort,\n target: edge.target,\n targetHandle: FLOW_INPUT_HANDLE_ID,\n selected: selectedEdgeIds.includes(edge.edgeId),\n markerEnd: { type: MarkerType.ArrowClosed, width: 16, height: 16 },\n data: {\n label: edge.sourcePort === 'next' ? undefined : labels.ports[edge.sourcePort],\n traversed: traversed.has(edge.edgeId),\n issueSeverity: worstSeverity(safeIssues.filter((issue) => issue.edgeId === edge.edgeId)),\n insertableBlocks,\n onInsert: readOnly ? undefined : (type: string) => store.getState().insertNodeOnEdge(type, edge.edgeId),\n },\n }));\n }, [graph.edges, selectedEdgeIds, runOverlay, labels, safeIssues, insertableBlocks, readOnly, store]);\n\n const handleNodesChange = useCallback(\n (changes: Array<NodeChange<TFlowBlockNode>>) => {\n const positions: Record<string, IFlowPosition> = {};\n const sizes: Record<string, { width: number; height: number }> = {};\n let selection: Array<string> | null = null;\n for (const change of changes) {\n if (change.type === 'dimensions' && change.dimensions) sizes[change.id] = change.dimensions;\n if (change.type === 'position' && change.position && !readOnly) positions[change.id] = change.position;\n if (change.type === 'select') {\n const current: Array<string> = selection ?? store.getState().selectedNodeIds;\n selection = change.selected\n ? [...new Set([...current, change.id])]\n : current.filter((id) => id !== change.id);\n }\n }\n if (Object.keys(sizes).length > 0) setMeasuredSizes((current) => ({ ...current, ...sizes }));\n if (Object.keys(positions).length > 0) store.getState().moveNodes(positions);\n if (selection) store.getState().setSelection({ nodeIds: selection });\n },\n [store, readOnly],\n );\n\n const handleEdgesChange = useCallback(\n (changes: Array<EdgeChange<TFlowConnectionEdge>>) => {\n let selection: Array<string> | null = null;\n for (const change of changes) {\n if (change.type !== 'select') continue;\n const current: Array<string> = selection ?? store.getState().selectedEdgeIds;\n selection = change.selected ? [...new Set([...current, change.id])] : current.filter((id) => id !== change.id);\n }\n if (selection) store.getState().setSelection({ edgeIds: selection });\n },\n [store],\n );\n\n const handleConnect = useCallback(\n (connection: Connection) => {\n if (readOnly || !isPortId(connection.sourceHandle)) return;\n store.getState().connect(connection.source, connection.sourceHandle, connection.target);\n },\n [store, readOnly],\n );\n\n const isValidConnection = useCallback(\n (connection: Connection | TFlowConnectionEdge) =>\n isPortId(connection.sourceHandle) &&\n canConnect(store.getState().graph, registry, {\n source: connection.source,\n sourcePort: connection.sourceHandle,\n target: connection.target,\n }),\n [store, registry],\n );\n\n const handleKeyDown = useCallback(\n (event: KeyboardEvent<HTMLDivElement>) => {\n if (readOnly || isTypingTarget(event.target)) return;\n const state = store.getState();\n const withModifier = event.metaKey || event.ctrlKey;\n const key = event.key.toLowerCase();\n\n if (event.key === 'Delete' || event.key === 'Backspace') state.removeSelected();\n else if (withModifier && key === 'z' && event.shiftKey) state.redo();\n else if (withModifier && key === 'z') state.undo();\n else if (withModifier && key === 'y') state.redo();\n else if (withModifier && key === 'c') state.copySelected();\n else if (withModifier && key === 'v') state.paste();\n else if (withModifier && key === 'd') state.duplicateSelected();\n else return;\n event.preventDefault();\n },\n [store, readOnly],\n );\n\n const handleLayout = useCallback(() => {\n const state = store.getState();\n state.recordHistory();\n state.moveNodes(layoutFlowByLevels(state.graph, registry));\n }, [store, registry]);\n\n return (\n <div\n role=\"region\"\n aria-label={ariaLabel ?? t('flow.canvas')}\n tabIndex={0}\n onKeyDown={handleKeyDown}\n className={twMerge(\n 'umode-flow relative h-full min-h-[24rem] w-full rounded-xl bg-canvas focus:outline-none',\n className,\n )}\n >\n <ReactFlow<TFlowBlockNode, TFlowConnectionEdge>\n nodes={nodes}\n edges={edges}\n nodeTypes={NODE_TYPES}\n edgeTypes={EDGE_TYPES}\n onNodesChange={handleNodesChange}\n onEdgesChange={handleEdgesChange}\n onConnect={handleConnect}\n isValidConnection={isValidConnection}\n onNodeDragStart={() => {\n stopRevealing();\n store.getState().recordHistory();\n }}\n onMoveStart={(event) => {\n if (event) stopRevealing();\n }}\n onPaneClick={() => store.getState().setSelection({ nodeIds: [], edgeIds: [] })}\n deleteKeyCode={null}\n nodesDraggable={!readOnly}\n nodesConnectable={!readOnly}\n snapToGrid\n snapGrid={SNAP_GRID}\n minZoom={0.25}\n maxZoom={2}\n fitView\n fitViewOptions={{ padding: 0.2, maxZoom: 1 }}\n proOptions={PRO_OPTIONS}\n >\n {background === 'dots' && <Background variant={BackgroundVariant.Dots} gap={16} size={1} />}\n {nodes.length >= MINIMAP_MIN_NODES && (\n <MiniMap pannable zoomable position=\"bottom-right\" style={MINIMAP_SIZE} />\n )}\n <FlowZoomControls\n onUndo={readOnly ? undefined : () => store.getState().undo()}\n onRedo={readOnly ? undefined : () => store.getState().redo()}\n canUndo={canUndo}\n canRedo={canRedo}\n onLayout={readOnly ? undefined : handleLayout}\n position={controlsPosition}\n />\n </ReactFlow>\n </div>\n );\n};\n\nexport default FlowCanvas;\n"],"names":["useReactFlowStore"],"mappings":"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"}
|
|
@@ -98,6 +98,7 @@ const FlowInspector = ({
|
|
|
98
98
|
size: "sm",
|
|
99
99
|
variant: "outlined",
|
|
100
100
|
tone: "neutral",
|
|
101
|
+
className: "normal-case",
|
|
101
102
|
onClick: () => store.getState().duplicateSelected(),
|
|
102
103
|
children: [
|
|
103
104
|
/* @__PURE__ */ jsx(MaterialSymbol, { icon: "content_copy", size: 16, "aria-hidden": true }),
|
|
@@ -111,6 +112,7 @@ const FlowInspector = ({
|
|
|
111
112
|
size: "sm",
|
|
112
113
|
variant: "outlined",
|
|
113
114
|
tone: "danger",
|
|
115
|
+
className: "normal-case",
|
|
114
116
|
onClick: () => store.getState().removeSelected(),
|
|
115
117
|
children: [
|
|
116
118
|
/* @__PURE__ */ jsx(MaterialSymbol, { icon: "delete", size: 16, "aria-hidden": true }),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FlowInspector.js","sources":["../../../../src/components/UI/Flow/FlowInspector.tsx"],"sourcesContent":["import { useTranslation } from 'react-i18next';\nimport { twMerge } from 'tailwind-merge';\n\nimport ControlledButton from '@/components/Form/ControlledButton';\nimport ControlledInput from '@/components/Form/ControlledInput';\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { MaterialSymbol } from '@/components/UI/MaterialSymbol';\nimport NoDataFound from '@/components/UI/NoDataFound';\n\nimport { useFlowBlockRegistry, useFlowEditor, useFlowEditorStore } from './FlowEditorProvider';\nimport { isFlowNodeRemovable } from './flowGraph';\nimport type { IFlowIssue } from './flowTypes';\nimport { useFlowLabels } from './useFlowLabels';\n\ninterface IFlowInspectorProps {\n issues?: Array<IFlowIssue>;\n readOnly?: boolean;\n /** Renders nothing while no block is selected, instead of the empty state. */\n hideWhenEmpty?: boolean;\n className?: string;\n}\n\n/** The side panel of the selected block: its name, its own settings panel and its issues. */\nconst FlowInspector: React.FC<IFlowInspectorProps> = ({\n issues = [],\n readOnly = false,\n hideWhenEmpty = false,\n className,\n}) => {\n const { t } = useTranslation('ui');\n const labels = useFlowLabels();\n const registry = useFlowBlockRegistry();\n const store = useFlowEditorStore();\n const selectedNodeIds = useFlowEditor((state) => state.selectedNodeIds);\n const selectedNode = useFlowEditor((state) =>\n state.selectedNodeIds.length === 1\n ? state.graph.nodes.find((node) => node.nodeId === state.selectedNodeIds[0])\n : undefined,\n );\n\n const shellClass = twMerge(\n 'flex h-full min-h-0 flex-col gap-y-3 rounded-xl border border-border bg-surface p-4',\n className,\n );\n\n if (!selectedNode || selectedNodeIds.length !== 1) {\n if (hideWhenEmpty) return null;\n return (\n <aside className={shellClass} aria-label={t('flow.inspector.title')}>\n <NoDataFound size=\"sm\" variant=\"waiting\" message={t('flow.inspector.empty')} />\n </aside>\n );\n }\n\n const definition = registry.get(selectedNode.type);\n const nodeIssues = (Array.isArray(issues) ? issues : []).filter((issue) => issue.nodeId === selectedNode.nodeId);\n const ConfigPanel = definition?.ConfigPanel;\n const canDuplicate = definition?.category !== 'trigger';\n const canRemove = isFlowNodeRemovable(selectedNode, registry);\n\n return (\n <aside className={shellClass} aria-label={t('flow.inspector.title')}>\n <div className=\"flex items-center gap-x-2\">\n <span className=\"flex size-8 shrink-0 items-center justify-center rounded-lg bg-muted text-foreground-muted\">\n <MaterialSymbol\n icon={definition?.resolveIcon?.(selectedNode.config) ?? definition?.icon ?? 'help'}\n size={18}\n aria-hidden\n />\n </span>\n <div className=\"flex min-w-0 flex-col\">\n {definition && (\n <span className=\"text-xxs font-semibold uppercase tracking-wide text-foreground-subtle\">\n {labels.categories[definition.category]}\n </span>\n )}\n <span className=\"truncate text-sm font-semibold text-foreground\">\n {definition?.resolveLabel?.(selectedNode.config) ?? definition?.label ?? selectedNode.type}\n </span>\n </div>\n <ControlledTooltip content={t('flow.inspector.close')} placement=\"left\">\n <ControlledButton\n defaultHtmlButton\n onClick={() => store.getState().setSelection({ nodeIds: [], edgeIds: [] })}\n className=\"interactive ml-auto flex size-8 shrink-0 items-center justify-center rounded-lg text-foreground-muted hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n >\n <MaterialSymbol icon=\"close\" size={18} aria-hidden />\n </ControlledButton>\n </ControlledTooltip>\n </div>\n\n <ControlledInput\n label={t('flow.inspector.label')}\n value={selectedNode.label ?? ''}\n disabled={readOnly}\n onChange={(event) => store.getState().updateNodeLabel(selectedNode.nodeId, event.target.value)}\n />\n\n <div className=\"flex min-h-0 flex-1 flex-col gap-y-3 overflow-y-auto\">\n {ConfigPanel ? (\n <ConfigPanel\n config={selectedNode.config}\n readOnly={readOnly}\n onChange={(next) => store.getState().updateNodeConfig(selectedNode.nodeId, next)}\n />\n ) : (\n <NoDataFound compact variant=\"error\" message={t('flow.inspector.unknownBlock')} />\n )}\n\n {nodeIssues.length > 0 && (\n <ul className=\"flex flex-col gap-y-1\">\n {nodeIssues.map((issue) => (\n <li key={`${issue.code}|${issue.edgeId ?? ''}`} className=\"flex items-start gap-x-2\">\n <MaterialSymbol\n icon={issue.severity === 'error' ? 'error' : 'warning'}\n size={16}\n className={issue.severity === 'error' ? 'text-danger-emphasis' : 'text-warning-emphasis'}\n aria-hidden\n />\n <span className=\"text-xs text-foreground-muted\">{labels.issueMessage(issue)}</span>\n </li>\n ))}\n </ul>\n )}\n </div>\n\n {!readOnly && (canDuplicate || canRemove) && (\n <div className=\"flex gap-x-2\">\n {canDuplicate && (\n <ControlledButton\n size=\"sm\"\n variant=\"outlined\"\n tone=\"neutral\"\n onClick={() => store.getState().duplicateSelected()}\n >\n <MaterialSymbol icon=\"content_copy\" size={16} aria-hidden />\n {t('flow.inspector.duplicate')}\n </ControlledButton>\n )}\n {canRemove && (\n <ControlledButton\n size=\"sm\"\n variant=\"outlined\"\n tone=\"danger\"\n onClick={() => store.getState().removeSelected()}\n >\n <MaterialSymbol icon=\"delete\" size={16} aria-hidden />\n {t('flow.inspector.delete')}\n </ControlledButton>\n )}\n </div>\n )}\n </aside>\n );\n};\n\nexport default FlowInspector;\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"FlowInspector.js","sources":["../../../../src/components/UI/Flow/FlowInspector.tsx"],"sourcesContent":["import { useTranslation } from 'react-i18next';\nimport { twMerge } from 'tailwind-merge';\n\nimport ControlledButton from '@/components/Form/ControlledButton';\nimport ControlledInput from '@/components/Form/ControlledInput';\nimport ControlledTooltip from '@/components/UI/ControlledTooltip';\nimport { MaterialSymbol } from '@/components/UI/MaterialSymbol';\nimport NoDataFound from '@/components/UI/NoDataFound';\n\nimport { useFlowBlockRegistry, useFlowEditor, useFlowEditorStore } from './FlowEditorProvider';\nimport { isFlowNodeRemovable } from './flowGraph';\nimport type { IFlowIssue } from './flowTypes';\nimport { useFlowLabels } from './useFlowLabels';\n\ninterface IFlowInspectorProps {\n issues?: Array<IFlowIssue>;\n readOnly?: boolean;\n /** Renders nothing while no block is selected, instead of the empty state. */\n hideWhenEmpty?: boolean;\n className?: string;\n}\n\n/** The side panel of the selected block: its name, its own settings panel and its issues. */\nconst FlowInspector: React.FC<IFlowInspectorProps> = ({\n issues = [],\n readOnly = false,\n hideWhenEmpty = false,\n className,\n}) => {\n const { t } = useTranslation('ui');\n const labels = useFlowLabels();\n const registry = useFlowBlockRegistry();\n const store = useFlowEditorStore();\n const selectedNodeIds = useFlowEditor((state) => state.selectedNodeIds);\n const selectedNode = useFlowEditor((state) =>\n state.selectedNodeIds.length === 1\n ? state.graph.nodes.find((node) => node.nodeId === state.selectedNodeIds[0])\n : undefined,\n );\n\n const shellClass = twMerge(\n 'flex h-full min-h-0 flex-col gap-y-3 rounded-xl border border-border bg-surface p-4',\n className,\n );\n\n if (!selectedNode || selectedNodeIds.length !== 1) {\n if (hideWhenEmpty) return null;\n return (\n <aside className={shellClass} aria-label={t('flow.inspector.title')}>\n <NoDataFound size=\"sm\" variant=\"waiting\" message={t('flow.inspector.empty')} />\n </aside>\n );\n }\n\n const definition = registry.get(selectedNode.type);\n const nodeIssues = (Array.isArray(issues) ? issues : []).filter((issue) => issue.nodeId === selectedNode.nodeId);\n const ConfigPanel = definition?.ConfigPanel;\n const canDuplicate = definition?.category !== 'trigger';\n const canRemove = isFlowNodeRemovable(selectedNode, registry);\n\n return (\n <aside className={shellClass} aria-label={t('flow.inspector.title')}>\n <div className=\"flex items-center gap-x-2\">\n <span className=\"flex size-8 shrink-0 items-center justify-center rounded-lg bg-muted text-foreground-muted\">\n <MaterialSymbol\n icon={definition?.resolveIcon?.(selectedNode.config) ?? definition?.icon ?? 'help'}\n size={18}\n aria-hidden\n />\n </span>\n <div className=\"flex min-w-0 flex-col\">\n {definition && (\n <span className=\"text-xxs font-semibold uppercase tracking-wide text-foreground-subtle\">\n {labels.categories[definition.category]}\n </span>\n )}\n <span className=\"truncate text-sm font-semibold text-foreground\">\n {definition?.resolveLabel?.(selectedNode.config) ?? definition?.label ?? selectedNode.type}\n </span>\n </div>\n <ControlledTooltip content={t('flow.inspector.close')} placement=\"left\">\n <ControlledButton\n defaultHtmlButton\n onClick={() => store.getState().setSelection({ nodeIds: [], edgeIds: [] })}\n className=\"interactive ml-auto flex size-8 shrink-0 items-center justify-center rounded-lg text-foreground-muted hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n >\n <MaterialSymbol icon=\"close\" size={18} aria-hidden />\n </ControlledButton>\n </ControlledTooltip>\n </div>\n\n <ControlledInput\n label={t('flow.inspector.label')}\n value={selectedNode.label ?? ''}\n disabled={readOnly}\n onChange={(event) => store.getState().updateNodeLabel(selectedNode.nodeId, event.target.value)}\n />\n\n <div className=\"flex min-h-0 flex-1 flex-col gap-y-3 overflow-y-auto\">\n {ConfigPanel ? (\n <ConfigPanel\n config={selectedNode.config}\n readOnly={readOnly}\n onChange={(next) => store.getState().updateNodeConfig(selectedNode.nodeId, next)}\n />\n ) : (\n <NoDataFound compact variant=\"error\" message={t('flow.inspector.unknownBlock')} />\n )}\n\n {nodeIssues.length > 0 && (\n <ul className=\"flex flex-col gap-y-1\">\n {nodeIssues.map((issue) => (\n <li key={`${issue.code}|${issue.edgeId ?? ''}`} className=\"flex items-start gap-x-2\">\n <MaterialSymbol\n icon={issue.severity === 'error' ? 'error' : 'warning'}\n size={16}\n className={issue.severity === 'error' ? 'text-danger-emphasis' : 'text-warning-emphasis'}\n aria-hidden\n />\n <span className=\"text-xs text-foreground-muted\">{labels.issueMessage(issue)}</span>\n </li>\n ))}\n </ul>\n )}\n </div>\n\n {!readOnly && (canDuplicate || canRemove) && (\n <div className=\"flex gap-x-2\">\n {canDuplicate && (\n <ControlledButton\n size=\"sm\"\n variant=\"outlined\"\n tone=\"neutral\"\n className=\"normal-case\"\n onClick={() => store.getState().duplicateSelected()}\n >\n <MaterialSymbol icon=\"content_copy\" size={16} aria-hidden />\n {t('flow.inspector.duplicate')}\n </ControlledButton>\n )}\n {canRemove && (\n <ControlledButton\n size=\"sm\"\n variant=\"outlined\"\n tone=\"danger\"\n className=\"normal-case\"\n onClick={() => store.getState().removeSelected()}\n >\n <MaterialSymbol icon=\"delete\" size={16} aria-hidden />\n {t('flow.inspector.delete')}\n </ControlledButton>\n )}\n </div>\n )}\n </aside>\n );\n};\n\nexport default FlowInspector;\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface IFlowViewport {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
zoom: number;
|
|
5
|
+
}
|
|
6
|
+
export interface IFlowRect {
|
|
7
|
+
x: number;
|
|
8
|
+
y: number;
|
|
9
|
+
width: number;
|
|
10
|
+
height: number;
|
|
11
|
+
}
|
|
12
|
+
export interface IFlowSize {
|
|
13
|
+
width: number;
|
|
14
|
+
height: number;
|
|
15
|
+
}
|
|
16
|
+
/** Breathing room between a revealed block and the edge of the canvas, in screen pixels. */
|
|
17
|
+
export declare const FLOW_REVEAL_PADDING = 32;
|
|
18
|
+
/**
|
|
19
|
+
* The smallest pan that brings `rect` (flow coordinates) fully inside a canvas of `canvasSize`
|
|
20
|
+
* (screen pixels), keeping the zoom. Returns null when the rect is already fully visible, so a
|
|
21
|
+
* block on screen never moves the view. A rect larger than the canvas is aligned to its top-left.
|
|
22
|
+
*/
|
|
23
|
+
export declare const viewportToReveal: (viewport: IFlowViewport, rect: IFlowRect, canvasSize: IFlowSize, padding?: number) => IFlowViewport | null;
|
|
24
|
+
/** The box around several rects; null for none. */
|
|
25
|
+
export declare const boundsOfRects: (rects: Array<IFlowRect>) => IFlowRect | null;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const FLOW_REVEAL_PADDING = 32;
|
|
2
|
+
const shiftOnAxis = (start, end, min, max) => {
|
|
3
|
+
if (start < min) return min - start;
|
|
4
|
+
if (end > max) return Math.max(max - end, min - start);
|
|
5
|
+
return 0;
|
|
6
|
+
};
|
|
7
|
+
const viewportToReveal = (viewport, rect, canvasSize, padding = FLOW_REVEAL_PADDING) => {
|
|
8
|
+
if (canvasSize.width <= 0 || canvasSize.height <= 0) return null;
|
|
9
|
+
const left = viewport.x + rect.x * viewport.zoom;
|
|
10
|
+
const top = viewport.y + rect.y * viewport.zoom;
|
|
11
|
+
const right = left + rect.width * viewport.zoom;
|
|
12
|
+
const bottom = top + rect.height * viewport.zoom;
|
|
13
|
+
const deltaX = shiftOnAxis(left, right, padding, canvasSize.width - padding);
|
|
14
|
+
const deltaY = shiftOnAxis(top, bottom, padding, canvasSize.height - padding);
|
|
15
|
+
if (deltaX === 0 && deltaY === 0) return null;
|
|
16
|
+
return { x: viewport.x + deltaX, y: viewport.y + deltaY, zoom: viewport.zoom };
|
|
17
|
+
};
|
|
18
|
+
const boundsOfRects = (rects) => {
|
|
19
|
+
if (rects.length === 0) return null;
|
|
20
|
+
const minX = Math.min(...rects.map((rect) => rect.x));
|
|
21
|
+
const minY = Math.min(...rects.map((rect) => rect.y));
|
|
22
|
+
const maxX = Math.max(...rects.map((rect) => rect.x + rect.width));
|
|
23
|
+
const maxY = Math.max(...rects.map((rect) => rect.y + rect.height));
|
|
24
|
+
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
|
25
|
+
};
|
|
26
|
+
export {
|
|
27
|
+
FLOW_REVEAL_PADDING,
|
|
28
|
+
boundsOfRects,
|
|
29
|
+
viewportToReveal
|
|
30
|
+
};
|
|
31
|
+
//# sourceMappingURL=flowViewport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flowViewport.js","sources":["../../../../src/components/UI/Flow/flowViewport.ts"],"sourcesContent":["export interface IFlowViewport {\n x: number;\n y: number;\n zoom: number;\n}\n\nexport interface IFlowRect {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\nexport interface IFlowSize {\n width: number;\n height: number;\n}\n\n/** Breathing room between a revealed block and the edge of the canvas, in screen pixels. */\nexport const FLOW_REVEAL_PADDING = 32;\n\nconst shiftOnAxis = (start: number, end: number, min: number, max: number): number => {\n if (start < min) return min - start;\n if (end > max) return Math.max(max - end, min - start);\n return 0;\n};\n\n/**\n * The smallest pan that brings `rect` (flow coordinates) fully inside a canvas of `canvasSize`\n * (screen pixels), keeping the zoom. Returns null when the rect is already fully visible, so a\n * block on screen never moves the view. A rect larger than the canvas is aligned to its top-left.\n */\nexport const viewportToReveal = (\n viewport: IFlowViewport,\n rect: IFlowRect,\n canvasSize: IFlowSize,\n padding: number = FLOW_REVEAL_PADDING,\n): IFlowViewport | null => {\n if (canvasSize.width <= 0 || canvasSize.height <= 0) return null;\n const left = viewport.x + rect.x * viewport.zoom;\n const top = viewport.y + rect.y * viewport.zoom;\n const right = left + rect.width * viewport.zoom;\n const bottom = top + rect.height * viewport.zoom;\n\n const deltaX = shiftOnAxis(left, right, padding, canvasSize.width - padding);\n const deltaY = shiftOnAxis(top, bottom, padding, canvasSize.height - padding);\n if (deltaX === 0 && deltaY === 0) return null;\n return { x: viewport.x + deltaX, y: viewport.y + deltaY, zoom: viewport.zoom };\n};\n\n/** The box around several rects; null for none. */\nexport const boundsOfRects = (rects: Array<IFlowRect>): IFlowRect | null => {\n if (rects.length === 0) return null;\n const minX = Math.min(...rects.map((rect) => rect.x));\n const minY = Math.min(...rects.map((rect) => rect.y));\n const maxX = Math.max(...rects.map((rect) => rect.x + rect.width));\n const maxY = Math.max(...rects.map((rect) => rect.y + rect.height));\n return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };\n};\n"],"names":[],"mappings":"AAmBO,MAAM,sBAAsB;AAEnC,MAAM,cAAc,CAAC,OAAe,KAAa,KAAa,QAAwB;AACpF,MAAI,QAAQ,IAAK,QAAO,MAAM;AAC9B,MAAI,MAAM,IAAK,QAAO,KAAK,IAAI,MAAM,KAAK,MAAM,KAAK;AACrD,SAAO;AACT;AAOO,MAAM,mBAAmB,CAC9B,UACA,MACA,YACA,UAAkB,wBACO;AACzB,MAAI,WAAW,SAAS,KAAK,WAAW,UAAU,EAAG,QAAO;AAC5D,QAAM,OAAO,SAAS,IAAI,KAAK,IAAI,SAAS;AAC5C,QAAM,MAAM,SAAS,IAAI,KAAK,IAAI,SAAS;AAC3C,QAAM,QAAQ,OAAO,KAAK,QAAQ,SAAS;AAC3C,QAAM,SAAS,MAAM,KAAK,SAAS,SAAS;AAE5C,QAAM,SAAS,YAAY,MAAM,OAAO,SAAS,WAAW,QAAQ,OAAO;AAC3E,QAAM,SAAS,YAAY,KAAK,QAAQ,SAAS,WAAW,SAAS,OAAO;AAC5E,MAAI,WAAW,KAAK,WAAW,EAAG,QAAO;AACzC,SAAO,EAAE,GAAG,SAAS,IAAI,QAAQ,GAAG,SAAS,IAAI,QAAQ,MAAM,SAAS,KAAA;AAC1E;AAGO,MAAM,gBAAgB,CAAC,UAA8C;AAC1E,MAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,QAAM,OAAO,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC,SAAS,KAAK,CAAC,CAAC;AACpD,QAAM,OAAO,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC,SAAS,KAAK,CAAC,CAAC;AACpD,QAAM,OAAO,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC,SAAS,KAAK,IAAI,KAAK,KAAK,CAAC;AACjE,QAAM,OAAO,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC,SAAS,KAAK,IAAI,KAAK,MAAM,CAAC;AAClE,SAAO,EAAE,GAAG,MAAM,GAAG,MAAM,OAAO,OAAO,MAAM,QAAQ,OAAO,KAAA;AAChE;"}
|
|
@@ -226,9 +226,9 @@ declare const _default: {
|
|
|
226
226
|
"error": "Error"
|
|
227
227
|
},
|
|
228
228
|
"categories": {
|
|
229
|
-
"trigger": "
|
|
230
|
-
"condition": "
|
|
231
|
-
"action": "
|
|
229
|
+
"trigger": "Trigger",
|
|
230
|
+
"condition": "Condition",
|
|
231
|
+
"action": "Action"
|
|
232
232
|
},
|
|
233
233
|
"inspector": {
|
|
234
234
|
"title": "Block settings",
|
|
@@ -12,7 +12,7 @@ const detailsTable = { "size": "Size", "quantity": "Qty" };
|
|
|
12
12
|
const errorPage = { "message": "This screen could not load", "headline": "Something unexpected happened while building the page. Refreshing usually fixes it. If it happens again, contact support and tell them what you were doing.", "backToStart": "Back to start" };
|
|
13
13
|
const filters = { "activeFilters": "Applied filters", "appliedCount_one": "{{count}} filter applied", "appliedCount_other": "{{count}} filters applied", "clearAll": "Clear all", "noFields": "No filters available for this list.", "noOptions": "No options found.", "optionsError": "Could not load the options for {{field}}.", "rangeMax": "Maximum", "rangeMin": "Minimum", "rangeSeparator": "to", "searchOptions": "Search options" };
|
|
14
14
|
const filtersDrawer = { "selectedFilters": "Selected filters:", "pendingChanges": "Pending changes", "discardChangesTitle": "Discard changes?", "discardChangesMessage": "You have unapplied filter changes. Discard them?", "keepEditing": "Keep editing", "discard": "Discard" };
|
|
15
|
-
const flow = { "canvas": "Flow editor", "resizePanel": "Drag to adjust the width. Double click resets", "ports": { "next": "Next", "true": "Yes", "false": "No", "error": "Error" }, "categories": { "trigger": "
|
|
15
|
+
const flow = { "canvas": "Flow editor", "resizePanel": "Drag to adjust the width. Double click resets", "ports": { "next": "Next", "true": "Yes", "false": "No", "error": "Error" }, "categories": { "trigger": "Trigger", "condition": "Condition", "action": "Action" }, "inspector": { "title": "Block settings", "empty": "Select a block to configure it", "label": "Block name", "delete": "Delete block", "duplicate": "Duplicate block", "unknownBlock": "This block type no longer exists", "close": "Close" }, "issues": { "title": "Issues", "none": "No issues", "missingTrigger": "The flow needs a trigger", "multipleTriggers": "A flow can have only one trigger", "unknownBlock": "Block of a type that no longer exists", "unreachableNode": "Loose block: no path from the trigger reaches it", "multipleInputs": "Block with more than one input", "portFanOut": "An output can connect to one block only", "cycle": "The blocks form a cycle", "missingTrueBranch": "The condition has no yes path" }, "run": { "passed": "Passed", "failed": "Did not pass", "skipped": "Skipped", "wouldRun": "Would run", "error": "Error", "blocked": "Blocked" }, "controls": { "zoomIn": "Zoom in", "zoomOut": "Zoom out", "fitView": "Fit view", "undo": "Undo", "redo": "Redo", "layout": "Arrange blocks" }, "edge": { "insertBlock": "Insert a block here" }, "node": { "disabled": "Disabled", "addBlock": "Add the next block", "issueCount_one": "{{count}} issue", "issueCount_other": "{{count}} issues" }, "laneMap": { "label": "Flow map" } };
|
|
16
16
|
const followUp = { "messages": "Messages", "chat": { "loadingMessages": "Loading messages", "placeholder": "Write a message...", "invalidMessage": "Please enter a valid message", "noMessagesYet": "No messages yet", "startConversation": "Start a conversation by adding the first message", "loadError": "Messages could not be loaded", "sendFailed": "The message could not be sent. Try again.", "sendFailedInline": "Failed to send", "resend": "Resend", "discard": "Discard", "today": "Today", "yesterday": "Yesterday", "typingOne": "{{name}} is typing...", "typingMany": "{{count}} people are typing...", "someone": "Someone", "you": "You", "newMessages": "New messages", "backToLatest": "Back to latest", "deletedMessage": "Message deleted", "edited": "edited", "readBy": "Read by {{names}}", "copied": "Message copied", "replyingTo": "Replying to {{name}}", "attachmentFallback": "Attachment", "editTitle": "Edit message", "deleteTitle": "Delete message?", "deleteSubtitle": "The message will be shown as deleted for every participant", "sendHint": "Enter sends, Shift+Enter adds a line break", "formatting": "Formatting" }, "actions": { "reply": "Reply", "copy": "Copy text", "edit": "Edit", "delete": "Delete" }, "attachments": { "attach": "Attach", "pickFile": "File", "pickImage": "Image", "takePhoto": "Take photo", "dropHere": "Drop files to attach", "tooMany": "Limit of {{max}} attachments per message", "tooLarge": "The file {{name}} exceeds the 50 MB limit", "uploadFailed": "Upload failed", "uploading": "Uploading attachments...", "imagePreview": "Preview image", "showTranscript": "Show transcript", "hideTranscript": "Hide transcript" }, "voice": { "record": "Record audio", "recording": "Recording", "stop": "Finish recording", "cancel": "Cancel recording", "rerecord": "Record again", "attach": "Add to message", "discard": "Discard", "transcriptLabel": "Transcript", "transcriptPlaceholder": "The speech transcript appears here and can be edited" }, "camera": { "title": "Take photo", "unavailable": "Camera unavailable", "unavailableHint": "Check the browser camera permission", "switch": "Switch camera", "capture": "Capture photo" }, "search": { "open": "Search in conversation", "placeholder": "Search messages", "noResults": "No messages found", "error": "Messages could not be searched" } };
|
|
17
17
|
const generatedByAI = { "tooltip": "Generated by AI" };
|
|
18
18
|
const imageAiAdjust = { "action": "Adjust with AI", "title": "Adjust image with AI", "subtitle": "Describe the adjustment. The AI changes only what you ask and preserves the rest of the image.", "original": "Original", "result": "Result", "resultHint": "Describe the adjustment and click Generate to see the result here.", "promptLabel": "Desired adjustment", "promptPlaceholder": "E.g. remove the background and make it white, smooth the sleeve creases, brighten the lighting", "generate": "Generate", "generating": "Generating the image...", "tryAgain": "Generate another", "useResult": "Use this image", "errorGenerate": "Could not adjust the image with AI", "errorSave": "Could not save the adjusted image", "errorRefused": "The AI could not make this adjustment: {{reason}}", "mask": { "start": "Mark the area", "stop": "Use the whole image", "hint": "Paint the area the AI may change. The rest of the image is kept." }, "verification": { "changedMoreThanRequested": "The AI changed more than requested. Check the areas outside the adjustment before using it.", "colorMismatch": "The garment color ({{dominant}}) differs from the requested color ({{target}})." } };
|
|
@@ -226,9 +226,9 @@ declare const _default: {
|
|
|
226
226
|
"error": "Erro"
|
|
227
227
|
},
|
|
228
228
|
"categories": {
|
|
229
|
-
"trigger": "
|
|
230
|
-
"condition": "
|
|
231
|
-
"action": "
|
|
229
|
+
"trigger": "Gatilho",
|
|
230
|
+
"condition": "Condição",
|
|
231
|
+
"action": "Ação"
|
|
232
232
|
},
|
|
233
233
|
"inspector": {
|
|
234
234
|
"title": "Configuração do bloco",
|
|
@@ -12,7 +12,7 @@ const detailsTable = { "size": "Tam", "quantity": "Qtd" };
|
|
|
12
12
|
const errorPage = { "message": "Esta tela não conseguiu carregar", "headline": "Algo inesperado aconteceu ao montar a página. Atualizar costuma resolver. Se acontecer de novo, fale com o suporte contando o que você estava fazendo.", "backToStart": "Voltar para o início" };
|
|
13
13
|
const filters = { "activeFilters": "Filtros aplicados", "appliedCount_one": "{{count}} filtro aplicado", "appliedCount_other": "{{count}} filtros aplicados", "clearAll": "Limpar tudo", "noFields": "Nenhum filtro disponível para esta listagem.", "noOptions": "Nenhuma opção encontrada.", "optionsError": "Não foi possível carregar as opções de {{field}}.", "rangeMax": "Máximo", "rangeMin": "Mínimo", "rangeSeparator": "até", "searchOptions": "Buscar opções" };
|
|
14
14
|
const filtersDrawer = { "selectedFilters": "Filtros Selecionados:", "pendingChanges": "Alterações pendentes", "discardChangesTitle": "Descartar alterações?", "discardChangesMessage": "Você tem alterações de filtro não aplicadas. Deseja descartá-las?", "keepEditing": "Continuar editando", "discard": "Descartar" };
|
|
15
|
-
const flow = { "canvas": "Editor de fluxo", "resizePanel": "Arraste para ajustar a largura. Clique duplo restaura", "ports": { "next": "Próximo", "true": "Sim", "false": "Não", "error": "Erro" }, "categories": { "trigger": "
|
|
15
|
+
const flow = { "canvas": "Editor de fluxo", "resizePanel": "Arraste para ajustar a largura. Clique duplo restaura", "ports": { "next": "Próximo", "true": "Sim", "false": "Não", "error": "Erro" }, "categories": { "trigger": "Gatilho", "condition": "Condição", "action": "Ação" }, "inspector": { "title": "Configuração do bloco", "empty": "Selecione um bloco para configurar", "label": "Nome do bloco", "delete": "Excluir bloco", "duplicate": "Duplicar bloco", "unknownBlock": "Este tipo de bloco não existe mais", "close": "Fechar" }, "issues": { "title": "Pendências", "none": "Nenhuma pendência", "missingTrigger": "O fluxo precisa de um gatilho", "multipleTriggers": "O fluxo só pode ter um gatilho", "unknownBlock": "Bloco de um tipo que não existe mais", "unreachableNode": "Bloco solto: nenhum caminho do gatilho chega até ele", "multipleInputs": "Bloco com mais de uma entrada", "portFanOut": "Uma saída só pode ligar a um bloco", "cycle": "Os blocos formam um ciclo", "missingTrueBranch": "A condição não tem o caminho do sim" }, "run": { "passed": "Passou", "failed": "Não passou", "skipped": "Pulado", "wouldRun": "Executaria", "error": "Erro", "blocked": "Bloqueado" }, "controls": { "zoomIn": "Aproximar", "zoomOut": "Afastar", "fitView": "Enquadrar", "undo": "Desfazer", "redo": "Refazer", "layout": "Organizar blocos" }, "edge": { "insertBlock": "Inserir um bloco aqui" }, "node": { "disabled": "Desativado", "addBlock": "Adicionar o próximo bloco", "issueCount_one": "{{count}} pendência", "issueCount_other": "{{count}} pendências" }, "laneMap": { "label": "Mapa do fluxo" } };
|
|
16
16
|
const followUp = { "messages": "Mensagens", "chat": { "loadingMessages": "Carregando mensagens", "placeholder": "Escrever mensagem...", "invalidMessage": "Por favor, insira uma mensagem válida", "noMessagesYet": "Nenhuma mensagem por enquanto", "startConversation": "Inicie uma conversa adicionando a primeira mensagem", "loadError": "Não foi possível carregar as mensagens", "sendFailed": "Não foi possível enviar a mensagem. Tente novamente.", "sendFailedInline": "Falha no envio", "resend": "Reenviar", "discard": "Descartar", "today": "Hoje", "yesterday": "Ontem", "typingOne": "{{name}} está digitando...", "typingMany": "{{count}} pessoas estão digitando...", "someone": "Alguém", "you": "Você", "newMessages": "Novas mensagens", "backToLatest": "Voltar para as recentes", "deletedMessage": "Mensagem apagada", "edited": "editada", "readBy": "Lida por {{names}}", "copied": "Mensagem copiada", "replyingTo": "Respondendo a {{name}}", "attachmentFallback": "Anexo", "editTitle": "Editar mensagem", "deleteTitle": "Apagar mensagem?", "deleteSubtitle": "A mensagem será marcada como apagada para todos os participantes", "sendHint": "Enter envia, Shift+Enter quebra a linha", "formatting": "Formatação" }, "actions": { "reply": "Responder", "copy": "Copiar texto", "edit": "Editar", "delete": "Apagar" }, "attachments": { "attach": "Anexar", "pickFile": "Arquivo", "pickImage": "Imagem", "takePhoto": "Tirar foto", "dropHere": "Solte os arquivos para anexar", "tooMany": "Limite de {{max}} anexos por mensagem", "tooLarge": "O arquivo {{name}} passa do limite de 50 MB", "uploadFailed": "Falha no upload", "uploading": "Enviando anexos...", "imagePreview": "Visualizar imagem", "showTranscript": "Ver transcrição", "hideTranscript": "Ocultar transcrição" }, "voice": { "record": "Gravar áudio", "recording": "Gravando", "stop": "Concluir gravação", "cancel": "Cancelar gravação", "rerecord": "Regravar", "attach": "Adicionar ao envio", "discard": "Descartar", "transcriptLabel": "Transcrição", "transcriptPlaceholder": "A transcrição da fala aparece aqui e pode ser editada" }, "camera": { "title": "Tirar foto", "unavailable": "Câmera indisponível", "unavailableHint": "Verifique a permissão de câmera do navegador", "switch": "Alternar câmera", "capture": "Capturar foto" }, "search": { "open": "Buscar na conversa", "placeholder": "Buscar mensagens", "noResults": "Nenhuma mensagem encontrada", "error": "Não foi possível buscar as mensagens" } };
|
|
17
17
|
const generatedByAI = { "tooltip": "Gerado por IA" };
|
|
18
18
|
const imageAiAdjust = { "action": "Ajustar com IA", "title": "Ajustar imagem com IA", "subtitle": "Descreva o ajuste. A IA altera só o que você pedir e preserva o resto da imagem.", "original": "Original", "result": "Resultado", "resultHint": "Descreva o ajuste e clique em Gerar para ver o resultado aqui.", "promptLabel": "Ajuste desejado", "promptPlaceholder": "Ex.: remover o fundo e deixar branco, tirar os vincos da manga, clarear a iluminação", "generate": "Gerar", "generating": "Gerando a imagem...", "tryAgain": "Gerar outra", "useResult": "Usar esta imagem", "errorGenerate": "Não foi possível ajustar a imagem com IA", "errorSave": "Não foi possível salvar a imagem ajustada", "errorRefused": "A IA não conseguiu fazer este ajuste: {{reason}}", "mask": { "start": "Marcar a área", "stop": "Usar a imagem toda", "hint": "Pinte a área que a IA pode alterar. O resto da imagem é mantido." }, "verification": { "changedMoreThanRequested": "A IA mudou mais do que o pedido. Confira as áreas fora do ajuste antes de usar.", "colorMismatch": "A cor da peça ({{dominant}}) ficou diferente da cor pedida ({{target}})." } };
|