mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-28 03:00:29 -04:00
feat(workflow-preview): added workflow preview for version control, with the ability to revert to old states of the workflow (#330)
* generic workflow preview and view deployed modal * improved styling and functionality * cleaning up throwing errors * Remove node_modules from tracking * coped the deployment-controls from main * added revert to current state * added subblock fields * preview cleaned up * added mrge changes * added support for all sub-block types * updated package-lock * add hidden dialogheader --------- Co-authored-by: Adam Gough <adam_gough@brown.edu> Co-authored-by: Adam Gough <adamgough@Adams-MacBook-Pro.local>
This commit is contained in:
63
sim/app/api/workflows/[id]/deployed/route.ts
Normal file
63
sim/app/api/workflows/[id]/deployed/route.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { NextRequest } from 'next/server'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { db } from '@/db'
|
||||
import { workflow } from '@/db/schema'
|
||||
import { validateWorkflowAccess } from '../../middleware'
|
||||
import { createErrorResponse, createSuccessResponse } from '../../utils'
|
||||
|
||||
const logger = createLogger('WorkflowDeployedAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
export const runtime = 'nodejs'
|
||||
|
||||
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const requestId = crypto.randomUUID().slice(0, 8)
|
||||
const { id } = await params
|
||||
|
||||
try {
|
||||
logger.debug(`[${requestId}] Fetching deployed state for workflow: ${id}`)
|
||||
const validation = await validateWorkflowAccess(request, id, false)
|
||||
|
||||
if (validation.error) {
|
||||
logger.warn(`[${requestId}] Failed to fetch deployed state: ${validation.error.message}`)
|
||||
return createErrorResponse(validation.error.message, validation.error.status)
|
||||
}
|
||||
|
||||
// Fetch just the deployed state
|
||||
const result = await db
|
||||
.select({
|
||||
deployedState: workflow.deployedState,
|
||||
isDeployed: workflow.isDeployed
|
||||
})
|
||||
.from(workflow)
|
||||
.where(eq(workflow.id, id))
|
||||
.limit(1)
|
||||
|
||||
if (result.length === 0) {
|
||||
logger.warn(`[${requestId}] Workflow not found: ${id}`)
|
||||
return createErrorResponse('Workflow not found', 404)
|
||||
}
|
||||
|
||||
const workflowData = result[0]
|
||||
|
||||
// If the workflow is not deployed, return appropriate response
|
||||
if (!workflowData.isDeployed || !workflowData.deployedState) {
|
||||
logger.info(`[${requestId}] No deployed state available for workflow: ${id}`)
|
||||
return createSuccessResponse({
|
||||
deployedState: null,
|
||||
isDeployed: false
|
||||
})
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Successfully retrieved DEPLOYED state: ${id}`)
|
||||
return createSuccessResponse({
|
||||
deployedState: workflowData.deployedState,
|
||||
isDeployed: true
|
||||
})
|
||||
|
||||
} catch (error: any) {
|
||||
logger.error(`[${requestId}] Error fetching deployed state: ${id}`, error)
|
||||
return createErrorResponse(error.message || 'Failed to fetch deployed state', 500)
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,8 @@ import { ApiEndpoint } from '@/app/w/[id]/components/control-bar/components/depl
|
||||
import { ApiKey } from '@/app/w/[id]/components/control-bar/components/deploy-modal/components/deployment-info/components/api-key/api-key'
|
||||
import { DeployStatus } from '@/app/w/[id]/components/control-bar/components/deploy-modal/components/deployment-info/components/deploy-status/deploy-status'
|
||||
import { ExampleCommand } from '@/app/w/[id]/components/control-bar/components/deploy-modal/components/deployment-info/components/example-command/example-command'
|
||||
import { DeployedWorkflowModal } from '../../../deployment-controls/components/deployed-workflow-modal'
|
||||
import { useNotificationStore } from '@/stores/notifications/store'
|
||||
|
||||
interface DeploymentInfoProps {
|
||||
isLoading: boolean
|
||||
@@ -34,6 +36,7 @@ interface DeploymentInfoProps {
|
||||
onUndeploy: () => void
|
||||
isSubmitting: boolean
|
||||
isUndeploying: boolean
|
||||
workflowId?: string
|
||||
}
|
||||
|
||||
export function DeploymentInfo({
|
||||
@@ -43,7 +46,51 @@ export function DeploymentInfo({
|
||||
onUndeploy,
|
||||
isSubmitting,
|
||||
isUndeploying,
|
||||
workflowId,
|
||||
}: DeploymentInfoProps) {
|
||||
const [isViewingDeployed, setIsViewingDeployed] = useState(false)
|
||||
const [deployedWorkflowState, setDeployedWorkflowState] = useState<any>(null)
|
||||
const { addNotification } = useNotificationStore()
|
||||
|
||||
const handleViewDeployed = async () => {
|
||||
if (!workflowId) {
|
||||
addNotification(
|
||||
'error',
|
||||
'Cannot view deployment: Workflow ID is missing',
|
||||
null
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/workflows/${workflowId}/deployed`)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch deployed workflow')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (data && data.deployedState) {
|
||||
setDeployedWorkflowState(data.deployedState)
|
||||
setIsViewingDeployed(true)
|
||||
} else {
|
||||
addNotification(
|
||||
'error',
|
||||
'Failed to view deployment: No deployment state found',
|
||||
workflowId
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching deployed workflow:', error)
|
||||
addNotification(
|
||||
'error',
|
||||
`Failed to fetch deployed workflow: ${(error as Error).message}`,
|
||||
workflowId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (isLoading || !deploymentInfo) {
|
||||
return (
|
||||
<div className="space-y-4 px-1 overflow-y-auto">
|
||||
@@ -78,51 +125,68 @@ export function DeploymentInfo({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4 px-1 overflow-y-auto">
|
||||
<div className="space-y-4">
|
||||
<ApiEndpoint endpoint={deploymentInfo.endpoint} />
|
||||
<ApiKey apiKey={deploymentInfo.apiKey} />
|
||||
<ExampleCommand command={deploymentInfo.exampleCommand} apiKey={deploymentInfo.apiKey} />
|
||||
</div>
|
||||
<>
|
||||
<div className="space-y-4 px-1 overflow-y-auto">
|
||||
<div className="space-y-4">
|
||||
<ApiEndpoint endpoint={deploymentInfo.endpoint} />
|
||||
<ApiKey apiKey={deploymentInfo.apiKey} />
|
||||
<ExampleCommand command={deploymentInfo.exampleCommand} apiKey={deploymentInfo.apiKey} />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between pt-2 mt-4">
|
||||
<DeployStatus needsRedeployment={deploymentInfo.needsRedeployment} />
|
||||
<div className="flex items-center justify-between pt-2 mt-4">
|
||||
<DeployStatus needsRedeployment={deploymentInfo.needsRedeployment} />
|
||||
|
||||
<div className="flex gap-2">
|
||||
{deploymentInfo.needsRedeployment && (
|
||||
<Button variant="outline" size="sm" onClick={onRedeploy} disabled={isSubmitting}>
|
||||
{isSubmitting ? <Loader2 className="h-3.5 w-3.5 animate-spin mr-1.5" /> : null}
|
||||
{isSubmitting ? 'Redeploying...' : 'Redeploy'}
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleViewDeployed}
|
||||
>
|
||||
View Deployment
|
||||
</Button>
|
||||
)}
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="destructive" size="sm" disabled={isUndeploying}>
|
||||
{isUndeploying ? <Loader2 className="h-3.5 w-3.5 animate-spin mr-1.5" /> : null}
|
||||
{isUndeploying ? 'Undeploying...' : 'Undeploy'}
|
||||
{deploymentInfo.needsRedeployment && (
|
||||
<Button variant="outline" size="sm" onClick={onRedeploy} disabled={isSubmitting}>
|
||||
{isSubmitting ? <Loader2 className="h-3.5 w-3.5 animate-spin mr-1.5" /> : null}
|
||||
{isSubmitting ? 'Redeploying...' : 'Redeploy'}
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Undeploy API</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to undeploy this workflow? This will remove the API endpoint
|
||||
and make it unavailable to external users.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={onUndeploy}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
Undeploy
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)}
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="destructive" size="sm" disabled={isUndeploying}>
|
||||
{isUndeploying ? <Loader2 className="h-3.5 w-3.5 animate-spin mr-1.5" /> : null}
|
||||
{isUndeploying ? 'Undeploying...' : 'Undeploy'}
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Undeploy API</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to undeploy this workflow? This will remove the API endpoint
|
||||
and make it unavailable to external users.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={onUndeploy}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
Undeploy
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{deployedWorkflowState && (
|
||||
<DeployedWorkflowModal
|
||||
isOpen={isViewingDeployed}
|
||||
onClose={() => setIsViewingDeployed(false)}
|
||||
deployedWorkflowState={deployedWorkflowState}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -589,6 +589,7 @@ export function DeployModal({
|
||||
onUndeploy={handleUndeploy}
|
||||
isSubmitting={isSubmitting}
|
||||
isUndeploying={isUndeploying}
|
||||
workflowId={workflowId || undefined}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Card, CardContent, CardHeader } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { WorkflowPreview } from '@/app/w/components/workflow-preview/generic-workflow-preview'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface DeployedWorkflowCardProps {
|
||||
// Current workflow state (if any)
|
||||
currentWorkflowState?: {
|
||||
blocks: Record<string, any>
|
||||
edges: Array<any>
|
||||
loops: Record<string, any>
|
||||
}
|
||||
// Deployed workflow state from Supabase
|
||||
deployedWorkflowState: {
|
||||
blocks: Record<string, any>
|
||||
edges: Array<any>
|
||||
loops: Record<string, any>
|
||||
}
|
||||
// Optional className for styling
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function DeployedWorkflowCard({
|
||||
currentWorkflowState,
|
||||
deployedWorkflowState,
|
||||
className,
|
||||
}: DeployedWorkflowCardProps) {
|
||||
// State for toggling between deployed and current workflow
|
||||
const [showingDeployed, setShowingDeployed] = useState(true)
|
||||
|
||||
// Determine which workflow state to show
|
||||
const workflowToShow = showingDeployed ? deployedWorkflowState : currentWorkflowState
|
||||
|
||||
return (
|
||||
<Card className={cn('overflow-hidden', className)}>
|
||||
<CardHeader className="space-y-4 p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-medium">
|
||||
{showingDeployed ? 'Deployed Workflow' : 'Current Workflow'}
|
||||
</h3>
|
||||
{/* Controls */}
|
||||
<div className="flex items-center gap-4">
|
||||
{/* Version toggle - only show if there's a current version */}
|
||||
{currentWorkflowState && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setShowingDeployed(!showingDeployed)}
|
||||
>
|
||||
{showingDeployed ? 'Show Current' : 'Show Deployed'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="p-0">
|
||||
{/* Workflow preview with fixed height */}
|
||||
<div className="h-[500px] w-full">
|
||||
{workflowToShow ? (
|
||||
<WorkflowPreview
|
||||
workflowState={workflowToShow}
|
||||
showSubBlocks={true}
|
||||
height="100%"
|
||||
width="100%"
|
||||
isPannable={true}
|
||||
defaultPosition={{ x: 0, y: 0 }}
|
||||
defaultZoom={1}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center text-muted-foreground">
|
||||
No workflow data available
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
'use client'
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog'
|
||||
import { DeployedWorkflowCard } from './deployed-workflow-card'
|
||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
import { useState } from 'react'
|
||||
import { mergeSubblockState } from '@/stores/workflows/utils'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
|
||||
interface DeployedWorkflowModalProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
deployedWorkflowState: {
|
||||
blocks: Record<string, any>
|
||||
edges: Array<any>
|
||||
loops: Record<string, any>
|
||||
}
|
||||
}
|
||||
|
||||
export function DeployedWorkflowModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
deployedWorkflowState,
|
||||
}: DeployedWorkflowModalProps) {
|
||||
const [showRevertDialog, setShowRevertDialog] = useState(false)
|
||||
const { revertToDeployedState } = useWorkflowStore()
|
||||
const activeWorkflowId = useWorkflowRegistry((state) => state.activeWorkflowId)
|
||||
|
||||
// Get current workflow state to compare with deployed state
|
||||
const currentWorkflowState = useWorkflowStore((state) => ({
|
||||
blocks: activeWorkflowId ? mergeSubblockState(state.blocks, activeWorkflowId) : state.blocks,
|
||||
edges: state.edges,
|
||||
loops: state.loops,
|
||||
}))
|
||||
|
||||
const handleRevert = () => {
|
||||
revertToDeployedState(deployedWorkflowState)
|
||||
setShowRevertDialog(false)
|
||||
onClose()
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent
|
||||
className="sm:max-w-[1100px] max-h-[100vh] overflow-y-auto"
|
||||
style={{ zIndex: 1000 }}
|
||||
onInteractOutside={(e) => e.preventDefault()}
|
||||
onEscapeKeyDown={(e) => e.preventDefault()}
|
||||
hideCloseButton={true}
|
||||
>
|
||||
<div className="sr-only">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Deployed Workflow</DialogTitle>
|
||||
</DialogHeader>
|
||||
</div>
|
||||
|
||||
<DeployedWorkflowCard
|
||||
currentWorkflowState={currentWorkflowState}
|
||||
deployedWorkflowState={deployedWorkflowState}
|
||||
/>
|
||||
|
||||
<div className="flex justify-between mt-6">
|
||||
<AlertDialog open={showRevertDialog} onOpenChange={setShowRevertDialog}>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="destructive">
|
||||
Revert to Deployed
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent
|
||||
style={{ zIndex: 1001 }}
|
||||
className="sm:max-w-[425px]"
|
||||
>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Revert to Deployed Version?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will replace your current workflow with the deployed version.
|
||||
Any unsaved changes will be lost. This action cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleRevert}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
Revert
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -22,20 +22,10 @@ export function DeploymentControls({
|
||||
needsRedeployment,
|
||||
setNeedsRedeployment,
|
||||
}: DeploymentControlsProps) {
|
||||
// Store hooks
|
||||
const { isDeployed } = useWorkflowStore()
|
||||
|
||||
// Local state
|
||||
const [isDeploying, setIsDeploying] = useState(false)
|
||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||
|
||||
/**
|
||||
* Open the deployment modal
|
||||
*/
|
||||
const handleOpenModal = () => {
|
||||
setIsModalOpen(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip>
|
||||
@@ -44,7 +34,7 @@ export function DeploymentControls({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleOpenModal}
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
disabled={isDeploying}
|
||||
className={cn('hover:text-[#802FFF]', isDeployed && 'text-[#802FFF]')}
|
||||
>
|
||||
@@ -56,7 +46,6 @@ export function DeploymentControls({
|
||||
<span className="sr-only">Deploy API</span>
|
||||
</Button>
|
||||
|
||||
{/* Improved redeploy indicator with animation */}
|
||||
{isDeployed && needsRedeployment && (
|
||||
<div className="absolute top-0.5 right-0.5 flex items-center justify-center">
|
||||
<div className="relative">
|
||||
@@ -88,4 +77,4 @@ export function DeploymentControls({
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -804,37 +804,37 @@ export function ControlBar() {
|
||||
/**
|
||||
* Render publish button
|
||||
*/
|
||||
const renderPublishButton = () => {
|
||||
const isPublished = isPublishedToMarketplace()
|
||||
// const renderPublishButton = () => {
|
||||
// const isPublished = isPublishedToMarketplace()
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handlePublishWorkflow}
|
||||
disabled={isPublishing}
|
||||
className={cn('hover:text-[#802FFF]', isPublished && 'text-[#802FFF]')}
|
||||
>
|
||||
{isPublishing ? (
|
||||
<Loader2 className="h-5 w-5 animate-spin" />
|
||||
) : (
|
||||
<Store className="h-5 w-5" />
|
||||
)}
|
||||
<span className="sr-only">Publish to Marketplace</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{isPublishing
|
||||
? 'Publishing...'
|
||||
: isPublished
|
||||
? 'Published to Marketplace'
|
||||
: 'Publish to Marketplace'}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
// return (
|
||||
// <Tooltip>
|
||||
// <TooltipTrigger asChild>
|
||||
// <Button
|
||||
// variant="ghost"
|
||||
// size="icon"
|
||||
// onClick={handlePublishWorkflow}
|
||||
// disabled={isPublishing}
|
||||
// className={cn('hover:text-[#802FFF]', isPublished && 'text-[#802FFF]')}
|
||||
// >
|
||||
// {isPublishing ? (
|
||||
// <Loader2 className="h-5 w-5 animate-spin" />
|
||||
// ) : (
|
||||
// <Store className="h-5 w-5" />
|
||||
// )}
|
||||
// <span className="sr-only">Publish to Marketplace</span>
|
||||
// </Button>
|
||||
// </TooltipTrigger>
|
||||
// <TooltipContent>
|
||||
// {isPublishing
|
||||
// ? 'Publishing...'
|
||||
// : isPublished
|
||||
// ? 'Published to Marketplace'
|
||||
// : 'Publish to Marketplace'}
|
||||
// </TooltipContent>
|
||||
// </Tooltip>
|
||||
// )
|
||||
// }
|
||||
|
||||
/**
|
||||
* Render workflow duplicate button
|
||||
|
||||
@@ -639,4 +639,4 @@ export function NotificationAlert({ notification, isFading, onHide }: Notificati
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -24,18 +24,8 @@ import { createLoopNode } from '@/app/w/[id]/components/workflow-loop/workflow-l
|
||||
import { getBlock } from '@/blocks'
|
||||
import { SubBlockConfig } from '@/blocks/types'
|
||||
|
||||
/**
|
||||
* Extended SubBlockConfig interface with optional value property
|
||||
* Extends the base SubBlockConfig to include the current value of the subblock
|
||||
*/
|
||||
interface ExtendedSubBlockConfig extends SubBlockConfig {
|
||||
value?: any
|
||||
}
|
||||
|
||||
/**
|
||||
* WorkflowPreviewProps interface - defines the properties for the WorkflowPreview component
|
||||
*/
|
||||
interface WorkflowPreviewProps {
|
||||
// The workflow state to render
|
||||
workflowState: {
|
||||
blocks: Record<string, any>
|
||||
edges: Array<{
|
||||
@@ -47,51 +37,66 @@ interface WorkflowPreviewProps {
|
||||
}>
|
||||
loops: Record<string, any>
|
||||
}
|
||||
// Whether to show subblocks
|
||||
showSubBlocks?: boolean
|
||||
// Optional className for container styling
|
||||
className?: string
|
||||
// Optional height/width overrides
|
||||
height?: string | number
|
||||
width?: string | number
|
||||
isPannable?: boolean
|
||||
defaultPosition?: { x: number, y: number }
|
||||
defaultZoom?: number
|
||||
}
|
||||
|
||||
interface ExtendedSubBlockConfig extends SubBlockConfig {
|
||||
value?: any
|
||||
}
|
||||
|
||||
// Define node types
|
||||
const nodeTypes: NodeTypes = {
|
||||
workflowBlock: PreviewWorkflowBlock,
|
||||
loopLabel: LoopLabel,
|
||||
loopInput: LoopInput,
|
||||
}
|
||||
|
||||
// Define edge types
|
||||
const edgeTypes: EdgeTypes = {
|
||||
workflowEdge: WorkflowEdge,
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares subblocks by combining block state with block configuration
|
||||
* @param blockSubBlocks - The subblocks from the block state
|
||||
* @param blockConfig - The configuration for the block
|
||||
* @returns Array of prepared subblocks with values and configuration
|
||||
*/
|
||||
function prepareSubBlocks(blockSubBlocks: Record<string, any> = {}, blockConfig: any) {
|
||||
// Get the subBlocks from the block config
|
||||
const configSubBlocks = blockConfig?.subBlocks || []
|
||||
|
||||
// Convert the subBlocks object to an array with proper structure
|
||||
return Object.entries(blockSubBlocks)
|
||||
.map(([id, subBlock]) => {
|
||||
// Find the matching config for this subBlock to get the title and other properties
|
||||
const matchingConfig = configSubBlocks.find((config: any) => config.id === id)
|
||||
|
||||
// Skip if no value or value is null/undefined/empty string
|
||||
const value = subBlock.value
|
||||
const hasValue = value !== undefined && value !== null && value !== ''
|
||||
|
||||
// Only include subblocks with values
|
||||
if (!hasValue) return null
|
||||
|
||||
return {
|
||||
...matchingConfig, // Include title and other properties from config
|
||||
...subBlock, // Include value and other properties from state
|
||||
id, // Ensure id is included
|
||||
...matchingConfig,
|
||||
...subBlock,
|
||||
id,
|
||||
}
|
||||
})
|
||||
.filter(Boolean) // Filter out any undefined or null entries
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
/**
|
||||
* Groups subblocks into rows for layout
|
||||
* @param subBlocks - Array of subblocks to group
|
||||
* @returns 2D array of subblocks grouped into rows
|
||||
*/
|
||||
function groupSubBlocks(subBlocks: ExtendedSubBlockConfig[]) {
|
||||
const rows: ExtendedSubBlockConfig[][] = []
|
||||
let currentRow: ExtendedSubBlockConfig[] = []
|
||||
let currentRowWidth = 0
|
||||
|
||||
// Filter visible blocks
|
||||
const visibleSubBlocks = subBlocks.filter((block) => !block.hidden)
|
||||
|
||||
visibleSubBlocks.forEach((block) => {
|
||||
@@ -128,8 +133,8 @@ function PreviewSubBlock({ config }: { config: ExtendedSubBlockConfig }) {
|
||||
switch (config.type) {
|
||||
case 'short-input':
|
||||
return (
|
||||
<div className="h-7 rounded-md border border-input bg-background px-3 py-1 text-xs text-muted-foreground">
|
||||
{config.value || config.placeholder || 'Text input'}
|
||||
<div className="h-7 rounded-md border border-input bg-background px-3 py-1.5 text-xs text-muted-foreground">
|
||||
{config.password ? '**********************' : (config.value || config.placeholder || 'Text input')}
|
||||
</div>
|
||||
)
|
||||
case 'long-input':
|
||||
@@ -198,6 +203,174 @@ function PreviewSubBlock({ config }: { config: ExtendedSubBlockConfig }) {
|
||||
Tool configuration
|
||||
</div>
|
||||
)
|
||||
case 'oauth-input':
|
||||
return (
|
||||
<div className="h-7 rounded-md border border-input bg-background px-3 py-1 text-xs text-muted-foreground flex items-center justify-between">
|
||||
<span>
|
||||
{config.value ? 'Connected account' : config.placeholder || 'Select account'}
|
||||
</span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="ml-2"
|
||||
>
|
||||
<path d="m6 9 6 6 6-6" />
|
||||
</svg>
|
||||
</div>
|
||||
)
|
||||
case 'file-selector':
|
||||
return (
|
||||
<div className="h-7 rounded-md border border-input bg-background px-3 py-1 text-xs text-muted-foreground flex items-center justify-between">
|
||||
<span>
|
||||
{config.value ? 'File selected' : config.placeholder || 'Select file'}
|
||||
</span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="ml-2"
|
||||
>
|
||||
<path d="m6 9 6 6 6-6" />
|
||||
</svg>
|
||||
</div>
|
||||
)
|
||||
case 'folder-selector':
|
||||
return (
|
||||
<div className="h-7 rounded-md border border-input bg-background px-3 py-1 text-xs text-muted-foreground flex items-center justify-between">
|
||||
<span>
|
||||
{config.value ? 'Folder selected' : config.placeholder || 'Select folder'}
|
||||
</span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="ml-2"
|
||||
>
|
||||
<path d="m6 9 6 6 6-6" />
|
||||
</svg>
|
||||
</div>
|
||||
)
|
||||
case 'project-selector':
|
||||
return (
|
||||
<div className="h-7 rounded-md border border-input bg-background px-3 py-1 text-xs text-muted-foreground flex items-center justify-between">
|
||||
<span>
|
||||
{config.value ? 'Project selected' : config.placeholder || 'Select project'}
|
||||
</span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="ml-2"
|
||||
>
|
||||
<path d="m6 9 6 6 6-6" />
|
||||
</svg>
|
||||
</div>
|
||||
)
|
||||
case 'condition-input':
|
||||
return (
|
||||
<div className="h-16 rounded-md border border-input bg-background p-2 text-xs text-muted-foreground">
|
||||
Condition configuration
|
||||
</div>
|
||||
)
|
||||
case 'eval-input':
|
||||
return (
|
||||
<div className="h-12 rounded-md border border-input bg-background p-2 text-xs font-mono text-muted-foreground">
|
||||
Eval expression
|
||||
</div>
|
||||
)
|
||||
case 'date-input':
|
||||
return (
|
||||
<div className="h-7 rounded-md border border-input bg-background px-3 py-1 text-xs text-muted-foreground flex items-center justify-between">
|
||||
<span>{config.value || config.placeholder || 'Select date'}</span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="ml-2"
|
||||
>
|
||||
<rect width="18" height="18" x="3" y="4" rx="2" ry="2" />
|
||||
<line x1="16" x2="16" y1="2" y2="6" />
|
||||
<line x1="8" x2="8" y1="2" y2="6" />
|
||||
<line x1="3" x2="21" y1="10" y2="10" />
|
||||
</svg>
|
||||
</div>
|
||||
)
|
||||
case 'time-input':
|
||||
return (
|
||||
<div className="h-7 rounded-md border border-input bg-background px-3 py-1 text-xs text-muted-foreground flex items-center justify-between">
|
||||
<span>{config.value || config.placeholder || 'Select time'}</span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="ml-2"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<polyline points="12 6 12 12 16 14" />
|
||||
</svg>
|
||||
</div>
|
||||
)
|
||||
case 'file-upload':
|
||||
return (
|
||||
<div className="h-7 rounded-md border border-dashed border-input bg-background px-3 py-1 text-xs text-muted-foreground flex items-center justify-center">
|
||||
{config.value ? 'File uploaded' : 'Upload file'}
|
||||
</div>
|
||||
)
|
||||
case 'webhook-config':
|
||||
return (
|
||||
<div className="h-7 rounded-md border border-input bg-background px-3 py-1 text-xs text-muted-foreground">
|
||||
Webhook configuration
|
||||
</div>
|
||||
)
|
||||
case 'schedule-config':
|
||||
return (
|
||||
<div className="h-7 rounded-md border border-input bg-background px-3 py-1 text-xs text-muted-foreground">
|
||||
Schedule configuration
|
||||
</div>
|
||||
)
|
||||
case 'input-format':
|
||||
return (
|
||||
<div className="h-7 rounded-md border border-input bg-background px-3 py-1 text-xs text-muted-foreground">
|
||||
Input format configuration
|
||||
</div>
|
||||
)
|
||||
case 'slider':
|
||||
return (
|
||||
<div className="h-7 px-1 py-2">
|
||||
@@ -230,37 +403,32 @@ function PreviewSubBlock({ config }: { config: ExtendedSubBlockConfig }) {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* PreviewWorkflowBlock component - Simplified version of WorkflowBlock for preview
|
||||
* Renders a block with its subblocks in a card layout
|
||||
* @param id - The ID of the block
|
||||
* @param data - The data for the block
|
||||
*/
|
||||
function PreviewWorkflowBlock({ id, data }: NodeProps<any>) {
|
||||
const { type, config, name, blockState } = data
|
||||
const { type, config, name, blockState, showSubBlocks = true } = data
|
||||
|
||||
// Prepare subblocks from block state and config
|
||||
const preparedSubBlocks = prepareSubBlocks(blockState?.subBlocks, config)
|
||||
// Only prepare subblocks if they should be shown
|
||||
const preparedSubBlocks = useMemo(() => {
|
||||
if (!showSubBlocks) return []
|
||||
return prepareSubBlocks(blockState?.subBlocks, config)
|
||||
}, [blockState?.subBlocks, config, showSubBlocks])
|
||||
|
||||
// Group subblocks for layout
|
||||
const subBlockRows = groupSubBlocks(preparedSubBlocks)
|
||||
const subBlockRows = useMemo(() => {
|
||||
return groupSubBlocks(preparedSubBlocks)
|
||||
}, [preparedSubBlocks])
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<Card
|
||||
className={cn(
|
||||
'shadow-md select-none relative',
|
||||
'transition-ring transition-block-bg',
|
||||
blockState?.isWide ? 'w-[400px]' : 'w-[260px]'
|
||||
)}
|
||||
>
|
||||
<Card className={cn(
|
||||
'shadow-md select-none relative',
|
||||
'transition-ring transition-block-bg',
|
||||
blockState?.isWide ? 'w-[400px]' : 'w-[260px]'
|
||||
)}>
|
||||
{/* Block Header */}
|
||||
<div className="flex items-center justify-between p-2 border-b">
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="flex items-center justify-center w-6 h-6 rounded"
|
||||
style={{ backgroundColor: config.bgColor }}
|
||||
>
|
||||
<div className="flex items-center justify-center w-6 h-6 rounded"
|
||||
style={{ backgroundColor: config.bgColor }}>
|
||||
<config.icon className="w-4 h-4 text-white" />
|
||||
</div>
|
||||
<span className="font-medium text-sm truncate max-w-[180px]" title={name}>
|
||||
@@ -269,27 +437,27 @@ function PreviewWorkflowBlock({ id, data }: NodeProps<any>) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Block Content with SubBlocks */}
|
||||
<div className="px-3 py-2 space-y-2">
|
||||
{subBlockRows.length > 0 ? (
|
||||
subBlockRows.map((row, rowIndex) => (
|
||||
<div key={`row-${rowIndex}`} className="flex gap-2">
|
||||
{row.map((subBlock, blockIndex) => (
|
||||
<div
|
||||
key={`${id}-${rowIndex}-${blockIndex}`}
|
||||
className={cn('space-y-1', subBlock.layout === 'half' ? 'flex-1' : 'w-full')}
|
||||
>
|
||||
<PreviewSubBlock config={subBlock} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="text-xs text-muted-foreground py-2">No configured items</div>
|
||||
)}
|
||||
</div>
|
||||
{/* Block Content */}
|
||||
{showSubBlocks && (
|
||||
<div className="px-3 py-2 space-y-2">
|
||||
{subBlockRows.length > 0 ? (
|
||||
subBlockRows.map((row, rowIndex) => (
|
||||
<div key={`row-${rowIndex}`} className="flex gap-2">
|
||||
{row.map((subBlock, blockIndex) => (
|
||||
<div key={`${id}-${rowIndex}-${blockIndex}`}
|
||||
className={cn('space-y-1', subBlock.layout === 'half' ? 'flex-1' : 'w-full')}>
|
||||
<PreviewSubBlock config={subBlock} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="text-xs text-muted-foreground py-2">No configured items</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Input Handle - Don't show for starter blocks */}
|
||||
{/* Handles */}
|
||||
{type !== 'starter' && (
|
||||
<Handle
|
||||
type="target"
|
||||
@@ -304,7 +472,6 @@ function PreviewWorkflowBlock({ id, data }: NodeProps<any>) {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Output Handle */}
|
||||
{type !== 'condition' && (
|
||||
<Handle
|
||||
type="source"
|
||||
@@ -323,46 +490,20 @@ function PreviewWorkflowBlock({ id, data }: NodeProps<any>) {
|
||||
)
|
||||
}
|
||||
|
||||
// Define node types and edge types for ReactFlow
|
||||
const nodeTypes: NodeTypes = {
|
||||
workflowBlock: PreviewWorkflowBlock,
|
||||
loopLabel: LoopLabel,
|
||||
loopInput: LoopInput,
|
||||
}
|
||||
|
||||
const edgeTypes: EdgeTypes = { workflowEdge: WorkflowEdge }
|
||||
|
||||
/**
|
||||
* WorkflowPreviewContent component - Inner content of the workflow preview
|
||||
* Handles the transformation of workflow state into ReactFlow nodes and edges
|
||||
* @param workflowState - The state of the workflow to preview
|
||||
*/
|
||||
function WorkflowPreviewContent({ workflowState }: WorkflowPreviewProps) {
|
||||
// Transform blocks into ReactFlow nodes
|
||||
function WorkflowPreviewContent({
|
||||
workflowState,
|
||||
showSubBlocks = true,
|
||||
className,
|
||||
height = '100%',
|
||||
width = '100%',
|
||||
isPannable = false,
|
||||
defaultPosition,
|
||||
defaultZoom,
|
||||
}: WorkflowPreviewProps) {
|
||||
// Transform blocks and loops into ReactFlow nodes
|
||||
const nodes: Node[] = useMemo(() => {
|
||||
const nodeArray: Node[] = []
|
||||
|
||||
// Add block nodes
|
||||
Object.entries(workflowState.blocks).forEach(([blockId, block]) => {
|
||||
// Get block configuration from registry
|
||||
const blockConfig = getBlock(block.type)
|
||||
if (!blockConfig) return
|
||||
|
||||
// Create node
|
||||
nodeArray.push({
|
||||
id: blockId,
|
||||
type: 'workflowBlock',
|
||||
position: block.position,
|
||||
data: {
|
||||
type: block.type,
|
||||
config: blockConfig,
|
||||
name: block.name,
|
||||
blockState: block, // Pass the entire block state
|
||||
},
|
||||
draggable: false,
|
||||
})
|
||||
})
|
||||
|
||||
// Add loop nodes
|
||||
Object.entries(workflowState.loops || {}).forEach(([loopId, loop]) => {
|
||||
const loopNodes = createLoopNode({
|
||||
@@ -380,10 +521,30 @@ function WorkflowPreviewContent({ workflowState }: WorkflowPreviewProps) {
|
||||
}
|
||||
})
|
||||
|
||||
return nodeArray
|
||||
}, [workflowState.blocks, workflowState.loops])
|
||||
// Add block nodes
|
||||
Object.entries(workflowState.blocks).forEach(([blockId, block]) => {
|
||||
const blockConfig = getBlock(block.type)
|
||||
if (!blockConfig) return
|
||||
|
||||
// Transform edges into ReactFlow edges
|
||||
nodeArray.push({
|
||||
id: blockId,
|
||||
type: 'workflowBlock',
|
||||
position: block.position,
|
||||
data: {
|
||||
type: block.type,
|
||||
config: blockConfig,
|
||||
name: block.name,
|
||||
blockState: block,
|
||||
showSubBlocks,
|
||||
},
|
||||
draggable: false,
|
||||
})
|
||||
})
|
||||
|
||||
return nodeArray
|
||||
}, [workflowState.blocks, workflowState.loops, showSubBlocks])
|
||||
|
||||
// Transform edges
|
||||
const edges: Edge[] = useMemo(() => {
|
||||
return workflowState.edges.map((edge) => ({
|
||||
id: edge.id,
|
||||
@@ -392,66 +553,41 @@ function WorkflowPreviewContent({ workflowState }: WorkflowPreviewProps) {
|
||||
sourceHandle: edge.sourceHandle,
|
||||
targetHandle: edge.targetHandle,
|
||||
type: 'workflowEdge',
|
||||
data: {
|
||||
// No edge deletion in preview mode
|
||||
onDelete: undefined,
|
||||
selectedEdgeId: null,
|
||||
},
|
||||
}))
|
||||
}, [workflowState.edges])
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
fitView
|
||||
fitViewOptions={{
|
||||
padding: 0,
|
||||
minZoom: 0.2,
|
||||
maxZoom: 3,
|
||||
}}
|
||||
minZoom={0.2}
|
||||
maxZoom={3}
|
||||
defaultEdgeOptions={{ type: 'workflowEdge' }}
|
||||
proOptions={{ hideAttribution: true }}
|
||||
connectionLineType={ConnectionLineType.SmoothStep}
|
||||
connectionLineStyle={{
|
||||
stroke: '#94a3b8',
|
||||
strokeWidth: 1,
|
||||
strokeDasharray: '3,3',
|
||||
}}
|
||||
nodesDraggable={false}
|
||||
nodesConnectable={false}
|
||||
elementsSelectable={false}
|
||||
panOnScroll={false}
|
||||
zoomOnScroll={false}
|
||||
zoomOnPinch={false}
|
||||
zoomOnDoubleClick={false}
|
||||
panOnDrag={false}
|
||||
preventScrolling={false}
|
||||
disableKeyboardA11y={true}
|
||||
attributionPosition="bottom-right"
|
||||
className="w-full h-full pointer-events-none"
|
||||
style={{ background: 'transparent', pointerEvents: 'none' }}
|
||||
>
|
||||
<Background gap={12} size={1} className="opacity-30 pointer-events-none" />
|
||||
</ReactFlow>
|
||||
<div style={{ height, width }} className={className}>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
connectionLineType={ConnectionLineType.SmoothStep}
|
||||
fitView
|
||||
panOnScroll={false}
|
||||
panOnDrag={isPannable}
|
||||
zoomOnScroll={false}
|
||||
draggable={false}
|
||||
defaultViewport={{
|
||||
x: defaultPosition?.x ?? 0,
|
||||
y: defaultPosition?.y ?? 0,
|
||||
zoom: defaultZoom ?? 1,
|
||||
}}
|
||||
minZoom={0.1}
|
||||
maxZoom={2}
|
||||
proOptions={{ hideAttribution: true }}
|
||||
>
|
||||
<Background />
|
||||
</ReactFlow>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* WorkflowPreview component - Main exported component for workflow preview
|
||||
* Wraps the preview content in a ReactFlowProvider
|
||||
* @param workflowState - The state of the workflow to preview
|
||||
*/
|
||||
export function WorkflowPreview({ workflowState }: WorkflowPreviewProps) {
|
||||
export function WorkflowPreview(props: WorkflowPreviewProps) {
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<div className="h-full w-full -m-1">
|
||||
<WorkflowPreviewContent workflowState={workflowState} />
|
||||
</div>
|
||||
<WorkflowPreviewContent {...props} />
|
||||
</ReactFlowProvider>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import { Eye } from 'lucide-react'
|
||||
import { Card, CardContent, CardFooter, CardHeader } from '@/components/ui/card'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { Workflow } from '../marketplace'
|
||||
import { WorkflowPreview } from './workflow-preview'
|
||||
import { WorkflowPreview } from '@/app/w/components/workflow-preview/generic-workflow-preview'
|
||||
|
||||
/**
|
||||
* WorkflowCardProps interface - defines the properties for the WorkflowCard component
|
||||
@@ -30,6 +30,7 @@ export function WorkflowCard({ workflow, onHover }: WorkflowCardProps) {
|
||||
const router = useRouter()
|
||||
const { createWorkflow } = useWorkflowRegistry()
|
||||
|
||||
|
||||
// When workflow state becomes available, update preview ready state
|
||||
useEffect(() => {
|
||||
if (workflow.workflowState && !isPreviewReady) {
|
||||
@@ -93,7 +94,7 @@ export function WorkflowCard({ workflow, onHover }: WorkflowCardProps) {
|
||||
{/* Workflow preview/thumbnail area */}
|
||||
<div className="h-40 relative overflow-hidden bg-gradient-to-br from-slate-100 to-slate-200 dark:from-slate-800 dark:to-slate-900">
|
||||
{isPreviewReady && workflow.workflowState ? (
|
||||
// Show interactive workflow preview if state is available
|
||||
// Interactive Preview
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="w-full h-full transform-gpu scale-[0.9]">
|
||||
<WorkflowPreview workflowState={workflow.workflowState} />
|
||||
@@ -108,7 +109,8 @@ export function WorkflowCard({ workflow, onHover }: WorkflowCardProps) {
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center top',
|
||||
}}
|
||||
/>
|
||||
>
|
||||
</div>
|
||||
) : (
|
||||
// Fallback to text if no preview or thumbnail is available
|
||||
<div className="h-full w-full flex items-center justify-center">
|
||||
|
||||
2786
sim/package-lock.json
generated
2786
sim/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -17,11 +17,17 @@ export interface NotificationSection {
|
||||
content: string
|
||||
}
|
||||
|
||||
export interface NotificationAction {
|
||||
label: string
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
export interface NotificationOptions {
|
||||
copyableContent?: string
|
||||
isPersistent?: boolean
|
||||
sections?: NotificationSection[]
|
||||
needsRedeployment?: boolean
|
||||
actions?: NotificationAction[]
|
||||
}
|
||||
|
||||
export interface NotificationStore {
|
||||
|
||||
@@ -32,6 +32,7 @@ const MAX_HISTORY_LENGTH = 20
|
||||
// Types for workflow store with history management capabilities
|
||||
export interface WorkflowStoreWithHistory extends WorkflowStore, HistoryActions {
|
||||
history: WorkflowHistory
|
||||
revertToDeployedState: (deployedState: WorkflowState) => void
|
||||
}
|
||||
|
||||
// Higher-order store middleware that adds undo/redo functionality
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useWorkflowRegistry } from '../registry/store'
|
||||
import { useSubBlockStore } from '../subblock/store'
|
||||
import { workflowSync } from '../sync'
|
||||
import { mergeSubblockState } from '../utils'
|
||||
import { Loop, Position, SubBlockState } from './types'
|
||||
import { Loop, Position, SubBlockState, WorkflowState } from './types'
|
||||
import { detectCycle } from './utils'
|
||||
|
||||
const initialState = {
|
||||
@@ -669,6 +669,21 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
|
||||
get().updateLastSaved()
|
||||
}
|
||||
},
|
||||
|
||||
revertToDeployedState: (deployedState: WorkflowState) => {
|
||||
const newState = {
|
||||
blocks: deployedState.blocks,
|
||||
edges: deployedState.edges,
|
||||
loops: deployedState.loops,
|
||||
isDeployed: true,
|
||||
needsRedeployment: false,
|
||||
}
|
||||
|
||||
set(newState)
|
||||
pushHistory(set, get, newState, 'Reverted to deployed state')
|
||||
get().updateLastSaved()
|
||||
workflowSync.sync()
|
||||
},
|
||||
})),
|
||||
{ name: 'workflow-store' }
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user