feat: ADS Entity List Tree (#38493)
Co-authored-by: Ankita Kinger <ankita@appsmith.com>
This commit is contained in:
parent
d52a0f283a
commit
f8784fcf6b
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import styled from "styled-components";
|
||||
import { Text } from "../../..";
|
||||
import { Text } from "../../../Text";
|
||||
|
||||
export const EntityEditableName = styled(Text)`
|
||||
overflow: hidden;
|
||||
|
|
|
|||
|
|
@ -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 <Spinner size="md" />;
|
||||
}
|
||||
|
||||
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 <Spinner size="md" />;
|
||||
}
|
||||
|
||||
return props.startIcon;
|
||||
}, [isLoading, props.startIcon]);
|
||||
|
||||
// Use List Item custom title prop to show the editable name
|
||||
const customTitle = useMemo(() => {
|
||||
return (
|
||||
<Tooltip
|
||||
|
|
@ -75,6 +80,15 @@ export const EntityItem = (props: EntityItemProps) => {
|
|||
);
|
||||
}, [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 (
|
||||
<ListItem
|
||||
{...props}
|
||||
|
|
@ -82,6 +96,7 @@ export const EntityItem = (props: EntityItemProps) => {
|
|||
customTitleComponent={customTitle}
|
||||
data-testid={`t--entity-item-${props.title}`}
|
||||
id={"entity-" + props.id}
|
||||
rightControl={rightControl}
|
||||
startIcon={startIcon}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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<typeof EntityListTree> = {
|
||||
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: <Icon name="apps-line" />,
|
||||
id: "1",
|
||||
title: "Parent 1",
|
||||
isExpanded: true,
|
||||
onClick,
|
||||
nameEditorConfig,
|
||||
children: [
|
||||
{
|
||||
startIcon: <Icon name="apps-line" />,
|
||||
id: "1.1",
|
||||
title: "Child 1",
|
||||
isExpanded: false,
|
||||
isSelected: true,
|
||||
onClick,
|
||||
nameEditorConfig,
|
||||
children: [
|
||||
{
|
||||
startIcon: <Icon name="apps-line" />,
|
||||
id: "1.1.1",
|
||||
title: "Grandchild 1",
|
||||
isExpanded: false,
|
||||
onClick,
|
||||
nameEditorConfig,
|
||||
},
|
||||
{
|
||||
startIcon: <Icon name="apps-line" />,
|
||||
id: "1.1.2",
|
||||
isDisabled: true,
|
||||
title: "Grandchild 2",
|
||||
isExpanded: false,
|
||||
onClick,
|
||||
nameEditorConfig,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
startIcon: <Icon name="apps-line" />,
|
||||
id: "1.2",
|
||||
title: "Child 2",
|
||||
isExpanded: false,
|
||||
onClick,
|
||||
nameEditorConfig,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
startIcon: <Icon name="apps-line" />,
|
||||
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<Record<string, boolean>>({});
|
||||
const [selected, setSelected] = React.useState<string | null>(
|
||||
props.outsideSelection,
|
||||
);
|
||||
const [editing, setEditing] = React.useState<string | null>(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 (
|
||||
<Flex bg="white" overflow="hidden" width="400px">
|
||||
<ExplorerContainer borderRight="STANDARD" height="500px" width="255px">
|
||||
<Flex flexDirection="column" gap="spaces-2" p="spaces-3">
|
||||
<EntityListTree items={updatedTree} onItemExpand={onExpandClick} />
|
||||
</Flex>
|
||||
</ExplorerContainer>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export const Basic = Template.bind({}) as StoryObj;
|
||||
|
||||
Basic.args = {
|
||||
outsideSelection: "1",
|
||||
};
|
||||
|
|
@ -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"] * 8;
|
||||
}}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);
|
||||
}
|
||||
`;
|
||||
|
|
@ -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(<EntityListTree {...defaultProps} />);
|
||||
expect(screen.getByRole("tree")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("calls onItemExpand when expand icon is clicked", () => {
|
||||
render(<EntityListTree {...defaultProps} />);
|
||||
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(<EntityListTree {...props} />);
|
||||
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(<EntityListTree {...props} />);
|
||||
|
||||
expect(screen.getByRole("treeitem", { name: "Child" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not render nested EntityListTree when item is not expanded", () => {
|
||||
render(<EntityListTree {...defaultProps} />);
|
||||
|
||||
expect(screen.queryByRole("treeitem", { name: "Child" })).toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
@ -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<HTMLSpanElement, 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 (
|
||||
<Flex
|
||||
flex="1"
|
||||
flexDirection="column"
|
||||
role={currentDepth == 0 ? "tree" : undefined}
|
||||
>
|
||||
{props.items.map((item) => (
|
||||
<Flex flex="1" flexDirection="column" key={item.id}>
|
||||
<EntityItemWrapper
|
||||
alignItems="center"
|
||||
aria-expanded={item.isExpanded}
|
||||
aria-level={currentDepth}
|
||||
aria-selected={item.isSelected}
|
||||
data-depth={currentDepth}
|
||||
data-disabled={item.isDisabled || false}
|
||||
data-selected={item.isSelected}
|
||||
flexDirection="row"
|
||||
role="treeitem"
|
||||
>
|
||||
{item.children && item.children.length ? (
|
||||
<CollapseWrapper
|
||||
data-itemid={item.id}
|
||||
data-testid="entity-item-expand-icon"
|
||||
onClick={handleOnExpandClick}
|
||||
>
|
||||
<Icon
|
||||
name={
|
||||
item.isExpanded ? "arrow-down-s-line" : "arrow-right-s-line"
|
||||
}
|
||||
size="md"
|
||||
/>
|
||||
</CollapseWrapper>
|
||||
) : (
|
||||
<CollapseSpacer />
|
||||
)}
|
||||
<PaddingOverrider>
|
||||
<EntityItem {...item} />
|
||||
</PaddingOverrider>
|
||||
</EntityItemWrapper>
|
||||
{item.children && item.isExpanded ? (
|
||||
<EntityListTree
|
||||
depth={childrenDepth}
|
||||
items={item.children}
|
||||
onItemExpand={onItemExpand}
|
||||
/>
|
||||
) : null}
|
||||
</Flex>
|
||||
))}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export { EntityListTree } from "./EntityListTree";
|
||||
export { type EntityListTreeItem } from "./EntityListTree.types";
|
||||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -8,3 +8,4 @@ export * from "./ExplorerContainer";
|
|||
export * from "./EntityItem";
|
||||
export { useEditableText } from "./Editable";
|
||||
export * from "./EntityGroupsList";
|
||||
export * from "./EntityListTree";
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
export { EditableName } from "./EditableName";
|
||||
export { RenameMenuItem } from "./RenameMenuItem";
|
||||
export { useIsRenaming } from "./useIsRenaming";
|
||||
export { useValidateEntityName } from "./useValidateEntityName";
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ export {
|
|||
EditableName,
|
||||
RenameMenuItem,
|
||||
useIsRenaming,
|
||||
useValidateEntityName,
|
||||
} from "./Components/EditableName";
|
||||
|
||||
/* ====================================================
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -1072,9 +1072,6 @@ export const getExistingActionNames = createSelector(
|
|||
},
|
||||
);
|
||||
|
||||
export const getEditingEntityName = (state: AppState) =>
|
||||
state.ui.explorer.entity.editingEntityName;
|
||||
|
||||
export const getExistingJSCollectionNames = createSelector(
|
||||
getJSCollections,
|
||||
(jsActions) =>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<ListContainer flexDirection="column">
|
||||
{widgets.children.map((child) => (
|
||||
<WidgetEntity
|
||||
basePageId={basePageId}
|
||||
childWidgets={child.children}
|
||||
key={child.widgetId}
|
||||
searchKeyword=""
|
||||
step={1}
|
||||
widgetId={child.widgetId}
|
||||
widgetName={child.widgetName}
|
||||
widgetType={child.type}
|
||||
widgetsInStep={widgetsInStep}
|
||||
/>
|
||||
))}
|
||||
</ListContainer>
|
||||
);
|
||||
};
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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[] };
|
||||
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<ListContainer
|
||||
flexDirection="column"
|
||||
gap="spaces-3"
|
||||
overflow="hidden"
|
||||
py="spaces-3"
|
||||
>
|
||||
<Flex flexDirection="column" gap="spaces-3" overflow="hidden" py="spaces-3">
|
||||
{!widgetsExist ? (
|
||||
/* If no widgets exist, show the blank state */
|
||||
<EmptyState
|
||||
|
|
@ -102,25 +87,18 @@ const ListWidgets = (props: {
|
|||
data-testid="t--ide-list"
|
||||
flex="1"
|
||||
flexDirection={"column"}
|
||||
overflowX="hidden"
|
||||
overflowY="auto"
|
||||
px="spaces-3"
|
||||
>
|
||||
{widgets?.children?.map((child) => (
|
||||
<WidgetEntity
|
||||
basePageId={basePageId}
|
||||
childWidgets={child.children}
|
||||
key={child.widgetId}
|
||||
searchKeyword=""
|
||||
step={1}
|
||||
widgetId={child.widgetId}
|
||||
widgetName={child.widgetName}
|
||||
widgetType={child.type}
|
||||
widgetsInStep={widgetsInStep}
|
||||
/>
|
||||
))}
|
||||
{isNewWidgetTreeEnabled ? (
|
||||
<UIEntityListTree />
|
||||
) : (
|
||||
<OldWidgetEntityList />
|
||||
)}
|
||||
</Flex>
|
||||
) : null}
|
||||
</ListContainer>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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: <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} />;
|
||||
};
|
||||
|
|
@ -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 (
|
||||
<>
|
||||
<MenuItem onClick={showBinding}>
|
||||
{createMessage(CONTEXT_SHOW_BINDING)}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
disabled={!canManagePages}
|
||||
onClick={editWidgetName}
|
||||
startIcon="input-cursor-move"
|
||||
>
|
||||
{createMessage(CONTEXT_RENAME)}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
className="error-menuitem"
|
||||
disabled={!canManagePages && widget?.isDeletable !== false}
|
||||
onClick={deleteWidget}
|
||||
startIcon="trash"
|
||||
>
|
||||
{createMessage(CONTEXT_DELETE)}
|
||||
</MenuItem>
|
||||
</>
|
||||
);
|
||||
}, [
|
||||
canManagePages,
|
||||
deleteWidget,
|
||||
editWidgetName,
|
||||
showBinding,
|
||||
widget?.isDeletable,
|
||||
]);
|
||||
|
||||
return (
|
||||
<Menu onOpenChange={toggleMenuOpen} open={isMenuOpen}>
|
||||
<MenuTrigger>
|
||||
<Button
|
||||
data-testid="t--more-action-trigger"
|
||||
isIconButton
|
||||
kind="tertiary"
|
||||
startIcon="more-2-fill"
|
||||
/>
|
||||
</MenuTrigger>
|
||||
<MenuContent align="start" key={widgetId} side="right" width="300px">
|
||||
{menuContent}
|
||||
</MenuContent>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
|
@ -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<WidgetTypeIconProps> = React.memo(
|
||||
({ type }) => {
|
||||
const { IconCmp } = WidgetFactory.getWidgetMethods(type);
|
||||
|
||||
if (IconCmp) {
|
||||
return <IconCmp />;
|
||||
}
|
||||
|
||||
return <WidgetIcon type={type} />;
|
||||
},
|
||||
);
|
||||
|
||||
WidgetTypeIcon.displayName = "WidgetTypeIcon";
|
||||
|
|
@ -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]);
|
||||
}
|
||||
|
|
@ -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],
|
||||
);
|
||||
}
|
||||
|
|
@ -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<string[]>(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,
|
||||
};
|
||||
};
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { UIEntityListTree } from "./UIEntityListTree";
|
||||
|
|
@ -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,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user