mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
feat(marketplace): adding marketplace workflow to registry (#219)
* feat(marketplace): adding marketplace workflow to registry * fix(db): updated schema
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE "workflow" ADD COLUMN "marketplace_data" json DEFAULT 'null'::json;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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', {
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user