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
5 changes: 5 additions & 0 deletions .changeset/node-panel-disabled.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@workflowbuilder/ui': minor
---

`NodePanel.Root`, `NodeDescription` and `NodeIcon` accept `disabled` and render the Node Disabled variant: muted surface, title, subtitle, icon and no hover reaction. New public properties: `--wb-public-node-background-color-disabled`, `--wb-public-node-title-color-disabled`, `--wb-public-node-subtitle-color-disabled`, `--wb-public-node-icon-color-disabled`, `--wb-public-node-icon-container-background-color-disabled`.
5 changes: 5 additions & 0 deletions .changeset/palette-node-states.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@workflowbuilder/sdk': minor
---

Palette entries render the canvas Node states instead of their own styling: hover is the Node Hover state, the drag preview is the Node Active state (outline and ring), and entries that cannot be added (read-only mode) use the Node Disabled state instead of a faded copy. The node templates (`WorkflowNodeTemplate`, `StartNodeTemplate`, `DecisionNodeTemplate`, `AiAgentNodeTemplate`) accept a `disabled` prop.
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ Both lines are single-line. Text that does not fit the node width is cut with an
full text is available through the element's native tooltip (`title`). The block never widens its
node: a node keeps the design width and grows in height only through its body content.

## Disabled

`disabled` mutes both lines with the Node Disabled text color. The node templates pass it
down together with `NodePanel.Root` and `NodeIcon`, so a whole node switches state at once.

## Props

<PropsTable slug="node-description" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ function NodeHeader({ label, description }) {
}
```

## Disabled

`disabled` mutes the glyph and the container with the Node Disabled colors.

## Props

<PropsTable slug="node-icon" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ function WorkflowNode({ id, icon, label, description, selected, isValid, showHan
}
```

## States

`selected` draws the active outline and shadow. `disabled` renders the Node Disabled
Comment thread
librowski marked this conversation as resolved.
variant: muted surface, title, subtitle and icon, and no hover reaction. The palette uses
it for node types that cannot be added at the moment, for example in read-only mode.

## Parts

| Part | Props | Description |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ type Props = {
label: string;
description: string;
selected?: boolean;
/** Render the Node Disabled variant (palette entries that cannot be added). */
disabled?: boolean;
isConnecting?: boolean;
showHandles?: boolean;
chatModel?: ItemOption | undefined;
Expand All @@ -40,6 +42,7 @@ export const AiAgentNodeTemplate = memo(
label,
description,
selected = false,
disabled = false,
showHandles = true,
chatModel,
memoryModel,
Expand All @@ -62,10 +65,10 @@ export const AiAgentNodeTemplate = memo(

return (
<Collapsible expandLabel={t('common.expand')} collapseLabel={t('common.collapse')}>
<NodePanel.Root selected={selected}>
<NodePanel.Root selected={selected} disabled={disabled}>
<NodePanel.Header className={styles['header']}>
<NodeIcon className={styles['icon']} icon={iconElement} />
<NodeDescription label={label} description={description} />
<NodeIcon className={styles['icon']} icon={iconElement} disabled={disabled} />
<NodeDescription label={label} description={description} disabled={disabled} />
{isCanvasNode && <Collapsible.Button />}
</NodePanel.Header>
<NodePanel.Content className={styles['content']} isVisible={isCanvasNode}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ type Props = {
label: string;
description: string;
selected?: boolean;
/** Render the Node Disabled variant (palette entries that cannot be added). */
disabled?: boolean;
layoutDirection?: LayoutDirection;
isConnecting?: boolean;
showHandles?: boolean;
Expand All @@ -37,6 +39,7 @@ export const DecisionNodeTemplate = memo(
description,
showHandles,
selected = false,
disabled = false,
isValid,
decisionBranches,
layoutDirection = 'RIGHT',
Expand All @@ -52,10 +55,10 @@ export const DecisionNodeTemplate = memo(
const handlesAlignment = getHandlesAlignment({ layoutDirection });

return (
<NodePanel.Root selected={selected} className={styles['decision-node']}>
<NodePanel.Root selected={selected} disabled={disabled} className={styles['decision-node']}>
<NodePanel.Header>
<NodeIcon icon={iconElement} />
<NodeDescription label={label} description={description} />
<NodeIcon icon={iconElement} disabled={disabled} />
<NodeDescription label={label} description={description} disabled={disabled} />
</NodePanel.Header>
<NodePanel.Content isVisible={isCanvasNode}>
<OptionalNodeContent nodeId={id}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ type StartNodeTemplateProps = {
description: string;
data?: NodeData;
selected?: boolean;
/** Render the Node Disabled variant (palette entries that cannot be added). */
disabled?: boolean;
layoutDirection?: LayoutDirection;
isConnecting?: boolean;
showHandles?: boolean;
Expand All @@ -37,6 +39,7 @@ const StartNodeTemplateComponent = memo(
description,
layoutDirection = 'RIGHT',
selected = false,
disabled = false,
showHandles = true,
isValid,
children,
Expand All @@ -54,10 +57,10 @@ const StartNodeTemplateComponent = memo(

return (
<Collapsible expandLabel={t('common.expand')} collapseLabel={t('common.collapse')}>
<NodePanel.Root selected={selected} className={styles['content']}>
<NodePanel.Root selected={selected} disabled={disabled} className={styles['content']}>
<NodePanel.Header>
<NodeIcon icon={iconElement} />
<NodeDescription label={label} description={description} />
<NodeIcon icon={iconElement} disabled={disabled} />
<NodeDescription label={label} description={description} disabled={disabled} />
{!!children && <Collapsible.Button />}
</NodePanel.Header>
<NodePanel.Content isVisible={isCanvasNode}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ export type WorkflowNodeTemplateProps<P = BaseNodeProperties & Record<string, un
description: string;
data?: NodeData<P>;
selected?: boolean;
/** Render the Node Disabled variant (palette entries that cannot be added). */
disabled?: boolean;
layoutDirection?: LayoutDirection;
isConnecting?: boolean;
showHandles?: boolean;
Expand All @@ -61,6 +63,7 @@ const WorkflowNodeTemplateComponent = memo(
description,
layoutDirection = 'RIGHT',
selected = false,
disabled = false,
showHandles = true,
isValid,
children,
Expand All @@ -80,10 +83,10 @@ const WorkflowNodeTemplateComponent = memo(

return (
<Collapsible expandLabel={t('common.expand')} collapseLabel={t('common.collapse')}>
<NodePanel.Root selected={selected} className={styles['content']}>
<NodePanel.Root selected={selected} disabled={disabled} className={styles['content']}>
<NodePanel.Header>
<NodeIcon icon={iconElement} />
<NodeDescription label={label} description={description} />
<NodeIcon icon={iconElement} disabled={disabled} />
<NodeDescription label={label} description={description} disabled={disabled} />
{!!children && <Collapsible.Button />}
</NodePanel.Header>
<NodePanel.Content isVisible={isCanvasNode}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
.item {
border-radius: 0.75rem;
cursor: grab;
outline-offset: -1px;
outline: 1px solid transparent;
}

.disabled {
cursor: default;
user-select: none;
Comment thread
librowski marked this conversation as resolved.
opacity: 0.5;
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export function PaletteItem({ item, onDragStart, onMouseDown, isDisabled = false
onMouseDown={() => onMouseDown(item.type)}
onDragStart={onDragStart}
>
<NodePreviewContainer type={item.type} />
<NodePreviewContainer type={item.type} disabled={isDisabled} />
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ vi.mock('../diagram/nodes/start-node-template/start-node-template', () => ({
StartNodeTemplate: () => null,
}));
vi.mock('../diagram/nodes/workflow-node-template/workflow-node-template', () => ({
WorkflowNodeTemplate: (props: WorkflowNodeTemplateProps) => <div data-testid="built-in-template">{props.label}</div>,
WorkflowNodeTemplate: (props: WorkflowNodeTemplateProps) => (
<div data-testid="built-in-template" data-selected={props.selected} data-disabled={props.disabled}>
{props.label}
</div>
),
}));

let mockNodeDefinition: PaletteItem | undefined;
Expand Down Expand Up @@ -78,6 +82,14 @@ describe('NodePreviewContainer', () => {
expect(screen.queryByTestId('custom-template')).toBeNull();
});

it('passes the selected and disabled node states to the template', () => {
render(<NodePreviewContainer type="multi-port" selected disabled />);

const element = screen.getByTestId('built-in-template');
expect(element.dataset.selected).toBe('true');
expect(element.dataset.disabled).toBe('true');
});

it('renders nothing when the palette type is unknown', () => {
mockNodeDefinition = undefined;

Expand Down
29 changes: 23 additions & 6 deletions packages/sdk/src/features/palette/node-preview-container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,26 +27,33 @@ const BUILT_IN_TEMPLATES: NodeTemplateRegistry = {
[NodeType.DecisionNode]: DecisionNodeTemplate,
};

type NodePreviewContainerProps = {
type NodeStateProps = {
/** Drag preview: the Node Active state (outline and ring). */
selected?: boolean;
/** Unavailable palette entry: the Node Disabled state. */
disabled?: boolean;
};

type NodePreviewContainerProps = NodeStateProps & {
type: string;
};

export function NodePreviewContainer({ type }: NodePreviewContainerProps) {
export function NodePreviewContainer({ type, selected, disabled }: NodePreviewContainerProps) {
const getNodeDefinition = useStore((state) => state.getNodeDefinition);

const nodeDefinition = getNodeDefinition(type);
if (!nodeDefinition) {
return;
}

return <NodePreview nodeDefinition={nodeDefinition} />;
return <NodePreview nodeDefinition={nodeDefinition} selected={selected} disabled={disabled} />;
}

type NodePreviewProps = {
type NodePreviewProps = NodeStateProps & {
nodeDefinition: PaletteItem;
};

function NodePreview({ nodeDefinition }: NodePreviewProps) {
function NodePreview({ nodeDefinition, selected, disabled }: NodePreviewProps) {
const { type, icon, label, description, templateType = NodeType.Node } = nodeDefinition;

const translateIfPossible = useTranslateIfPossible();
Expand All @@ -58,5 +65,15 @@ function NodePreview({ nodeDefinition }: NodePreviewProps) {
const templateKey = resolveReactFlowNodeType(type, templateType, custom);
const TemplateComponent = custom[templateKey] ?? BUILT_IN_TEMPLATES[templateKey] ?? BUILT_IN_TEMPLATES[NodeType.Node];

return <TemplateComponent icon={icon} label={nodeLabel} description={nodeDescription} showHandles={false} id={''} />;
return (
<TemplateComponent
icon={icon}
label={nodeLabel}
description={nodeDescription}
showHandles={false}
selected={selected}
disabled={disabled}
id={''}
/>
);
}
3 changes: 1 addition & 2 deletions packages/sdk/src/features/palette/palette-container.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { useEffect } from 'react';

import styles from './palette-container.module.css';
import './variables.css';

import { Sidebar } from '../../components/sidebar/sidebar';
import { useStore } from '../../store/store';
Expand Down Expand Up @@ -50,7 +49,7 @@ export function PaletteContainer() {
/>
{draggedItem && (
<DraggedItem ref={ref} zoom={zoom}>
<NodePreviewContainer type={draggedItem.type} />
<NodePreviewContainer type={draggedItem.type} selected />
</DraggedItem>
)}
</Sidebar>
Expand Down
4 changes: 0 additions & 4 deletions packages/sdk/src/features/palette/variables.css

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
:root {
--wb-public-node-title-color: var(--wb-ds-ui-text-default);
--wb-public-node-title-subtitle: var(--wb-ds-ui-text-subtle-default);
--wb-public-node-title-color-disabled: var(--wb-ds-canvas-node-text-disabled);
--wb-public-node-subtitle-color-disabled: var(--wb-ds-canvas-node-text-disabled);
}

@layer ui.component {
Expand Down Expand Up @@ -29,5 +31,15 @@
.subtitle {
color: var(--wb-public-node-title-subtitle);
}

&.disabled {
.title {
color: var(--wb-public-node-title-color-disabled);
}

.subtitle {
color: var(--wb-public-node-subtitle-color-disabled);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ describe('NodeDescription', () => {
expect(subtitle.getAttribute('title')).toBe('Sends the ticket to the right responder.');
});

it('marks the block as disabled', () => {
act(() => root.render(<NodeDescription label="Start" disabled />));

expect(container.firstElementChild?.className).toMatch(/disabled/);
});

it('renders no tooltip attribute when there is no description', () => {
act(() => root.render(<NodeDescription label="Start" />));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ import styles from './node-description.module.css';
export type NodeDescriptionProps = {
label: string;
description?: string;
/** Muted title and subtitle of the Node Disabled variant. */
disabled?: boolean;
className?: string;
};

export function NodeDescription({ label, description, className }: NodeDescriptionProps) {
export function NodeDescription({ label, description, disabled = false, className }: NodeDescriptionProps) {
return (
<div className={clsx(styles['container'], className)}>
<div className={clsx(styles['container'], { [styles['disabled']]: disabled }, className)}>
<span className={clsx('wb-text-title-s-emphasized', styles['title'])} title={label}>
{label}
</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
--wb-public-node-icon-color: var(--wb-ds-canvas-node-icon-primary);
--wb-public-node-icon-container-border-color: var(--wb-ds-canvas-node-stroke-default);
--wb-public-node-icon-container-background-color: var(--wb-ds-canvas-node-bg-secondary-default);
--wb-public-node-icon-color-disabled: var(--wb-ds-ui-icon-disabled);
--wb-public-node-icon-container-background-color-disabled: var(--wb-ds-canvas-node-bg-primary-disabled);
}

@layer ui.component {
Expand All @@ -19,5 +21,10 @@
border-radius: var(--wb-public-node-icon-border-radius);
border: var(--wb-public-node-icon-border-size) solid var(--wb-public-node-icon-container-border-color);
background: var(--wb-public-node-icon-container-background-color);

&.disabled {
color: var(--wb-public-node-icon-color-disabled);
background: var(--wb-public-node-icon-container-background-color-disabled);
}
}
}
6 changes: 4 additions & 2 deletions packages/ui/src/components/node/node-icon/node-icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import styles from './node-icon.module.css';

export type NodeIconProps = {
icon: ReactNode;
/** Muted glyph and container of the Node Disabled variant. */
disabled?: boolean;
className?: string;
};

export function NodeIcon({ icon, className }: NodeIconProps) {
return <div className={clsx(styles['container'], className)}>{icon}</div>;
export function NodeIcon({ icon, disabled = false, className }: NodeIconProps) {
return <div className={clsx(styles['container'], { [styles['disabled']]: disabled }, className)}>{icon}</div>;
}
Loading
Loading