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 ( + <> + + {createMessage(CONTEXT_SHOW_BINDING)} + + + {createMessage(CONTEXT_RENAME)} + + + {createMessage(CONTEXT_DELETE)} + + + ); + }, [ + 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;