Skip to content

Commit 4427fec

Browse files
authored
fix: be able to close building blocks sidebar when rest of it is disabled (#4004)
Closes: #3963 ## Summary - Fix building blocks sidebar close button being unclickable when the sidebar is in disabled/read-only state (e.g. viewing a live canvas with versioning enabled but not in edit mode) - The disabled overlay (z-30) covered the entire sidebar including the close button — fixed by giving both close buttons z-40 so they remain clickable above the overlay - Added data-testid="close-sidebar-button" to the close buttons for testability ## Root Cause When versioning is enabled and the user is viewing the live canvas (not in edit mode), isReadOnly becomes true, which passes disabled={true} to the BuildingBlocksSidebar. A full-coverage overlay with absolute inset-0 z-30 renders to block component interactions, but it also blocks the close button since the sidebar container is z-21 and the close buttons had no explicit z-index. Signed-off-by: Pedro F. Leao <pedroforestileao@gmail.com>
1 parent ce4d600 commit 4427fec

File tree

3 files changed

+164
-2
lines changed

3 files changed

+164
-2
lines changed
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package e2e
2+
3+
import (
4+
"testing"
5+
"time"
6+
7+
pw "github.com/playwright-community/playwright-go"
8+
"github.com/stretchr/testify/require"
9+
10+
"github.com/superplanehq/superplane/pkg/database"
11+
"github.com/superplanehq/superplane/pkg/models"
12+
q "github.com/superplanehq/superplane/test/e2e/queries"
13+
"github.com/superplanehq/superplane/test/e2e/session"
14+
"github.com/superplanehq/superplane/test/e2e/shared"
15+
)
16+
17+
func TestCanvasSidebarClose(t *testing.T) {
18+
t.Run("sidebar close button works after exiting edit mode on versioned canvas", func(t *testing.T) {
19+
steps := &sidebarCloseSteps{t: t}
20+
steps.start()
21+
steps.givenCanvasWithVersioningEnabled("E2E Sidebar Close")
22+
steps.enterEditMode()
23+
steps.openBuildingBlocksSidebar()
24+
steps.assertSidebarVisible()
25+
steps.exitEditMode()
26+
steps.assertSidebarVisible()
27+
steps.closeSidebarViaButton()
28+
steps.assertSidebarHidden()
29+
})
30+
}
31+
32+
type sidebarCloseSteps struct {
33+
t *testing.T
34+
session *session.TestSession
35+
canvas *shared.CanvasSteps
36+
}
37+
38+
func (s *sidebarCloseSteps) start() {
39+
s.session = ctx.NewSession(s.t)
40+
s.session.Start()
41+
s.session.Login()
42+
}
43+
44+
func (s *sidebarCloseSteps) givenCanvasWithVersioningEnabled(name string) {
45+
err := database.Conn().
46+
Model(&models.Organization{}).
47+
Where("id = ?", s.session.OrgID).
48+
Update("versioning_enabled", true).
49+
Error
50+
require.NoError(s.t, err)
51+
52+
s.canvas = shared.NewCanvasSteps(name, s.t, s.session)
53+
s.canvas.Create()
54+
s.canvas.Visit()
55+
56+
s.session.AssertVisible(q.Locator(`header button:has-text("Edit")`))
57+
}
58+
59+
func (s *sidebarCloseSteps) enterEditMode() {
60+
editButton := q.Locator(`header button:has-text("Edit")`).Run(s.session)
61+
deadline := time.Now().Add(15 * time.Second)
62+
63+
for {
64+
disabled, err := editButton.IsDisabled()
65+
require.NoError(s.t, err)
66+
if !disabled {
67+
break
68+
}
69+
70+
if time.Now().After(deadline) {
71+
s.t.Fatalf("edit button did not become enabled")
72+
}
73+
74+
time.Sleep(200 * time.Millisecond)
75+
}
76+
77+
require.NoError(s.t, editButton.Click(pw.LocatorClickOptions{Timeout: pw.Float(15000)}))
78+
s.session.AssertVisible(q.Locator(`header button:has-text("Propose Change")`))
79+
}
80+
81+
func (s *sidebarCloseSteps) openBuildingBlocksSidebar() {
82+
s.canvas.OpenBuildingBlocksSidebar()
83+
}
84+
85+
func (s *sidebarCloseSteps) assertSidebarVisible() {
86+
s.session.AssertVisible(q.TestID("building-blocks-sidebar"))
87+
}
88+
89+
func (s *sidebarCloseSteps) assertSidebarHidden() {
90+
s.session.AssertHidden(q.TestID("building-blocks-sidebar"))
91+
}
92+
93+
func (s *sidebarCloseSteps) exitEditMode() {
94+
exitButton := q.Locator(`button[aria-label="Exit edit mode"]`).Run(s.session)
95+
require.NoError(s.t, exitButton.Click(pw.LocatorClickOptions{Timeout: pw.Float(15000)}))
96+
s.session.AssertVisible(q.Locator(`header button:has-text("Edit")`))
97+
s.session.Sleep(500)
98+
}
99+
100+
func (s *sidebarCloseSteps) closeSidebarViaButton() {
101+
closeButton := q.TestID("close-sidebar-button").Run(s.session)
102+
require.NoError(s.t, closeButton.Click(pw.LocatorClickOptions{Timeout: pw.Float(15000)}))
103+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { fireEvent, render, screen } from "@testing-library/react";
2+
import { describe, expect, it, vi } from "vitest";
3+
import { BuildingBlocksSidebar } from "./index";
4+
5+
const defaultProps = {
6+
isOpen: true,
7+
onToggle: vi.fn(),
8+
blocks: [],
9+
canvasZoom: 1,
10+
};
11+
12+
describe("BuildingBlocksSidebar", () => {
13+
it("calls onToggle(false) when close button is clicked while disabled", () => {
14+
const onToggle = vi.fn();
15+
render(
16+
<BuildingBlocksSidebar
17+
{...defaultProps}
18+
onToggle={onToggle}
19+
disabled={true}
20+
disabledMessage="You don't have permission to edit this canvas."
21+
/>,
22+
);
23+
24+
const closeButton = screen.getByTestId("close-sidebar-button");
25+
fireEvent.click(closeButton);
26+
27+
expect(onToggle).toHaveBeenCalledWith(false);
28+
});
29+
30+
it("calls onToggle(false) when close button is clicked while not disabled", () => {
31+
const onToggle = vi.fn();
32+
render(<BuildingBlocksSidebar {...defaultProps} onToggle={onToggle} disabled={false} />);
33+
34+
const closeButton = screen.getByTestId("close-sidebar-button");
35+
fireEvent.click(closeButton);
36+
37+
expect(onToggle).toHaveBeenCalledWith(false);
38+
});
39+
40+
it("renders the disabled overlay when disabled", () => {
41+
const { container } = render(
42+
<BuildingBlocksSidebar
43+
{...defaultProps}
44+
disabled={true}
45+
disabledMessage="You don't have permission to edit this canvas."
46+
/>,
47+
);
48+
49+
expect(container.querySelector(".cursor-not-allowed")).toBeInTheDocument();
50+
});
51+
52+
it("does not render when isOpen is false", () => {
53+
const { container } = render(<BuildingBlocksSidebar {...defaultProps} isOpen={false} />);
54+
55+
expect(container.querySelector('[data-testid="building-blocks-sidebar"]')).not.toBeInTheDocument();
56+
});
57+
});

web_src/src/ui/BuildingBlocksSidebar/index.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -760,7 +760,8 @@ function OpenBuildingBlocksSidebar({
760760
</div>
761761
<div
762762
onClick={() => onToggle(false)}
763-
className="absolute top-4 right-4 w-6 h-6 hover:bg-slate-950/5 rounded flex items-center justify-center cursor-pointer leading-none"
763+
data-testid="close-sidebar-button"
764+
className="absolute top-4 right-4 z-40 w-6 h-6 hover:bg-slate-950/5 rounded flex items-center justify-center cursor-pointer leading-none"
764765
>
765766
<X size={16} />
766767
</div>
@@ -792,7 +793,8 @@ function OpenBuildingBlocksSidebar({
792793
</TabsList>
793794
<div
794795
onClick={() => onToggle(false)}
795-
className="absolute top-4 right-4 w-6 h-6 hover:bg-slate-950/5 rounded flex items-center justify-center cursor-pointer leading-none"
796+
data-testid="close-sidebar-button"
797+
className="absolute top-4 right-4 z-40 w-6 h-6 hover:bg-slate-950/5 rounded flex items-center justify-center cursor-pointer leading-none"
796798
>
797799
<X size={16} />
798800
</div>

0 commit comments

Comments
 (0)