feat: add minimize and expand functionality for UI nodes (#4388)
* ✨ (frontend): improve UI by dynamically showing/hiding elements based on showNode state 🔧 (frontend): update node internals when toggling showNode state 📝 (frontend): add test for minimizing and expanding components * 🔧 (handleRenderComponent/index.tsx): refactor getHandleClasses function to improve code readability and maintainability 🔧 (handleRenderComponent/index.tsx): refactor handleClick function to improve code readability and maintainability --------- Co-authored-by: Gabriel Luiz Freitas Almeida <gabriel@langflow.org>
This commit is contained in:
parent
7cc497aa52
commit
84d07d9ec3
10 changed files with 193 additions and 62 deletions
File diff suppressed because one or more lines are too long
|
|
@ -68,13 +68,20 @@ export default function NodeName({
|
|||
<div className="group flex w-full items-center gap-1">
|
||||
<div
|
||||
onDoubleClick={(event) => {
|
||||
if (!showNode) {
|
||||
return;
|
||||
}
|
||||
setInputName(true);
|
||||
takeSnapshot();
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}}
|
||||
data-testid={"title-" + display_name}
|
||||
className="nodoubleclick w-full cursor-text truncate font-medium text-primary"
|
||||
className={
|
||||
showNode
|
||||
? "nodoubleclick w-full cursor-text truncate font-medium text-primary"
|
||||
: "cursor-default"
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
|
|
@ -82,6 +89,7 @@ export default function NodeName({
|
|||
"max-w-44 truncate text-[14px]",
|
||||
validationStatus?.data?.duration && "max-w-36",
|
||||
isOutdated && "max-w-40",
|
||||
!showNode && "max-w-28",
|
||||
)}
|
||||
>
|
||||
{display_name}
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ export default function NodeOutputField({
|
|||
);
|
||||
|
||||
return !showNode ? (
|
||||
<></>
|
||||
<>{Handle}</>
|
||||
) : (
|
||||
<div
|
||||
ref={ref}
|
||||
|
|
|
|||
|
|
@ -265,9 +265,34 @@ export default function HandleRenderComponent({
|
|||
const handleRef = useRef<HTMLDivElement>(null);
|
||||
const invisibleDivRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const getHandleClasses = ({
|
||||
left,
|
||||
showNode,
|
||||
}: {
|
||||
left: boolean;
|
||||
showNode: boolean;
|
||||
}) => {
|
||||
return cn(
|
||||
"noflow nowheel nopan noselect absolute left-3.5 -translate-y-1/2 translate-x-1/3 cursor-crosshair rounded-full",
|
||||
left && "-left-5 -translate-x-1/2",
|
||||
left && !showNode && "-translate-y-5 translate-x-4",
|
||||
!left && !showNode && "-translate-y-5 translate-x-[10.8rem]",
|
||||
);
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
setFilterEdge(groupByFamily(myData, tooltipTitle!, left, nodes!));
|
||||
setFilterType(currentFilter);
|
||||
if (filterOpenHandle && filterType) {
|
||||
onConnect(getConnection(filterType));
|
||||
setFilterType(undefined);
|
||||
setFilterEdge([]);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="relative">
|
||||
<div className={`${!showNode ? "" : "relative"}`}>
|
||||
<ShadTooltip
|
||||
open={openTooltip}
|
||||
setOpen={setOpenTooltip}
|
||||
|
|
@ -299,18 +324,11 @@ export default function HandleRenderComponent({
|
|||
isValidConnection={(connection) =>
|
||||
isValidConnection(connection, nodes, edges)
|
||||
}
|
||||
className={classNames(
|
||||
`group/handle z-50 h-12 w-12 border-none bg-transparent transition-all`,
|
||||
className={cn(
|
||||
`group/handle z-50 transition-all`,
|
||||
!showNode && "no-show",
|
||||
)}
|
||||
onClick={() => {
|
||||
setFilterEdge(groupByFamily(myData, tooltipTitle!, left, nodes!));
|
||||
setFilterType(currentFilter);
|
||||
if (filterOpenHandle && filterType) {
|
||||
onConnect(getConnection(filterType));
|
||||
setFilterType(undefined);
|
||||
setFilterEdge([]);
|
||||
}
|
||||
}}
|
||||
onClick={handleClick}
|
||||
onMouseUp={() => {
|
||||
setOpenTooltip(false);
|
||||
}}
|
||||
|
|
@ -342,10 +360,7 @@ export default function HandleRenderComponent({
|
|||
!showNode ? (left ? "target" : "source") : left ? "left" : "right"
|
||||
}`}
|
||||
ref={invisibleDivRef}
|
||||
className={cn(
|
||||
"noflow nowheel nopan noselect absolute left-3.5 -translate-y-1/2 translate-x-1/3 cursor-crosshair rounded-full",
|
||||
left && "-left-5 -translate-x-1/2",
|
||||
)}
|
||||
className={getHandleClasses({ left, showNode })}
|
||||
style={{
|
||||
background: isNullHandle ? "hsl(var(--border))" : handleColor,
|
||||
width: "10px",
|
||||
|
|
|
|||
|
|
@ -39,11 +39,19 @@ export function NodeIcon({
|
|||
|
||||
if (isLucideIcon) {
|
||||
return (
|
||||
<div className="bg-lucide-icon text-foreground">
|
||||
<div
|
||||
className={cn(
|
||||
"bg-lucide-icon text-foreground",
|
||||
!showNode && "min-h-8 min-w-8",
|
||||
)}
|
||||
>
|
||||
<IconComponent
|
||||
strokeWidth={ICON_STROKE_WIDTH}
|
||||
name={iconName}
|
||||
className={iconClassName}
|
||||
className={cn(
|
||||
iconClassName,
|
||||
!showNode && "absolute -translate-x-0.5",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -286,8 +286,10 @@ export default function GenericNode({
|
|||
<div
|
||||
className={cn(
|
||||
borderColor,
|
||||
showNode ? "w-80 rounded-xl" : "w-26 h-26 rounded-full",
|
||||
"generic-node-div group/node",
|
||||
showNode
|
||||
? "w-80 rounded-xl"
|
||||
: `h-[4.065rem] w-48 rounded-[0.75rem] ${!selected ? "border-[1px] border-border ring-[0.5px] ring-border" : ""}`,
|
||||
"generic-node-div group/node relative",
|
||||
!hasOutputs && "pb-4",
|
||||
)}
|
||||
>
|
||||
|
|
@ -308,21 +310,23 @@ export default function GenericNode({
|
|||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 truncate text-wrap border-b p-4 leading-5">
|
||||
<div
|
||||
data-testid={`${data.id}-main-node`}
|
||||
className={cn(
|
||||
"grid gap-3 truncate text-wrap p-4 leading-5",
|
||||
showNode && "border-b",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
data-testid={"div-generic-node"}
|
||||
className={
|
||||
"generic-node-div-title justify-between" +
|
||||
(!showNode
|
||||
? " relative h-24 w-24 rounded-full"
|
||||
: " justify-between rounded-t-lg")
|
||||
!showNode
|
||||
? ""
|
||||
: "generic-node-div-title justify-between rounded-t-lg"
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
"generic-node-title-arrangement " +
|
||||
(!showNode ? " justify-center" : "")
|
||||
}
|
||||
className={"generic-node-title-arrangement"}
|
||||
data-testid="generic-node-title-arrangement"
|
||||
>
|
||||
<NodeIcon
|
||||
|
|
@ -331,18 +335,16 @@ export default function GenericNode({
|
|||
icon={data.node?.icon}
|
||||
isGroup={!!data.node?.flow}
|
||||
/>
|
||||
{showNode && (
|
||||
<div className="generic-node-tooltip-div">
|
||||
<NodeName
|
||||
display_name={data.node?.display_name}
|
||||
nodeId={data.id}
|
||||
selected={selected}
|
||||
showNode={showNode}
|
||||
validationStatus={validationStatus}
|
||||
isOutdated={isOutdated}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="generic-node-tooltip-div">
|
||||
<NodeName
|
||||
display_name={data.node?.display_name}
|
||||
nodeId={data.id}
|
||||
selected={selected}
|
||||
showNode={showNode}
|
||||
validationStatus={validationStatus}
|
||||
isOutdated={isOutdated}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{!showNode && (
|
||||
|
|
@ -378,13 +380,15 @@ export default function GenericNode({
|
|||
/>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<NodeDescription
|
||||
description={data.node?.description}
|
||||
nodeId={data.id}
|
||||
selected={selected}
|
||||
/>
|
||||
</div>
|
||||
{showNode && (
|
||||
<div>
|
||||
<NodeDescription
|
||||
description={data.node?.description}
|
||||
nodeId={data.id}
|
||||
selected={selected}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showNode && (
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ export default function NodeToolbarComponent({
|
|||
function minimize() {
|
||||
if (isMinimal) {
|
||||
setShowNode((data.showNode ?? true) ? false : true);
|
||||
updateNodeInternals(data.id);
|
||||
return;
|
||||
}
|
||||
setNoticeData({
|
||||
|
|
@ -549,7 +550,10 @@ export default function NodeToolbarComponent({
|
|||
/>
|
||||
</SelectItem>
|
||||
{isMinimal && (
|
||||
<SelectItem value={"show"}>
|
||||
<SelectItem
|
||||
value={"show"}
|
||||
data-testid={`${showNode ? "minimize" : "expand"}-button-modal`}
|
||||
>
|
||||
<ToolbarSelectItem
|
||||
shortcut={
|
||||
shortcuts.find((obj) => obj.name === "Minimize")?.shortcut!
|
||||
|
|
|
|||
|
|
@ -191,7 +191,7 @@
|
|||
|
||||
/* The same as primary-input but no-truncate */
|
||||
.textarea-primary {
|
||||
@apply placeholder:text-placeholder-foreground form-input block w-full rounded-md border-[1px] border-border bg-background px-3 text-left shadow-sm hover:border-muted focus:border-muted focus:placeholder-transparent focus:ring-[0.75px] focus:ring-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-secondary disabled:text-muted disabled:opacity-100 placeholder:disabled:text-muted-foreground sm:text-sm;
|
||||
@apply form-input block w-full rounded-md border-[1px] border-border bg-background px-3 text-left shadow-sm placeholder:text-placeholder-foreground hover:border-muted focus:border-muted focus:placeholder-transparent focus:ring-[0.75px] focus:ring-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-secondary disabled:text-muted disabled:opacity-100 placeholder:disabled:text-muted-foreground sm:text-sm;
|
||||
}
|
||||
|
||||
.input-edit-node {
|
||||
|
|
@ -319,7 +319,7 @@
|
|||
@apply grid w-full gap-4 p-4 md:grid-cols-2 lg:grid-cols-4;
|
||||
}
|
||||
.generic-node-div {
|
||||
@apply relative flex flex-col justify-center bg-background transition-all;
|
||||
@apply flex flex-col justify-center bg-background transition-all;
|
||||
}
|
||||
.generic-node-div-title {
|
||||
@apply flex w-full items-center gap-2;
|
||||
|
|
@ -1219,7 +1219,7 @@
|
|||
}
|
||||
|
||||
.disabled-state {
|
||||
@apply text-hard-zinc pointer-events-none bg-secondary;
|
||||
@apply pointer-events-none bg-secondary text-hard-zinc;
|
||||
}
|
||||
|
||||
.background-fade-input {
|
||||
|
|
|
|||
|
|
@ -209,6 +209,12 @@ textarea[class^="ag-"]:focus {
|
|||
transform: translate(-50%, -50%) !important;
|
||||
}
|
||||
|
||||
/* Modified transform when showNode is false */
|
||||
.react-flow__handle-right.no-show,
|
||||
.react-flow__handle-left.no-show {
|
||||
transform: translate(0%, -50%) !important;
|
||||
}
|
||||
|
||||
.react-flow__node-noteNode:not(.selected) {
|
||||
z-index: -1 !important;
|
||||
}
|
||||
|
|
|
|||
82
src/frontend/tests/extended/features/minimize.spec.ts
Normal file
82
src/frontend/tests/extended/features/minimize.spec.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test("user must be able to minimize and expand a component", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
await page.waitForSelector('[data-testid="mainpage_title"]', {
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
await page.waitForSelector('[id="new-project-btn"]', {
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
let modalCount = 0;
|
||||
try {
|
||||
const modalTitleElement = await page?.getByTestId("modal-title");
|
||||
if (modalTitleElement) {
|
||||
modalCount = await modalTitleElement.count();
|
||||
}
|
||||
} catch (error) {
|
||||
modalCount = 0;
|
||||
}
|
||||
|
||||
while (modalCount === 0) {
|
||||
await page.getByText("New Flow", { exact: true }).click();
|
||||
await page.waitForTimeout(3000);
|
||||
modalCount = await page.getByTestId("modal-title")?.count();
|
||||
}
|
||||
|
||||
await page.getByTestId("blank-flow").click();
|
||||
|
||||
await page.getByTestId("sidebar-search-input").click();
|
||||
await page.getByTestId("sidebar-search-input").fill("text input");
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page
|
||||
.getByTestId("inputsText Input")
|
||||
.dragTo(page.locator('//*[@id="react-flow-id"]'));
|
||||
|
||||
await page.getByTestId("zoom_out").click();
|
||||
await page
|
||||
.locator('//*[@id="react-flow-id"]')
|
||||
.hover()
|
||||
.then(async () => {
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(-800, 300);
|
||||
});
|
||||
|
||||
await page.mouse.up();
|
||||
|
||||
await page.getByTestId("fit_view").click();
|
||||
await page.getByTestId("zoom_out").click();
|
||||
await page.getByTestId("zoom_out").click();
|
||||
await page.getByTestId("zoom_out").click();
|
||||
|
||||
await page.getByTestId("more-options-modal").click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.getByTestId("minimize-button-modal").first().click();
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await expect(
|
||||
page.locator(".react-flow__handle-left.no-show").first(),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
page.locator(".react-flow__handle-right.no-show").first(),
|
||||
).toBeVisible();
|
||||
|
||||
await page.getByTestId("more-options-modal").click();
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
await page.getByTestId("expand-button-modal").first().click();
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await expect(page.locator(".react-flow__handle-left").first()).toBeVisible();
|
||||
|
||||
await expect(page.locator(".react-flow__handle-right").first()).toBeVisible();
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue