Skip to content
Open
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
@@ -1,3 +1,10 @@
import {
hidePropertiesIn,
hidePropertyIn,
Problem,
Properties,
transformGroupsIntoTabs
} from "@mendix/pluggable-widgets-tools";
import {
ContainerProps,
DropZoneProps,
Expand All @@ -6,13 +13,6 @@ import {
StructurePreviewProps,
TextProps
} from "@mendix/widget-plugin-platform/preview/structure-preview-api";
import {
hidePropertiesIn,
hidePropertyIn,
Problem,
Properties,
transformGroupsIntoTabs
} from "@mendix/pluggable-widgets-tools";

import { HeaderTypeEnum, TreeNodePreviewProps } from "../typings/TreeNodeProps";

Expand All @@ -35,8 +35,12 @@ export function getProperties(
hidePropertyIn(defaultProperties, values, "headerCaption");
}

if (!values.hasChildren) {
hidePropertiesIn(defaultProperties, values, ["startExpanded", "children"]);
if (values.parentAssociation) {
hidePropertyIn(defaultProperties, values, "hasChildren");
} else {
if (!values.hasChildren) {
hidePropertiesIn(defaultProperties, values, ["startExpanded", "children"]);
}
}

if (platform === "web") {
Expand All @@ -59,6 +63,7 @@ export function getProperties(

export function getPreview(values: TreeNodePreviewProps, isDarkMode: boolean): StructurePreviewProps | null {
const palette = structurePreviewPalette[isDarkMode ? "dark" : "light"];
const showChildren = values.hasChildren || values.parentAssociation !== null;

const titleHeader: RowLayoutProps = {
type: "RowLayout",
Expand Down Expand Up @@ -91,7 +96,7 @@ export function getPreview(values: TreeNodePreviewProps, isDarkMode: boolean): S
columnSize: "grow",
padding: 4,
children: [
...(values.showIcon === "left" && values.hasChildren
...(values.showIcon === "left" && showChildren
? [getChevronIconPreview(values.headerType, isDarkMode)]
: []),
values.headerType === "text"
Expand All @@ -115,7 +120,7 @@ export function getPreview(values: TreeNodePreviewProps, isDarkMode: boolean): S
]
} as RowLayoutProps),

...(values.showIcon === "right" && values.hasChildren
...(values.showIcon === "right" && showChildren
? [getChevronIconPreview(values.headerType, isDarkMode)]
: [])
]
Expand All @@ -124,7 +129,7 @@ export function getPreview(values: TreeNodePreviewProps, isDarkMode: boolean): S
};

const getTreeNodeContent: () => StructurePreviewProps[] = () =>
values.hasChildren
showChildren
? [
{
type: "RowLayout",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { mapPreviewIconToWebIcon } from "@mendix/widget-plugin-platform/preview/map-icon";
import { parseStyle } from "@mendix/widget-plugin-platform/preview/parse-style";
import { GUID } from "mendix";

Check warning on line 3 in packages/pluggableWidgets/tree-node-web/src/TreeNode.editorPreview.tsx

View workflow job for this annotation

GitHub Actions / Run code quality check

`mendix` import should occur before import of `@mendix/widget-plugin-platform/preview/map-icon`
import { ReactElement } from "react";

Check warning on line 4 in packages/pluggableWidgets/tree-node-web/src/TreeNode.editorPreview.tsx

View workflow job for this annotation

GitHub Actions / Run code quality check

`react` import should occur before import of `@mendix/widget-plugin-platform/preview/map-icon`
import { TreeNodePreviewProps } from "../typings/TreeNodeProps";
import { TreeNode } from "./components/TreeNode";
import { TreeNode } from "./components/v1/TreeNode";

function renderTextTemplateWithFallback(textTemplateValue: string, placeholder: string): string {
if (textTemplateValue.trim().length === 0) {
Expand Down
56 changes: 8 additions & 48 deletions packages/pluggableWidgets/tree-node-web/src/TreeNode.tsx
Original file line number Diff line number Diff line change
@@ -1,52 +1,12 @@
import { ObjectItem, ValueStatus } from "mendix";
import { ReactElement, useEffect, useState } from "react";
import { ReactElement } from "react";
import { TreeNodeContainerProps } from "../typings/TreeNodeProps";
import { InfoTreeNodeItem, TreeNode as TreeNodeComponent, TreeNodeItem } from "./components/TreeNode";

function mapDataSourceItemToTreeNodeItem(item: ObjectItem, props: TreeNodeContainerProps): TreeNodeItem {
return {
id: item.id,
headerContent:
props.headerType === "text" ? props.headerCaption?.get(item).value : props.headerContent?.get(item),
bodyContent: props.children?.get(item),
isUserDefinedLeafNode: props.hasChildren?.get(item).value === false
};
}
import { TreeNodeV1 } from "./components/v1/Root";
import { TreeNodeV2 } from "./components/v2/TreeNode";

export function TreeNode(props: TreeNodeContainerProps): ReactElement {
const { datasource } = props;
const [treeNodeItems, setTreeNodeItems] = useState<TreeNodeItem[] | InfoTreeNodeItem | null>([]);

useEffect(() => {
// only get the items when datasource is actually available
// this is to prevent treenode resetting it's render while datasource is loading.
if (datasource.status === ValueStatus.Available) {
if (datasource.items && datasource.items.length) {
setTreeNodeItems(datasource.items.map(item => mapDataSourceItemToTreeNodeItem(item, props)));
} else {
setTreeNodeItems({
Message: "No data available"
});
}
}
}, [datasource.status, datasource.items]);
const expandedIcon = props.expandedIcon?.status === ValueStatus.Available ? props.expandedIcon.value : undefined;
const collapsedIcon = props.collapsedIcon?.status === ValueStatus.Available ? props.collapsedIcon.value : undefined;

return (
<TreeNodeComponent
class={props.class}
style={props.style}
items={treeNodeItems}
startExpanded={props.startExpanded}
showCustomIcon={Boolean(props.expandedIcon) || Boolean(props.collapsedIcon)}
iconPlacement={props.showIcon}
expandedIcon={expandedIcon}
collapsedIcon={collapsedIcon}
tabIndex={props.tabIndex}
animateIcon={props.animate && props.animateIcon}
animateTreeNodeContent={props.animate}
openNodeOn={props.openNodeOn}
/>
);
if (props.parentAssociation) {
return <TreeNodeV2 {...props} />;
} else {
return <TreeNodeV1 {...props} />;
}
}
7 changes: 7 additions & 0 deletions packages/pluggableWidgets/tree-node-web/src/TreeNode.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@
<caption>Data source</caption>
<description />
</property>
<property key="parentAssociation" type="association" dataSource="datasource" selectableObjects="datasource" required="false">
<caption>Parent association</caption>
<description>Select the self-referencing association that connects each item to its parent, enabling infinite depth hierarchies.</description>
<associationTypes>
<associationType name="Reference" />
</associationTypes>
</property>
<property key="headerType" type="enumeration" defaultValue="text">
<caption>Header type</caption>
<description />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import classNames from "classnames";
import { WebIcon } from "mendix";
import { ReactNode } from "react";

import { ShowIconEnum } from "../../typings/TreeNodeProps";
import loadingCircleSvg from "../assets/loading-circle.svg";
import { ShowIconEnum } from "../../../typings/TreeNodeProps";
import loadingCircleSvg from "../../assets/loading-circle.svg";

import { ChevronIcon, CustomHeaderIcon } from "./Icons";
import { TreeNodeProps, TreeNodeState } from "./TreeNode";
import { ChevronIcon, CustomHeaderIcon } from "../v1/Icons";
import { TreeNodeState } from "./TreeNodeState";

Check warning on line 9 in packages/pluggableWidgets/tree-node-web/src/components/common/HeaderIcon.tsx

View workflow job for this annotation

GitHub Actions / Run code quality check

`./TreeNodeState` import should occur before import of `../../../typings/TreeNodeProps`

export type IconOptions = Pick<TreeNodeProps, "animateIcon" | "collapsedIcon" | "expandedIcon" | "showCustomIcon">;
export interface IconOptions {
animateIcon: boolean;
collapsedIcon?: WebIcon;
expandedIcon?: WebIcon;
showCustomIcon: boolean;
}

export type TreeNodeHeaderIcon = (
treeNodeState: TreeNodeState,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const enum TreeNodeState {
COLLAPSED_WITH_JS = "COLLAPSED_WITH_JS",
COLLAPSED_WITH_CSS = "COLLAPSED_WITH_CSS",
EXPANDED = "EXPANDED",
LOADING = "LOADING"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { IconOptions, renderTreeNodeHeaderIcon, TreeNodeHeaderIcon } from "../common/HeaderIcon";
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { ObjectItem, ValueStatus } from "mendix";
import { ReactElement, useEffect, useState } from "react";

import { InfoTreeNodeItem, TreeNode, TreeNodeItem } from "./TreeNode";
import { TreeNodeContainerProps } from "../../../typings/TreeNodeProps";

function mapDataSourceItemToTreeNodeItem(item: ObjectItem, props: TreeNodeContainerProps): TreeNodeItem {
return {
id: item.id,
headerContent:
props.headerType === "text" ? props.headerCaption?.get(item).value : props.headerContent?.get(item),
bodyContent: props.children?.get(item),
isUserDefinedLeafNode: props.hasChildren?.get(item).value === false
};
}

export function TreeNodeV1(props: TreeNodeContainerProps): ReactElement {
const { datasource } = props;
const [treeNodeItems, setTreeNodeItems] = useState<TreeNodeItem[] | InfoTreeNodeItem | null>([]);

useEffect(() => {
// Only process datasource items when they are available to avoid rendering resets while loading.
if (datasource.status === ValueStatus.Available) {
if (datasource.items && datasource.items.length) {
setTreeNodeItems(datasource.items.map(item => mapDataSourceItemToTreeNodeItem(item, props)));
} else {
setTreeNodeItems({
Message: "No data available"
});
}
}
}, [datasource.status, datasource.items]);

const expandedIcon = props.expandedIcon?.status === ValueStatus.Available ? props.expandedIcon.value : undefined;
const collapsedIcon = props.collapsedIcon?.status === ValueStatus.Available ? props.collapsedIcon.value : undefined;

return (
<TreeNode
class={props.class}
style={props.style}
items={treeNodeItems}
startExpanded={props.startExpanded}
showCustomIcon={Boolean(props.expandedIcon) || Boolean(props.collapsedIcon)}
iconPlacement={props.showIcon}
expandedIcon={expandedIcon}
collapsedIcon={collapsedIcon}
tabIndex={props.tabIndex}
animateIcon={props.animate && props.animateIcon}
animateTreeNodeContent={props.animate}
openNodeOn={props.openNodeOn}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@ import classNames from "classnames";
import { ObjectItem, WebIcon } from "mendix";
import { CSSProperties, ReactElement, ReactNode, useCallback, useContext } from "react";

import { OpenNodeOnEnum, TreeNodeContainerProps } from "../../typings/TreeNodeProps";
import { OpenNodeOnEnum, TreeNodeContainerProps } from "../../../typings/TreeNodeProps";
import { renderTreeNodeHeaderIcon, TreeNodeHeaderIcon } from "../common/HeaderIcon";
import { TreeNodeState } from "../common/TreeNodeState";

import { useTreeNodeFocusChangeHandler } from "./hooks/TreeNodeAccessibility";
import { useTreeNodeRef } from "./hooks/useTreeNodeRef";
import { renderTreeNodeHeaderIcon, TreeNodeHeaderIcon } from "./HeaderIcon";
import { TreeNodeBranch, TreeNodeBranchProps, treeNodeBranchUtils } from "./TreeNodeBranch";
import { TreeNodeBranchContext, useInformParentContextOfChildNodes } from "./TreeNodeBranchContext";

export { TreeNodeState };

export interface TreeNodeItem extends ObjectItem {
headerContent: ReactNode;
bodyContent: ReactNode;
Expand Down Expand Up @@ -105,10 +108,3 @@ export function TreeNode({
</ul>
);
}

export const enum TreeNodeState {
COLLAPSED_WITH_JS = "COLLAPSED_WITH_JS",
COLLAPSED_WITH_CSS = "COLLAPSED_WITH_CSS",
EXPANDED = "EXPANDED",
LOADING = "LOADING"
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ import {
useState
} from "react";

import { OpenNodeOnEnum, ShowIconEnum } from "../../typings/TreeNodeProps";
import { OpenNodeOnEnum, ShowIconEnum } from "../../../typings/TreeNodeProps";
import { TreeNodeHeaderIcon } from "../common/HeaderIcon";

import { useTreeNodeLazyLoading } from "./hooks/lazyLoading";
import { useAnimatedTreeNodeContentHeight } from "./hooks/useAnimatedHeight";
import { TreeNodeFocusChangeHandler, useTreeNodeBranchKeyboardHandler } from "./hooks/TreeNodeAccessibility";
import { useAnimatedTreeNodeContentHeight } from "./hooks/useAnimatedHeight";

import { TreeNodeHeaderIcon } from "./HeaderIcon";
import { TreeNodeItem, TreeNodeState } from "./TreeNode";
import { TreeNodeBranchContext, TreeNodeBranchContextProps } from "./TreeNodeBranchContext";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { isValidElement, ReactElement, ReactNode } from "react";
import { renderTreeNodeHeaderIcon } from "../HeaderIcon";
import { TreeNode, TreeNodeProps, TreeNodeState } from "../TreeNode";

jest.mock("../../assets/loading-circle.svg", () => "loading-logo.svg");
jest.mock("../../../assets/loading-circle.svg", () => "loading-logo.svg");

interface TreeNodeItem {
id: GUID;
Expand Down
File renamed without changes.
File renamed without changes.
Loading
Loading