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 index 93ac90474a..c35e443c04 100644 --- 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 @@ -9,6 +9,7 @@ import type { } from "./EntityListTree.types"; import { ExplorerContainer } from "../ExplorerContainer"; import { Flex, Icon } from "../../.."; +import { EntityItem } from "../EntityItem"; import { noop } from "lodash"; const meta: Meta = { @@ -18,7 +19,6 @@ const meta: Meta = { export default meta; -const onClick = noop; const nameEditorConfig = { canEdit: true, isEditing: false, @@ -28,60 +28,50 @@ const nameEditorConfig = { validateName: () => null, }; +const names = { + "1": "Parent 1", + "1.1": "Child 1.1", + "1.1.1": "Child 1.1.1", + "1.1.2": "Child 1.1.2", + "1.2": "Child 1.2", + "2": "Parent 2", +}; + const Tree: EntityListTreeProps["items"] = [ { - startIcon: , id: "1", - title: "Parent 1", isExpanded: true, - onClick, - nameEditorConfig, + isSelected: false, 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, + isSelected: false, }, { - startIcon: , id: "1.1.2", isDisabled: true, - title: "Grandchild 2", isExpanded: false, - onClick, - nameEditorConfig, + isSelected: false, }, ], }, { - startIcon: , id: "1.2", - title: "Child 2", isExpanded: false, - onClick, - nameEditorConfig, + isSelected: false, }, ], }, { - startIcon: , id: "2", - title: "Parent 2", isExpanded: false, - onClick, - nameEditorConfig, + isSelected: false, }, ]; @@ -97,28 +87,9 @@ const treeUpdate = ( }); }; -const Template = (props: { outsideSelection: string }) => { - const [expanded, setExpanded] = React.useState>({}); - const [selected, setSelected] = React.useState( - props.outsideSelection, - ); +const EntityItemComponent = (props: { item: EntityListTreeItem }) => { + const { item } = props; 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); }; @@ -127,27 +98,56 @@ const Template = (props: { outsideSelection: string }) => { setEditing(null); }; + return ( + null, + }} + onClick={noop} + onDoubleClick={() => onItemEdit(item.id)} + startIcon={} + title={names[item.id as keyof typeof names] || item.id} + /> + ); +}; + +const Template = (props: { selectedItem: string }) => { + const [expanded, setExpanded] = React.useState>({}); + const [selected, setSelected] = React.useState( + props.selectedItem, + ); + + useEffect( + function handleSyncOfSelection() { + setSelected(props.selectedItem); + }, + [props.selectedItem], + ); + + const onExpandClick = (id: string) => { + setExpanded((prev) => ({ ...prev, [id]: !Boolean(prev[id]) })); + }; + 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 ( - + @@ -157,5 +157,5 @@ const Template = (props: { outsideSelection: string }) => { export const Basic = Template.bind({}) as StoryObj; Basic.args = { - outsideSelection: "1", + selectedItem: "1", }; 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 index db07f6d656..bcb195043f 100644 --- 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 @@ -1,39 +1,41 @@ import React from "react"; import { render, screen, fireEvent } from "@testing-library/react"; import { EntityListTree } from "./EntityListTree"; -import type { EntityListTreeProps } from "./EntityListTree.types"; +import type { + EntityListTreeItem, + 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 name = { + "1": "Parent 1", + "1.1": "Child 1.1", + "1.1.1": "Child 1.1.1", + "1.1.2": "Child 1.1.2", + "1.2": "Child 1.2", + "2": "No Children Parent", + "1-1": "Child", }; -const mockOnClick = jest.fn(); +const ItemComponent = ({ item }: { item: EntityListTreeItem }) => { + return
{name[item.id as keyof typeof name] || item.id}
; +}; const defaultProps: EntityListTreeProps = { + ItemComponent, 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: [], }, ], @@ -50,7 +52,7 @@ describe("EntityListTree", () => { it("calls onItemExpand when expand icon is clicked", () => { render(); - const expandIcon = screen.getByTestId("entity-item-expand-icon"); + const expandIcon = screen.getByTestId("t--entity-item-expand-icon"); fireEvent.click(expandIcon); expect(mockOnItemExpand).toHaveBeenCalledWith("1"); @@ -62,19 +64,16 @@ describe("EntityListTree", () => { 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"); + const expandIcon = screen.queryByTestId("t--entity-item-expand-icon"); expect( screen.getByRole("treeitem", { name: "No Children Parent" }), @@ -88,21 +87,15 @@ describe("EntityListTree", () => { 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: [], }, ], 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 index 00d06cfa55..86c8a56333 100644 --- 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 @@ -2,7 +2,6 @@ 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, @@ -11,7 +10,7 @@ import { } from "./EntityListTree.styles"; export function EntityListTree(props: EntityListTreeProps) { - const { onItemExpand } = props; + const { ItemComponent, onItemExpand } = props; const handleOnExpandClick = useCallback( (event: React.MouseEvent) => { @@ -51,7 +50,7 @@ export function EntityListTree(props: EntityListTreeProps) { {item.children && item.children.length ? ( )} - + {item.children && item.isExpanded ? ( ; onItemExpand: (id: string) => void; } diff --git a/app/client/src/IDE/Components/EditableName/useValidateEntityName.ts b/app/client/src/IDE/Components/EditableName/useValidateEntityName.ts index a1ebb5a8b4..28505d10a5 100644 --- a/app/client/src/IDE/Components/EditableName/useValidateEntityName.ts +++ b/app/client/src/IDE/Components/EditableName/useValidateEntityName.ts @@ -10,7 +10,8 @@ import { getUsedActionNames } from "selectors/actionSelectors"; import { isNameValid } from "utils/helpers"; interface UseValidateEntityNameProps { - entityName?: string; + entityName: string; + entityId?: string; nameErrorMessage?: (name: string) => string; } @@ -18,18 +19,22 @@ interface UseValidateEntityNameProps { * Provides a unified way to validate entity names. */ export function useValidateEntityName(props: UseValidateEntityNameProps) { - const { entityName, nameErrorMessage = ACTION_NAME_CONFLICT_ERROR } = props; + const { + entityId = "", + entityName, + nameErrorMessage = ACTION_NAME_CONFLICT_ERROR, + } = props; const usedEntityNames = useSelector( - (state: AppState) => getUsedActionNames(state, ""), + (state: AppState) => getUsedActionNames(state, entityId), shallowEqual, ); return useCallback( - (name: string, oldName: string | undefined = entityName): string | null => { + (name: string): string | null => { if (!name || name.trim().length === 0) { return createMessage(ACTION_INVALID_NAME_ERROR); - } else if (name !== oldName && !isNameValid(name, usedEntityNames)) { + } else if (name !== entityName && !isNameValid(name, usedEntityNames)) { return createMessage(nameErrorMessage, name); } diff --git a/app/client/src/pages/AppIDE/components/JSEntityItem/JSEntityItem.tsx b/app/client/src/pages/AppIDE/components/JSEntityItem/JSEntityItem.tsx index def9e6e290..45cc149bce 100644 --- a/app/client/src/pages/AppIDE/components/JSEntityItem/JSEntityItem.tsx +++ b/app/client/src/pages/AppIDE/components/JSEntityItem/JSEntityItem.tsx @@ -83,7 +83,7 @@ export const JSEntityItem = ({ item }: { item: EntityItemProps }) => { onEditComplete: exitEditMode, onNameSave: (newName: string) => dispatch(saveJSObjectNameBasedOnIdeType(jsAction.id, newName, ideType)), - validateName: (newName: string) => validateName(newName, item.title), + validateName: (newName: string) => validateName(newName), }; }, [ canManageJSAction, diff --git a/app/client/src/pages/AppIDE/components/QueryEntityItem/QueryEntityItem.tsx b/app/client/src/pages/AppIDE/components/QueryEntityItem/QueryEntityItem.tsx index 22119c28f9..037ef273ff 100644 --- a/app/client/src/pages/AppIDE/components/QueryEntityItem/QueryEntityItem.tsx +++ b/app/client/src/pages/AppIDE/components/QueryEntityItem/QueryEntityItem.tsx @@ -99,7 +99,7 @@ export const QueryEntityItem = ({ item }: { item: EntityItemProps }) => { onEditComplete: exitEditMode, onNameSave: (newName: string) => dispatch(saveActionNameBasedOnIdeType(action.id, newName, ideType)), - validateName: (newName: string) => validateName(newName, item.title), + validateName: (newName: string) => validateName(newName), }; }, [ canManageAction, diff --git a/app/client/src/pages/AppIDE/components/UIEntityListTree/UIEntityListTree.tsx b/app/client/src/pages/AppIDE/components/UIEntityListTree/UIEntityListTree.tsx index 49785b554e..dc7352cbe6 100644 --- a/app/client/src/pages/AppIDE/components/UIEntityListTree/UIEntityListTree.tsx +++ b/app/client/src/pages/AppIDE/components/UIEntityListTree/UIEntityListTree.tsx @@ -1,73 +1,29 @@ -import React, { useCallback } from "react"; +import React from "react"; import { EntityListTree } from "@appsmith/ads"; -import { useDispatch, useSelector } from "react-redux"; +import { 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 "IDE/hooks/useNameEditorState"; +import { WidgetTreeItem } from "./WidgetTreeItem"; 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 ; + return ( + + ); }; diff --git a/app/client/src/pages/AppIDE/components/UIEntityListTree/WidgetTreeItem.tsx b/app/client/src/pages/AppIDE/components/UIEntityListTree/WidgetTreeItem.tsx new file mode 100644 index 0000000000..5513a0542f --- /dev/null +++ b/app/client/src/pages/AppIDE/components/UIEntityListTree/WidgetTreeItem.tsx @@ -0,0 +1,107 @@ +import React, { useCallback, useMemo } from "react"; +import { type EntityListTreeItem, EntityItem } from "@appsmith/ads"; +import { WidgetContextMenu } from "./WidgetContextMenu"; +import { useDispatch, useSelector } from "react-redux"; +import { getWidgetByID } from "sagas/selectors"; +import { updateWidgetName } from "actions/propertyPaneActions"; +import { FEATURE_FLAG } from "ee/entities/FeatureFlag"; +import { getHasManagePagePermission } from "ee/utils/BusinessFeatures/permissionPageHelpers"; +import { useValidateEntityName } from "IDE"; +import { useNameEditorState } from "IDE/hooks/useNameEditorState"; +import { getPagePermissions } from "selectors/editorSelectors"; +import { useFeatureFlag } from "utils/hooks/useFeatureFlag"; +import { useSwitchToWidget } from "./hooks/useSwitchToWidget"; +import { WidgetTypeIcon } from "./WidgetTypeIcon"; + +export const WidgetTreeItem = ({ item }: { item: EntityListTreeItem }) => { + const widget = useSelector(getWidgetByID(item.id)); + 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({ + entityName: widget.widgetName, + entityId: widget.widgetId, + }); + + const isLoading = updatingEntity === widget.widgetId; + const isEditing = editingEntity === widget.widgetId; + const onNameSave = useCallback( + (newName: string) => handleNameSave(widget.widgetId, newName), + [handleNameSave, widget.widgetId], + ); + + const nameEditorConfig = useMemo( + () => ({ + canEdit: canManagePages, + isLoading, + isEditing, + onNameSave, + onEditComplete: exitEditMode, + validateName, + }), + [ + canManagePages, + exitEditMode, + isEditing, + isLoading, + onNameSave, + validateName, + ], + ); + + const startIcon = useMemo( + () => , + [widget.type], + ); + + const onClick = useCallback( + (e: React.MouseEvent) => switchToWidget(e, widget), + [switchToWidget, widget], + ); + + const onDoubleClick = useCallback( + () => enterEditMode(widget.widgetId), + [enterEditMode, widget.widgetId], + ); + + const rightControl = useMemo( + () => ( + + ), + [canManagePages, widget.widgetId], + ); + + return ( + + ); +}; diff --git a/app/client/src/pages/AppIDE/components/UIEntityListTree/hooks/useSwitchToWidget.ts b/app/client/src/pages/AppIDE/components/UIEntityListTree/hooks/useSwitchToWidget.ts index 726ca03023..6f7df8ba6e 100644 --- a/app/client/src/pages/AppIDE/components/UIEntityListTree/hooks/useSwitchToWidget.ts +++ b/app/client/src/pages/AppIDE/components/UIEntityListTree/hooks/useSwitchToWidget.ts @@ -1,5 +1,4 @@ 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"; @@ -7,6 +6,7 @@ import { useNavigateToWidget } from "pages/Editor/Explorer/Widgets/useNavigateTo import { useSelector } from "react-redux"; import { getCurrentBasePageId } from "selectors/editorSelectors"; import { getSelectedWidgets } from "selectors/ui"; +import type { WidgetType } from "constants/WidgetConstants"; export function useSwitchToWidget() { const { navigateToWidget } = useNavigateToWidget(); @@ -14,7 +14,10 @@ export function useSwitchToWidget() { const selectedWidgets = useSelector(getSelectedWidgets); return useCallback( - (e: MouseEvent, widget: CanvasStructure) => { + ( + e: MouseEvent, + widget: { widgetId: string; widgetName: string; type: WidgetType }, + ) => { const isMultiSelect = e.metaKey || e.ctrlKey; const isShiftSelect = e.shiftKey;