Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -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<typeof EntityListTree> = {
Expand All @@ -18,7 +19,6 @@ const meta: Meta<typeof EntityListTree> = {

export default meta;

const onClick = noop;
const nameEditorConfig = {
canEdit: true,
isEditing: false,
Expand All @@ -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: <Icon name="apps-line" />,
id: "1",
title: "Parent 1",
isExpanded: true,
onClick,
nameEditorConfig,
isSelected: false,
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,
isSelected: false,
},
{
startIcon: <Icon name="apps-line" />,
id: "1.1.2",
isDisabled: true,
title: "Grandchild 2",
isExpanded: false,
onClick,
nameEditorConfig,
isSelected: false,
},
],
},
{
startIcon: <Icon name="apps-line" />,
id: "1.2",
title: "Child 2",
isExpanded: false,
onClick,
nameEditorConfig,
isSelected: false,
},
],
},
{
startIcon: <Icon name="apps-line" />,
id: "2",
title: "Parent 2",
isExpanded: false,
onClick,
nameEditorConfig,
isSelected: false,
},
];

Expand All @@ -97,57 +87,67 @@ const treeUpdate = (
});
};

const Template = (props: { outsideSelection: string }) => {
const EntityItemComponent = (props: { item: EntityListTreeItem }) => {
const { item } = props;
const [editing, setEditing] = React.useState<string | null>(null);
const onItemEdit = (id: string) => {
setEditing(id);
};

const completeEdit = () => {
setEditing(null);
};

return (
<EntityItem
{...item}
nameEditorConfig={{
...nameEditorConfig,
isEditing: item.id === editing,
onEditComplete: completeEdit,
onNameSave: noop,
validateName: () => null,
}}
onClick={noop}
onDoubleClick={() => onItemEdit(item.id)}
startIcon={<Icon name="apps-line" />}
title={names[item.id as keyof typeof names] || item.id}
/>
);
};

const Template = (props: { selectedItem: string }) => {
const [expanded, setExpanded] = React.useState<Record<string, boolean>>({});
const [selected, setSelected] = React.useState<string | null>(
props.outsideSelection,
props.selectedItem,
);
const [editing, setEditing] = React.useState<string | null>(null);

useEffect(
function handleSyncOfSelection() {
setSelected(props.outsideSelection);
setSelected(props.selectedItem);
},
[props.outsideSelection],
[props.selectedItem],
);

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} />
<EntityListTree
ItemComponent={EntityItemComponent}
items={updatedTree}
onItemExpand={onExpandClick}
/>
</Flex>
</ExplorerContainer>
</Flex>
Expand All @@ -157,5 +157,5 @@ const Template = (props: { outsideSelection: string }) => {
export const Basic = Template.bind({}) as StoryObj;

Basic.args = {
outsideSelection: "1",
selectedItem: "1",
};
Original file line number Diff line number Diff line change
@@ -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 <div>{name[item.id as keyof typeof name] || item.id}</div>;
};

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: [],
},
],
Expand All @@ -50,7 +52,7 @@ describe("EntityListTree", () => {

it("calls onItemExpand when expand icon is clicked", () => {
render(<EntityListTree {...defaultProps} />);
const expandIcon = screen.getByTestId("entity-item-expand-icon");
const expandIcon = screen.getByTestId("t--entity-item-expand-icon");

fireEvent.click(expandIcon);
expect(mockOnItemExpand).toHaveBeenCalledWith("1");
Expand All @@ -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(<EntityListTree {...props} />);
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" }),
Expand All @@ -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: [],
},
],
Expand Down
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
Loading