chore: Move Sidebar to IDE/Components (#34487)

## Description

Separate the Sidebar Component from the various IDEs and move it to the
IDE module.

Fixes #34554

## Automation

/ok-to-test tags="@tag.IDE"



## Communication
Should the DevRel and Marketing teams inform users about this change?
- [ ] Yes
- [x] No


<!-- This is an auto-generated comment: Cypress test results  -->
> [!TIP]
> 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉
> Workflow run:
<https://github.com/appsmithorg/appsmith/actions/runs/9869212078>
> Commit: 0b685d46ba18bb98e37fde87a96c930172fc5c15
> <a
href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=9869212078&attempt=1"
target="_blank">Cypress dashboard</a>.
> Tags: `@tag.IDE`
> Spec:
> <hr>Wed, 10 Jul 2024 06:47:16 UTC
<!-- end of auto-generated comment: Cypress test results  -->





<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Introduced a new sidebar component (`IDESidebar`) managing button
states and handling interactions.
- Added `Condition` enum for better condition management with icons and
colors in the sidebar buttons.
  
- **Enhancements**
- Improved click handling for sidebar buttons with a new `handleOnClick`
function.
  
- **Tests**
- Added test cases for `SidebarButton` component to validate different
conditions and click behaviors.

- **Components**
- New React components and interfaces for managing the IDE's sidebar
functionality and state.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Hetu Nandu 2024-07-10 14:22:58 +05:30 committed by GitHub
parent cdd33a846f
commit 00a7d3c9a8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 326 additions and 293 deletions

View File

@ -3,8 +3,8 @@ import styled from "styled-components";
import Resizer, {
ResizerCSS,
} from "components/editorComponents/Debugger/Resizer";
import { CodeEditorWithGutterStyles } from "pages/Editor/JSEditor/constants";
import { ViewHideBehaviour, ViewDisplayMode } from "IDE/Interfaces/View";
import { CodeEditorWithGutterStyles } from "pages/Editor/JSEditor/styledComponents";
import { ViewDisplayMode, ViewHideBehaviour } from "IDE/Interfaces/View";
import { Button } from "design-system";
const VIEW_MIN_HEIGHT = 38;

View File

@ -0,0 +1,69 @@
import React from "react";
import styled from "styled-components";
import SidebarButton from "./SidebarButton";
import type { EditorState } from "@appsmith/entities/IDE/constants";
import type { SidebarButtonProps } from "./SidebarButton/SidebarButton";
import { Flex } from "design-system";
const Container = styled(Flex)`
width: 50px;
border-right: 1px solid var(--ads-v2-color-border);
height: 100%;
flex-direction: column;
justify-content: space-between;
background-color: var(--ads-v2-color-bg);
position: relative;
`;
// Sidebar handles the correct handling of sidebar button. It will check if
// the button should be selected and only handle calling the onClick
export interface IDESidebarButton
extends Omit<SidebarButtonProps, "onClick" | "selected"> {
state: EditorState;
urlSuffix: string;
}
interface IDESidebarProps {
id?: string;
topButtons: IDESidebarButton[];
bottomButtons: IDESidebarButton[];
editorState: EditorState;
onClick: (suffix: string) => void;
}
function IDESidebar(props: IDESidebarProps) {
const { bottomButtons, editorState, onClick, topButtons } = props;
return (
<Container className="t--sidebar" id={props.id}>
<div>
{topButtons.map((button) => (
<SidebarButton
icon={button.icon}
key={button.state}
onClick={onClick}
selected={editorState === button.state}
title={button.title}
tooltip={button.tooltip}
urlSuffix={button.urlSuffix}
/>
))}
</div>
<div>
{bottomButtons.map((button) => (
<SidebarButton
icon={button.icon}
key={button.state}
onClick={onClick}
selected={editorState === button.state}
title={button.title}
tooltip={button.tooltip}
urlSuffix={button.urlSuffix}
/>
))}
</div>
</Container>
);
}
export default IDESidebar;

View File

@ -0,0 +1,51 @@
import { render } from "test/testUtils";
import React from "react";
import SidebarButton, { type SidebarButtonProps } from "./SidebarButton";
import { Condition } from "../../../enums";
import userEvent from "@testing-library/user-event";
const sidebarButtonProps: SidebarButtonProps = {
icon: "down-arrow",
onClick: () => {},
selected: false,
title: "Test",
urlSuffix: "/test",
};
describe("SidebarButton", () => {
it("should render the warning icon in case the datasource list is empty", () => {
const withWarningCondition = {
...sidebarButtonProps,
condition: Condition.Warn,
};
const { container } = render(<SidebarButton {...withWarningCondition} />);
const svgs = container.querySelectorAll("svg");
expect(svgs).toHaveLength(2);
});
it("should call onClick with urlSuffix", async () => {
const checkOnClick = {
...sidebarButtonProps,
onClick: jest.fn(),
};
const { getByRole } = render(<SidebarButton {...checkOnClick} />);
await userEvent.click(getByRole("button"));
expect(checkOnClick.onClick).toHaveBeenCalledWith(checkOnClick.urlSuffix);
});
it("should not call onClick when button is already selected", async () => {
const withSelected = {
...sidebarButtonProps,
selected: true,
onClick: jest.fn(),
};
const { getByRole } = render(<SidebarButton {...withSelected} />);
await userEvent.click(getByRole("button"));
expect(withSelected.onClick).not.toHaveBeenCalled();
});
});

View File

@ -0,0 +1,106 @@
import React, { useCallback } from "react";
import { Flex, Icon, Text, Tooltip } from "design-system";
import styled from "styled-components";
import { Condition } from "../../../enums";
const ConditionConfig: Record<Condition, { icon: string; color: string }> = {
[Condition.Warn]: {
icon: "warning",
color: "#ffe283",
},
// TODO add this information for further conditions
// Error: { color: "", icon: "" },
// Success: { color: "", icon: "" },
};
export interface SidebarButtonProps {
title?: string;
selected: boolean;
icon: string;
onClick: (urlSuffix: string) => void;
urlSuffix: string;
tooltip?: string;
condition?: Condition;
}
const Container = styled(Flex)`
justify-content: center;
flex-direction: column;
width: 50px;
text-align: center;
align-items: center;
padding: 8px 0;
`;
const IconContainer = styled.div<{ selected: boolean }>`
padding: 2px;
background-color: ${(props) =>
props.selected ? "var(--colors-raw-orange-100, #fbe6dc)" : "white"};
border-radius: 3px;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
position: relative;
&:hover {
background: ${(props) =>
props.selected
? "var(--colors-raw-orange-100, #fbe6dc)"
: "var(--ads-v2-color-bg-subtle, #f1f5f9);"};
}
`;
const ConditionIcon = styled(Icon)`
position: absolute;
bottom: 3px;
right: -1px;
&.t--sidebar-${Condition.Warn}-condition-icon {
color: ${ConditionConfig[Condition.Warn].color};
}
// TODO add more condition colors here
`;
function SidebarButton(props: SidebarButtonProps) {
const { condition, icon, onClick, selected, title, tooltip, urlSuffix } =
props;
const handleOnClick = useCallback(() => {
if (!selected) {
onClick(urlSuffix);
}
}, [selected, onClick, urlSuffix]);
return (
<Container>
<Tooltip
content={tooltip}
isDisabled={!!title && !tooltip}
placement={"right"}
>
<IconContainer
className={`t--sidebar-${title || tooltip}`}
data-selected={selected}
onClick={handleOnClick}
role="button"
selected={selected}
>
<Icon name={icon} size="lg" />
{condition && (
<ConditionIcon
className={`t--sidebar-${condition}-condition-icon`}
name={ConditionConfig[condition].icon}
size="md"
/>
)}
</IconContainer>
</Tooltip>
{title ? <Text kind="body-s">{title}</Text> : null}
</Container>
);
}
export default SidebarButton;

View File

@ -0,0 +1 @@
export { default } from "./SidebarButton";

View File

@ -0,0 +1,2 @@
export { default } from "./Sidebar";
export type { IDESidebarButton } from "./Sidebar";

View File

@ -0,0 +1,5 @@
export enum Condition {
Warn = "Warn",
// Error = "Error",
// Success = "Success",
}

View File

@ -40,9 +40,17 @@ export { default as IDEHeaderDropdown } from "./Components/HeaderDropdown";
*/
export { default as IDEBottomView } from "./Components/BottomView";
/**
* IDESidebar is used inside the IDE to have a navigation menu on the left side of the screen.
* It switches between different editor states
*/
export { default as IDESidebar } from "./Components/Sidebar";
/* ====================================================
**** Interfaces ****
Common types that are used by the different components of the IDE
=======================================================**/
export { ViewHideBehaviour, ViewDisplayMode } from "./Interfaces/View";
export { Condition } from "./enums";
export type { IDESidebarButton } from "./Components/Sidebar";

View File

@ -21,7 +21,7 @@ export default class LibraryApi extends Api {
library: Partial<JSLibrary>,
) {
const url = LibraryApi.getUpdateLibraryBaseURL(applicationId) + "/remove";
return Api.patch(url, { accessor: library.accessor, url: library.url });
return Api.patch(url, library);
}
static async getLibraries(applicationId: string, mode: APP_MODE) {

View File

@ -20,11 +20,8 @@ import {
SAAS_EDITOR_DATASOURCE_ID_PATH,
} from "pages/Editor/SaaSEditor/constants";
import type { PluginType } from "entities/Action";
import type { ReactNode, ComponentType } from "react";
import {
EMPTY_DATASOURCE_TOOLTIP_SIDEBUTTON,
createMessage,
} from "@appsmith/constants/messages";
import type { ComponentType, ReactNode } from "react";
import type { IDESidebarButton } from "IDE";
export enum EditorState {
DATA = "DATA",
@ -60,20 +57,7 @@ export enum EditorViewMode {
SplitScreen = "SplitScreen",
}
export enum SideButtonType {
DATSOURCE = "DATASOURCE",
}
export interface SidebarButton {
state: EditorState;
icon: string;
title?: string;
urlSuffix: string;
conditionType?: SideButtonType;
conditionTooltip?: string;
}
export const TopButtons: SidebarButton[] = [
export const TopButtons: IDESidebarButton[] = [
{
state: EditorState.EDITOR,
icon: "editor-v3",
@ -85,22 +69,20 @@ export const TopButtons: SidebarButton[] = [
icon: "datasource-v3",
title: SidebarTopButtonTitles.DATA,
urlSuffix: "datasource",
conditionType: SideButtonType.DATSOURCE,
conditionTooltip: createMessage(EMPTY_DATASOURCE_TOOLTIP_SIDEBUTTON),
},
];
export const BottomButtons: SidebarButton[] = [
export const BottomButtons: IDESidebarButton[] = [
{
state: EditorState.LIBRARIES,
icon: "packages-v3",
title: SidebarBottomButtonTitles.LIBRARIES,
tooltip: SidebarBottomButtonTitles.LIBRARIES,
urlSuffix: "libraries",
},
{
state: EditorState.SETTINGS,
icon: "settings-v3",
title: SidebarBottomButtonTitles.SETTINGS,
tooltip: SidebarBottomButtonTitles.SETTINGS,
urlSuffix: "settings",
},
];

View File

@ -0,0 +1,75 @@
import React, { useCallback, useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { builderURL } from "@appsmith/RouteBuilder";
import { getCurrentPageId } from "selectors/editorSelectors";
import history, { NavigationMethod } from "utils/history";
import { useCurrentAppState } from "./hooks";
import { getCurrentWorkspaceId } from "@appsmith/selectors/selectedWorkspaceSelectors";
import { fetchWorkspace } from "@appsmith/actions/workspaceActions";
import { IDESidebar, Condition } from "IDE";
import {
BottomButtons,
EditorState,
TopButtons,
} from "@appsmith/entities/IDE/constants";
import { getDatasources } from "@appsmith/selectors/entitiesSelector";
import {
createMessage,
EMPTY_DATASOURCE_TOOLTIP_SIDEBUTTON,
} from "@appsmith/constants/messages";
function Sidebar() {
const dispatch = useDispatch();
const appState = useCurrentAppState();
const pageId = useSelector(getCurrentPageId);
const currentWorkspaceId = useSelector(getCurrentWorkspaceId);
const datasources = useSelector(getDatasources);
const datasourcesExist = datasources.length > 0;
// Updates the top button config based on datasource existence
const topButtons = React.useMemo(() => {
return datasourcesExist
? TopButtons
: TopButtons.map((button) => {
if (button.state === EditorState.DATA) {
return {
...button,
condition: Condition.Warn,
tooltip: createMessage(EMPTY_DATASOURCE_TOOLTIP_SIDEBUTTON),
};
}
return button;
});
}, [datasourcesExist]);
useEffect(() => {
dispatch(fetchWorkspace(currentWorkspaceId));
}, [currentWorkspaceId, dispatch]);
const onClick = useCallback(
(suffix) => {
history.push(
builderURL({
pageId,
suffix,
}),
{
invokedBy: NavigationMethod.AppSidebar,
},
);
},
[pageId],
);
return (
<IDESidebar
bottomButtons={BottomButtons}
editorState={appState}
id={"t--app-sidebar"}
onClick={onClick}
topButtons={topButtons}
/>
);
}
export default Sidebar;

View File

@ -1,23 +0,0 @@
import { render } from "test/testUtils";
import React from "react";
import type { SidebarButtonProps } from "./SidebarButton";
import SidebarButton from "./SidebarButton";
import { TopButtons } from "@appsmith/entities/IDE/constants";
const sidebarButtonProps: SidebarButtonProps = {
icon: TopButtons[1].icon,
onClick: () => {},
selected: false,
title: TopButtons[1].title,
conditionIcon: "warning",
tooltip: TopButtons[1].conditionTooltip,
};
describe("SidebarButton", () => {
it("should render the warning icon incase the datasource list is empty", () => {
const { container } = render(<SidebarButton {...sidebarButtonProps} />);
const svgs = container.querySelectorAll("svg");
expect(svgs).toHaveLength(2);
});
});

View File

@ -1,84 +0,0 @@
import React from "react";
import { Icon, Text, Tooltip } from "design-system";
import styled from "styled-components";
import { SidebarTopButtonTitles } from "@appsmith/entities/IDE/constants";
export interface SidebarButtonProps {
title?: string;
selected: boolean;
icon: string;
onClick: () => void;
tooltip?: string;
conditionIcon?: string;
}
const Container = styled.div`
display: flex;
justify-content: center;
flex-direction: column;
width: 50px;
text-align: center;
align-items: center;
padding: 8px 0;
`;
const IconContainer = styled.div<{ selected: boolean }>`
padding: 2px;
background-color: ${(props) =>
props.selected ? "var(--colors-raw-orange-100, #fbe6dc)" : "white"};
border-radius: 3px;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
position: relative;
&:hover {
background: ${(props) =>
props.selected
? "var(--colors-raw-orange-100, #fbe6dc)"
: "var(--ads-v2-color-bg-subtle, #f1f5f9);"};
}
`;
const ConditionIcon = styled(Icon)`
position: absolute;
bottom: 3px;
right: -1px;
&.t--sidebar-${SidebarTopButtonTitles.DATA}-condition-icon {
color: #ffe283;
}
`;
function SidebarButton(props: SidebarButtonProps) {
return (
<Container>
{props.title === SidebarTopButtonTitles.DATA}
<Tooltip
content={props.tooltip}
isDisabled={!!props.title && !props.tooltip}
placement={"right"}
>
<IconContainer
className={`t--sidebar-${props.title || props.tooltip}`}
data-selected={props.selected}
onClick={props.onClick}
selected={props.selected}
>
<Icon name={props.icon} size="lg" />
{props.conditionIcon && (
<ConditionIcon
className={`t--sidebar-${props.title}-condition-icon`}
name={props.conditionIcon}
size="md"
/>
)}
</IconContainer>
</Tooltip>
{props.title ? <Text kind="body-s">{props.title}</Text> : null}
</Container>
);
}
export default SidebarButton;

View File

@ -1,87 +0,0 @@
import React from "react";
import styled from "styled-components";
import SidebarButton from "./SidebarButton";
import type { SidebarButton as SidebarButtonType } from "@appsmith/entities/IDE/constants";
import { SideButtonType } from "@appsmith/entities/IDE/constants";
import { useSelector } from "react-redux";
import { getDatasources } from "@appsmith/selectors/entitiesSelector";
const Container = styled.div`
width: 50px;
border-right: 1px solid var(--ads-v2-color-border);
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
background-color: var(--ads-v2-color-bg);
position: relative;
`;
interface SidebarComponentProps {
topButtons: SidebarButtonType[];
bottomButtons: SidebarButtonType[];
appState: string;
onClick: (suffix: string) => void;
}
function SidebarComponent(props: SidebarComponentProps) {
const { appState, bottomButtons, onClick, topButtons } = props;
const datasources = useSelector(getDatasources);
const getConditionalIconAndTooltip = (
type?: SideButtonType,
conditionTooltip?: string,
) => {
switch (type) {
case SideButtonType.DATSOURCE:
if (datasources.length === 0)
return {
conditionIcon: "warning",
tooltip: conditionTooltip,
};
return {};
default:
return {};
}
};
return (
<Container className="t--sidebar" id="t--app-sidebar">
<div>
{topButtons.map((b) => (
<SidebarButton
icon={b.icon}
key={b.state}
onClick={() => {
if (appState !== b.state) {
onClick(b.urlSuffix);
}
}}
selected={appState === b.state}
title={b.title}
{...getConditionalIconAndTooltip(
b.conditionType,
b.conditionTooltip,
)}
/>
))}
</div>
<div>
{bottomButtons.map((b) => (
<SidebarButton
icon={b.icon}
key={b.state}
onClick={() => {
if (appState !== b.state) {
onClick(b.urlSuffix);
}
}}
selected={appState === b.state}
tooltip={b.title}
/>
))}
</div>
</Container>
);
}
export default SidebarComponent;

View File

@ -1,48 +0,0 @@
import React, { useCallback, useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { builderURL } from "@appsmith/RouteBuilder";
import { getCurrentPageId } from "selectors/editorSelectors";
import history, { NavigationMethod } from "utils/history";
import { useCurrentAppState } from "../hooks";
import { getCurrentWorkspaceId } from "@appsmith/selectors/selectedWorkspaceSelectors";
import { fetchWorkspace } from "@appsmith/actions/workspaceActions";
import SidebarComponent from "./SidebarComponent";
import { BottomButtons, TopButtons } from "@appsmith/entities/IDE/constants";
function Sidebar() {
const dispatch = useDispatch();
const appState = useCurrentAppState();
const pageId = useSelector(getCurrentPageId);
const currentWorkspaceId = useSelector(getCurrentWorkspaceId);
useEffect(() => {
dispatch(fetchWorkspace(currentWorkspaceId));
}, [currentWorkspaceId]);
const onClick = useCallback(
(suffix) => {
history.push(
builderURL({
pageId,
suffix,
}),
{
invokedBy: NavigationMethod.AppSidebar,
},
);
},
[pageId],
);
return (
<SidebarComponent
appState={appState}
bottomButtons={BottomButtons}
onClick={onClick}
topButtons={TopButtons}
/>
);
}
export default Sidebar;

View File

@ -1,4 +1,3 @@
import { css } from "styled-components";
import type { JSActionDropdownOption } from "./utils";
export const RUN_BUTTON_DEFAULTS = {
@ -52,26 +51,3 @@ export const ANIMATE_RUN_GUTTER = "animate-run-marker";
export const testLocators = {
runJSAction: "run-js-action",
};
export const CodeEditorWithGutterStyles = css`
.${RUN_GUTTER_ID} {
width: 0.5em;
background: #f0f0f0;
margin-left: 5px;
}
.${RUN_GUTTER_CLASSNAME} {
cursor: pointer;
color: var(--ads-v2-color-fg-brand);
}
.CodeMirror-linenumbers {
width: max-content;
}
.CodeMirror-linenumber {
text-align: right;
padding-left: 0;
}
.cm-s-duotone-light.CodeMirror {
padding: 0;
}
`;