diff --git a/app/client/packages/design-system/ads/src/Templates/EntityExplorer/EmptyState/EmptyState.tsx b/app/client/packages/design-system/ads/src/Templates/EntityExplorer/EmptyState/EmptyState.tsx
index 269bc1ecdc49..6f2795ba8967 100644
--- a/app/client/packages/design-system/ads/src/Templates/EntityExplorer/EmptyState/EmptyState.tsx
+++ b/app/client/packages/design-system/ads/src/Templates/EntityExplorer/EmptyState/EmptyState.tsx
@@ -1,5 +1,8 @@
import React from "react";
-import { Button, Flex, Icon, Text } from "../../..";
+import { Button } from "../../../Button";
+import { Flex } from "../../../Flex";
+import { Icon } from "../../../Icon";
+import { Text } from "../../../Text";
import type { EmptyStateProps } from "./EmptyState.types";
const EmptyState = ({ button, description, icon }: EmptyStateProps) => {
diff --git a/app/client/packages/design-system/ads/src/Templates/EntityExplorer/EmptyState/EmptyState.types.ts b/app/client/packages/design-system/ads/src/Templates/EntityExplorer/EmptyState/EmptyState.types.ts
index e77047e508b9..c71f4d88f604 100644
--- a/app/client/packages/design-system/ads/src/Templates/EntityExplorer/EmptyState/EmptyState.types.ts
+++ b/app/client/packages/design-system/ads/src/Templates/EntityExplorer/EmptyState/EmptyState.types.ts
@@ -1,4 +1,5 @@
-import { type IconNames, type ButtonKind } from "../../..";
+import type { IconNames } from "../../../Icon";
+import type { ButtonKind } from "../../../Button";
export interface EmptyStateProps {
icon: IconNames;
diff --git a/app/client/packages/design-system/ads/src/Templates/EntityExplorer/EntityItem/EntityItem.styles.ts b/app/client/packages/design-system/ads/src/Templates/EntityExplorer/EntityItem/EntityItem.styles.ts
index 263b2a15a16c..5a1eafaa1dea 100644
--- a/app/client/packages/design-system/ads/src/Templates/EntityExplorer/EntityItem/EntityItem.styles.ts
+++ b/app/client/packages/design-system/ads/src/Templates/EntityExplorer/EntityItem/EntityItem.styles.ts
@@ -1,5 +1,5 @@
import styled from "styled-components";
-import { Text } from "../../..";
+import { Text } from "../../../Text";
export const EntityEditableName = styled(Text)`
overflow: hidden;
diff --git a/app/client/packages/design-system/ads/src/Templates/EntityExplorer/EntityItem/EntityItem.tsx b/app/client/packages/design-system/ads/src/Templates/EntityExplorer/EntityItem/EntityItem.tsx
index dfeb0279eb49..def9e57dcbbd 100644
--- a/app/client/packages/design-system/ads/src/Templates/EntityExplorer/EntityItem/EntityItem.tsx
+++ b/app/client/packages/design-system/ads/src/Templates/EntityExplorer/EntityItem/EntityItem.tsx
@@ -1,5 +1,7 @@
import React, { useMemo } from "react";
-import { ListItem, Spinner, Tooltip } from "../../..";
+import { ListItem } from "../../../List";
+import { Spinner } from "../../../Spinner";
+import { Tooltip } from "../../../Tooltip";
import type { EntityItemProps } from "./EntityItem.types";
import { EntityEditableName } from "./EntityItem.styles";
@@ -32,11 +34,21 @@ export const EntityItem = (props: EntityItemProps) => {
onNameSave,
);
+ // When in loading state, start icon becomes the loading icon
+ const startIcon = useMemo(() => {
+ if (isLoading) {
+ return ;
+ }
+
+ return props.startIcon;
+ }, [isLoading, props.startIcon]);
+
const inputProps = useMemo(
() => ({
onChange: handleTitleChange,
onKeyUp: handleKeyUp,
style: {
+ backgroundColor: "var(--ads-v2-color-bg)",
paddingTop: 0,
paddingBottom: 0,
height: "32px",
@@ -46,14 +58,7 @@ export const EntityItem = (props: EntityItemProps) => {
[handleKeyUp, handleTitleChange],
);
- const startIcon = useMemo(() => {
- if (isLoading) {
- return ;
- }
-
- return props.startIcon;
- }, [isLoading, props.startIcon]);
-
+ // Use List Item custom title prop to show the editable name
const customTitle = useMemo(() => {
return (
{
);
}, [editableName, inputProps, inputRef, inEditMode, validationError]);
+ // Do not show right control if the visibility is hover and the item is in edit mode
+ const rightControl = useMemo(() => {
+ if (props.rightControlVisibility === "hover" && inEditMode) {
+ return null;
+ }
+
+ return props.rightControl;
+ }, [inEditMode, props.rightControl, props.rightControlVisibility]);
+
return (
{
customTitleComponent={customTitle}
data-testid={`t--entity-item-${props.title}`}
id={"entity-" + props.id}
+ rightControl={rightControl}
startIcon={startIcon}
/>
);
diff --git a/app/client/packages/design-system/ads/src/Templates/EntityExplorer/EntityItem/index.ts b/app/client/packages/design-system/ads/src/Templates/EntityExplorer/EntityItem/index.ts
index 7f3dc03d3fbb..b857f670eba1 100644
--- a/app/client/packages/design-system/ads/src/Templates/EntityExplorer/EntityItem/index.ts
+++ b/app/client/packages/design-system/ads/src/Templates/EntityExplorer/EntityItem/index.ts
@@ -1 +1,2 @@
export { EntityItem } from "./EntityItem";
+export type { EntityItemProps } from "./EntityItem.types";
diff --git a/app/client/packages/design-system/ads/src/Templates/EntityExplorer/EntityListTree/EntityListTree.stories.tsx b/app/client/packages/design-system/ads/src/Templates/EntityExplorer/EntityListTree/EntityListTree.stories.tsx
new file mode 100644
index 000000000000..93ac90474a1b
--- /dev/null
+++ b/app/client/packages/design-system/ads/src/Templates/EntityExplorer/EntityListTree/EntityListTree.stories.tsx
@@ -0,0 +1,161 @@
+/* eslint-disable no-console */
+import React, { useEffect } from "react";
+import type { Meta, StoryObj } from "@storybook/react";
+
+import { EntityListTree } from "./EntityListTree";
+import type {
+ EntityListTreeItem,
+ EntityListTreeProps,
+} from "./EntityListTree.types";
+import { ExplorerContainer } from "../ExplorerContainer";
+import { Flex, Icon } from "../../..";
+import { noop } from "lodash";
+
+const meta: Meta = {
+ title: "ADS/Templates/Entity Explorer/Entity List Tree",
+ component: EntityListTree,
+};
+
+export default meta;
+
+const onClick = noop;
+const nameEditorConfig = {
+ canEdit: true,
+ isEditing: false,
+ isLoading: false,
+ onEditComplete: noop,
+ onNameSave: noop,
+ validateName: () => null,
+};
+
+const Tree: EntityListTreeProps["items"] = [
+ {
+ startIcon: ,
+ id: "1",
+ title: "Parent 1",
+ isExpanded: true,
+ onClick,
+ nameEditorConfig,
+ children: [
+ {
+ startIcon: ,
+ id: "1.1",
+ title: "Child 1",
+ isExpanded: false,
+ isSelected: true,
+ onClick,
+ nameEditorConfig,
+ children: [
+ {
+ startIcon: ,
+ id: "1.1.1",
+ title: "Grandchild 1",
+ isExpanded: false,
+ onClick,
+ nameEditorConfig,
+ },
+ {
+ startIcon: ,
+ id: "1.1.2",
+ isDisabled: true,
+ title: "Grandchild 2",
+ isExpanded: false,
+ onClick,
+ nameEditorConfig,
+ },
+ ],
+ },
+ {
+ startIcon: ,
+ id: "1.2",
+ title: "Child 2",
+ isExpanded: false,
+ onClick,
+ nameEditorConfig,
+ },
+ ],
+ },
+ {
+ startIcon: ,
+ id: "2",
+ title: "Parent 2",
+ isExpanded: false,
+ onClick,
+ nameEditorConfig,
+ },
+];
+
+const treeUpdate = (
+ items: EntityListTreeProps["items"],
+ updater: (item: EntityListTreeItem) => EntityListTreeItem,
+) => {
+ return items.map((item): EntityListTreeItem => {
+ return {
+ ...updater(item),
+ children: item.children ? treeUpdate(item.children, updater) : undefined,
+ };
+ });
+};
+
+const Template = (props: { outsideSelection: string }) => {
+ const [expanded, setExpanded] = React.useState>({});
+ const [selected, setSelected] = React.useState(
+ props.outsideSelection,
+ );
+ const [editing, setEditing] = React.useState(null);
+
+ useEffect(
+ function handleSyncOfSelection() {
+ setSelected(props.outsideSelection);
+ },
+ [props.outsideSelection],
+ );
+
+ const onExpandClick = (id: string) => {
+ setExpanded((prev) => ({ ...prev, [id]: !Boolean(prev[id]) }));
+ };
+
+ const onItemSelect = (id: string) => {
+ setSelected(id);
+ };
+
+ const onItemEdit = (id: string) => {
+ setEditing(id);
+ };
+
+ const completeEdit = () => {
+ setEditing(null);
+ };
+
+ const updatedTree = treeUpdate(Tree, (item) => ({
+ ...item,
+ isExpanded: Boolean(expanded[item.id]),
+ isSelected: item.id === selected,
+ onClick: () => onItemSelect(item.id),
+ onDoubleClick: () => onItemEdit(item.id),
+ nameEditorConfig: {
+ canEdit: true,
+ isEditing: item.id === editing,
+ isLoading: false,
+ onEditComplete: completeEdit,
+ onNameSave: noop,
+ validateName: () => null,
+ },
+ }));
+
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+export const Basic = Template.bind({}) as StoryObj;
+
+Basic.args = {
+ outsideSelection: "1",
+};
diff --git a/app/client/packages/design-system/ads/src/Templates/EntityExplorer/EntityListTree/EntityListTree.styles.ts b/app/client/packages/design-system/ads/src/Templates/EntityExplorer/EntityListTree/EntityListTree.styles.ts
new file mode 100644
index 000000000000..3e508d4c898e
--- /dev/null
+++ b/app/client/packages/design-system/ads/src/Templates/EntityExplorer/EntityListTree/EntityListTree.styles.ts
@@ -0,0 +1,68 @@
+import styled from "styled-components";
+import { Flex } from "../../../Flex";
+
+/**
+ * This is used to add a spacing when collapse icon is not present
+ **/
+export const CollapseSpacer = styled.div`
+ width: 17px;
+`;
+
+export const PaddingOverrider = styled.div`
+ width: 100%;
+
+ & > div {
+ /* Override the padding of the entity item since collapsible icon can be on the left
+ * By default the padding on the left is 8px, so we need to reduce it to 4px
+ **/
+ padding-left: 4px;
+ }
+`;
+
+export const CollapseWrapper = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ height: 16px;
+ width: 16px;
+ border-radius: var(--ads-v2-border-radius);
+ cursor: pointer;
+`;
+
+export const EntityItemWrapper = styled(Flex)<{ "data-depth": number }>`
+ border-radius: var(--ads-v2-border-radius);
+ cursor: pointer;
+
+ padding-left: ${(props) => {
+ return 4 + props["data-depth"] * 10;
+ }}px;
+
+ &[data-selected="true"] {
+ background-color: var(--ads-v2-colors-content-surface-active-bg);
+ }
+
+ /* disabled style */
+
+ &[data-disabled="true"] {
+ cursor: not-allowed;
+ opacity: var(--ads-v2-opacity-disabled);
+ background-color: var(--ads-v2-colors-content-surface-default-bg);
+ }
+
+ &:hover {
+ background-color: var(--ads-v2-colors-content-surface-hover-bg);
+ }
+
+ &:active {
+ background-color: var(--ads-v2-colors-content-surface-active-bg);
+ }
+
+ /* Focus styles */
+
+ &:focus-visible {
+ outline: var(--ads-v2-border-width-outline) solid
+ var(--ads-v2-color-outline);
+ outline-offset: var(--ads-v2-offset-outline);
+ }
+`;
diff --git a/app/client/packages/design-system/ads/src/Templates/EntityExplorer/EntityListTree/EntityListTree.test.tsx b/app/client/packages/design-system/ads/src/Templates/EntityExplorer/EntityListTree/EntityListTree.test.tsx
new file mode 100644
index 000000000000..db07f6d6567e
--- /dev/null
+++ b/app/client/packages/design-system/ads/src/Templates/EntityExplorer/EntityListTree/EntityListTree.test.tsx
@@ -0,0 +1,123 @@
+import React from "react";
+import { render, screen, fireEvent } from "@testing-library/react";
+import { EntityListTree } from "./EntityListTree";
+import type { EntityListTreeProps } from "./EntityListTree.types";
+
+const mockOnItemExpand = jest.fn();
+const mockNameEditorConfig = {
+ canEdit: true,
+ isEditing: false,
+ isLoading: false,
+ onEditComplete: jest.fn(),
+ onNameSave: jest.fn(),
+ validateName: jest.fn(),
+};
+
+const mockOnClick = jest.fn();
+
+const defaultProps: EntityListTreeProps = {
+ items: [
+ {
+ id: "1",
+ title: "Parent",
+ isExpanded: false,
+ isSelected: false,
+ isDisabled: false,
+ nameEditorConfig: mockNameEditorConfig,
+ onClick: mockOnClick,
+ children: [
+ {
+ id: "1-1",
+ title: "Child",
+ isExpanded: false,
+ isSelected: false,
+ isDisabled: false,
+ nameEditorConfig: mockNameEditorConfig,
+ onClick: mockOnClick,
+ children: [],
+ },
+ ],
+ },
+ ],
+ onItemExpand: mockOnItemExpand,
+};
+
+describe("EntityListTree", () => {
+ it("renders the EntityListTree component", () => {
+ render();
+ expect(screen.getByRole("tree")).toBeInTheDocument();
+ });
+
+ it("calls onItemExpand when expand icon is clicked", () => {
+ render();
+ const expandIcon = screen.getByTestId("entity-item-expand-icon");
+
+ fireEvent.click(expandIcon);
+ expect(mockOnItemExpand).toHaveBeenCalledWith("1");
+ });
+
+ it("does not call onItemExpand when item has no children", () => {
+ const props = {
+ ...defaultProps,
+ items: [
+ {
+ id: "2",
+ title: "No Children Parent",
+ isExpanded: false,
+ isSelected: false,
+ isDisabled: false,
+ children: [],
+ nameEditorConfig: mockNameEditorConfig,
+ onClick: mockOnClick,
+ },
+ ],
+ };
+
+ render();
+ const expandIcon = screen.queryByTestId("entity-item-expand-icon");
+
+ expect(
+ screen.getByRole("treeitem", { name: "No Children Parent" }),
+ ).toBeInTheDocument();
+ expect(expandIcon).toBeNull();
+ });
+
+ it("renders nested EntityListTree when item is expanded", () => {
+ const props = {
+ ...defaultProps,
+ items: [
+ {
+ id: "1",
+ title: "Parent",
+ isExpanded: true,
+ isSelected: false,
+ isDisabled: false,
+ nameEditorConfig: mockNameEditorConfig,
+ onClick: mockOnClick,
+ children: [
+ {
+ id: "1-1",
+ title: "Child",
+ isExpanded: false,
+ isSelected: false,
+ isDisabled: false,
+ nameEditorConfig: mockNameEditorConfig,
+ onClick: mockOnClick,
+ children: [],
+ },
+ ],
+ },
+ ],
+ };
+
+ render();
+
+ expect(screen.getByRole("treeitem", { name: "Child" })).toBeInTheDocument();
+ });
+
+ it("does not render nested EntityListTree when item is not expanded", () => {
+ render();
+
+ expect(screen.queryByRole("treeitem", { name: "Child" })).toBeNull();
+ });
+});
diff --git a/app/client/packages/design-system/ads/src/Templates/EntityExplorer/EntityListTree/EntityListTree.tsx b/app/client/packages/design-system/ads/src/Templates/EntityExplorer/EntityListTree/EntityListTree.tsx
new file mode 100644
index 000000000000..00d06cfa55f8
--- /dev/null
+++ b/app/client/packages/design-system/ads/src/Templates/EntityExplorer/EntityListTree/EntityListTree.tsx
@@ -0,0 +1,82 @@
+import React, { useCallback } from "react";
+import type { EntityListTreeProps } from "./EntityListTree.types";
+import { Flex } from "../../../Flex";
+import { Icon } from "../../../Icon";
+import { EntityItem } from "../EntityItem";
+import {
+ CollapseSpacer,
+ PaddingOverrider,
+ CollapseWrapper,
+ EntityItemWrapper,
+} from "./EntityListTree.styles";
+
+export function EntityListTree(props: EntityListTreeProps) {
+ const { onItemExpand } = props;
+
+ const handleOnExpandClick = useCallback(
+ (event: React.MouseEvent) => {
+ // Stop the event from bubbling up to the parent to avoid selection of the item
+ event.stopPropagation();
+ const id = event.currentTarget.getAttribute("data-itemid");
+
+ if (id) {
+ onItemExpand(id);
+ }
+ },
+ [onItemExpand],
+ );
+
+ const currentDepth = props.depth || 0;
+ const childrenDepth = currentDepth + 1;
+
+ return (
+
+ {props.items.map((item) => (
+
+
+ {item.children && item.children.length ? (
+
+
+
+ ) : (
+
+ )}
+
+
+
+
+ {item.children && item.isExpanded ? (
+
+ ) : null}
+
+ ))}
+
+ );
+}
diff --git a/app/client/packages/design-system/ads/src/Templates/EntityExplorer/EntityListTree/EntityListTree.types.ts b/app/client/packages/design-system/ads/src/Templates/EntityExplorer/EntityListTree/EntityListTree.types.ts
new file mode 100644
index 000000000000..add71e09b3b5
--- /dev/null
+++ b/app/client/packages/design-system/ads/src/Templates/EntityExplorer/EntityListTree/EntityListTree.types.ts
@@ -0,0 +1,12 @@
+import type { EntityItemProps } from "../EntityItem/EntityItem.types";
+
+export interface EntityListTreeItem extends EntityItemProps {
+ children?: EntityListTreeItem[];
+ isExpanded: boolean;
+}
+
+export interface EntityListTreeProps {
+ depth?: number;
+ items: EntityListTreeItem[];
+ onItemExpand: (id: string) => void;
+}
diff --git a/app/client/packages/design-system/ads/src/Templates/EntityExplorer/EntityListTree/index.ts b/app/client/packages/design-system/ads/src/Templates/EntityExplorer/EntityListTree/index.ts
new file mode 100644
index 000000000000..9551e1952d2b
--- /dev/null
+++ b/app/client/packages/design-system/ads/src/Templates/EntityExplorer/EntityListTree/index.ts
@@ -0,0 +1,2 @@
+export { EntityListTree } from "./EntityListTree";
+export { type EntityListTreeItem } from "./EntityListTree.types";
diff --git a/app/client/packages/design-system/ads/src/Templates/EntityExplorer/ExplorerContainer/ExplorerContainer.tsx b/app/client/packages/design-system/ads/src/Templates/EntityExplorer/ExplorerContainer/ExplorerContainer.tsx
index 2674552b4400..b53a3ddee8c8 100644
--- a/app/client/packages/design-system/ads/src/Templates/EntityExplorer/ExplorerContainer/ExplorerContainer.tsx
+++ b/app/client/packages/design-system/ads/src/Templates/EntityExplorer/ExplorerContainer/ExplorerContainer.tsx
@@ -1,5 +1,6 @@
import React from "react";
-import { ExplorerContainerBorder, Flex } from "../../..";
+import { Flex } from "../../../Flex";
+import { ExplorerContainerBorder } from "./ExplorerContainer.constants";
import type { ExplorerContainerProps } from "./ExplorerContainer.types";
export const ExplorerContainer = (props: ExplorerContainerProps) => {
diff --git a/app/client/packages/design-system/ads/src/Templates/EntityExplorer/NoSearchResults/NoSearchResults.tsx b/app/client/packages/design-system/ads/src/Templates/EntityExplorer/NoSearchResults/NoSearchResults.tsx
index 645b4c5d1a4e..e89b9e7e4344 100644
--- a/app/client/packages/design-system/ads/src/Templates/EntityExplorer/NoSearchResults/NoSearchResults.tsx
+++ b/app/client/packages/design-system/ads/src/Templates/EntityExplorer/NoSearchResults/NoSearchResults.tsx
@@ -1,5 +1,5 @@
import React from "react";
-import { Text } from "../../..";
+import { Text } from "../../../Text";
import type { NoSearchResultsProps } from "./NoSearchResults.types";
const NoSearchResults = ({ text }: NoSearchResultsProps) => {
diff --git a/app/client/packages/design-system/ads/src/Templates/EntityExplorer/SearchAndAdd/SearchAndAdd.styles.tsx b/app/client/packages/design-system/ads/src/Templates/EntityExplorer/SearchAndAdd/SearchAndAdd.styles.tsx
index cb734378ea67..6268a6498888 100644
--- a/app/client/packages/design-system/ads/src/Templates/EntityExplorer/SearchAndAdd/SearchAndAdd.styles.tsx
+++ b/app/client/packages/design-system/ads/src/Templates/EntityExplorer/SearchAndAdd/SearchAndAdd.styles.tsx
@@ -1,6 +1,6 @@
import styled from "styled-components";
-import { Button } from "@appsmith/ads";
+import { Button } from "../../../Button";
export const Root = styled.div`
display: flex;
diff --git a/app/client/packages/design-system/ads/src/Templates/EntityExplorer/SearchAndAdd/SearchAndAdd.tsx b/app/client/packages/design-system/ads/src/Templates/EntityExplorer/SearchAndAdd/SearchAndAdd.tsx
index e00766fc71a5..665f1c03fd25 100644
--- a/app/client/packages/design-system/ads/src/Templates/EntityExplorer/SearchAndAdd/SearchAndAdd.tsx
+++ b/app/client/packages/design-system/ads/src/Templates/EntityExplorer/SearchAndAdd/SearchAndAdd.tsx
@@ -1,6 +1,6 @@
import React, { forwardRef } from "react";
-import { SearchInput } from "@appsmith/ads";
+import { SearchInput } from "../../../SearchInput";
import * as Styles from "./SearchAndAdd.styles";
import type { SearchAndAddProps } from "./SearchAndAdd.types";
diff --git a/app/client/packages/design-system/ads/src/Templates/EntityExplorer/index.ts b/app/client/packages/design-system/ads/src/Templates/EntityExplorer/index.ts
index 51c3e9097eaf..690512da326a 100644
--- a/app/client/packages/design-system/ads/src/Templates/EntityExplorer/index.ts
+++ b/app/client/packages/design-system/ads/src/Templates/EntityExplorer/index.ts
@@ -7,3 +7,4 @@ export { NoSearchResults } from "./NoSearchResults";
export * from "./ExplorerContainer";
export * from "./EntityItem";
export { useEditableText } from "./Editable";
+export * from "./EntityListTree";
diff --git a/app/client/src/IDE/Components/EditableName/index.ts b/app/client/src/IDE/Components/EditableName/index.ts
index f714786de369..5711fd3c048e 100644
--- a/app/client/src/IDE/Components/EditableName/index.ts
+++ b/app/client/src/IDE/Components/EditableName/index.ts
@@ -1,3 +1,4 @@
export { EditableName } from "./EditableName";
export { RenameMenuItem } from "./RenameMenuItem";
export { useIsRenaming } from "./useIsRenaming";
+export { useValidateEntityName } from "./useValidateEntityName";
diff --git a/app/client/src/IDE/Components/EditableName/useValidateEntityName.ts b/app/client/src/IDE/Components/EditableName/useValidateEntityName.ts
index bbccf88991b2..a1ebb5a8b419 100644
--- a/app/client/src/IDE/Components/EditableName/useValidateEntityName.ts
+++ b/app/client/src/IDE/Components/EditableName/useValidateEntityName.ts
@@ -10,7 +10,7 @@ import { getUsedActionNames } from "selectors/actionSelectors";
import { isNameValid } from "utils/helpers";
interface UseValidateEntityNameProps {
- entityName: string;
+ entityName?: string;
nameErrorMessage?: (name: string) => string;
}
@@ -26,10 +26,10 @@ export function useValidateEntityName(props: UseValidateEntityNameProps) {
);
return useCallback(
- (name: string): string | null => {
+ (name: string, oldName: string | undefined = entityName): string | null => {
if (!name || name.trim().length === 0) {
return createMessage(ACTION_INVALID_NAME_ERROR);
- } else if (name !== entityName && !isNameValid(name, usedEntityNames)) {
+ } else if (name !== oldName && !isNameValid(name, usedEntityNames)) {
return createMessage(nameErrorMessage, name);
}
diff --git a/app/client/src/IDE/index.ts b/app/client/src/IDE/index.ts
index a151c77f1b75..7c9d5c768c06 100644
--- a/app/client/src/IDE/index.ts
+++ b/app/client/src/IDE/index.ts
@@ -48,6 +48,7 @@ export {
EditableName,
RenameMenuItem,
useIsRenaming,
+ useValidateEntityName,
} from "./Components/EditableName";
/* ====================================================
diff --git a/app/client/src/actions/explorerActions.ts b/app/client/src/actions/explorerActions.ts
index 9e0c855795aa..6f7cdbd263e5 100644
--- a/app/client/src/actions/explorerActions.ts
+++ b/app/client/src/actions/explorerActions.ts
@@ -1,13 +1,18 @@
import { ReduxActionTypes } from "ee/constants/ReduxActionConstants";
-export const initExplorerEntityNameEdit = (actionId: string) => {
+export const initExplorerEntityNameEdit = (entityId: string) => {
return {
type: ReduxActionTypes.INIT_EXPLORER_ENTITY_NAME_EDIT,
payload: {
- id: actionId,
+ id: entityId,
},
};
};
+export const endExplorerEntityNameEdit = () => {
+ return {
+ type: ReduxActionTypes.END_EXPLORER_ENTITY_NAME_EDIT,
+ };
+};
/**
* action that make explorer pin/unpin
diff --git a/app/client/src/ce/entities/FeatureFlag.ts b/app/client/src/ce/entities/FeatureFlag.ts
index 4e3020fd98e2..a662c4807e10 100644
--- a/app/client/src/ce/entities/FeatureFlag.ts
+++ b/app/client/src/ce/entities/FeatureFlag.ts
@@ -55,6 +55,7 @@ export const FEATURE_FLAG = {
"config_mask_session_recordings_enabled",
config_user_session_recordings_enabled:
"config_user_session_recordings_enabled",
+ release_ads_entity_item_enabled: "release_ads_entity_item_enabled",
} as const;
export type FeatureFlag = keyof typeof FEATURE_FLAG;
@@ -101,6 +102,7 @@ export const DEFAULT_FEATURE_FLAG_VALUE: FeatureFlags = {
kill_session_recordings_enabled: false,
config_user_session_recordings_enabled: true,
config_mask_session_recordings_enabled: true,
+ release_ads_entity_item_enabled: false,
};
export const AB_TESTING_EVENT_KEYS = {
diff --git a/app/client/src/ce/selectors/entitiesSelector.ts b/app/client/src/ce/selectors/entitiesSelector.ts
index cf7870b146d8..a512b77ed0de 100644
--- a/app/client/src/ce/selectors/entitiesSelector.ts
+++ b/app/client/src/ce/selectors/entitiesSelector.ts
@@ -1072,9 +1072,6 @@ export const getExistingActionNames = createSelector(
},
);
-export const getEditingEntityName = (state: AppState) =>
- state.ui.explorer.entity.editingEntityName;
-
export const getExistingJSCollectionNames = createSelector(
getJSCollections,
(jsActions) =>
diff --git a/app/client/src/pages/Editor/Explorer/Widgets/OldWidgetEntityList.tsx b/app/client/src/pages/Editor/Explorer/Widgets/OldWidgetEntityList.tsx
new file mode 100644
index 000000000000..2edfd027f93b
--- /dev/null
+++ b/app/client/src/pages/Editor/Explorer/Widgets/OldWidgetEntityList.tsx
@@ -0,0 +1,43 @@
+import React, { useMemo } from "react";
+import styled from "styled-components";
+import { Flex } from "@appsmith/ads";
+import { useSelector } from "react-redux";
+import { getCurrentBasePageId } from "selectors/editorSelectors";
+import { selectWidgetsForCurrentPage } from "ee/selectors/entitiesSelector";
+import WidgetEntity from "./WidgetEntity";
+
+const ListContainer = styled(Flex)`
+ & .t--entity-item {
+ height: 32px;
+ }
+`;
+
+export const OldWidgetEntityList = () => {
+ const basePageId = useSelector(getCurrentBasePageId) as string;
+ const widgets = useSelector(selectWidgetsForCurrentPage);
+ const widgetsInStep = useMemo(() => {
+ return widgets?.children?.map((child) => child.widgetId) || [];
+ }, [widgets?.children]);
+
+ if (!widgets) return null;
+
+ if (!widgets.children) return null;
+
+ return (
+
+ {widgets.children.map((child) => (
+
+ ))}
+
+ );
+};
diff --git a/app/client/src/pages/Editor/Explorer/Widgets/WidgetContextMenu.tsx b/app/client/src/pages/Editor/Explorer/Widgets/WidgetContextMenu.tsx
index 01fbb2624ef9..921bfd15a228 100644
--- a/app/client/src/pages/Editor/Explorer/Widgets/WidgetContextMenu.tsx
+++ b/app/client/src/pages/Editor/Explorer/Widgets/WidgetContextMenu.tsx
@@ -2,15 +2,11 @@ import React, { useCallback } from "react";
import { useDispatch, useSelector } from "react-redux";
import { initExplorerEntityNameEdit } from "actions/explorerActions";
import type { AppState } from "ee/reducers";
-import {
- ReduxActionTypes,
- WidgetReduxActionTypes,
-} from "ee/constants/ReduxActionConstants";
-import WidgetFactory from "WidgetProvider/factory";
+import { ReduxActionTypes } from "ee/constants/ReduxActionConstants";
import { ENTITY_TYPE } from "entities/DataTree/dataTreeFactory";
import type { TreeDropdownOption } from "pages/Editor/Explorer/ContextMenu";
import ContextMenu from "pages/Editor/Explorer/ContextMenu";
-const WidgetTypes = WidgetFactory.widgetTypes;
+import { useDeleteWidget } from "pages/Editor/IDE/EditorPane/UI/UIEntityListTree/hooks/useDeleteWidget";
export function WidgetContextMenu(props: {
widgetId: string;
@@ -19,46 +15,14 @@ export function WidgetContextMenu(props: {
canManagePages?: boolean;
}) {
const { widgetId } = props;
- const parentId = useSelector((state: AppState) => {
- return state.ui.pageWidgets[props.pageId].dsl[props.widgetId].parentId;
- });
+
const widget = useSelector((state: AppState) => {
return state.ui.pageWidgets[props.pageId].dsl[props.widgetId];
});
- // TODO: Fix this the next time the file is edited
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const parentWidget: any = useSelector((state: AppState) => {
- if (parentId) return state.ui.pageWidgets[props.pageId].dsl[parentId];
-
- return {};
- });
const dispatch = useDispatch();
- const dispatchDelete = useCallback(() => {
- // If the widget is a tab we are updating the `tabs` of the property of the widget
- // This is similar to deleting a tab from the property pane
- if (widget.tabName && parentWidget.type === WidgetTypes.TABS_WIDGET) {
- const tabsObj = { ...parentWidget.tabsObj };
- const filteredTabs = Object.values(tabsObj);
-
- if (widget.parentId && !!filteredTabs.length) {
- dispatch({
- type: ReduxActionTypes.WIDGET_DELETE_TAB_CHILD,
- payload: { ...tabsObj[widget.tabId] },
- });
- }
- return;
- }
-
- dispatch({
- type: WidgetReduxActionTypes.WIDGET_DELETE,
- payload: {
- widgetId,
- parentId,
- },
- });
- }, [dispatch, widgetId, parentId, widget, parentWidget]);
+ const deleteWidget = useDeleteWidget(widgetId);
const showBinding = useCallback((widgetId, widgetName) => {
dispatch({
@@ -97,7 +61,7 @@ export function WidgetContextMenu(props: {
if (widget.isDeletable !== false && props.canManagePages) {
const option: TreeDropdownOption = {
value: "delete",
- onSelect: dispatchDelete,
+ onSelect: deleteWidget,
label: "Delete",
intent: "danger",
confirmDelete: true,
diff --git a/app/client/src/pages/Editor/Explorer/Widgets/WidgetEntity.tsx b/app/client/src/pages/Editor/Explorer/Widgets/WidgetEntity.tsx
index 13286bb4a76b..0783bfd93982 100644
--- a/app/client/src/pages/Editor/Explorer/Widgets/WidgetEntity.tsx
+++ b/app/client/src/pages/Editor/Explorer/Widgets/WidgetEntity.tsx
@@ -4,7 +4,6 @@ import Entity, { EntityClassNames } from "../Entity";
import type { WidgetProps } from "widgets/BaseWidget";
import type { WidgetType } from "constants/WidgetConstants";
import { useSelector } from "react-redux";
-import WidgetContextMenu from "./WidgetContextMenu";
import { updateWidgetName } from "actions/propertyPaneActions";
import type { CanvasStructure } from "reducers/uiReducers/pageCanvasStructureReducer";
import { getLastSelectedWidget, getSelectedWidgets } from "selectors/ui";
@@ -20,6 +19,7 @@ import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
import { getHasManagePagePermission } from "ee/utils/BusinessFeatures/permissionPageHelpers";
import { convertToPageIdSelector } from "selectors/pageListSelectors";
+import WidgetContextMenu from "./WidgetContextMenu";
export type WidgetTree = WidgetProps & { children?: WidgetTree[] };
diff --git a/app/client/src/pages/Editor/IDE/EditorPane/UI/List.tsx b/app/client/src/pages/Editor/IDE/EditorPane/UI/List.tsx
index 7bd081a473e0..843e9b33ecc8 100644
--- a/app/client/src/pages/Editor/IDE/EditorPane/UI/List.tsx
+++ b/app/client/src/pages/Editor/IDE/EditorPane/UI/List.tsx
@@ -1,13 +1,9 @@
import React, { useCallback, useEffect, useMemo } from "react";
import { Button, Flex } from "@appsmith/ads";
-import WidgetEntity from "pages/Editor/Explorer/Widgets/WidgetEntity";
import { useSelector } from "react-redux";
import { selectWidgetsForCurrentPage } from "ee/selectors/entitiesSelector";
-import {
- getCurrentBasePageId,
- getPagePermissions,
-} from "selectors/editorSelectors";
+import { getPagePermissions } from "selectors/editorSelectors";
import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
import { getHasManagePagePermission } from "ee/utils/BusinessFeatures/permissionPageHelpers";
@@ -15,19 +11,13 @@ import { createMessage, EDITOR_PANE_TEXTS } from "ee/constants/messages";
import { EmptyState } from "@appsmith/ads";
import history from "utils/history";
import { builderURL } from "ee/RouteBuilder";
-import styled from "styled-components";
-
-const ListContainer = styled(Flex)`
- & .t--entity-item {
- height: 32px;
- }
-`;
+import { UIEntityListTree } from "./UIEntityListTree";
+import { OldWidgetEntityList } from "pages/Editor/Explorer/Widgets/OldWidgetEntityList";
const ListWidgets = (props: {
setFocusSearchInput: (focusSearchInput: boolean) => void;
}) => {
const { setFocusSearchInput } = props;
- const basePageId = useSelector(getCurrentBasePageId) as string;
const widgets = useSelector(selectWidgetsForCurrentPage);
const pagePermissions = useSelector(getPagePermissions);
const isFeatureEnabled = useFeatureFlag(FEATURE_FLAG.license_gac_enabled);
@@ -37,10 +27,6 @@ const ListWidgets = (props: {
pagePermissions,
);
- const widgetsInStep = useMemo(() => {
- return widgets?.children?.map((child) => child.widgetId) || [];
- }, [widgets?.children]);
-
const addButtonClickHandler = useCallback(() => {
setFocusSearchInput(true);
history.push(builderURL({}));
@@ -66,13 +52,12 @@ const ListWidgets = (props: {
[addButtonClickHandler, canManagePages],
);
+ const isNewWidgetTreeEnabled = useFeatureFlag(
+ FEATURE_FLAG.release_ads_entity_item_enabled,
+ );
+
return (
-
+
{!widgetsExist ? (
/* If no widgets exist, show the blank state */
- {widgets?.children?.map((child) => (
-
- ))}
+ {isNewWidgetTreeEnabled ? (
+
+ ) : (
+
+ )}
) : null}
-
+
);
};
diff --git a/app/client/src/pages/Editor/IDE/EditorPane/UI/UIEntityListTree/UIEntityListTree.tsx b/app/client/src/pages/Editor/IDE/EditorPane/UI/UIEntityListTree/UIEntityListTree.tsx
new file mode 100644
index 000000000000..de93158cfbad
--- /dev/null
+++ b/app/client/src/pages/Editor/IDE/EditorPane/UI/UIEntityListTree/UIEntityListTree.tsx
@@ -0,0 +1,73 @@
+import React, { useCallback } from "react";
+import { EntityListTree } from "@appsmith/ads";
+import { useDispatch, useSelector } from "react-redux";
+import { selectWidgetsForCurrentPage } from "ee/selectors/entitiesSelector";
+import { getSelectedWidgets } from "selectors/ui";
+import { getPagePermissions } from "selectors/editorSelectors";
+import { getHasManagePagePermission } from "ee/utils/BusinessFeatures/permissionPageHelpers";
+import { useFeatureFlag } from "utils/hooks/useFeatureFlag";
+import { FEATURE_FLAG } from "ee/entities/FeatureFlag";
+import { useValidateEntityName } from "IDE";
+import { updateWidgetName } from "actions/propertyPaneActions";
+import { WidgetContextMenu } from "./WidgetContextMenu";
+import { useSwitchToWidget } from "./hooks/useSwitchToWidget";
+import { WidgetTypeIcon } from "./WidgetTypeIcon";
+import { useWidgetTreeState } from "./hooks/useWidgetTreeExpandedState";
+import { enhanceItemsTree } from "./utils/enhanceTree";
+import { useNameEditorState } from "../../hooks/useNameEditorState";
+
+export const UIEntityListTree = () => {
+ const widgets = useSelector(selectWidgetsForCurrentPage);
+ const selectedWidgets = useSelector(getSelectedWidgets);
+
+ const switchToWidget = useSwitchToWidget();
+
+ const isFeatureEnabled = useFeatureFlag(FEATURE_FLAG.license_gac_enabled);
+ const pagePermissions = useSelector(getPagePermissions);
+ const canManagePages = getHasManagePagePermission(
+ isFeatureEnabled,
+ pagePermissions,
+ );
+ const dispatch = useDispatch();
+
+ const handleNameSave = useCallback(
+ (id: string, newName: string) => {
+ dispatch(updateWidgetName(id, newName));
+ },
+ [dispatch],
+ );
+
+ const { editingEntity, enterEditMode, exitEditMode, updatingEntity } =
+ useNameEditorState();
+
+ const validateName = useValidateEntityName({});
+
+ const { expandedWidgets, handleExpand } = useWidgetTreeState();
+
+ const items = enhanceItemsTree(widgets?.children || [], (widget) => ({
+ id: widget.widgetId,
+ title: widget.widgetName,
+ startIcon: ,
+ isSelected: selectedWidgets.includes(widget.widgetId),
+ isExpanded: expandedWidgets.includes(widget.widgetId),
+ onClick: (e) => switchToWidget(e, widget),
+ onDoubleClick: () => enterEditMode(widget.widgetId),
+ rightControl: (
+
+ ),
+ rightControlVisibility: "hover",
+ nameEditorConfig: {
+ canEdit: canManagePages,
+ isLoading: updatingEntity === widget.widgetId,
+ isEditing: editingEntity === widget.widgetId,
+ onNameSave: (newName) => handleNameSave(widget.widgetId, newName),
+ onEditComplete: exitEditMode,
+ validateName: (newName) => validateName(newName, widget.widgetName),
+ },
+ }));
+
+ return ;
+};
diff --git a/app/client/src/pages/Editor/IDE/EditorPane/UI/UIEntityListTree/WidgetContextMenu.tsx b/app/client/src/pages/Editor/IDE/EditorPane/UI/UIEntityListTree/WidgetContextMenu.tsx
new file mode 100644
index 000000000000..94793e042162
--- /dev/null
+++ b/app/client/src/pages/Editor/IDE/EditorPane/UI/UIEntityListTree/WidgetContextMenu.tsx
@@ -0,0 +1,101 @@
+import React, { useMemo } from "react";
+import { useDispatch, useSelector } from "react-redux";
+import { getWidgetByID } from "sagas/selectors";
+import { useCallback } from "react";
+import { ReduxActionTypes } from "ee/constants/ReduxActionConstants";
+import { ENTITY_TYPE } from "ee/entities/DataTree/types";
+import { initExplorerEntityNameEdit } from "actions/explorerActions";
+import {
+ Button,
+ Menu,
+ MenuContent,
+ MenuItem,
+ MenuTrigger,
+} from "@appsmith/ads";
+import { useBoolean } from "usehooks-ts";
+import {
+ CONTEXT_DELETE,
+ CONTEXT_RENAME,
+ CONTEXT_SHOW_BINDING,
+ createMessage,
+} from "ee/constants/messages";
+import { useDeleteWidget } from "./hooks/useDeleteWidget";
+
+export const WidgetContextMenu = (props: {
+ widgetId: string;
+ canManagePages: boolean;
+}) => {
+ const { canManagePages, widgetId } = props;
+ const { toggle: toggleMenuOpen, value: isMenuOpen } = useBoolean(false);
+ const dispatch = useDispatch();
+
+ const widget = useSelector(getWidgetByID(widgetId));
+
+ const showBinding = useCallback(() => {
+ dispatch({
+ type: ReduxActionTypes.SET_ENTITY_INFO,
+ payload: {
+ entityId: widgetId,
+ entityName: widget?.widgetName,
+ entityType: ENTITY_TYPE.WIDGET,
+ show: true,
+ },
+ });
+ }, [dispatch, widget?.widgetName, widgetId]);
+
+ const editWidgetName = useCallback(() => {
+ // We add a delay to avoid having the focus stuck in the menu trigger
+ setTimeout(() => {
+ dispatch(initExplorerEntityNameEdit(widgetId));
+ }, 100);
+ }, [dispatch, widgetId]);
+
+ const deleteWidget = useDeleteWidget(widgetId);
+
+ const menuContent = useMemo(() => {
+ return (
+ <>
+
+
+
+ >
+ );
+ }, [
+ canManagePages,
+ deleteWidget,
+ editWidgetName,
+ showBinding,
+ widget?.isDeletable,
+ ]);
+
+ return (
+
+ );
+};
diff --git a/app/client/src/pages/Editor/IDE/EditorPane/UI/UIEntityListTree/WidgetTypeIcon.tsx b/app/client/src/pages/Editor/IDE/EditorPane/UI/UIEntityListTree/WidgetTypeIcon.tsx
new file mode 100644
index 000000000000..77f88d885a84
--- /dev/null
+++ b/app/client/src/pages/Editor/IDE/EditorPane/UI/UIEntityListTree/WidgetTypeIcon.tsx
@@ -0,0 +1,21 @@
+import React from "react";
+import WidgetFactory from "WidgetProvider/factory";
+import WidgetIcon from "pages/Editor/Explorer/Widgets/WidgetIcon";
+
+interface WidgetTypeIconProps {
+ type: string;
+}
+
+export const WidgetTypeIcon: React.FC = React.memo(
+ ({ type }) => {
+ const { IconCmp } = WidgetFactory.getWidgetMethods(type);
+
+ if (IconCmp) {
+ return ;
+ }
+
+ return ;
+ },
+);
+
+WidgetTypeIcon.displayName = "WidgetTypeIcon";
diff --git a/app/client/src/pages/Editor/IDE/EditorPane/UI/UIEntityListTree/hooks/useDeleteWidget.ts b/app/client/src/pages/Editor/IDE/EditorPane/UI/UIEntityListTree/hooks/useDeleteWidget.ts
new file mode 100644
index 000000000000..5acb4e87b908
--- /dev/null
+++ b/app/client/src/pages/Editor/IDE/EditorPane/UI/UIEntityListTree/hooks/useDeleteWidget.ts
@@ -0,0 +1,48 @@
+import { useDispatch, useSelector } from "react-redux";
+import { getWidgetByID } from "sagas/selectors";
+import { useCallback } from "react";
+import {
+ ReduxActionTypes,
+ WidgetReduxActionTypes,
+} from "ee/constants/ReduxActionConstants";
+import { getParentWidget } from "selectors/widgetSelectors";
+import WidgetFactory from "WidgetProvider/factory";
+
+const WidgetTypes = WidgetFactory.widgetTypes;
+
+export function useDeleteWidget(widgetId: string): () => void {
+ const dispatch = useDispatch();
+ const widget = useSelector(getWidgetByID(widgetId));
+
+ const parentWidget = useSelector((state) => getParentWidget(state, widgetId));
+
+ return useCallback(() => {
+ // If the widget is a tab we are updating the `tabs` of the property of the widget
+ // This is similar to deleting a tab from the property pane
+ if (
+ widget?.tabName &&
+ parentWidget &&
+ parentWidget.type === WidgetTypes.TABS_WIDGET
+ ) {
+ const tabsObj = { ...parentWidget.tabsObj };
+ const filteredTabs = Object.values(tabsObj);
+
+ if (widget?.parentId && !!filteredTabs.length) {
+ dispatch({
+ type: ReduxActionTypes.WIDGET_DELETE_TAB_CHILD,
+ payload: { ...tabsObj[widget?.tabId] },
+ });
+ }
+
+ return;
+ }
+
+ dispatch({
+ type: WidgetReduxActionTypes.WIDGET_DELETE,
+ payload: {
+ widgetId,
+ parentId: widget?.parentId,
+ },
+ });
+ }, [dispatch, parentWidget, widget, widgetId]);
+}
diff --git a/app/client/src/pages/Editor/IDE/EditorPane/UI/UIEntityListTree/hooks/useSwitchToWidget.ts b/app/client/src/pages/Editor/IDE/EditorPane/UI/UIEntityListTree/hooks/useSwitchToWidget.ts
new file mode 100644
index 000000000000..726ca03023ff
--- /dev/null
+++ b/app/client/src/pages/Editor/IDE/EditorPane/UI/UIEntityListTree/hooks/useSwitchToWidget.ts
@@ -0,0 +1,42 @@
+import { useCallback, type MouseEvent } from "react";
+import type { CanvasStructure } from "reducers/uiReducers/pageCanvasStructureReducer";
+import AnalyticsUtil from "ee/utils/AnalyticsUtil";
+import { builderURL } from "ee/RouteBuilder";
+import { NavigationMethod } from "utils/history";
+import { useNavigateToWidget } from "pages/Editor/Explorer/Widgets/useNavigateToWidget";
+import { useSelector } from "react-redux";
+import { getCurrentBasePageId } from "selectors/editorSelectors";
+import { getSelectedWidgets } from "selectors/ui";
+
+export function useSwitchToWidget() {
+ const { navigateToWidget } = useNavigateToWidget();
+ const basePageId = useSelector(getCurrentBasePageId) as string;
+ const selectedWidgets = useSelector(getSelectedWidgets);
+
+ return useCallback(
+ (e: MouseEvent, widget: CanvasStructure) => {
+ const isMultiSelect = e.metaKey || e.ctrlKey;
+ const isShiftSelect = e.shiftKey;
+
+ AnalyticsUtil.logEvent("ENTITY_EXPLORER_CLICK", {
+ type: "WIDGETS",
+ fromUrl: location.pathname,
+ toUrl: `${builderURL({
+ basePageId,
+ hash: widget.widgetId,
+ })}`,
+ name: widget.widgetName,
+ });
+ navigateToWidget(
+ widget.widgetId,
+ widget.type,
+ basePageId,
+ NavigationMethod.EntityExplorer,
+ selectedWidgets.includes(widget.widgetId),
+ isMultiSelect,
+ isShiftSelect,
+ );
+ },
+ [basePageId, navigateToWidget, selectedWidgets],
+ );
+}
diff --git a/app/client/src/pages/Editor/IDE/EditorPane/UI/UIEntityListTree/hooks/useWidgetTreeExpandedState.ts b/app/client/src/pages/Editor/IDE/EditorPane/UI/UIEntityListTree/hooks/useWidgetTreeExpandedState.ts
new file mode 100644
index 000000000000..fcbcb74320ed
--- /dev/null
+++ b/app/client/src/pages/Editor/IDE/EditorPane/UI/UIEntityListTree/hooks/useWidgetTreeExpandedState.ts
@@ -0,0 +1,34 @@
+import { useCallback, useEffect, useState } from "react";
+import { useSelector } from "react-redux";
+import { getEntityExplorerWidgetsToExpand } from "selectors/widgetSelectors";
+
+export const useWidgetTreeState = () => {
+ const widgetsToExpand = useSelector(getEntityExplorerWidgetsToExpand);
+ const [expandedWidgets, setExpandedWidgets] =
+ useState(widgetsToExpand);
+
+ const handleExpand = useCallback((id: string) => {
+ setExpandedWidgets((prev) =>
+ prev.includes(id)
+ ? prev.filter((widgetId) => widgetId !== id)
+ : [...prev, id],
+ );
+ }, []);
+
+ useEffect(
+ function handleExpandedWidgetsUpdate() {
+ // Merge current expanded with new list
+ // This is to ensure that the expanded widgets are not lost when the list is updated
+ setExpandedWidgets((prev) => [
+ ...prev,
+ ...widgetsToExpand.filter((widgetId) => !prev.includes(widgetId)),
+ ]);
+ },
+ [widgetsToExpand],
+ );
+
+ return {
+ expandedWidgets,
+ handleExpand,
+ };
+};
diff --git a/app/client/src/pages/Editor/IDE/EditorPane/UI/UIEntityListTree/index.ts b/app/client/src/pages/Editor/IDE/EditorPane/UI/UIEntityListTree/index.ts
new file mode 100644
index 000000000000..144913371e46
--- /dev/null
+++ b/app/client/src/pages/Editor/IDE/EditorPane/UI/UIEntityListTree/index.ts
@@ -0,0 +1 @@
+export { UIEntityListTree } from "./UIEntityListTree";
diff --git a/app/client/src/pages/Editor/IDE/EditorPane/UI/UIEntityListTree/utils/enhanceTree.ts b/app/client/src/pages/Editor/IDE/EditorPane/UI/UIEntityListTree/utils/enhanceTree.ts
new file mode 100644
index 000000000000..f39a39bc40fe
--- /dev/null
+++ b/app/client/src/pages/Editor/IDE/EditorPane/UI/UIEntityListTree/utils/enhanceTree.ts
@@ -0,0 +1,16 @@
+import type { CanvasStructure } from "reducers/uiReducers/pageCanvasStructureReducer";
+import type { EntityListTreeItem } from "@appsmith/ads";
+
+export const enhanceItemsTree = (
+ items: CanvasStructure[],
+ enhancer: (item: CanvasStructure) => EntityListTreeItem,
+) => {
+ return items.map((child): EntityListTreeItem => {
+ return {
+ ...enhancer(child),
+ children: child.children
+ ? enhanceItemsTree(child.children, enhancer)
+ : undefined,
+ };
+ });
+};
diff --git a/app/client/src/pages/Editor/IDE/EditorPane/hooks/useNameEditorState.ts b/app/client/src/pages/Editor/IDE/EditorPane/hooks/useNameEditorState.ts
new file mode 100644
index 000000000000..364bf7a23432
--- /dev/null
+++ b/app/client/src/pages/Editor/IDE/EditorPane/hooks/useNameEditorState.ts
@@ -0,0 +1,36 @@
+import { useCallback } from "react";
+import { useDispatch, useSelector } from "react-redux";
+import { initExplorerEntityNameEdit } from "actions/explorerActions";
+import { ReduxActionTypes } from "ee/constants/ReduxActionConstants";
+import {
+ getUpdatingEntity,
+ getEditingEntityName,
+} from "selectors/explorerSelector";
+
+export function useNameEditorState() {
+ const dispatch = useDispatch();
+
+ const editingEntity = useSelector(getEditingEntityName);
+
+ const updatingEntity = useSelector(getUpdatingEntity);
+
+ const enterEditMode = useCallback(
+ (id: string) => {
+ dispatch(initExplorerEntityNameEdit(id));
+ },
+ [dispatch],
+ );
+
+ const exitEditMode = useCallback(() => {
+ dispatch({
+ type: ReduxActionTypes.END_EXPLORER_ENTITY_NAME_EDIT,
+ });
+ }, [dispatch]);
+
+ return {
+ enterEditMode,
+ exitEditMode,
+ editingEntity,
+ updatingEntity,
+ };
+}
diff --git a/app/client/src/selectors/explorerSelector.ts b/app/client/src/selectors/explorerSelector.ts
index 58df2735748b..609c77d95559 100644
--- a/app/client/src/selectors/explorerSelector.ts
+++ b/app/client/src/selectors/explorerSelector.ts
@@ -34,3 +34,5 @@ export const getExplorerActive = (state: AppState) => {
export const getUpdatingEntity = (state: AppState) => {
return state.ui.explorer.entity.updatingEntity;
};
+export const getEditingEntityName = (state: AppState) =>
+ state.ui.explorer.entity.editingEntityName;