feat(marketplace): adding marketplace workflow to registry (#219)

* feat(marketplace): adding marketplace workflow to registry

* fix(db): updated schema
This commit is contained in:
Emir Karabeg
2025-04-02 18:15:14 -07:00
committed by GitHub
parent 854667034d
commit e14c705640
8 changed files with 1548 additions and 112 deletions
@@ -1,8 +1,10 @@
'use client'
import { useEffect, useState } from 'react'
import { useRouter } from 'next/navigation'
import { Eye, Star } 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'
@@ -25,6 +27,8 @@ interface WorkflowCardProps {
*/
export function WorkflowCard({ workflow, onHover }: WorkflowCardProps) {
const [isPreviewReady, setIsPreviewReady] = useState(!!workflow.workflowState)
const router = useRouter()
const { createWorkflow } = useWorkflowRegistry()
// When workflow state becomes available, update preview ready state
useEffect(() => {
@@ -44,10 +48,11 @@ export function WorkflowCard({ workflow, onHover }: WorkflowCardProps) {
}
/**
* Handle workflow card click - track views
* Handle workflow card click - track views and import workflow
*/
const handleClick = async () => {
try {
// Track view
await fetch(`/api/marketplace/workflows`, {
method: 'POST',
headers: {
@@ -55,8 +60,23 @@ export function WorkflowCard({ workflow, onHover }: WorkflowCardProps) {
},
body: JSON.stringify({ id: workflow.id }),
})
// Create a local copy of the marketplace workflow
if (workflow.workflowState) {
const newWorkflowId = createWorkflow({
name: `${workflow.name} (Copy)`,
description: workflow.description,
marketplaceId: workflow.id,
marketplaceState: workflow.workflowState,
})
// Navigate to the new workflow
router.push(`/w/${newWorkflowId}`)
} else {
console.error('Cannot import workflow: state is not available')
}
} catch (error) {
console.error('Failed to track workflow view:', error)
console.error('Failed to handle workflow click:', error)
}
}
@@ -74,6 +74,11 @@ export const getCategoryByValue = (value: string): Category => {
}
export const getCategoryLabel = (value: string): string => {
// Special handling for "popular" and "recent" sections
if (value === 'popular') return 'Popular'
if (value === 'recent') return 'Recent'
// Default handling for regular categories
return getCategoryByValue(value).label
}
+1
View File
@@ -0,0 +1 @@
ALTER TABLE "workflow" ADD COLUMN "marketplace_data" json DEFAULT 'null'::json;
File diff suppressed because it is too large Load Diff
+8 -1
View File
@@ -169,6 +169,13 @@
"when": 1743024111706,
"tag": "0023_nervous_tyger_tiger",
"breakpoints": true
},
{
"idx": 24,
"version": "7",
"when": 1743642824678,
"tag": "0024_next_whizzer",
"breakpoints": true
}
]
}
}
+6 -1
View File
@@ -73,11 +73,16 @@ export const workflow = pgTable('workflow', {
updatedAt: timestamp('updated_at').notNull(),
isDeployed: boolean('is_deployed').notNull().default(false),
deployedAt: timestamp('deployed_at'),
isPublished: boolean('is_published').notNull().default(false),
collaborators: json('collaborators').notNull().default('[]'),
runCount: integer('run_count').notNull().default(0),
lastRunAt: timestamp('last_run_at'),
variables: json('variables').default('{}'),
marketplaceData: json('marketplace_data').default(null), // Format: { id: string, status: 'owner' | 'temp' | 'star' }
// These columns are kept for backward compatibility during migration
// and should be marked as deprecated
// @deprecated - Use marketplaceData instead
isPublished: boolean('is_published').notNull().default(false),
})
export const waitlist = pgTable('waitlist', {
+223 -107
View File
@@ -138,126 +138,241 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
// Generate workflow metadata with appropriate name and color
const newWorkflow: WorkflowMetadata = {
id,
name: generateUniqueName(workflows),
name: options.name || generateUniqueName(workflows),
lastModified: new Date(),
description: 'New workflow',
color: getNextWorkflowColor(workflows),
description: options.description || 'New workflow',
color: options.marketplaceId ? '#808080' : getNextWorkflowColor(workflows), // Gray for marketplace imports
marketplaceStatus: options.marketplaceId ? 'temp' : undefined,
marketplaceId: options.marketplaceId,
}
// Create starter block for new workflow
const starterId = crypto.randomUUID()
const starterBlock = {
id: starterId,
type: 'starter' as const,
name: 'Start',
position: { x: 100, y: 100 },
subBlocks: {
startWorkflow: {
id: 'startWorkflow',
type: 'dropdown' as const,
value: 'manual',
let initialState;
// If this is a marketplace import with existing state
if (options.marketplaceId && options.marketplaceState) {
initialState = {
blocks: options.marketplaceState.blocks || {},
edges: options.marketplaceState.edges || [],
loops: options.marketplaceState.loops || {},
isDeployed: false,
deployedAt: undefined,
history: {
past: [],
present: {
state: {
blocks: options.marketplaceState.blocks || {},
edges: options.marketplaceState.edges || [],
loops: options.marketplaceState.loops || {},
isDeployed: false,
deployedAt: undefined,
},
timestamp: Date.now(),
action: 'Imported from marketplace',
subblockValues: {},
},
future: [],
},
webhookPath: {
id: 'webhookPath',
type: 'short-input' as const,
value: '',
},
webhookSecret: {
id: 'webhookSecret',
type: 'short-input' as const,
value: '',
},
scheduleType: {
id: 'scheduleType',
type: 'dropdown' as const,
value: 'daily',
},
minutesInterval: {
id: 'minutesInterval',
type: 'short-input' as const,
value: '',
},
minutesStartingAt: {
id: 'minutesStartingAt',
type: 'short-input' as const,
value: '',
},
hourlyMinute: {
id: 'hourlyMinute',
type: 'short-input' as const,
value: '',
},
dailyTime: {
id: 'dailyTime',
type: 'short-input' as const,
value: '',
},
weeklyDay: {
id: 'weeklyDay',
type: 'dropdown' as const,
value: 'MON',
},
weeklyDayTime: {
id: 'weeklyDayTime',
type: 'short-input' as const,
value: '',
},
monthlyDay: {
id: 'monthlyDay',
type: 'short-input' as const,
value: '',
},
monthlyTime: {
id: 'monthlyTime',
type: 'short-input' as const,
value: '',
},
cronExpression: {
id: 'cronExpression',
type: 'short-input' as const,
value: '',
},
timezone: {
id: 'timezone',
type: 'dropdown' as const,
value: 'UTC',
},
},
outputs: {
response: {
type: {
input: 'any',
lastSaved: Date.now(),
}
logger.info(`Created workflow from marketplace: ${options.marketplaceId}`)
} else {
// Create starter block for new workflow
const starterId = crypto.randomUUID()
const starterBlock = {
id: starterId,
type: 'starter' as const,
name: 'Start',
position: { x: 100, y: 100 },
subBlocks: {
startWorkflow: {
id: 'startWorkflow',
type: 'dropdown' as const,
value: 'manual',
},
webhookPath: {
id: 'webhookPath',
type: 'short-input' as const,
value: '',
},
webhookSecret: {
id: 'webhookSecret',
type: 'short-input' as const,
value: '',
},
scheduleType: {
id: 'scheduleType',
type: 'dropdown' as const,
value: 'daily',
},
minutesInterval: {
id: 'minutesInterval',
type: 'short-input' as const,
value: '',
},
minutesStartingAt: {
id: 'minutesStartingAt',
type: 'short-input' as const,
value: '',
},
hourlyMinute: {
id: 'hourlyMinute',
type: 'short-input' as const,
value: '',
},
dailyTime: {
id: 'dailyTime',
type: 'short-input' as const,
value: '',
},
weeklyDay: {
id: 'weeklyDay',
type: 'dropdown' as const,
value: 'MON',
},
weeklyDayTime: {
id: 'weeklyDayTime',
type: 'short-input' as const,
value: '',
},
monthlyDay: {
id: 'monthlyDay',
type: 'short-input' as const,
value: '',
},
monthlyTime: {
id: 'monthlyTime',
type: 'short-input' as const,
value: '',
},
cronExpression: {
id: 'cronExpression',
type: 'short-input' as const,
value: '',
},
timezone: {
id: 'timezone',
type: 'dropdown' as const,
value: 'UTC',
},
},
},
enabled: true,
horizontalHandles: true,
isWide: false,
height: 0,
outputs: {
response: {
type: {
input: 'any',
},
},
},
enabled: true,
horizontalHandles: true,
isWide: false,
height: 0,
}
initialState = {
blocks: {
[starterId]: starterBlock,
},
edges: [],
loops: {},
isDeployed: false,
deployedAt: undefined,
history: {
past: [],
present: {
state: {
blocks: {
[starterId]: starterBlock,
},
edges: [],
loops: {},
isDeployed: false,
deployedAt: undefined,
},
timestamp: Date.now(),
action: 'Initial state',
subblockValues: {},
},
future: [],
},
lastSaved: Date.now(),
}
}
const initialState = {
blocks: {
[starterId]: starterBlock,
// Add workflow to registry
set((state) => ({
workflows: {
...state.workflows,
[id]: newWorkflow,
},
edges: [],
loops: {},
error: null,
}))
// Save workflow list to localStorage
const updatedWorkflows = get().workflows
saveRegistry(updatedWorkflows)
// Save initial workflow state to localStorage
saveWorkflowState(id, initialState)
// Initialize subblock values if this is a marketplace import
if (options.marketplaceId && options.marketplaceState?.blocks) {
useSubBlockStore.getState().initializeFromWorkflow(id, options.marketplaceState.blocks)
}
// If this is the first workflow or it's an initial workflow, set it as active
if (options.isInitial || Object.keys(updatedWorkflows).length === 1) {
set({ activeWorkflowId: id })
useWorkflowStore.setState(initialState)
}
// Trigger sync
workflowSync.sync()
return id
},
/**
* Creates a new workflow from a marketplace workflow
* @param marketplaceId - The ID of the marketplace workflow to import
* @param state - The state of the marketplace workflow (blocks, edges, loops)
* @param metadata - Additional metadata like name, description from marketplace
* @returns The ID of the newly created workflow
*/
createMarketplaceWorkflow: (marketplaceId: string, state: any, metadata: Partial<WorkflowMetadata>) => {
const { workflows } = get()
const id = crypto.randomUUID()
// Generate workflow metadata with marketplace properties
const newWorkflow: WorkflowMetadata = {
id,
name: metadata.name || `Marketplace workflow`,
lastModified: new Date(),
description: metadata.description || 'Imported from marketplace',
color: metadata.color || getNextWorkflowColor(workflows),
marketplaceStatus: 'temp', // Initial status is temp, user can star it later
marketplaceId: marketplaceId, // Reference to original marketplace workflow
}
// Prepare workflow state based on the marketplace workflow state
const initialState = {
blocks: state.blocks || {},
edges: state.edges || [],
loops: state.loops || {},
isDeployed: false,
deployedAt: undefined,
history: {
past: [],
present: {
state: {
blocks: {
[starterId]: starterBlock,
},
edges: [],
loops: {},
blocks: state.blocks || {},
edges: state.edges || [],
loops: state.loops || {},
isDeployed: false,
deployedAt: undefined,
},
timestamp: Date.now(),
action: 'Initial state',
action: 'Imported from marketplace',
subblockValues: {},
},
future: [],
@@ -278,18 +393,19 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
const updatedWorkflows = get().workflows
saveRegistry(updatedWorkflows)
// Save initial workflow state to localStorage
// Save workflow state to localStorage
saveWorkflowState(id, initialState)
// If this is the first workflow or it's an initial workflow, set it as active
if (options.isInitial || Object.keys(updatedWorkflows).length === 1) {
set({ activeWorkflowId: id })
useWorkflowStore.setState(initialState)
// Initialize subblock values from state blocks
if (state.blocks) {
useSubBlockStore.getState().initializeFromWorkflow(id, state.blocks)
}
// Trigger sync
// Trigger sync to save to the database with marketplace attributes
workflowSync.sync()
logger.info(`Created marketplace workflow ${id} imported from ${marketplaceId}`)
return id
},
+9 -1
View File
@@ -4,6 +4,8 @@ export interface WorkflowMetadata {
lastModified: Date
description?: string
color: string
marketplaceStatus?: 'temp' | 'star' | null
marketplaceId?: string
}
export interface WorkflowRegistryState {
@@ -17,7 +19,13 @@ export interface WorkflowRegistryActions {
setActiveWorkflow: (id: string) => Promise<void>
removeWorkflow: (id: string) => void
updateWorkflow: (id: string, metadata: Partial<WorkflowMetadata>) => void
createWorkflow: (options?: { isInitial?: boolean }) => string
createWorkflow: (options?: {
isInitial?: boolean,
marketplaceId?: string,
marketplaceState?: any,
name?: string,
description?: string
}) => string
}
export type WorkflowRegistry = WorkflowRegistryState & WorkflowRegistryActions