improvement(stores): added deploy store and added subblock value to history

This commit is contained in:
Emir Karabeg
2025-02-28 20:16:51 -08:00
parent 756c3293eb
commit cad3db9cae
8 changed files with 501 additions and 263 deletions
+3 -2
View File
@@ -21,6 +21,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
// Generate a new API key
const apiKey = `wf_${uuidv4().replace(/-/g, '')}`
const deployedAt = new Date()
// Update the workflow with the API key and deployment status
await db
@@ -28,11 +29,11 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
.set({
apiKey,
isDeployed: true,
deployedAt: new Date(),
deployedAt,
})
.where(eq(workflow.id, id))
return createSuccessResponse({ apiKey })
return createSuccessResponse({ apiKey, isDeployed: true, deployedAt })
} catch (error: any) {
console.error('Error deploying workflow:', error)
return createErrorResponse(error.message || 'Failed to deploy workflow', 500)
+297 -241
View File
@@ -32,49 +32,44 @@ import { useWorkflowExecution } from '../../hooks/use-workflow-execution'
import { HistoryDropdownItem } from './components/history-dropdown-item'
import { NotificationDropdownItem } from './components/notification-dropdown-item'
/**
* Control bar for managing workflows - handles editing, deletion, deployment,
* history, notifications and execution.
*/
export function ControlBar() {
const { notifications, getWorkflowNotifications, addNotification } = useNotificationStore()
const { history, undo, redo, revertToHistoryState, lastSaved } = useWorkflowStore()
const [isEditing, setIsEditing] = useState(false)
const [editedName, setEditedName] = useState('')
const [historyOpen, setHistoryOpen] = useState(false)
const [notificationsOpen, setNotificationsOpen] = useState(false)
const { workflows, updateWorkflow, activeWorkflowId, removeWorkflow } = useWorkflowRegistry()
const [, forceUpdate] = useState({})
const { isExecuting, handleRunWorkflow } = useWorkflowExecution()
const router = useRouter()
// Use client-side only rendering for the timestamp
// Store hooks
const { notifications, getWorkflowNotifications, addNotification } = useNotificationStore()
const { history, revertToHistoryState, lastSaved, isDeployed, setDeploymentStatus } =
useWorkflowStore()
const { workflows, updateWorkflow, activeWorkflowId, removeWorkflow } = useWorkflowRegistry()
const { isExecuting, handleRunWorkflow } = useWorkflowExecution()
// Local state
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
const [, forceUpdate] = useState({})
// Workflow name editing state
const [isEditing, setIsEditing] = useState(false)
const [editedName, setEditedName] = useState('')
// Dropdown states
const [historyOpen, setHistoryOpen] = useState(false)
const [notificationsOpen, setNotificationsOpen] = useState(false)
// Deployment states
const [isDeploying, setIsDeploying] = useState(false)
// Get notifications for current workflow
const workflowNotifications = activeWorkflowId
? getWorkflowNotifications(activeWorkflowId)
: notifications // Show all if no workflow is active
const handleDeleteWorkflow = () => {
if (!activeWorkflowId) return
// Remove the workflow from the registry
const newWorkflows = { ...workflows }
delete newWorkflows[activeWorkflowId]
// Get remaining workflow IDs
const remainingIds = Object.keys(newWorkflows)
// Navigate before removing the workflow to avoid any state inconsistencies
if (remainingIds.length > 0) {
router.push(`/w/${remainingIds[0]}`)
} else {
router.push('/')
}
// Remove the workflow from the registry
removeWorkflow(activeWorkflowId)
}
// Client-side only rendering for the timestamp
useEffect(() => {
setMounted(true)
}, [])
// Update the time display every minute
useEffect(() => {
@@ -82,6 +77,30 @@ export function ControlBar() {
return () => clearInterval(interval)
}, [])
// Check deployment status on mount or when activeWorkflowId changes
useEffect(() => {
async function checkStatus() {
if (!activeWorkflowId) return
try {
const response = await fetch(`/api/workflow/${activeWorkflowId}/status`)
if (response.ok) {
const data = await response.json()
// Update the store with the deployment status from the API
setDeploymentStatus(
data.isDeployed,
data.deployedAt ? new Date(data.deployedAt) : undefined
)
}
} catch (error) {
console.error('Failed to check deployment status:', error)
}
}
checkStatus()
}, [activeWorkflowId, setDeploymentStatus])
/**
* Workflow name handlers
*/
const handleNameClick = () => {
if (activeWorkflowId) {
setEditedName(workflows[activeWorkflowId].name)
@@ -107,28 +126,29 @@ export function ControlBar() {
}
}
// Add the deployment state and handlers
const [isDeploying, setIsDeploying] = useState(false)
const [isDeployed, setIsDeployed] = useState(false)
/**
* Workflow deletion handler
*/
const handleDeleteWorkflow = () => {
if (!activeWorkflowId) return
// Check deployment status on mount
useEffect(() => {
async function checkStatus() {
if (!activeWorkflowId) return
try {
const response = await fetch(`/api/workflow/${activeWorkflowId}/status`)
if (response.ok) {
const data = await response.json()
setIsDeployed(data.isDeployed)
}
} catch (error) {
console.error('Failed to check deployment status:', error)
}
// Get remaining workflow IDs
const remainingIds = Object.keys(workflows).filter((id) => id !== activeWorkflowId)
// Navigate before removing the workflow to avoid any state inconsistencies
if (remainingIds.length > 0) {
router.push(`/w/${remainingIds[0]}`)
} else {
router.push('/')
}
checkStatus()
}, [activeWorkflowId])
// Deploy the workflow
// Remove the workflow from the registry
removeWorkflow(activeWorkflowId)
}
/**
* Workflow deployment handler
*/
const handleDeploy = async () => {
if (!activeWorkflowId) return
try {
@@ -142,9 +162,12 @@ export function ControlBar() {
if (!response.ok) throw new Error('Failed to deploy workflow')
const { apiKey } = await response.json()
const { apiKey, isDeployed: newDeployStatus, deployedAt } = await response.json()
const endpoint = `${process.env.NEXT_PUBLIC_APP_URL}/api/workflow/${activeWorkflowId}/execute`
// Update the store with the deployment status
setDeploymentStatus(newDeployStatus, deployedAt ? new Date(deployedAt) : undefined)
addNotification('api', 'Workflow successfully deployed', activeWorkflowId, {
isPersistent: true,
sections: [
@@ -162,8 +185,6 @@ export function ControlBar() {
},
],
})
setIsDeployed(true)
} catch (error) {
addNotification('error', 'Failed to deploy workflow. Please try again.', activeWorkflowId)
} finally {
@@ -171,201 +192,236 @@ export function ControlBar() {
}
}
/**
* Render workflow name section (editable/non-editable)
*/
const renderWorkflowName = () => (
<div className="flex flex-col gap-[2px]">
{isEditing ? (
<input
type="text"
value={editedName}
onChange={(e) => setEditedName(e.target.value)}
onBlur={handleNameSubmit}
onKeyDown={handleNameKeyDown}
autoFocus
className="font-semibold text-sm bg-transparent border-none outline-none p-0 w-[200px]"
/>
) : (
<h2
className="font-semibold text-sm hover:text-muted-foreground w-fit"
onClick={handleNameClick}
>
{activeWorkflowId ? workflows[activeWorkflowId]?.name : 'Workflow'}
</h2>
)}
{mounted && (
<p className="text-xs text-muted-foreground">
Saved{' '}
{formatDistanceToNow(lastSaved || Date.now(), {
addSuffix: true,
})}
</p>
)}
</div>
)
/**
* Render delete workflow button with confirmation dialog
*/
const renderDeleteButton = () => (
<AlertDialog>
<Tooltip>
<TooltipTrigger asChild>
<AlertDialogTrigger asChild>
<Button
variant="ghost"
size="icon"
disabled={Object.keys(workflows).length <= 1}
className="hover:text-red-600"
>
<Trash2 className="h-5 w-5" />
<span className="sr-only">Delete Workflow</span>
</Button>
</AlertDialogTrigger>
</TooltipTrigger>
<TooltipContent>Delete Workflow</TooltipContent>
</Tooltip>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Workflow</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete this workflow? This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={handleDeleteWorkflow} className="bg-red-600 hover:bg-red-700">
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
/**
* Render deploy button with tooltip
*/
const renderDeployButton = () => (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={handleDeploy}
disabled={isDeploying}
className={cn(
'hover:text-foreground',
isDeployed && 'text-green-500 hover:text-green-500'
)}
>
<Rocket className={`h-5 w-5 ${isDeploying ? 'animate-pulse' : ''}`} />
<span className="sr-only">Deploy API</span>
</Button>
</TooltipTrigger>
<TooltipContent>
{isDeploying ? 'Deploying...' : isDeployed ? 'Deployed' : 'Deploy as API Endpoint'}
</TooltipContent>
</Tooltip>
)
/**
* Render history dropdown
*/
const renderHistoryDropdown = () => (
<DropdownMenu open={historyOpen} onOpenChange={setHistoryOpen}>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<History />
<span className="sr-only">Version History</span>
</Button>
</DropdownMenuTrigger>
</TooltipTrigger>
{!historyOpen && <TooltipContent>History</TooltipContent>}
</Tooltip>
{history.past.length === 0 && history.future.length === 0 ? (
<DropdownMenuContent align="end" className="w-40">
<DropdownMenuItem className="text-sm text-muted-foreground">
No history available
</DropdownMenuItem>
</DropdownMenuContent>
) : (
<DropdownMenuContent align="end" className="w-60 max-h-[300px] overflow-y-auto">
<>
{[...history.future].reverse().map((entry, index) => (
<HistoryDropdownItem
key={`future-${entry.timestamp}-${index}`}
action={entry.action}
timestamp={entry.timestamp}
onClick={() =>
revertToHistoryState(
history.past.length + 1 + (history.future.length - 1 - index)
)
}
isFuture={true}
/>
))}
<HistoryDropdownItem
key={`current-${history.present.timestamp}`}
action={history.present.action}
timestamp={history.present.timestamp}
isCurrent={true}
onClick={() => {}}
/>
{[...history.past].reverse().map((entry, index) => (
<HistoryDropdownItem
key={`past-${entry.timestamp}-${index}`}
action={entry.action}
timestamp={entry.timestamp}
onClick={() => revertToHistoryState(history.past.length - 1 - index)}
/>
))}
</>
</DropdownMenuContent>
)}
</DropdownMenu>
)
/**
* Render notifications dropdown
*/
const renderNotificationsDropdown = () => (
<DropdownMenu open={notificationsOpen} onOpenChange={setNotificationsOpen}>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<Bell />
<span className="sr-only">Notifications</span>
</Button>
</DropdownMenuTrigger>
</TooltipTrigger>
{!notificationsOpen && <TooltipContent>Notifications</TooltipContent>}
</Tooltip>
{workflowNotifications.length === 0 ? (
<DropdownMenuContent align="end" className="w-40">
<DropdownMenuItem className="text-sm text-muted-foreground">
No new notifications
</DropdownMenuItem>
</DropdownMenuContent>
) : (
<DropdownMenuContent align="end" className="w-60 max-h-[300px] overflow-y-auto">
{[...workflowNotifications]
.sort((a, b) => b.timestamp - a.timestamp)
.map((notification) => (
<NotificationDropdownItem
key={notification.id}
id={notification.id}
type={notification.type}
message={notification.message}
timestamp={notification.timestamp}
options={notification.options}
/>
))}
</DropdownMenuContent>
)}
</DropdownMenu>
)
/**
* Render run workflow button
*/
const renderRunButton = () => (
<Button
className="gap-2 bg-[#7F2FFF] hover:bg-[#7F2FFF]/90 text-white"
onClick={handleRunWorkflow}
disabled={isExecuting}
>
<Play fill="white" stroke="white" className="!h-3.5 !w-3.5" />
{isExecuting ? 'Running...' : 'Run'}
</Button>
)
return (
<div className="flex h-16 w-full items-center justify-between bg-background px-6 border-b transition-all duration-300">
{/* Left Section - Workflow Info */}
<div className="flex flex-col gap-[2px]">
{isEditing ? (
<input
type="text"
value={editedName}
onChange={(e) => setEditedName(e.target.value)}
onBlur={handleNameSubmit}
onKeyDown={handleNameKeyDown}
autoFocus
className="font-semibold text-sm bg-transparent border-none outline-none p-0 w-[200px]"
/>
) : (
<h2
className="font-semibold text-sm hover:text-muted-foreground w-fit"
onClick={handleNameClick}
>
{activeWorkflowId ? workflows[activeWorkflowId].name : 'Workflow'}
</h2>
)}
{mounted && (
<p className="text-xs text-muted-foreground">
Saved{' '}
{formatDistanceToNow(lastSaved || Date.now(), {
addSuffix: true,
})}
</p>
)}
</div>
{renderWorkflowName()}
{/* Middle Section - Reserved for future use */}
<div className="flex-1" />
{/* Right Section - Actions */}
<div className="flex items-center gap-3">
<AlertDialog>
<Tooltip>
<TooltipTrigger asChild>
<AlertDialogTrigger asChild>
<Button
variant="ghost"
size="icon"
disabled={Object.keys(workflows).length <= 1}
className="hover:text-red-600"
>
<Trash2 className="h-5 w-5" />
<span className="sr-only">Delete Workflow</span>
</Button>
</AlertDialogTrigger>
</TooltipTrigger>
<TooltipContent>Delete Workflow</TooltipContent>
</Tooltip>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Workflow</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete this workflow? This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={handleDeleteWorkflow}
className="bg-red-600 hover:bg-red-700"
>
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={handleDeploy}
disabled={isDeploying}
className={cn(
'hover:text-foreground',
isDeployed && 'text-green-500 hover:text-green-500'
)}
>
<Rocket className={`h-5 w-5 ${isDeploying ? 'animate-pulse' : ''}`} />
<span className="sr-only">Deploy API</span>
</Button>
</TooltipTrigger>
<TooltipContent>
{isDeploying ? 'Deploying...' : isDeployed ? 'Deployed' : 'Deploy as API Endpoint'}
</TooltipContent>
</Tooltip>
<DropdownMenu open={historyOpen} onOpenChange={setHistoryOpen}>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<History />
<span className="sr-only">Version History</span>
</Button>
</DropdownMenuTrigger>
</TooltipTrigger>
{!historyOpen && <TooltipContent>History</TooltipContent>}
</Tooltip>
{history.past.length === 0 && history.future.length === 0 ? (
<DropdownMenuContent align="end" className="w-40">
<DropdownMenuItem className="text-sm text-muted-foreground">
No history available
</DropdownMenuItem>
</DropdownMenuContent>
) : (
<DropdownMenuContent align="end" className="w-60 max-h-[300px] overflow-y-auto">
<>
{[...history.future].reverse().map((entry, index) => (
<HistoryDropdownItem
key={`future-${entry.timestamp}-${index}`}
action={entry.action}
timestamp={entry.timestamp}
onClick={() =>
revertToHistoryState(
history.past.length + 1 + (history.future.length - 1 - index)
)
}
isFuture={true}
/>
))}
<HistoryDropdownItem
key={`current-${history.present.timestamp}`}
action={history.present.action}
timestamp={history.present.timestamp}
isCurrent={true}
onClick={() => {}}
/>
{[...history.past].reverse().map((entry, index) => (
<HistoryDropdownItem
key={`past-${entry.timestamp}-${index}`}
action={entry.action}
timestamp={entry.timestamp}
onClick={() => revertToHistoryState(history.past.length - 1 - index)}
/>
))}
</>
</DropdownMenuContent>
)}
</DropdownMenu>
<DropdownMenu open={notificationsOpen} onOpenChange={setNotificationsOpen}>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<Bell />
<span className="sr-only">Notifications</span>
</Button>
</DropdownMenuTrigger>
</TooltipTrigger>
{!notificationsOpen && <TooltipContent>Notifications</TooltipContent>}
</Tooltip>
{workflowNotifications.length === 0 ? (
<DropdownMenuContent align="end" className="w-40">
<DropdownMenuItem className="text-sm text-muted-foreground">
No new notifications
</DropdownMenuItem>
</DropdownMenuContent>
) : (
<DropdownMenuContent align="end" className="w-60 max-h-[300px] overflow-y-auto">
{[...workflowNotifications]
.sort((a, b) => b.timestamp - a.timestamp)
.map((notification) => (
<NotificationDropdownItem
key={notification.id}
id={notification.id}
type={notification.type}
message={notification.message}
timestamp={notification.timestamp}
options={notification.options}
/>
))}
</DropdownMenuContent>
)}
</DropdownMenu>
<Button
className="gap-2 bg-[#7F2FFF] hover:bg-[#7F2FFF]/90 text-white"
onClick={handleRunWorkflow}
disabled={isExecuting}
>
<Play fill="white" stroke="white" className="!h-3.5 !w-3.5" />
{isExecuting ? 'Running...' : 'Run'}
</Button>
{renderDeleteButton()}
{renderDeployButton()}
{renderHistoryDropdown()}
{renderNotificationsDropdown()}
{renderRunButton()}
</div>
</div>
)
+1
View File
@@ -4,6 +4,7 @@ export interface HistoryEntry {
state: WorkflowState
timestamp: number
action: string
subblockValues: Record<string, Record<string, any>>
}
export interface WorkflowHistory {
+101 -7
View File
@@ -1,6 +1,9 @@
import { StateCreator } from 'zustand'
import { HistoryActions, HistoryEntry, WorkflowHistory } from './history-types'
import { useWorkflowRegistry } from './registry/store'
import { useSubBlockStore } from './subblock/store'
import { WorkflowState, WorkflowStore } from './types'
import { mergeSubblockState } from './utils'
// MAX for each individual workflow
const MAX_HISTORY_LENGTH = 20
@@ -22,9 +25,12 @@ export const withHistory = (
blocks: initialState.blocks,
edges: initialState.edges,
loops: initialState.loops,
isDeployed: initialState.isDeployed || false,
deployedAt: initialState.deployedAt,
},
timestamp: Date.now(),
action: 'Initial state',
subblockValues: {}, // Add storage for subblock values
}
return {
@@ -49,6 +55,11 @@ export const withHistory = (
const previous = history.past[history.past.length - 1]
const newPast = history.past.slice(0, history.past.length - 1)
// Get active workflow ID for subblock handling
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
if (!activeWorkflowId) return
// Apply the state change
set({
...state,
...previous.state,
@@ -58,6 +69,23 @@ export const withHistory = (
future: [history.present, ...history.future],
},
})
// Restore subblock values from the previous state's snapshot
if (previous.subblockValues && activeWorkflowId) {
// Update the subblock store with the saved values
useSubBlockStore.setState({
workflowValues: {
...useSubBlockStore.getState().workflowValues,
[activeWorkflowId]: previous.subblockValues,
},
})
// Also update localStorage for backup
localStorage.setItem(
`subblock-values-${activeWorkflowId}`,
JSON.stringify(previous.subblockValues)
)
}
},
// Restore next state from history
@@ -68,6 +96,11 @@ export const withHistory = (
const next = history.future[0]
const newFuture = history.future.slice(1)
// Get active workflow ID for subblock handling
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
if (!activeWorkflowId) return
// Apply the state change
set({
...state,
...next.state,
@@ -77,6 +110,23 @@ export const withHistory = (
future: newFuture,
},
})
// Restore subblock values from the next state's snapshot
if (next.subblockValues && activeWorkflowId) {
// Update the subblock store with the saved values
useSubBlockStore.setState({
workflowValues: {
...useSubBlockStore.getState().workflowValues,
[activeWorkflowId]: next.subblockValues,
},
})
// Also update localStorage for backup
localStorage.setItem(
`subblock-values-${activeWorkflowId}`,
JSON.stringify(next.subblockValues)
)
}
},
// Reset workflow to empty state
@@ -88,9 +138,10 @@ export const withHistory = (
history: {
past: [],
present: {
state: { blocks: {}, edges: [], loops: {} },
state: { blocks: {}, edges: [], loops: {}, isDeployed: false },
timestamp: Date.now(),
action: 'Clear workflow',
subblockValues: {},
},
future: [],
},
@@ -107,6 +158,10 @@ export const withHistory = (
if (!targetState) return
// Get active workflow ID for subblock handling
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
if (!activeWorkflowId) return
const newPast = allStates.slice(0, index)
const newFuture = allStates.slice(index + 1)
@@ -119,21 +174,60 @@ export const withHistory = (
future: newFuture,
},
})
// Restore subblock values from the target state's snapshot
if (targetState.subblockValues && activeWorkflowId) {
// Update the subblock store with the saved values
useSubBlockStore.setState({
workflowValues: {
...useSubBlockStore.getState().workflowValues,
[activeWorkflowId]: targetState.subblockValues,
},
})
// Also update localStorage for backup
localStorage.setItem(
`subblock-values-${activeWorkflowId}`,
JSON.stringify(targetState.subblockValues)
)
}
},
}
}
}
// Create a new history entry with current state snapshot
export const createHistoryEntry = (state: WorkflowState, action: string): HistoryEntry => ({
state: {
export const createHistoryEntry = (state: WorkflowState, action: string): HistoryEntry => {
// Get active workflow ID for subblock handling
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
// Create a deep copy of the state
const stateCopy = {
blocks: { ...state.blocks },
edges: [...state.edges],
loops: { ...state.loops },
},
timestamp: Date.now(),
action,
})
isDeployed: state.isDeployed !== undefined ? state.isDeployed : false,
deployedAt: state.deployedAt,
}
// Capture the current subblock values for this workflow
let subblockValues = {}
if (activeWorkflowId) {
// Get the current subblock values from the store
const currentValues = useSubBlockStore.getState().workflowValues[activeWorkflowId] || {}
// Create a deep copy to ensure we don't have reference issues
subblockValues = JSON.parse(JSON.stringify(currentValues))
}
return {
state: stateCopy,
timestamp: Date.now(),
action,
subblockValues,
}
}
// Add new entry to history and maintain history size limit
export const pushHistory = (
+47 -6
View File
@@ -34,6 +34,8 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
edges: currentState.edges,
loops: currentState.loops,
history: currentState.history,
isDeployed: currentState.isDeployed,
deployedAt: currentState.deployedAt,
})
)
}
@@ -41,7 +43,8 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
// Load workflow state
const savedState = localStorage.getItem(`workflow-${id}`)
if (savedState) {
const { blocks, edges, history, loops } = JSON.parse(savedState)
const parsedState = JSON.parse(savedState)
const { blocks, edges, history, loops } = parsedState
// Initialize subblock store with workflow values
useSubBlockStore.getState().initializeFromWorkflow(id, blocks)
@@ -50,12 +53,21 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
blocks,
edges,
loops,
isDeployed: parsedState.isDeployed !== undefined ? parsedState.isDeployed : false,
deployedAt: parsedState.deployedAt ? new Date(parsedState.deployedAt) : undefined,
history: history || {
past: [],
present: {
state: { blocks, edges, loops: {} },
state: {
blocks,
edges,
loops: {},
isDeployed: parsedState.isDeployed !== undefined ? parsedState.isDeployed : false,
deployedAt: parsedState.deployedAt,
},
timestamp: Date.now(),
action: 'Initial state',
subblockValues: {},
},
future: [],
},
@@ -65,12 +77,21 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
blocks: {},
edges: [],
loops: {},
isDeployed: false,
deployedAt: undefined,
history: {
past: [],
present: {
state: { blocks: {}, edges: [], loops: {} },
state: {
blocks: {},
edges: [],
loops: {},
isDeployed: false,
deployedAt: undefined,
},
timestamp: Date.now(),
action: 'Initial state',
subblockValues: {},
},
future: [],
},
@@ -197,6 +218,8 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
},
edges: [],
loops: {},
isDeployed: false,
deployedAt: undefined,
history: {
past: [],
present: {
@@ -206,9 +229,12 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
},
edges: [],
loops: {},
isDeployed: false,
deployedAt: undefined,
},
timestamp: Date.now(),
action: 'Initial state',
subblockValues: {},
},
future: [],
},
@@ -263,17 +289,21 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
newActiveWorkflowId = remainingIds[0]
const savedState = localStorage.getItem(`workflow-${newActiveWorkflowId}`)
if (savedState) {
const { blocks, edges, history, loops } = JSON.parse(savedState)
const { blocks, edges, history, loops, isDeployed, deployedAt } =
JSON.parse(savedState)
useWorkflowStore.setState({
blocks,
edges,
loops,
isDeployed: isDeployed || false,
deployedAt: deployedAt ? new Date(deployedAt) : undefined,
history: history || {
past: [],
present: {
state: { blocks, edges, loops },
state: { blocks, edges, loops, isDeployed: isDeployed || false, deployedAt },
timestamp: Date.now(),
action: 'Initial state',
subblockValues: {},
},
future: [],
},
@@ -283,12 +313,21 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
blocks: {},
edges: [],
loops: {},
isDeployed: false,
deployedAt: undefined,
history: {
past: [],
present: {
state: { blocks: {}, edges: [], loops: {} },
state: {
blocks: {},
edges: [],
loops: {},
isDeployed: false,
deployedAt: undefined,
},
timestamp: Date.now(),
action: 'Initial state',
subblockValues: {},
},
future: [],
},
@@ -354,6 +393,8 @@ const initializeRegistry = () => {
edges: currentState.edges,
loops: currentState.loops,
history: currentState.history,
isDeployed: currentState.isDeployed,
deployedAt: currentState.deployedAt,
})
)
}
+2 -5
View File
@@ -3,6 +3,7 @@ import { WorkflowMetadata } from './types'
// Available workflow colors
export const WORKFLOW_COLORS = ['#3972F6', '#F639DD', '#F6B539', '#8139F6', '#F64439']
// Generates a unique name for a new workflow
export function generateUniqueName(existingWorkflows: Record<string, WorkflowMetadata>): string {
// Extract numbers from existing workflow names using regex
const numbers = Object.values(existingWorkflows)
@@ -21,11 +22,7 @@ export function generateUniqueName(existingWorkflows: Record<string, WorkflowMet
return `Workflow ${nextNumber}`
}
/**
* Determines the next color to use for a new workflow based on the last used color
* @param existingWorkflows - Current workflows in the registry
* @returns The next color from the predefined color palette
*/
// Determines the next color to use for a new workflow based on the last used color
export function getNextWorkflowColor(existingWorkflows: Record<string, WorkflowMetadata>): string {
const workflowArray = Object.values(existingWorkflows)
+47 -2
View File
@@ -15,12 +15,15 @@ const initialState = {
edges: [],
loops: {},
lastSaved: undefined,
isDeployed: false,
deployedAt: undefined,
history: {
past: [],
present: {
state: { blocks: {}, edges: [], loops: {} },
state: { blocks: {}, edges: [], loops: {}, isDeployed: false },
timestamp: Date.now(),
action: 'Initial state',
subblockValues: {},
},
future: [],
},
@@ -70,6 +73,8 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
},
edges: [...get().edges],
loops: { ...get().loops },
isDeployed: get().isDeployed,
deployedAt: get().deployedAt,
}
set(newState)
@@ -100,6 +105,8 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
blocks: { ...get().blocks },
edges: [...get().edges].filter((edge) => edge.source !== id && edge.target !== id),
loops: { ...get().loops },
isDeployed: get().isDeployed || false,
deployedAt: get().deployedAt,
}
// Clean up subblock values before removing the block
@@ -192,6 +199,8 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
blocks: { ...get().blocks },
edges: newEdges,
loops: newLoops,
isDeployed: get().isDeployed || false,
deployedAt: get().deployedAt,
}
set(newState)
@@ -229,6 +238,8 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
blocks: { ...get().blocks },
edges: newEdges,
loops: newLoops,
isDeployed: get().isDeployed || false,
deployedAt: get().deployedAt,
}
set(newState)
@@ -241,12 +252,21 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
blocks: {},
edges: [],
loops: {},
isDeployed: false,
deployedAt: undefined,
history: {
past: [],
present: {
state: { blocks: {}, edges: [], loops: {} },
state: {
blocks: {},
edges: [],
loops: {},
isDeployed: false,
deployedAt: undefined,
},
timestamp: Date.now(),
action: 'Initial state',
subblockValues: {},
},
future: [],
},
@@ -270,6 +290,8 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
},
},
edges: [...get().edges],
isDeployed: get().isDeployed || false,
deployedAt: get().deployedAt,
}
set(newState)
@@ -319,6 +341,8 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
},
edges: [...get().edges],
loops: { ...get().loops },
isDeployed: get().isDeployed || false,
deployedAt: get().deployedAt,
}
// Update the subblock store with the duplicated values
@@ -352,6 +376,8 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
},
},
edges: [...get().edges],
isDeployed: get().isDeployed || false,
deployedAt: get().deployedAt,
}
set(newState)
@@ -369,6 +395,8 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
},
edges: [...get().edges],
loops: { ...get().loops },
isDeployed: get().isDeployed || false,
deployedAt: get().deployedAt,
}
set(newState)
@@ -387,6 +415,8 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
},
edges: [...state.edges],
loops: { ...get().loops },
isDeployed: state.isDeployed || false,
deployedAt: state.deployedAt,
}))
get().updateLastSaved()
},
@@ -401,6 +431,8 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
},
},
edges: [...state.edges],
isDeployed: state.isDeployed || false,
deployedAt: state.deployedAt,
}))
get().updateLastSaved()
},
@@ -416,6 +448,8 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
maxIterations: Math.max(1, Math.min(50, maxIterations)), // Clamp between 1-50
},
},
isDeployed: get().isDeployed || false,
deployedAt: get().deployedAt,
}
set(newState)
@@ -429,6 +463,17 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
lastUpdate: Date.now(),
}))
},
setDeploymentStatus: (isDeployed: boolean, deployedAt?: Date) => {
const newState = {
...get(),
isDeployed,
deployedAt: deployedAt || (isDeployed ? new Date() : undefined),
}
set(newState)
get().updateLastSaved()
},
})),
{ name: 'workflow-store' }
)
+3
View File
@@ -37,6 +37,8 @@ export interface WorkflowState {
lastSaved?: number
loops: Record<string, Loop>
lastUpdate?: number
isDeployed: boolean
deployedAt?: Date
}
export interface WorkflowActions {
@@ -55,6 +57,7 @@ export interface WorkflowActions {
updateBlockHeight: (id: string, height: number) => void
updateLoopMaxIterations: (loopId: string, maxIterations: number) => void
triggerUpdate: () => void
setDeploymentStatus: (isDeployed: boolean, deployedAt?: Date) => void
}
export type WorkflowStore = WorkflowState & WorkflowActions