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
Expand Up @@ -198,6 +198,24 @@ const sampleManifest: { nodes: NodeManifest[]; categories: CategoryManifest[] }
},
],
},
{
nodeType: 'uipath.control-flow.terminate',
version: '1.0.0',
category: 'control',
tags: ['control', 'terminate'],
sortOrder: 6,
display: {
label: 'Terminate',
icon: 'git-branch',
shape: 'circle',
},
handleConfiguration: [
{
position: 'right',
handles: [{ id: 'out', type: 'source', handleType: 'output', label: 'Output' }],
},
],
},
],
};

Expand Down Expand Up @@ -628,6 +646,53 @@ function DynamicHandlesStory() {
);
}

/**
* Creates nodes demonstrating loading/skeleton states for all shapes.
* Uses `data.loading: true` to enable the skeleton state.
*/
function createLoadingGrid(): Node<BaseNodeData>[] {
const shapes = [
{ type: 'uipath.blank-node', shape: 'square' as const, label: 'Square' },
{ type: 'uipath.control-flow.terminate', shape: 'circle' as const, label: 'Circle' },
{ type: 'uipath.agent', shape: 'rectangle' as const, label: 'Rectangle' },
];

return shapes.map((config, index) =>
createNode({
id: `loading-${config.shape}`,
type: config.type,
position: { x: 96 + index * 192, y: 96 },
data: {
nodeType: config.type,
version: '1.0.0',
loading: true, // Enable skeleton loading state
display: {
label: config.label,
subLabel: 'Loading...',
shape: config.shape,
},
},
})
);
}

function LoadingStory() {
const initialNodes = useMemo(() => createLoadingGrid(), []);
const { canvasProps } = useCanvasStory({ initialNodes });

return (
<BaseCanvas {...canvasProps} mode="design">
<Panel position="bottom-right">
<CanvasPositionControls translations={DefaultCanvasTranslations} />
</Panel>
<StoryInfoPanel
title="Loading State"
description="Nodes in skeleton loading state for circle, square, and rectangle shapes."
/>
</BaseCanvas>
);
}

// ============================================================================
// Exported Stories
// ============================================================================
Expand All @@ -646,3 +711,8 @@ export const DynamicHandles: Story = {
name: 'Dynamic Handles',
render: () => <DynamicHandlesStory />,
};

export const Loading: Story = {
name: 'Loading',
render: () => <LoadingStory />,
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { css, keyframes } from '@emotion/react';
import styled from '@emotion/styled';
import { ApSkeleton } from '../../../material/components/ap-skeleton';
import type { NodeShape } from '../../schema';
import type { FooterVariant } from './BaseNode.types';

Expand All @@ -10,6 +11,31 @@ const NODE_HEIGHT_FOOTER_BUTTON = GRID_UNIT * 9; // 144px
const NODE_HEIGHT_FOOTER_SINGLE = GRID_UNIT * 10; // 160px
const NODE_HEIGHT_FOOTER_DOUBLE = GRID_UNIT * 11; // 176px

/**
* Computes the icon wrapper dimensions for a given shape and node size.
* Shared between BaseIconWrapper and BaseSkeletonIcon for consistency.
*/
const getIconDimensions = (
shape: NodeShape | undefined,
nodeHeight: number | undefined,
nodeWidth: number | undefined
) => {
const height = nodeHeight ?? 96;
const width = nodeWidth ?? 96;

// Width: Use default 3/4 scaling, derived from the height for rectangle, and use width for other shapes
const widthDimension = height !== width && shape === 'rectangle' ? height : width;
const widthScaleFactor = widthDimension / 96;
const iconWidth = 72 * widthScaleFactor;

// Height: Use 7/8 scaling for a vertical rectangle (expandable node), and use default 3/4 scaling for other shapes
const heightScaleFactor = height / 96;
const isExpandable = height !== width && shape !== 'rectangle';
const iconHeight = isExpandable ? 84 * heightScaleFactor : 72 * heightScaleFactor;

return { iconWidth, iconHeight };
};

const pulseAnimation = (cssVar: string) => keyframes`
0% {
box-shadow: 0 0 0 0 color-mix(in srgb, var(${cssVar}) 20%, transparent);
Expand Down Expand Up @@ -213,19 +239,13 @@ export const BaseIconWrapper = styled.div<{
height?: number;
width?: number;
}>`
width: ${({ height, width, shape }) => {
// Use default 3/4 scaling, derived from the height for rectangle, and use width for other shapes
const dimension = height !== width && shape === 'rectangle' ? height : width;
const scaleFactor = dimension ? dimension / 96 : 1;
return `${72 * scaleFactor}px`;
}};
height: ${({ height, width, shape }) => {
// Use 7/8 scaling for a vertical rectangle, and use default 3/4 scaling for other shapes
const scaleFactor = height ? height / 96 : 1;
return height !== width && shape !== 'rectangle' // True for only a expandable node
? `${84 * scaleFactor}px`
: `${72 * scaleFactor}px`;
}};
${({ height, width, shape }) => {
const { iconWidth, iconHeight } = getIconDimensions(shape, height, width);
return css`
width: ${iconWidth}px;
height: ${iconHeight}px;
`;
}}
display: flex;
align-items: center;
justify-content: center;
Expand Down Expand Up @@ -420,3 +440,24 @@ export const BaseBadgeSlot = styled.div<{
}
}}
`;

/**
* Skeleton icon used in loading state. Uses the same dimension calculations as BaseIconWrapper.
*/
export const BaseSkeletonIcon = styled(ApSkeleton)<{
shape?: NodeShape;
nodeHeight?: number;
nodeWidth?: number;
}>`
Comment on lines +447 to +451
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BaseSkeletonIcon introduces custom props (shape, nodeHeight, nodeWidth) that will be forwarded through ApSkeleton onto the underlying <div> via {...rest}, resulting in invalid DOM attributes and React warnings. Use transient props (e.g. $shape, $nodeHeight, $nodeWidth) and/or shouldForwardProp to prevent these styling-only props from reaching the DOM.

Copilot uses AI. Check for mistakes.
flex-grow: 0;
${({ shape, nodeHeight, nodeWidth }) => {
const { iconWidth, iconHeight } = getIconDimensions(shape, nodeHeight, nodeWidth);
const isCircle = shape === 'circle';

return css`
width: ${iconWidth}px;
height: ${iconHeight}px;
border-radius: ${isCircle ? '50%' : '8px'};
`;
}}
`;
28 changes: 19 additions & 9 deletions packages/apollo-react/src/canvas/components/BaseNode/BaseNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
BaseContainer,
BaseHeader,
BaseIconWrapper,
BaseSkeletonIcon,
BaseSubHeader,
BaseTextContainer,
} from './BaseNode.styles';
Expand Down Expand Up @@ -483,16 +484,25 @@ const BaseNodeComponent = (props: NodeProps<Node<BaseNodeData>>) => {
hasFooter={!!displayFooter}
footerVariant={displayFooterVariant as FooterVariant}
>
{Icon && (
<BaseIconWrapper
{data.loading ? (
<BaseSkeletonIcon
variant="rectangle"
shape={displayShape}
color={displayColor}
backgroundColor={displayIconBackground}
height={displayFooter ? undefined : height}
width={displayFooter ? undefined : (width ?? height)}
>
{Icon}
</BaseIconWrapper>
nodeHeight={displayFooter ? undefined : height}
nodeWidth={displayFooter ? undefined : (width ?? height)}
/>
) : (
Icon && (
<BaseIconWrapper
shape={displayShape}
color={displayColor}
backgroundColor={displayIconBackground}
height={displayFooter ? undefined : height}
width={displayFooter ? undefined : (width ?? height)}
>
{Icon}
</BaseIconWrapper>
)
)}
Comment on lines +487 to 506
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new data.loading rendering path changes visible output but there are no automated tests covering the skeleton state (e.g., asserting ap-skeleton is rendered and the icon isn’t). Since this is library code under packages/, add a unit/component test for BaseNode’s loading behavior to prevent regressions.

Copilot generated this review using guidance from repository custom instructions.

{adornments?.topLeft && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ export interface BaseNodeData extends Record<string, unknown> {
* @default false
*/
isCollapsed?: boolean;

/**
* When true, renders the node in a skeleton loading state.
* The icon area displays a shimmer animation.
* @default false
*/
loading?: boolean;
}

export interface NodeAdornments {
Expand Down
Loading