diff --git a/app/client/src/IDE/Components/BottomView.tsx b/app/client/src/IDE/Components/BottomView.tsx index fe5bbb9e41b1..19bddbe3d705 100644 --- a/app/client/src/IDE/Components/BottomView.tsx +++ b/app/client/src/IDE/Components/BottomView.tsx @@ -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; diff --git a/app/client/src/IDE/Components/Sidebar/Sidebar.tsx b/app/client/src/IDE/Components/Sidebar/Sidebar.tsx new file mode 100644 index 000000000000..5fedfd10d3c1 --- /dev/null +++ b/app/client/src/IDE/Components/Sidebar/Sidebar.tsx @@ -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 { + 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 ( + +
+ {topButtons.map((button) => ( + + ))} +
+
+ {bottomButtons.map((button) => ( + + ))} +
+
+ ); +} + +export default IDESidebar; diff --git a/app/client/src/IDE/Components/Sidebar/SidebarButton/SidebarButton.test.tsx b/app/client/src/IDE/Components/Sidebar/SidebarButton/SidebarButton.test.tsx new file mode 100644 index 000000000000..ce8ad536826a --- /dev/null +++ b/app/client/src/IDE/Components/Sidebar/SidebarButton/SidebarButton.test.tsx @@ -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(); + + 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(); + + 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(); + + await userEvent.click(getByRole("button")); + expect(withSelected.onClick).not.toHaveBeenCalled(); + }); +}); diff --git a/app/client/src/IDE/Components/Sidebar/SidebarButton/SidebarButton.tsx b/app/client/src/IDE/Components/Sidebar/SidebarButton/SidebarButton.tsx new file mode 100644 index 000000000000..a5c57ff97d38 --- /dev/null +++ b/app/client/src/IDE/Components/Sidebar/SidebarButton/SidebarButton.tsx @@ -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.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 ( + + + + + {condition && ( + + )} + + + {title ? {title} : null} + + ); +} + +export default SidebarButton; diff --git a/app/client/src/IDE/Components/Sidebar/SidebarButton/index.ts b/app/client/src/IDE/Components/Sidebar/SidebarButton/index.ts new file mode 100644 index 000000000000..6b596087d7b6 --- /dev/null +++ b/app/client/src/IDE/Components/Sidebar/SidebarButton/index.ts @@ -0,0 +1 @@ +export { default } from "./SidebarButton"; diff --git a/app/client/src/IDE/Components/Sidebar/index.tsx b/app/client/src/IDE/Components/Sidebar/index.tsx new file mode 100644 index 000000000000..978e984b5163 --- /dev/null +++ b/app/client/src/IDE/Components/Sidebar/index.tsx @@ -0,0 +1,2 @@ +export { default } from "./Sidebar"; +export type { IDESidebarButton } from "./Sidebar"; diff --git a/app/client/src/IDE/enums.ts b/app/client/src/IDE/enums.ts new file mode 100644 index 000000000000..2c62e72c91ad --- /dev/null +++ b/app/client/src/IDE/enums.ts @@ -0,0 +1,5 @@ +export enum Condition { + Warn = "Warn", + // Error = "Error", + // Success = "Success", +} diff --git a/app/client/src/IDE/index.ts b/app/client/src/IDE/index.ts index 5568b8167342..0b765da22cea 100644 --- a/app/client/src/IDE/index.ts +++ b/app/client/src/IDE/index.ts @@ -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"; diff --git a/app/client/src/api/LibraryAPI.tsx b/app/client/src/api/LibraryAPI.tsx index a76a7a1fb207..73b5c0d879c7 100644 --- a/app/client/src/api/LibraryAPI.tsx +++ b/app/client/src/api/LibraryAPI.tsx @@ -21,7 +21,7 @@ export default class LibraryApi extends Api { library: Partial, ) { 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) { diff --git a/app/client/src/ce/entities/IDE/constants.ts b/app/client/src/ce/entities/IDE/constants.ts index 982b01243783..a0a77714ce44 100644 --- a/app/client/src/ce/entities/IDE/constants.ts +++ b/app/client/src/ce/entities/IDE/constants.ts @@ -21,11 +21,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", @@ -61,20 +58,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", @@ -86,22 +70,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", }, ]; diff --git a/app/client/src/pages/Editor/IDE/Sidebar.tsx b/app/client/src/pages/Editor/IDE/Sidebar.tsx new file mode 100644 index 000000000000..32c92e0a6393 --- /dev/null +++ b/app/client/src/pages/Editor/IDE/Sidebar.tsx @@ -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 ( + + ); +} + +export default Sidebar; diff --git a/app/client/src/pages/Editor/IDE/Sidebar/SidebarButton.test.tsx b/app/client/src/pages/Editor/IDE/Sidebar/SidebarButton.test.tsx deleted file mode 100644 index 15c2b9de9cd1..000000000000 --- a/app/client/src/pages/Editor/IDE/Sidebar/SidebarButton.test.tsx +++ /dev/null @@ -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(); - - const svgs = container.querySelectorAll("svg"); - expect(svgs).toHaveLength(2); - }); -}); diff --git a/app/client/src/pages/Editor/IDE/Sidebar/SidebarButton.tsx b/app/client/src/pages/Editor/IDE/Sidebar/SidebarButton.tsx deleted file mode 100644 index d53ef1fcb119..000000000000 --- a/app/client/src/pages/Editor/IDE/Sidebar/SidebarButton.tsx +++ /dev/null @@ -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 ( - - {props.title === SidebarTopButtonTitles.DATA} - - - - {props.conditionIcon && ( - - )} - - - {props.title ? {props.title} : null} - - ); -} - -export default SidebarButton; diff --git a/app/client/src/pages/Editor/IDE/Sidebar/SidebarComponent.tsx b/app/client/src/pages/Editor/IDE/Sidebar/SidebarComponent.tsx deleted file mode 100644 index d5e9ccf2f4c0..000000000000 --- a/app/client/src/pages/Editor/IDE/Sidebar/SidebarComponent.tsx +++ /dev/null @@ -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 ( - -
- {topButtons.map((b) => ( - { - if (appState !== b.state) { - onClick(b.urlSuffix); - } - }} - selected={appState === b.state} - title={b.title} - {...getConditionalIconAndTooltip( - b.conditionType, - b.conditionTooltip, - )} - /> - ))} -
-
- {bottomButtons.map((b) => ( - { - if (appState !== b.state) { - onClick(b.urlSuffix); - } - }} - selected={appState === b.state} - tooltip={b.title} - /> - ))} -
-
- ); -} - -export default SidebarComponent; diff --git a/app/client/src/pages/Editor/IDE/Sidebar/index.tsx b/app/client/src/pages/Editor/IDE/Sidebar/index.tsx deleted file mode 100644 index 35968c3099e4..000000000000 --- a/app/client/src/pages/Editor/IDE/Sidebar/index.tsx +++ /dev/null @@ -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 ( - - ); -} - -export default Sidebar; diff --git a/app/client/src/pages/Editor/JSEditor/constants.ts b/app/client/src/pages/Editor/JSEditor/constants.ts index ae9b8af8533c..da8b12eb319d 100644 --- a/app/client/src/pages/Editor/JSEditor/constants.ts +++ b/app/client/src/pages/Editor/JSEditor/constants.ts @@ -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; - } -`;