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 className="group flex w-full items-center gap-1">
|
||||||
<div
|
<div
|
||||||
onDoubleClick={(event) => {
|
onDoubleClick={(event) => {
|
||||||
|
if (!showNode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
setInputName(true);
|
setInputName(true);
|
||||||
takeSnapshot();
|
takeSnapshot();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}}
|
}}
|
||||||
data-testid={"title-" + display_name}
|
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">
|
<div className="flex items-center gap-2">
|
||||||
<span
|
<span
|
||||||
|
|
@ -82,6 +89,7 @@ export default function NodeName({
|
||||||
"max-w-44 truncate text-[14px]",
|
"max-w-44 truncate text-[14px]",
|
||||||
validationStatus?.data?.duration && "max-w-36",
|
validationStatus?.data?.duration && "max-w-36",
|
||||||
isOutdated && "max-w-40",
|
isOutdated && "max-w-40",
|
||||||
|
!showNode && "max-w-28",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{display_name}
|
{display_name}
|
||||||
|
|
|
||||||
|
|
@ -124,7 +124,7 @@ export default function NodeOutputField({
|
||||||
);
|
);
|
||||||
|
|
||||||
return !showNode ? (
|
return !showNode ? (
|
||||||
<></>
|
<>{Handle}</>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
|
|
||||||
|
|
@ -265,9 +265,34 @@ export default function HandleRenderComponent({
|
||||||
const handleRef = useRef<HTMLDivElement>(null);
|
const handleRef = useRef<HTMLDivElement>(null);
|
||||||
const invisibleDivRef = 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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="relative">
|
<div className={`${!showNode ? "" : "relative"}`}>
|
||||||
<ShadTooltip
|
<ShadTooltip
|
||||||
open={openTooltip}
|
open={openTooltip}
|
||||||
setOpen={setOpenTooltip}
|
setOpen={setOpenTooltip}
|
||||||
|
|
@ -299,18 +324,11 @@ export default function HandleRenderComponent({
|
||||||
isValidConnection={(connection) =>
|
isValidConnection={(connection) =>
|
||||||
isValidConnection(connection, nodes, edges)
|
isValidConnection(connection, nodes, edges)
|
||||||
}
|
}
|
||||||
className={classNames(
|
className={cn(
|
||||||
`group/handle z-50 h-12 w-12 border-none bg-transparent transition-all`,
|
`group/handle z-50 transition-all`,
|
||||||
|
!showNode && "no-show",
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={handleClick}
|
||||||
setFilterEdge(groupByFamily(myData, tooltipTitle!, left, nodes!));
|
|
||||||
setFilterType(currentFilter);
|
|
||||||
if (filterOpenHandle && filterType) {
|
|
||||||
onConnect(getConnection(filterType));
|
|
||||||
setFilterType(undefined);
|
|
||||||
setFilterEdge([]);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onMouseUp={() => {
|
onMouseUp={() => {
|
||||||
setOpenTooltip(false);
|
setOpenTooltip(false);
|
||||||
}}
|
}}
|
||||||
|
|
@ -342,10 +360,7 @@ export default function HandleRenderComponent({
|
||||||
!showNode ? (left ? "target" : "source") : left ? "left" : "right"
|
!showNode ? (left ? "target" : "source") : left ? "left" : "right"
|
||||||
}`}
|
}`}
|
||||||
ref={invisibleDivRef}
|
ref={invisibleDivRef}
|
||||||
className={cn(
|
className={getHandleClasses({ left, showNode })}
|
||||||
"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",
|
|
||||||
)}
|
|
||||||
style={{
|
style={{
|
||||||
background: isNullHandle ? "hsl(var(--border))" : handleColor,
|
background: isNullHandle ? "hsl(var(--border))" : handleColor,
|
||||||
width: "10px",
|
width: "10px",
|
||||||
|
|
|
||||||
|
|
@ -39,11 +39,19 @@ export function NodeIcon({
|
||||||
|
|
||||||
if (isLucideIcon) {
|
if (isLucideIcon) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-lucide-icon text-foreground">
|
<div
|
||||||
|
className={cn(
|
||||||
|
"bg-lucide-icon text-foreground",
|
||||||
|
!showNode && "min-h-8 min-w-8",
|
||||||
|
)}
|
||||||
|
>
|
||||||
<IconComponent
|
<IconComponent
|
||||||
strokeWidth={ICON_STROKE_WIDTH}
|
strokeWidth={ICON_STROKE_WIDTH}
|
||||||
name={iconName}
|
name={iconName}
|
||||||
className={iconClassName}
|
className={cn(
|
||||||
|
iconClassName,
|
||||||
|
!showNode && "absolute -translate-x-0.5",
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -286,8 +286,10 @@ export default function GenericNode({
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
borderColor,
|
borderColor,
|
||||||
showNode ? "w-80 rounded-xl" : "w-26 h-26 rounded-full",
|
showNode
|
||||||
"generic-node-div group/node",
|
? "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",
|
!hasOutputs && "pb-4",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
@ -308,21 +310,23 @@ export default function GenericNode({
|
||||||
)}
|
)}
|
||||||
</div>
|
</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
|
<div
|
||||||
data-testid={"div-generic-node"}
|
data-testid={"div-generic-node"}
|
||||||
className={
|
className={
|
||||||
"generic-node-div-title justify-between" +
|
!showNode
|
||||||
(!showNode
|
? ""
|
||||||
? " relative h-24 w-24 rounded-full"
|
: "generic-node-div-title justify-between rounded-t-lg"
|
||||||
: " justify-between rounded-t-lg")
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={
|
className={"generic-node-title-arrangement"}
|
||||||
"generic-node-title-arrangement " +
|
|
||||||
(!showNode ? " justify-center" : "")
|
|
||||||
}
|
|
||||||
data-testid="generic-node-title-arrangement"
|
data-testid="generic-node-title-arrangement"
|
||||||
>
|
>
|
||||||
<NodeIcon
|
<NodeIcon
|
||||||
|
|
@ -331,18 +335,16 @@ export default function GenericNode({
|
||||||
icon={data.node?.icon}
|
icon={data.node?.icon}
|
||||||
isGroup={!!data.node?.flow}
|
isGroup={!!data.node?.flow}
|
||||||
/>
|
/>
|
||||||
{showNode && (
|
<div className="generic-node-tooltip-div">
|
||||||
<div className="generic-node-tooltip-div">
|
<NodeName
|
||||||
<NodeName
|
display_name={data.node?.display_name}
|
||||||
display_name={data.node?.display_name}
|
nodeId={data.id}
|
||||||
nodeId={data.id}
|
selected={selected}
|
||||||
selected={selected}
|
showNode={showNode}
|
||||||
showNode={showNode}
|
validationStatus={validationStatus}
|
||||||
validationStatus={validationStatus}
|
isOutdated={isOutdated}
|
||||||
isOutdated={isOutdated}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{!showNode && (
|
{!showNode && (
|
||||||
|
|
@ -378,13 +380,15 @@ export default function GenericNode({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
{showNode && (
|
||||||
<NodeDescription
|
<div>
|
||||||
description={data.node?.description}
|
<NodeDescription
|
||||||
nodeId={data.id}
|
description={data.node?.description}
|
||||||
selected={selected}
|
nodeId={data.id}
|
||||||
/>
|
selected={selected}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showNode && (
|
{showNode && (
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,7 @@ export default function NodeToolbarComponent({
|
||||||
function minimize() {
|
function minimize() {
|
||||||
if (isMinimal) {
|
if (isMinimal) {
|
||||||
setShowNode((data.showNode ?? true) ? false : true);
|
setShowNode((data.showNode ?? true) ? false : true);
|
||||||
|
updateNodeInternals(data.id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setNoticeData({
|
setNoticeData({
|
||||||
|
|
@ -549,7 +550,10 @@ export default function NodeToolbarComponent({
|
||||||
/>
|
/>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
{isMinimal && (
|
{isMinimal && (
|
||||||
<SelectItem value={"show"}>
|
<SelectItem
|
||||||
|
value={"show"}
|
||||||
|
data-testid={`${showNode ? "minimize" : "expand"}-button-modal`}
|
||||||
|
>
|
||||||
<ToolbarSelectItem
|
<ToolbarSelectItem
|
||||||
shortcut={
|
shortcut={
|
||||||
shortcuts.find((obj) => obj.name === "Minimize")?.shortcut!
|
shortcuts.find((obj) => obj.name === "Minimize")?.shortcut!
|
||||||
|
|
|
||||||
|
|
@ -191,7 +191,7 @@
|
||||||
|
|
||||||
/* The same as primary-input but no-truncate */
|
/* The same as primary-input but no-truncate */
|
||||||
.textarea-primary {
|
.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 {
|
.input-edit-node {
|
||||||
|
|
@ -319,7 +319,7 @@
|
||||||
@apply grid w-full gap-4 p-4 md:grid-cols-2 lg:grid-cols-4;
|
@apply grid w-full gap-4 p-4 md:grid-cols-2 lg:grid-cols-4;
|
||||||
}
|
}
|
||||||
.generic-node-div {
|
.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 {
|
.generic-node-div-title {
|
||||||
@apply flex w-full items-center gap-2;
|
@apply flex w-full items-center gap-2;
|
||||||
|
|
@ -1219,7 +1219,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.disabled-state {
|
.disabled-state {
|
||||||
@apply text-hard-zinc pointer-events-none bg-secondary;
|
@apply pointer-events-none bg-secondary text-hard-zinc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.background-fade-input {
|
.background-fade-input {
|
||||||
|
|
|
||||||
|
|
@ -209,6 +209,12 @@ textarea[class^="ag-"]:focus {
|
||||||
transform: translate(-50%, -50%) !important;
|
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) {
|
.react-flow__node-noteNode:not(.selected) {
|
||||||
z-index: -1 !important;
|
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