Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React from "react";
import { ListHeaderContainer } from "./styles";
import { Text } from "../../Text";
import { Flex } from "../../Flex";

interface Props {
headerText: string;
headerControls?: React.ReactNode;
maxHeight?: string;
headerClassName?: string;
children: React.ReactNode | React.ReactNode[];
}

export const ListWithHeader = (props: Props) => {
return (
<Flex
flexDirection="column"
justifyContent="center"
maxHeight={props.maxHeight}
overflow="hidden"
>
<ListHeaderContainer className={props.headerClassName}>
<Text kind="heading-xs">{props.headerText}</Text>
{props.headerControls}
</ListHeaderContainer>
<Flex
alignItems="center"
flex="1"
flexDirection="column"
overflow="auto"
px="spaces-2"
width="100%"
>
{props.children}
</Flex>
</Flex>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { ListItemContainer, ListHeaderContainer } from "./styles";
export { ListWithHeader } from "./ListWithHeader";
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import styled from "styled-components";

export const ListItemContainer = styled.div`
width: 100%;

& .t--entity-item {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're probably just moving, but applying styling through hardcoded selectors to children is an anti-pattern. The way I see it - such styling should not exist in ADS.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is just a move. I have created this into EntityExplorer because this needs to be updated soon. These classes will soon be part of the EntityExplorer template itself.

grid-template-columns: 0 auto 1fr auto auto auto auto auto;
height: 32px;

& .t--entity-name {
padding-left: var(--ads-v2-spaces-3);
}
}
`;

export const ListHeaderContainer = styled.div`
padding: var(--ads-v2-spaces-3);
padding-right: var(--ads-v2-spaces-2);
display: flex;
justify-content: space-between;
align-items: center;
height: 40px;

span {
line-height: 20px;
}
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import styled from "styled-components";
import { PopoverContent } from "../../../Popover";

export const SwitchTrigger = styled.div<{ active: boolean }>`
display: flex;
border-radius: var(--ads-v2-border-radius);
background-color: ${(props) =>
props.active ? `var(--ads-v2-color-bg-subtle)` : "unset"};
cursor: pointer;
padding: var(--ads-v2-spaces-2);

:hover {
background-color: var(--ads-v2-color-bg-subtle);
}
`;

export const ContentContainer = styled(PopoverContent)`
padding: 0;
padding-bottom: 0.25em;
`;
Original file line number Diff line number Diff line change
@@ -1,77 +1,78 @@
import React from "react";
import { render, fireEvent } from "@testing-library/react";
import HeaderEditorSwitcher from "./HeaderEditorSwitcher";
import { render, fireEvent, screen } from "@testing-library/react";
import { IDEHeaderSwitcher } from "./IDEHeaderSwitcher";
import "@testing-library/jest-dom";

describe("HeaderEditorSwitcher", () => {
describe("HeaderSwitcher", () => {
const mockOnClick = jest.fn();
const mockSetActive = jest.fn();
const defaultProps = {
prefix: "Prefix",
title: "Title",
titleTestId: "titleTestId",
active: false,
onClick: mockOnClick,
setActive: mockSetActive,
children: <span>Test</span>,
};

it("renders with correct props", () => {
const { getByText } = render(<HeaderEditorSwitcher {...defaultProps} />);
render(<IDEHeaderSwitcher {...defaultProps} />);

// eslint-disable-next-line testing-library/no-node-access
const testIdElement = document.getElementsByClassName(
defaultProps.titleTestId,
);

expect(getByText("Prefix /")).toBeInTheDocument();
expect(getByText(defaultProps.title)).toBeInTheDocument();
expect(screen.getByText("Prefix /")).toBeInTheDocument();
expect(screen.getByText(defaultProps.title)).toBeInTheDocument();
expect(testIdElement).toBeDefined();
});

it("renders active state correctly", () => {
const { getByText } = render(
<HeaderEditorSwitcher {...defaultProps} active />,
);
render(<IDEHeaderSwitcher {...defaultProps} active />);

expect(getByText("Prefix /")).toHaveStyle(
expect(screen.getByText("Prefix /")).toHaveStyle(
"background-color: var(--ads-v2-color-bg-subtle)",
);
});

it("calls onClick handler when clicked", () => {
const { getByText } = render(<HeaderEditorSwitcher {...defaultProps} />);
render(<IDEHeaderSwitcher {...defaultProps} />);

fireEvent.click(getByText("Title"));
fireEvent.click(screen.getByText("Title"));

expect(mockOnClick).toHaveBeenCalled();
});

it("forwards ref correctly", () => {
const ref = React.createRef();
const ref = React.createRef<HTMLDivElement>();

render(<HeaderEditorSwitcher {...defaultProps} ref={ref} />);
render(<IDEHeaderSwitcher {...defaultProps} ref={ref} />);
expect(ref.current).toBeTruthy();
});

it("does not crash when onClick is not provided", () => {
const { getByText } = render(
<HeaderEditorSwitcher {...defaultProps} onClick={undefined} />,
);
render(<IDEHeaderSwitcher {...defaultProps} onClick={undefined} />);

fireEvent.click(getByText("Title")); // Should not throw error
fireEvent.click(screen.getByText("Title")); // Should not throw error
});

it("does not show separator and applies different inactive color to icon", () => {
const ref = React.createRef();
const { container, getByTestId } = render(
<HeaderEditorSwitcher
const ref = React.createRef<HTMLDivElement>();
const { container } = render(
<IDEHeaderSwitcher
{...defaultProps}
data-testid="root-div"
ref={ref}
title={undefined}
/>,
);

// eslint-disable-next-line testing-library/no-container,testing-library/no-node-access
const icon = container.querySelector(".remixicon-icon"); // Get chevron icon

expect(getByTestId("root-div")).toHaveTextContent("Prefix");
expect(screen.getByTestId("root-div")).toHaveTextContent("Prefix");
expect(icon).toHaveAttribute(
"fill",
"var(--ads-v2-colors-content-label-inactive-fg)",
Expand All @@ -83,16 +84,20 @@ describe("HeaderEditorSwitcher", () => {
const className = "custom-class";

const { container } = render(
<HeaderEditorSwitcher
<IDEHeaderSwitcher
active
className={className}
data-testid={testId} // Additional prop
prefix="Prefix"
setActive={mockSetActive}
title="Title"
titleTestId="titleTestId"
/>,
>
<span>Test</span>
</IDEHeaderSwitcher>,
);

// eslint-disable-next-line testing-library/no-container,testing-library/no-node-access
const firstDiv = container.querySelector("div"); // Get the first div element
const classNames = firstDiv?.getAttribute("class")?.split(" ") || [];

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import React, { type ForwardedRef, useCallback } from "react";
import { Flex } from "../../../Flex";
import { Icon } from "../../../Icon";
import { Popover, PopoverTrigger } from "../../../Popover";
import { Text } from "../../../Text";
import * as Styled from "./HeaderSwitcher.styles";

interface Props {
prefix: string;
title?: string;
titleTestId: string;
active: boolean;
setActive: (active: boolean) => void;
onClick?: React.MouseEventHandler<HTMLDivElement>;
className?: string;
children: React.ReactNode;
}

export const IDEHeaderSwitcher = React.forwardRef(
(props: Props, ref: ForwardedRef<HTMLDivElement>) => {
const {
active,
children,
className,
onClick,
prefix,
setActive,
title,
titleTestId,
...rest
} = props;

const separator = title ? " /" : "";

const closeSwitcher = useCallback(() => {
return setActive(false);
}, [setActive]);

return (
<Popover onOpenChange={setActive} open={active}>
<PopoverTrigger>
<Styled.SwitchTrigger
active={active}
className={`flex align-center items-center justify-center ${className}`}
data-testid={titleTestId}
onClick={onClick}
ref={ref}
{...rest}
>
<Text
color="var(--ads-v2-colors-content-label-inactive-fg)"
kind="body-m"
>
{prefix + separator}
</Text>
<Flex
alignItems="center"
className={titleTestId}
data-active={active}
gap="spaces-1"
height="100%"
justifyContent="center"
paddingLeft="spaces-2"
>
<Text isBold kind="body-m">
{title}
</Text>
<Icon
color={
title
? undefined
: "var(--ads-v2-colors-content-label-inactive-fg)"
}
name={active ? "arrow-up-s-line" : "arrow-down-s-line"}
size="md"
/>
</Flex>
</Styled.SwitchTrigger>
</PopoverTrigger>
<Styled.ContentContainer align="start" onEscapeKeyDown={closeSwitcher}>
{children}
</Styled.ContentContainer>
</Popover>
);
},
);

IDEHeaderSwitcher.displayName = "IDEHeaderSwitcher";
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { IDEHeaderSwitcher } from "./IDEHeaderSwitcher";
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const IDE_HEADER_HEIGHT = 40;
export const LOGO_WIDTH = 50;
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Canvas, Meta } from "@storybook/blocks";
import * as IDEHeaderStories from "./IDEHeader.stories";

<Meta of={IDEHeaderStories} />

# IDEHeader

IDEHeader sets the stage for the IDE experience. It is the topmost section of the IDE that contains the Appsmith logo, the app name, and the user profile.

<Canvas of={IDEHeaderStories.Default} />

## Anatomy

### Left Section options

The local title

#### Header Title

A title that is specific to the app state. It is displayed on the left side of the header.

<Canvas of={IDEHeaderStories.WithHeaderTitle} />

#### Header Dropdown

A dropdown that allows the user to switch between different pages.

<Canvas of={IDEHeaderStories.WithHeaderDropdown} />
Loading