From c9e0d5e4ba086391013e2775dcd0e939547b9aac Mon Sep 17 00:00:00 2001 From: Lucas Oliveira <62335616+lucaseduoli@users.noreply.github.com> Date: Tue, 19 Nov 2024 18:54:49 -0300 Subject: [PATCH] fix: updated node toolbar to be more stable on zoom, fix success and error component tooltips, unselect nodes on flow opening (#4610) * Unselect nodes and edges on enter * Start with viewport fit * Fix run component tooltip to match error and success * Update scrollbar design * Added node toolbar without portal to match the width of the node * Updated color of scrollbar * Updated NoteNode to have same behavior as GenericNode * Updated logic for tests to pass * Fix fitview to only occur when nodes are present * Update z index of resize handle * Sticky notes test fix * [autofix.ci] apply automated fixes * freeze test fixed --------- Co-authored-by: Mike Fortman Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: anovazzi1 Co-authored-by: Gabriel Luiz Freitas Almeida Co-authored-by: Cristhian Zanforlin Lousa --- src/frontend/src/App.css | 6 +- .../components/NodeStatus/index.tsx | 27 +- .../src/CustomNodes/GenericNode/index.tsx | 13 +- .../NoteNode/NoteToolbarComponent/index.tsx | 16 +- .../src/CustomNodes/NoteNode/index.tsx | 22 +- .../src/components/ui/select-custom.tsx | 29 ++ .../components/PageComponent/index.tsx | 2 + .../components/nodeToolbarComponent/index.tsx | 404 ++++++++---------- src/frontend/src/stores/flowStore.ts | 3 +- src/frontend/src/style/ag-theme-shadcn.css | 2 +- src/frontend/src/style/classes.css | 3 +- .../src/types/utils/reactflowUtils.ts | 7 +- src/frontend/src/utils/reactflowUtils.ts | 10 +- src/frontend/tailwind.config.mjs | 2 +- .../tests/core/features/freeze.spec.ts | 2 +- .../extended/features/sticky-notes.spec.ts | 2 +- 16 files changed, 281 insertions(+), 269 deletions(-) diff --git a/src/frontend/src/App.css b/src/frontend/src/App.css index 96907a865..1a20e99fb 100644 --- a/src/frontend/src/App.css +++ b/src/frontend/src/App.css @@ -93,7 +93,7 @@ body { } ::-webkit-scrollbar-thumb:hover { - background-color: hsl(var(--ring)) !important; + background-color: hsl(var(--placeholder-foreground)) !important; } .jv-indent::-webkit-scrollbar-track { @@ -107,7 +107,7 @@ body { } .jv-indent::-webkit-scrollbar-thumb:hover { - background-color: hsl(var(--ring)) !important; + background-color: hsl(var(--placeholder-foreground)) !important; } .custom-hover { @@ -170,7 +170,7 @@ body { height: 0.75rem !important; background-color: white !important; border-color: var(--border) !important; - z-index: 1000 !important; + z-index: 50 !important; border-radius: 20% !important; } diff --git a/src/frontend/src/CustomNodes/GenericNode/components/NodeStatus/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/NodeStatus/index.tsx index 4f5c2676a..579233cce 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/NodeStatus/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/NodeStatus/index.tsx @@ -151,7 +151,6 @@ export default function NodeStatus({ const divRef = useRef(null); const [isHovered, setIsHovered] = useState(false); - const runClass = "justify-left flex font-normal text-muted-foreground"; const stopBuilding = useFlowStore((state) => state.stopBuilding); const handleClickRun = () => { @@ -207,6 +206,12 @@ export default function NodeStatus({
{STATUS_BUILDING} @@ -215,26 +220,26 @@ export default function NodeStatus({ ) : !validationStatus ? ( {STATUS_BUILD} ) : ( -
-
+
+
{validationString && ( -
+
{validationString}
)} {lastRunTime && ( -
+
{RUN_TIMESTAMP_PREFIX}
-
+
{lastRunTime}
)} -
-
-
Duration:
-
- {validationStatus?.data.duration} +
+
Duration:
+
+ {validationStatus?.data.duration} +
diff --git a/src/frontend/src/CustomNodes/GenericNode/index.tsx b/src/frontend/src/CustomNodes/GenericNode/index.tsx index 3e4460245..07f92d823 100644 --- a/src/frontend/src/CustomNodes/GenericNode/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/index.tsx @@ -1,7 +1,7 @@ import { usePostValidateComponentCode } from "@/controllers/API/queries/nodes/use-post-validate-component-code"; import { useEffect, useMemo, useState } from "react"; import { useHotkeys } from "react-hotkeys-hook"; -import { NodeToolbar, useUpdateNodeInternals } from "reactflow"; +import { useUpdateNodeInternals } from "reactflow"; import { ForwardedIconComponent } from "../../components/genericIconComponent"; import ShadTooltip from "../../components/shadTooltipComponent"; import { Button } from "../../components/ui/button"; @@ -214,8 +214,8 @@ export default function GenericNode({ }, [hiddenOutputs]); const memoizedNodeToolbarComponent = useMemo(() => { - return ( - + return selected ? ( +
{ @@ -236,7 +236,9 @@ export default function GenericNode({ isOutdated={isOutdated && isUserEdited} setOpenShowMoreOptions={setOpenShowMoreOptions} /> - +
+ ) : ( + <> ); }, [ data, @@ -336,7 +338,6 @@ export default function GenericNode({ return ( <> - {memoizedNodeToolbarComponent}
+ {memoizedNodeToolbarComponent}
- +
diff --git a/src/frontend/src/CustomNodes/NoteNode/index.tsx b/src/frontend/src/CustomNodes/NoteNode/index.tsx index 0fa8e27d8..d4a7ee4f7 100644 --- a/src/frontend/src/CustomNodes/NoteNode/index.tsx +++ b/src/frontend/src/CustomNodes/NoteNode/index.tsx @@ -8,7 +8,7 @@ import { import { noteDataType } from "@/types/flow"; import { cn } from "@/utils/utils"; import { useEffect, useMemo, useRef, useState } from "react"; -import { NodeResizer, NodeToolbar } from "reactflow"; +import { NodeResizer } from "reactflow"; import NodeDescription from "../GenericNode/components/NodeDescription"; import NoteToolbarComponent from "./NoteToolbarComponent"; function NoteNode({ @@ -33,17 +33,20 @@ function NoteNode({ }); } }, []); + const MemoNoteToolbarComponent = useMemo( - () => ( - - - - ), - [data, bgColor], + () => + selected ? ( +
+ +
+ ) : ( + <> + ), + [data, bgColor, selected], ); return ( <> - {MemoNoteToolbarComponent} + {MemoNoteToolbarComponent}
, + React.ComponentPropsWithoutRef +>(({ className, children, position = "popper", ...props }, ref) => ( + + + {children} + + +)); +SelectContentWithoutPortal.displayName = SelectPrimitive.Content.displayName; + const SelectLabel = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef @@ -99,6 +127,7 @@ SelectSeparator.displayName = SelectPrimitive.Separator.displayName; export { Select, SelectContent, + SelectContentWithoutPortal, SelectGroup, SelectItem, SelectLabel, diff --git a/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx index 6e9f8a9c3..3b336b35e 100644 --- a/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/PageComponent/index.tsx @@ -91,6 +91,7 @@ export default function Page({ view }: { view?: boolean }): JSX.Element { ); const nodes = useFlowStore((state) => state.nodes); const edges = useFlowStore((state) => state.edges); + const isEmptyFlow = useRef(nodes.length === 0); const onNodesChange = useFlowStore((state) => state.onNodesChange); const onEdgesChange = useFlowStore((state) => state.onEdgesChange); const setNodes = useFlowStore((state) => state.setNodes); @@ -552,6 +553,7 @@ export default function Page({ view }: { view?: boolean }): JSX.Element { onDrop={onDrop} onSelectionChange={onSelectionChange} deleteKeyCode={[]} + fitView={isEmptyFlow.current ? false : true} className="theme-attribution" minZoom={0.01} maxZoom={8} diff --git a/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx index b4f33ff8b..f751ac6cf 100644 --- a/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/nodeToolbarComponent/index.tsx @@ -11,12 +11,12 @@ import CodeAreaModal from "@/modals/codeAreaModal"; import { APIClassType } from "@/types/api"; import _, { cloneDeep } from "lodash"; import { useEffect, useRef, useState } from "react"; -import { useStore, useUpdateNodeInternals } from "reactflow"; +import { useUpdateNodeInternals } from "reactflow"; import IconComponent from "../../../../components/genericIconComponent"; import ShadTooltip from "../../../../components/shadTooltipComponent"; import { Select, - SelectContent, + SelectContentWithoutPortal, SelectItem, SelectTrigger, } from "../../../../components/ui/select-custom"; @@ -370,31 +370,9 @@ export default function NodeToolbarComponent({ parameterId: "tool_mode", }); - // Use ReactFlow's store selector to get zoom updates - const zoom = useStore((state) => state.transform[2]); - const [scale, setScale] = useState(null); - - useEffect(() => { - if (!zoom) return; - if (zoom < 0.65) { - const newScale = Math.max(zoom * 1.2, 0.4); - setScale(newScale); - } else { - setScale(1); - } - }, [zoom]); - - if (scale === null) return <>; - return ( <> -
+
{hasCode && ( } side="top" - styleClasses="relative bottom-2" > - -
- + + obj.name === "Freeze Path") + ?.shortcut! + } + value={"Freeze Path"} + icon={"FreezeAll"} + dataTestId="freeze-path-button" + style={`${frozen ? " text-ice" : ""} transition-all`} + /> + + + obj.name === "Download")?.shortcut! + } + value={"Download"} + icon={"Download"} + dataTestId="download-button-modal" + /> + + +
+ {" "} + Delete{" "} + + + +
+
+ {hasToolMode && ( + + obj.name === "Tool Mode") + ?.shortcut! + } + value={"Tool Mode"} + icon={"Hammer"} + dataTestId="tool-mode-button" + style={`${toolMode ? "text-primary" : ""} transition-all`} + /> + + )} + + +
((set, get) => ({ let newEdges = cleanEdges(nodes, edges); const { inputs, outputs } = getInputsAndOutputs(nodes); get().updateComponentsToUpdate(nodes); + unselectAllNodesEdges(nodes, edges); set({ nodes, edges: newEdges, @@ -212,7 +214,6 @@ const useFlowStore = create((set, get) => ({ }, setReactFlowInstance: (newState) => { set({ reactFlowInstance: newState }); - get().reactFlowInstance?.fitView(); }, onNodesChange: (changes: NodeChange[]) => { set({ diff --git a/src/frontend/src/style/ag-theme-shadcn.css b/src/frontend/src/style/ag-theme-shadcn.css index 4fbed5c3d..d36b9ba52 100644 --- a/src/frontend/src/style/ag-theme-shadcn.css +++ b/src/frontend/src/style/ag-theme-shadcn.css @@ -68,7 +68,7 @@ .ag-body-horizontal-scroll-viewport::-webkit-scrollbar-thumb:hover, .ag-body-vertical-scroll-viewport::-webkit-scrollbar-thumb:hover { - background-color: hsl(var(--ring)); + background-color: hsl(var(--placeholder-foreground)); } .ag-paging-page-size { diff --git a/src/frontend/src/style/classes.css b/src/frontend/src/style/classes.css index b33254e45..46f46be2e 100644 --- a/src/frontend/src/style/classes.css +++ b/src/frontend/src/style/classes.css @@ -26,6 +26,7 @@ pre { } .react-flow__pane { + pointer-events: all; cursor: default; } @@ -147,7 +148,7 @@ textarea[class^="ag-"]:focus { } .ace_scrollbar::-webkit-scrollbar-thumb:hover { - background-color: hsl(var(--ring)); + background-color: hsl(var(--placeholder-foreground)); border-radius: 999px; } diff --git a/src/frontend/src/types/utils/reactflowUtils.ts b/src/frontend/src/types/utils/reactflowUtils.ts index 1eb2430a8..bb1d4c1d1 100644 --- a/src/frontend/src/types/utils/reactflowUtils.ts +++ b/src/frontend/src/types/utils/reactflowUtils.ts @@ -1,11 +1,6 @@ -import { Edge, Node } from "reactflow"; +import { Edge } from "reactflow"; import { FlowType, NodeType } from "../flow"; -export type unselectAllNodesType = { - updateNodes: (nodes: Node[]) => void; - data: Node[]; -}; - export type addEscapedHandleIdsToEdgesType = { edges: Edge[]; }; diff --git a/src/frontend/src/utils/reactflowUtils.ts b/src/frontend/src/utils/reactflowUtils.ts index 60fb36851..41ab8cb85 100644 --- a/src/frontend/src/utils/reactflowUtils.ts +++ b/src/frontend/src/utils/reactflowUtils.ts @@ -41,7 +41,6 @@ import { addEscapedHandleIdsToEdgesType, findLastNodeType, generateFlowType, - unselectAllNodesType, updateEdgesHandleIdsType, } from "../types/utils/reactflowUtils"; import { getLayoutedNodes } from "./layoutUtils"; @@ -202,12 +201,13 @@ export function detectBrokenEdgesEdges(nodes: NodeType[], edges: Edge[]) { return BrokenEdges; } -export function unselectAllNodes({ updateNodes, data }: unselectAllNodesType) { - let newNodes = cloneDeep(data); - newNodes.forEach((node: Node) => { +export function unselectAllNodesEdges(nodes: Node[], edges: Edge[]) { + nodes.forEach((node: Node) => { node.selected = false; }); - updateNodes(newNodes!); + edges.forEach((edge: Edge) => { + edge.selected = false; + }); } export function isValidConnection( diff --git a/src/frontend/tailwind.config.mjs b/src/frontend/tailwind.config.mjs index acb0a2e62..d920e4ac0 100644 --- a/src/frontend/tailwind.config.mjs +++ b/src/frontend/tailwind.config.mjs @@ -353,7 +353,7 @@ const config = { borderRadius: "999px", }, "&::-webkit-scrollbar-thumb:hover": { - backgroundColor: "hsl(var(--ring))", + backgroundColor: "hsl(var(--placeholder-foreground))", }, cursor: "auto", }, diff --git a/src/frontend/tests/core/features/freeze.spec.ts b/src/frontend/tests/core/features/freeze.spec.ts index 5ed418803..97d787bbc 100644 --- a/src/frontend/tests/core/features/freeze.spec.ts +++ b/src/frontend/tests/core/features/freeze.spec.ts @@ -305,7 +305,7 @@ test("user must be able to freeze a component", async ({ page }) => { await page.waitForTimeout(1000); - await page.getByTestId("icon-Snowflake").last().click(); + await page.getByText("Freeze", { exact: true }).click(); await page.waitForTimeout(1000); diff --git a/src/frontend/tests/extended/features/sticky-notes.spec.ts b/src/frontend/tests/extended/features/sticky-notes.spec.ts index b0587b5c4..34ebbe8bb 100644 --- a/src/frontend/tests/extended/features/sticky-notes.spec.ts +++ b/src/frontend/tests/extended/features/sticky-notes.spec.ts @@ -162,7 +162,7 @@ The future of AI is both exciting and uncertain. As the technology continues to await page.getByTestId("note_node").nth(0).focus(); await page.getByTestId("more-options-modal").click(); - await page.getByText("Delete").last().click(); + await page.getByText("Delete").first().click(); await page.waitForTimeout(1000);