Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<HTMLSpanElement, MouseEvent>) => {
Expand Down Expand Up @@ -51,7 +50,7 @@ export function EntityListTree(props: EntityListTreeProps) {
{item.children && item.children.length ? (
<CollapseWrapper
data-itemid={item.id}
data-testid="entity-item-expand-icon"
data-testid="t--entity-item-expand-icon"
onClick={handleOnExpandClick}
>
<Icon
Expand All @@ -65,11 +64,12 @@ export function EntityListTree(props: EntityListTreeProps) {
<CollapseSpacer />
)}
<PaddingOverrider>
<EntityItem {...item} />
<ItemComponent item={item} />
</PaddingOverrider>
</EntityItemWrapper>
{item.children && item.isExpanded ? (
<EntityListTree
ItemComponent={ItemComponent}
depth={childrenDepth}
items={item.children}
onItemExpand={onItemExpand}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import type { EntityItemProps } from "../EntityItem/EntityItem.types";

export interface EntityListTreeItem extends EntityItemProps {
export interface EntityListTreeItem {
children?: EntityListTreeItem[];
isExpanded: boolean;
isSelected: boolean;
isDisabled?: boolean;
id: string;
}
Comment thread
hetunandu marked this conversation as resolved.

export interface EntityListTreeProps {
depth?: number;
items: EntityListTreeItem[];
ItemComponent: React.ComponentType<{ item: EntityListTreeItem }>;
onItemExpand: (id: string) => void;
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,31 @@ import { getUsedActionNames } from "selectors/actionSelectors";
import { isNameValid } from "utils/helpers";

interface UseValidateEntityNameProps {
entityName?: string;
entityName: string;
entityId?: string;
nameErrorMessage?: (name: string) => string;
}

/**
* 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);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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: <WidgetTypeIcon type={widget.type} />,
isSelected: selectedWidgets.includes(widget.widgetId),
isExpanded: expandedWidgets.includes(widget.widgetId),
onClick: (e) => switchToWidget(e, widget),
onDoubleClick: () => enterEditMode(widget.widgetId),
rightControl: (
<WidgetContextMenu
canManagePages={canManagePages}
widgetId={widget.widgetId}
/>
),
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 <EntityListTree items={items} onItemExpand={handleExpand} />;
return (
<EntityListTree
ItemComponent={WidgetTreeItem}
items={items}
onItemExpand={handleExpand}
/>
);
};
Original file line number Diff line number Diff line change
@@ -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(
() => <WidgetTypeIcon type={widget.type} />,
[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(
() => (
<WidgetContextMenu
canManagePages={canManagePages}
widgetId={widget.widgetId}
/>
),
[canManagePages, widget.widgetId],
);

return (
<EntityItem
id={item.id}
isSelected={item.isSelected}
nameEditorConfig={nameEditorConfig}
onClick={onClick}
onDoubleClick={onDoubleClick}
rightControl={rightControl}
rightControlVisibility="hover"
startIcon={startIcon}
title={widget.widgetName}
/>
);
};
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
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";
import type { WidgetType } from "constants/WidgetConstants";

export function useSwitchToWidget() {
const { navigateToWidget } = useNavigateToWidget();
const basePageId = useSelector(getCurrentBasePageId) as string;
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;

Expand Down