mirror of
https://github.com/FlowiseAI/Flowise.git
synced 2026-10-12 02:02:17 +00:00
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:
@@ -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'
|
||||
|
||||
@@ -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>()
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user