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 @@ -6,7 +6,7 @@

import type { Meta, StoryObj } from '@storybook/react';
import { Column } from '@uipath/apollo-react/canvas/layouts';
import type { Edge, Node } from '@uipath/apollo-react/canvas/xyflow/react';
import type { Edge, Node, NodeProps } from '@uipath/apollo-react/canvas/xyflow/react';
import { Panel } from '@uipath/apollo-react/canvas/xyflow/react';
import { Button, cn, Input, Label, Slider, Switch } from '@uipath/apollo-wind';
import { useCallback, useEffect, useMemo, useState } from 'react';
Expand Down Expand Up @@ -34,9 +34,14 @@ import type { ValidationErrorSeverity } from '../../types/validation';
import { CanvasIcon } from '../../utils/icon-registry';
import { BaseCanvas } from '../BaseCanvas';
import { CanvasPositionControls } from '../CanvasPositionControls';
import { CanvasTooltip } from '../CanvasTooltip';
import { NodeInspector } from '../NodeInspector';
import type { BaseNodeData } from './BaseNode.types';
import { BaseNodeOverrideConfigProvider } from './BaseNodeConfigContext';
import { BaseNode } from './BaseNode';
import type { BaseNodeData, NodeAdornments } from './BaseNode.types';
import {
type BaseNodeOverrideConfig,
BaseNodeOverrideConfigProvider,
} from './BaseNodeConfigContext';

// ============================================================================
// Meta Configuration
Expand Down Expand Up @@ -1496,11 +1501,74 @@ const ADORNMENT_ROWS = [
{ key: 'status-inprogress', label: 'Status: InProgress (top-right)' },
{ key: 'status-failed', label: 'Status: Failed (top-right)' },
{ key: 'start-point', label: 'Start Point (bottom-left)' },
{ key: 'square-dashed', label: 'Square Dashed (bottom-right)' },
{ key: 'mock-static', label: 'Custom: static mock (bottom-right)' },
{ key: 'mock-generated', label: 'Custom: generated mock (bottom-right)' },
{ key: 'all', label: 'All Adornments' },
{ key: 'multi-exec', label: 'Multi-execution (count: 5)' },
] as const;

/**
* Both mocked-output icons come from the icon registry and share one truncated
* file outline, so static and generated read as siblings. See
* `FileSparklesCornerIcon` for how the sparkle is composed.
*/
const MOCK_OUTPUT_VARIANTS = {
static: {
generated: false,
icon: 'file-braces-corner',
tooltip: 'Node output is mocked',
},
generated: {
generated: true,
icon: 'file-sparkles-corner',
tooltip: 'Node output is generated by LLM',
},
} as const;

function MockOutputIndicator({ variant }: { variant: keyof typeof MOCK_OUTPUT_VARIANTS }) {
const { icon, tooltip } = MOCK_OUTPUT_VARIANTS[variant];

return (
<CanvasTooltip content={tooltip} placement="bottom">
<span className="inline-flex">
<CanvasIcon icon={icon} size={16} color="var(--color-foreground-emp)" />
</span>
</CanvasTooltip>
);
}

/**
* Bottom-right adornments per row, supplied through the node override config.
* The execution-status API no longer carries a mocked-output flag, so consumers
* own this slot and decide what it renders.
*/
const ADORNMENT_OVERRIDES: Record<string, NodeAdornments> = {
'mock-static': { bottomRight: <MockOutputIndicator variant="static" /> },
'mock-generated': { bottomRight: <MockOutputIndicator variant="generated" /> },
};

/** Adornment node ids are `adorn-<row key>-<shape>`, and row keys may contain dashes. */
function getAdornmentRowKey(nodeId: string): string {
return nodeId.split('-').slice(1, -1).join('-');
}

/**
* Story-only node component that wires each row's custom adornments in via
* `BaseNodeOverrideConfigProvider`, the same path product code uses.
*/
function AdornmentOverrideNode(props: NodeProps<Node<BaseNodeData>>) {
const overrideConfig = useMemo<BaseNodeOverrideConfig>(
() => ({ adornments: ADORNMENT_OVERRIDES[getAdornmentRowKey(props.id)] }),
[props.id]
);

return (
<BaseNodeOverrideConfigProvider value={overrideConfig}>
<BaseNode {...props} />
</BaseNodeOverrideConfigProvider>
);
}

/**
* Creates nodes demonstrating all adornment types across shapes.
*/
Expand Down Expand Up @@ -1549,14 +1617,14 @@ function getAdornmentExecutionState(key: string) {
return { status: 'Failed' as const };
case 'start-point':
return { status: 'None' as const, isExecutionStartPoint: true };
case 'square-dashed':
return { status: 'None' as const, isOutputPinned: true };
case 'mock-static':
case 'mock-generated':
return { status: 'None' as const };
case 'all':
return {
status: 'Completed' as const,
debug: true,
isExecutionStartPoint: true,
isOutputPinned: true,
};
case 'multi-exec':
return { status: 'Completed' as const, count: 5 };
Expand All @@ -1567,7 +1635,7 @@ function getAdornmentExecutionState(key: string) {

function AdornmentsStory() {
const initialNodes = useMemo(() => createAdornmentGrid(), []);
const { canvasProps } = useCanvasStory({ initialNodes });
const { canvasProps } = useCanvasStory({ initialNodes, nodeComponent: AdornmentOverrideNode });

return (
<BaseCanvas {...canvasProps} mode="design">
Expand All @@ -1576,7 +1644,7 @@ function AdornmentsStory() {
</Panel>
<StoryInfoPanel
title="Adornments"
description="Grid showing all adornment types across shapes. Each row demonstrates a different adornment: breakpoint (top-left), execution status (top-right), execution start point (bottom-left), square dashed (bottom-right), and all combined."
description="Grid showing all adornment types across shapes. Breakpoint (top-left), execution status (top-right), and execution start point (bottom-left) come from the execution-status API. The bottom-right slot is fully custom: the mocked-output icons here are supplied via BaseNodeOverrideConfigProvider (adornments.bottomRight), not by execution status."
/>
</BaseCanvas>
);
Expand All @@ -1587,13 +1655,8 @@ export const Adornments: Story = {
decorators: [
withCanvasProviders({
executionState: {
getNodeExecutionState: (nodeId: string) => {
// Extract the adornment key from node IDs like "adorn-breakpoint-circle"
const parts = nodeId.split('-');
// Rejoin all parts between first and last to get the key (handles keys with dashes)
const key = parts.slice(1, -1).join('-');
return getAdornmentExecutionState(key);
},
getNodeExecutionState: (nodeId: string) =>
getAdornmentExecutionState(getAdornmentRowKey(nodeId)),
getEdgeExecutionState: () => undefined,
},
validationState: {
Expand Down
1 change: 1 addition & 0 deletions packages/apollo-react/src/canvas/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export * from './CanvasLeftSidebar';
export * from './CanvasModeToolbar';
export * from './CanvasPositionControls';
export * from './CanvasTakeoverModal';
export * from './CanvasTooltip';
export * from './CanvasZoomControls';
export * from './CaseFlow';
export * from './CodedAgent';
Expand Down
51 changes: 51 additions & 0 deletions packages/apollo-react/src/canvas/icons/FileSparklesCornerIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* File with a sparkle in an opened corner, for nodes whose output is mocked by
* generation. Sibling of Lucide's `file-braces-corner`, which marks a static
* mock: both share the same truncated file outline so the two states read as one
* family, and the outline stops short of the glyph so no stroke passes behind it.
*
* The glyph follows Lucide's `sparkles`: a star with a cross up-right and a dot
* down-left, seated on a 45 degree axis at offsets of +/-8 scaled by the star's
* 0.5, so +/-4 from the star's centre at (8, 17). Every mark stays inside the
* 2..22 box Lucide keeps its paths in, and the cluster clears the outline's cut
* end at x=14.
*
* Every mark runs lighter than the outline's 2: the star's interior counter
* closes up and fills in solid at that weight, so a concave glyph needs a
* thinner stroke than a plain outline does to read at the same size.
*/
export const FileSparklesCornerIcon = ({
w = 24,
h = 24,
color = 'currentColor',
}: {
w?: number | string;
h?: number | string;
color?: string;
}) => (
<svg
xmlns="http://www.w3.org/2000/svg"
className="file-sparkles-corner-icon"
width={w}
height={h}
viewBox="0 0 24 24"
fill="none"
stroke={color}
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
>
{/* Outline truncated at the bottom-left, identical to `file-braces-corner`. */}
<path d="M14 22h4a2 2 0 0 0 2-2V8a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 14 2H6a2 2 0 0 0-2 2v6" />
<path d="M14 2v5a1 1 0 0 0 1 1h5" />
{/* Lucide `sparkle` at 0.5; stroke is 1.4/0.5 so it renders at 1.4. */}
<g transform="translate(2 11) scale(0.5)" strokeWidth={2.8}>
<path d="M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z" />
</g>
<g strokeWidth={0.75}>
<path d="M10.8 13h2.4" />
<path d="M12 11.8v2.4" />
</g>
<circle cx="4" cy="21" r="1" strokeWidth={0.75} />
</svg>
);
1 change: 1 addition & 0 deletions packages/apollo-react/src/canvas/icons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export { DecisionIcon } from './DecisionIcon';
export { EarlyExitStatusIcon } from './EarlyExitStatusIcon';
export { EntryConditionIcon } from './EntryConditionIcon';
export { ExitConditionIcon } from './ExitConditionIcon';
export { FileSparklesCornerIcon } from './FileSparklesCornerIcon';
export { FlaskRunIcon } from './FlaskRunIcon';
export { FlowProject } from './FlowProject';
export { FunctionProject } from './FunctionProject';
Expand Down
6 changes: 6 additions & 0 deletions packages/apollo-react/src/canvas/types/execution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ export interface NodeExecutionStateWithDebug {
count?: number;
debug?: boolean;
isExecutionStartPoint?: boolean;
/**
* @deprecated No longer read. The bottom-right adornment slot is owned by the
* consumer: supply `adornments.bottomRight` through `BaseNodeOverrideConfig`
* instead. Setting this has no effect and the field will be removed in the
* next major.
*/
isOutputPinned?: boolean;
}

Expand Down
10 changes: 0 additions & 10 deletions packages/apollo-react/src/canvas/utils/adornment-resolver.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,16 +78,6 @@ describe('resolveAdornments', () => {
expect(result.bottomLeft).toBeTruthy();
});

// ── Square dashed (bottomRight) ─────────────────────────

it('shows square dashed indicator when isOutputPinned', () => {
const result = resolveAdornments({
...baseContext,
executionState: { status: 'None', isOutputPinned: true },
});
expect(result.bottomRight).toBeTruthy();
});

// ── Validation errors (topRight) ────────────────────────

it('shows validation indicator for ERROR severity', () => {
Comment thread
BenGSchulz marked this conversation as resolved.
Expand Down
13 changes: 1 addition & 12 deletions packages/apollo-react/src/canvas/utils/adornment-resolver.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,16 +57,6 @@ function ExecutionStatusIndicatorInternal({ status, count }: { status?: string;
);
}

function SquareDashedIndicator() {
return (
<CanvasTooltip content="Node output is mocked" placement="bottom">
<span style={{ display: 'inline-flex' }}>
<CanvasIcon icon="square-dashed" size={16} color="var(--color-foreground-emp)" />
</span>
</CanvasTooltip>
);
}

export const ExecutionStatusIndicator = memo(ExecutionStatusIndicatorInternal);

export function ValidationErrorIndicator({ message }: { message?: string }) {
Expand Down Expand Up @@ -104,7 +94,6 @@ const getDefaultAdornments = (
const hasBreakpoint = typeof executionState === 'object' && executionState?.debug;
const isExecutionStartPoint =
typeof executionState === 'object' && executionState?.isExecutionStartPoint;
const isOutputPinned = typeof executionState === 'object' && executionState?.isOutputPinned;

const hasValidationError =
context.validationState?.validationStatus === ValidationErrorSeverity.ERROR ||
Expand Down Expand Up @@ -132,7 +121,7 @@ const getDefaultAdornments = (
topLeft: hasBreakpoint ? <BreakpointIndicator /> : undefined,
topRight: getTopRight(),
bottomLeft: isExecutionStartPoint ? <ExecutionStartPointIndicator /> : undefined,
bottomRight: isOutputPinned ? <SquareDashedIndicator /> : undefined,
bottomRight: undefined,
};
};

Expand Down
17 changes: 17 additions & 0 deletions packages/apollo-react/src/canvas/utils/icon-registry.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,23 @@ describe('getIcon', () => {
expect(container.querySelector('#case-management-project')).toBeInTheDocument();
});

it('returns the FileSparklesCornerIcon for the registered file-sparkles-corner id', () => {
const Icon = getIcon('file-sparkles-corner');
const { container } = render(<Icon w={24} h={24} />);
// There is no Lucide `FileSparklesCorner`, so a registry miss would degrade to Box.
const svg = container.querySelector('svg.file-sparkles-corner-icon');
expect(svg).toBeInTheDocument();
// Truncated outline plus the fold, the scaled star, and the cross's two arms.
expect(svg?.querySelectorAll('path')).toHaveLength(5);
expect(svg?.querySelector('circle')).toBeInTheDocument();
});

it('applies the color prop to the file-sparkles-corner stroke', () => {
const Icon = getIcon('file-sparkles-corner');
const { container } = render(<Icon w={24} h={24} color="rgb(1, 2, 3)" />);
expect(container.querySelector('svg')).toHaveAttribute('stroke', 'rgb(1, 2, 3)');
});

it('returns the LayersArrowUpRight icon for the registered layers-arrow-up-right id', () => {
const Icon = getIcon('layers-arrow-up-right');
const { container } = render(<Icon w={24} h={24} />);
Expand Down
3 changes: 3 additions & 0 deletions packages/apollo-react/src/canvas/utils/icon-registry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ const iconRegistry: Record<string, IconComponent> = {
mcp: ({ w, h }) => <Icons.McpIcon w={w ?? 29} h={h ?? 28} />,
a2a: ({ w, h }) => <Icons.A2aIcon w={w ?? 29} h={h ?? 28} />,
context: ({ w, h }) => <Icons.ContextIcon w={w ?? 29} h={h ?? 28} />,
'file-sparkles-corner': ({ w, h, color }) => (
<Icons.FileSparklesCornerIcon w={w ?? 24} h={h ?? 24} color={color} />
),
};

/**
Expand Down
Loading