feat(agentflow): optional cavasActions to allow additional buttons next to validate (#6224)

* feat(agentflow): optional cavasActions to allow additional buttons next to validate

- Updated Agentflow component to support canvasActions prop for rendering additional action buttons in the canvas overlay.
- Enhanced type definitions to include canvasActions in AgentflowProps.
- Added tests to verify the rendering of canvasActions in the Agentflow component.
- Added a new example  demonstrating custom FABs alongside the validation button.

* update jsdoc to match actual implementation
This commit is contained in:
Jocelyn Lin
2026-04-15 12:54:54 -07:00
committed by GitHub
parent 62a5d1d59c
commit a77194c8c2
7 changed files with 137 additions and 10 deletions
+9 -1
View File
@@ -15,7 +15,8 @@ import {
DarkModeExampleProps,
FilteredComponentsExampleProps,
MultiNodeFlowProps,
StatusIndicatorsExampleProps
StatusIndicatorsExampleProps,
ValidationActionsExampleProps
} from './demos'
import { PropsDisplay } from './PropsDisplay'
@@ -81,6 +82,13 @@ const examples: Array<{
description: 'Restrict available nodes with presets',
props: FilteredComponentsExampleProps,
component: lazy(() => import('./demos/FilteredComponentsExample').then((m) => ({ default: m.FilteredComponentsExample })))
},
{
id: 'canvas-actions',
name: 'Canvas Actions',
description: 'Custom FABs alongside the validation button via canvasActions',
props: ValidationActionsExampleProps,
component: lazy(() => import('./demos/ValidationActionsExample').then((m) => ({ default: m.ValidationActionsExample })))
}
]
@@ -0,0 +1,71 @@
/**
* Canvas Actions Example
*
* Demonstrates how to add custom FAB buttons alongside the built-in validation
* button in the top-right canvas overlay using the `canvasActions` prop.
*
* Mirrors the legacy v2 pattern where a chat FAB and validation FAB sit side-by-side.
*/
import { useState } from 'react'
import { Agentflow } from '@flowiseai/agentflow'
import { Box, Dialog, DialogContent, DialogTitle, Fab, IconButton, Typography } from '@mui/material'
import { IconMessage, IconX } from '@tabler/icons-react'
import { apiBaseUrl, token } from '../config'
function ChatFab() {
const [open, setOpen] = useState(false)
return (
<>
<Fab
size='small'
aria-label='chat'
title='Chat'
onClick={() => setOpen(true)}
sx={{
color: 'white',
backgroundColor: 'secondary.main',
'&:hover': {
backgroundColor: 'secondary.main',
backgroundImage: 'linear-gradient(rgb(0 0 0/10%) 0 0)'
}
}}
>
<IconMessage />
</Fab>
<Dialog open={open} onClose={() => setOpen(false)} maxWidth='sm' fullWidth>
<DialogTitle sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
Chat
<IconButton size='small' onClick={() => setOpen(false)}>
<IconX size={18} />
</IconButton>
</DialogTitle>
<DialogContent>
<Box sx={{ py: 4, textAlign: 'center' }}>
<Typography variant='body2' color='text.secondary'>
Your chat UI goes here. Full control — bring any component.
</Typography>
</Box>
</DialogContent>
</Dialog>
</>
)
}
export function ValidationActionsExample() {
return (
<div style={{ width: '100%', height: '100%' }}>
<Agentflow apiBaseUrl={apiBaseUrl} token={token ?? undefined} canvasActions={<ChatFab />} />
</div>
)
}
export const ValidationActionsExampleProps = {
apiBaseUrl: '{from environment variables}',
token: '{from environment variables}',
canvasActions: '<ChatFab />'
}
@@ -6,3 +6,4 @@ export * from './DarkModeExample'
export * from './FilteredComponentsExample'
export * from './MultiNodeFlow'
export * from './StatusIndicatorsExample'
export * from './ValidationActionsExample'
+24
View File
@@ -428,6 +428,30 @@ describe('Agentflow Component', () => {
})
})
describe('Canvas Actions', () => {
it('should render canvasActions content in the canvas', async () => {
const { getByTestId } = render(
<Agentflow {...defaultProps} canvasActions={<button data-testid='custom-action'>My Button</button>} />
)
await waitFor(() => {
expect(getByTestId('custom-action')).toBeInTheDocument()
})
})
it('should not render canvasActions in read-only mode', async () => {
const { container, queryByTestId } = render(
<Agentflow {...defaultProps} readOnly={true} canvasActions={<button data-testid='custom-action'>My Button</button>} />
)
await waitFor(() => {
expect(container.querySelector('.agentflow-container')).toBeInTheDocument()
})
expect(queryByTestId('custom-action')).not.toBeInTheDocument()
})
})
describe('Imperative Ref', () => {
it('should expose agentflow instance via ref', async () => {
const ref = createRef<AgentFlowInstance>()
+16 -8
View File
@@ -41,6 +41,7 @@ function AgentflowCanvas({
showDefaultHeader = true,
enableGenerator = true,
showDefaultPalette = true,
canvasActions,
renderHeader,
renderNodePalette
}: {
@@ -52,6 +53,7 @@ function AgentflowCanvas({
showDefaultHeader?: boolean
showDefaultPalette?: boolean
enableGenerator?: boolean
canvasActions?: AgentflowProps['canvasActions']
renderHeader?: AgentflowProps['renderHeader']
renderNodePalette?: AgentflowProps['renderNodePalette']
}) {
@@ -272,14 +274,17 @@ function AgentflowCanvas({
</StyledFab>
)}
{/* Validation Feedback - positioned at top right */}
{/* Canvas action buttons - positioned at top right */}
{!readOnly && (
<ValidationFeedback
nodes={nodes as FlowNode[]}
edges={edges as FlowEdge[]}
availableNodes={availableNodes}
setNodes={setLocalNodes as React.Dispatch<React.SetStateAction<FlowNode[]>>}
/>
<div style={{ position: 'absolute', right: 20, top: 20, zIndex: 1001, display: 'flex', gap: 8 }}>
<ValidationFeedback
nodes={nodes as FlowNode[]}
edges={edges as FlowEdge[]}
availableNodes={availableNodes}
setNodes={setLocalNodes as React.Dispatch<React.SetStateAction<FlowNode[]>>}
/>
{canvasActions}
</div>
)}
<ReactFlow
@@ -368,7 +373,8 @@ export const Agentflow = forwardRef<AgentFlowInstance, AgentflowProps>(function
renderHeader,
renderNodePalette,
showDefaultHeader = true,
showDefaultPalette = true
showDefaultPalette = true,
canvasActions
} = props
return (
@@ -394,6 +400,7 @@ export const Agentflow = forwardRef<AgentFlowInstance, AgentflowProps>(function
showDefaultPalette={showDefaultPalette}
renderHeader={renderHeader}
renderNodePalette={renderNodePalette}
canvasActions={canvasActions}
/>
</ReactFlowProvider>
</AgentflowProvider>
@@ -416,6 +423,7 @@ const AgentflowCanvasWithRef = forwardRef<
enableGenerator?: boolean
renderHeader?: AgentflowProps['renderHeader']
renderNodePalette?: AgentflowProps['renderNodePalette']
canvasActions?: AgentflowProps['canvasActions']
}
>(function AgentflowCanvasWithRef(props, ref) {
const agentflow = useAgentflow()
@@ -59,6 +59,21 @@ export interface AgentflowProps {
/** Whether the canvas is read-only */
readOnly?: boolean
/**
* Additional buttons rendered in the top-right canvas overlay, to the right of the built-in
* validation FAB. Consumers have full control over content — pass any ReactNode (FABs, icon
* buttons, popovers, dialogs, etc.). Hidden when `readOnly` is true.
*
* @example
* // Add a chat button alongside the validation FAB (mirrors legacy v2 pattern)
* canvasActions={
* <Fab size="small" color="secondary" onClick={() => setShowChat(true)}>
* <IconMessage />
* </Fab>
* }
*/
canvasActions?: ReactNode
/** Custom header renderer - receives save/export handlers */
renderHeader?: (props: HeaderRenderProps) => ReactNode
@@ -144,7 +144,7 @@ function ValidationFeedbackComponent({ nodes, edges, availableNodes, setNodes }:
return (
<>
<ClickAwayListener onClickAway={handleClose}>
<div ref={containerRef} style={{ position: 'absolute', right: 20, top: 20, zIndex: 1001 }}>
<div ref={containerRef} style={{ position: 'relative' }}>
<Fab
size='small'
aria-label='validation'