mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-15 01:47:59 -05:00
improvement: marketplace, sidebar, loading (#221)
This commit is contained in:
@@ -42,7 +42,6 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
|
||||
description: marketplaceEntry.description,
|
||||
category: marketplaceEntry.category,
|
||||
authorName: marketplaceEntry.authorName,
|
||||
stars: marketplaceEntry.stars,
|
||||
views: marketplaceEntry.views,
|
||||
createdAt: marketplaceEntry.createdAt,
|
||||
updatedAt: marketplaceEntry.updatedAt,
|
||||
|
||||
@@ -1,54 +1,108 @@
|
||||
import { NextRequest } from 'next/server'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { validateWorkflowAccess } from '@/app/api/workflows/middleware'
|
||||
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { db } from '@/db'
|
||||
import * as schema from '@/db/schema'
|
||||
|
||||
const logger = createLogger('MarketplaceUnpublishAPI')
|
||||
|
||||
/**
|
||||
* API endpoint to unpublish a workflow from the marketplace by its marketplace ID
|
||||
*
|
||||
* Security:
|
||||
* - Requires authentication
|
||||
* - Validates that the current user is the author of the marketplace entry
|
||||
* - Only allows the owner to unpublish
|
||||
*/
|
||||
export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const requestId = crypto.randomUUID().slice(0, 8)
|
||||
|
||||
try {
|
||||
const { id } = await params
|
||||
|
||||
// Validate access to the workflow (must be owner to unpublish)
|
||||
// Pass false to requireDeployment since unpublishing doesn't require the workflow to be deployed
|
||||
const validation = await validateWorkflowAccess(request, id, false)
|
||||
if (validation.error) {
|
||||
logger.warn(`[${requestId}] Workflow access validation failed: ${validation.error.message}`)
|
||||
return createErrorResponse(validation.error.message, validation.error.status)
|
||||
|
||||
// Get the session first for authorization
|
||||
const session = await getSession()
|
||||
if (!session?.user?.id) {
|
||||
logger.warn(`[${requestId}] Unauthorized unpublish attempt for marketplace ID: ${id}`)
|
||||
return createErrorResponse('Unauthorized', 401)
|
||||
}
|
||||
|
||||
// Check if workflow is published
|
||||
const userId = session.user.id
|
||||
|
||||
// Get the marketplace entry using the marketplace ID
|
||||
const marketplaceEntry = await db
|
||||
.select()
|
||||
.select({
|
||||
id: schema.marketplace.id,
|
||||
workflowId: schema.marketplace.workflowId,
|
||||
authorId: schema.marketplace.authorId,
|
||||
name: schema.marketplace.name,
|
||||
})
|
||||
.from(schema.marketplace)
|
||||
.where(eq(schema.marketplace.workflowId, id))
|
||||
.where(eq(schema.marketplace.id, id))
|
||||
.limit(1)
|
||||
.then((rows) => rows[0])
|
||||
|
||||
if (!marketplaceEntry) {
|
||||
logger.warn(`[${requestId}] No marketplace entry found for workflow: ${id}`)
|
||||
return createErrorResponse('Workflow is not published to marketplace', 404)
|
||||
logger.warn(`[${requestId}] No marketplace entry found with ID: ${id}`)
|
||||
return createErrorResponse('Marketplace entry not found', 404)
|
||||
}
|
||||
|
||||
// Check if the user is the author of the marketplace entry
|
||||
if (marketplaceEntry.authorId !== userId) {
|
||||
logger.warn(
|
||||
`[${requestId}] User ${userId} tried to unpublish marketplace entry they don't own: ${id}, author: ${marketplaceEntry.authorId}`
|
||||
)
|
||||
return createErrorResponse('You do not have permission to unpublish this workflow', 403)
|
||||
}
|
||||
|
||||
// Delete the marketplace entry
|
||||
await db.delete(schema.marketplace).where(eq(schema.marketplace.workflowId, id))
|
||||
|
||||
// Update the workflow to mark it as unpublished
|
||||
await db.update(schema.workflow).set({ isPublished: false }).where(eq(schema.workflow.id, id))
|
||||
|
||||
logger.info(`[${requestId}] Workflow unpublished from marketplace: ${id}`)
|
||||
|
||||
return createSuccessResponse({
|
||||
success: true,
|
||||
message: 'Workflow successfully unpublished from marketplace',
|
||||
})
|
||||
const workflowId = marketplaceEntry.workflowId
|
||||
|
||||
// Verify the workflow exists and belongs to the user
|
||||
const workflow = await db
|
||||
.select({
|
||||
id: schema.workflow.id,
|
||||
userId: schema.workflow.userId,
|
||||
})
|
||||
.from(schema.workflow)
|
||||
.where(eq(schema.workflow.id, workflowId))
|
||||
.limit(1)
|
||||
.then((rows) => rows[0])
|
||||
|
||||
if (!workflow) {
|
||||
logger.warn(`[${requestId}] Associated workflow not found: ${workflowId}`)
|
||||
// We'll still delete the marketplace entry even if the workflow is missing
|
||||
} else if (workflow.userId !== userId) {
|
||||
logger.warn(
|
||||
`[${requestId}] Workflow ${workflowId} belongs to user ${workflow.userId}, not current user ${userId}`
|
||||
)
|
||||
return createErrorResponse('You do not have permission to unpublish this workflow', 403)
|
||||
}
|
||||
|
||||
try {
|
||||
// Delete the marketplace entry - this is the primary action
|
||||
await db.delete(schema.marketplace).where(eq(schema.marketplace.id, id))
|
||||
|
||||
// Update the workflow to mark it as unpublished if it exists
|
||||
if (workflow) {
|
||||
await db.update(schema.workflow)
|
||||
.set({ isPublished: false })
|
||||
.where(eq(schema.workflow.id, workflowId))
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Workflow "${marketplaceEntry.name}" unpublished from marketplace: ID=${id}, workflowId=${workflowId}`)
|
||||
|
||||
return createSuccessResponse({
|
||||
success: true,
|
||||
message: 'Workflow successfully unpublished from marketplace',
|
||||
})
|
||||
} catch (dbError) {
|
||||
logger.error(`[${requestId}] Database error unpublishing marketplace entry:`, dbError)
|
||||
return createErrorResponse('Failed to unpublish workflow due to a database error', 500)
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error unpublishing workflow: ${(await params).id}`, error)
|
||||
logger.error(`[${requestId}] Error unpublishing marketplace entry: ${(await params).id}`, error)
|
||||
return createErrorResponse('Failed to unpublish workflow', 500)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +112,6 @@ export async function POST(request: NextRequest) {
|
||||
.values({
|
||||
...marketplaceEntry,
|
||||
createdAt: new Date(),
|
||||
stars: 0,
|
||||
views: 0,
|
||||
})
|
||||
.returning()
|
||||
|
||||
@@ -57,7 +57,6 @@ export async function GET(request: NextRequest) {
|
||||
authorId: schema.marketplace.authorId,
|
||||
authorName: schema.marketplace.authorName,
|
||||
state: schema.marketplace.state,
|
||||
stars: schema.marketplace.stars,
|
||||
views: schema.marketplace.views,
|
||||
category: schema.marketplace.category,
|
||||
createdAt: schema.marketplace.createdAt,
|
||||
@@ -77,7 +76,6 @@ export async function GET(request: NextRequest) {
|
||||
description: schema.marketplace.description,
|
||||
authorId: schema.marketplace.authorId,
|
||||
authorName: schema.marketplace.authorName,
|
||||
stars: schema.marketplace.stars,
|
||||
views: schema.marketplace.views,
|
||||
category: schema.marketplace.category,
|
||||
createdAt: schema.marketplace.createdAt,
|
||||
@@ -123,7 +121,6 @@ export async function GET(request: NextRequest) {
|
||||
authorId: schema.marketplace.authorId,
|
||||
authorName: schema.marketplace.authorName,
|
||||
state: schema.marketplace.state,
|
||||
stars: schema.marketplace.stars,
|
||||
views: schema.marketplace.views,
|
||||
category: schema.marketplace.category,
|
||||
createdAt: schema.marketplace.createdAt,
|
||||
@@ -143,7 +140,6 @@ export async function GET(request: NextRequest) {
|
||||
description: schema.marketplace.description,
|
||||
authorId: schema.marketplace.authorId,
|
||||
authorName: schema.marketplace.authorName,
|
||||
stars: schema.marketplace.stars,
|
||||
views: schema.marketplace.views,
|
||||
category: schema.marketplace.category,
|
||||
createdAt: schema.marketplace.createdAt,
|
||||
@@ -192,7 +188,6 @@ export async function GET(request: NextRequest) {
|
||||
name: schema.marketplace.name,
|
||||
description: schema.marketplace.description,
|
||||
authorName: schema.marketplace.authorName,
|
||||
stars: schema.marketplace.stars,
|
||||
views: schema.marketplace.views,
|
||||
category: schema.marketplace.category,
|
||||
createdAt: schema.marketplace.createdAt,
|
||||
@@ -212,7 +207,7 @@ export async function GET(request: NextRequest) {
|
||||
result.popular = await db
|
||||
.select(selectFields)
|
||||
.from(schema.marketplace)
|
||||
.orderBy(desc(schema.marketplace.stars), desc(schema.marketplace.views))
|
||||
.orderBy(desc(schema.marketplace.views))
|
||||
.limit(limit)
|
||||
}
|
||||
|
||||
@@ -262,7 +257,7 @@ export async function GET(request: NextRequest) {
|
||||
.select(selectFields)
|
||||
.from(schema.marketplace)
|
||||
.where(eq(schema.marketplace.category, categoryValue))
|
||||
.orderBy(desc(schema.marketplace.stars), desc(schema.marketplace.views))
|
||||
.orderBy(desc(schema.marketplace.views))
|
||||
.limit(limit)
|
||||
|
||||
// Always add the category to the result, even if empty
|
||||
@@ -277,15 +272,17 @@ export async function GET(request: NextRequest) {
|
||||
// Transform the data if state was included to match the expected format
|
||||
if (includeState) {
|
||||
const transformSection = (section: any[]) => {
|
||||
return section.map((item) =>
|
||||
'state' in item
|
||||
? {
|
||||
...item,
|
||||
workflowState: item.state,
|
||||
state: undefined,
|
||||
}
|
||||
: item
|
||||
)
|
||||
return section.map((item) => {
|
||||
if ('state' in item) {
|
||||
// Create a new object without the state field, but with workflowState
|
||||
const { state, ...rest } = item;
|
||||
return {
|
||||
...rest,
|
||||
workflowState: state
|
||||
};
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
if (result.popular.length > 0) {
|
||||
|
||||
@@ -11,7 +11,7 @@ const logger = createLogger('WorkflowAPI')
|
||||
// Define marketplace data schema
|
||||
const MarketplaceDataSchema = z.object({
|
||||
id: z.string(),
|
||||
status: z.enum(['owner', 'temp', 'star'])
|
||||
status: z.enum(['owner', 'temp'])
|
||||
}).nullable().optional()
|
||||
|
||||
// Schema for workflow data
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
LineChart,
|
||||
MailIcon,
|
||||
NotebookPen,
|
||||
Star,
|
||||
Store,
|
||||
TimerIcon,
|
||||
Trash,
|
||||
@@ -38,6 +37,7 @@ import {
|
||||
} from '@/components/ui/form'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { LoadingAgent } from '@/components/ui/loading-agent'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -131,7 +131,6 @@ interface MarketplaceInfo {
|
||||
description: string
|
||||
category: string
|
||||
authorName: string
|
||||
stars: number
|
||||
views: number
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
@@ -183,14 +182,25 @@ export function MarketplaceModal({ open, onOpenChange }: MarketplaceModalProps)
|
||||
|
||||
try {
|
||||
setIsLoading(true)
|
||||
const response = await fetch(`/api/marketplace/${activeWorkflowId}/info`)
|
||||
|
||||
// Get marketplace ID from the workflow's marketplaceData
|
||||
const marketplaceData = getMarketplaceData()
|
||||
if (!marketplaceData?.id) {
|
||||
throw new Error('No marketplace ID found in workflow data')
|
||||
}
|
||||
|
||||
// Use the marketplace ID to fetch details instead of workflow ID
|
||||
const response = await fetch(
|
||||
`/api/marketplace/workflows?marketplaceId=${marketplaceData.id}`
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch marketplace information')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
setMarketplaceInfo(data)
|
||||
// The API returns the data directly without wrapping
|
||||
const marketplaceEntry = await response.json()
|
||||
setMarketplaceInfo(marketplaceEntry)
|
||||
} catch (error) {
|
||||
console.error('Error fetching marketplace info:', error)
|
||||
addNotification('error', 'Failed to fetch marketplace information', activeWorkflowId)
|
||||
@@ -270,9 +280,13 @@ export function MarketplaceModal({ open, onOpenChange }: MarketplaceModalProps)
|
||||
throw new Error(errorData.error || 'Failed to publish workflow')
|
||||
}
|
||||
|
||||
// Get the marketplace ID from the response
|
||||
const responseData = await response.json()
|
||||
const marketplaceId = responseData.data.id
|
||||
|
||||
// Update the marketplace data in the workflow registry
|
||||
updateWorkflow(activeWorkflowId, {
|
||||
marketplaceData: { id: activeWorkflowId, status: 'owner' },
|
||||
marketplaceData: { id: marketplaceId, status: 'owner' },
|
||||
})
|
||||
|
||||
// Add a marketplace notification with detailed information
|
||||
@@ -301,29 +315,46 @@ export function MarketplaceModal({ open, onOpenChange }: MarketplaceModalProps)
|
||||
try {
|
||||
setIsUnpublishing(true)
|
||||
|
||||
const response = await fetch(`/api/marketplace/${activeWorkflowId}/unpublish`, {
|
||||
// Get marketplace ID from the workflow's marketplaceData
|
||||
const marketplaceData = getMarketplaceData()
|
||||
if (!marketplaceData?.id) {
|
||||
throw new Error('No marketplace ID found in workflow data')
|
||||
}
|
||||
|
||||
logger.info('Attempting to unpublish marketplace entry', {
|
||||
marketplaceId: marketplaceData.id,
|
||||
workflowId: activeWorkflowId,
|
||||
status: marketplaceData.status,
|
||||
})
|
||||
|
||||
const response = await fetch(`/api/marketplace/${marketplaceData.id}/unpublish`, {
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
logger.error('Error response from unpublish endpoint', {
|
||||
status: response.status,
|
||||
data: errorData,
|
||||
})
|
||||
throw new Error(errorData.error || 'Failed to unpublish workflow')
|
||||
}
|
||||
|
||||
// Remove the marketplace data from the workflow registry
|
||||
updateWorkflow(activeWorkflowId, {
|
||||
marketplaceData: null,
|
||||
logger.info('Successfully unpublished workflow from marketplace', {
|
||||
marketplaceId: marketplaceData.id,
|
||||
workflowId: activeWorkflowId,
|
||||
})
|
||||
|
||||
// Add a notification
|
||||
addNotification(
|
||||
'marketplace',
|
||||
`"${marketplaceInfo?.name || 'Workflow'}" successfully unpublished from marketplace`,
|
||||
activeWorkflowId
|
||||
)
|
||||
|
||||
// Close the modal after successful unpublishing
|
||||
// First close the modal to prevent any flashing
|
||||
onOpenChange(false)
|
||||
|
||||
// Then update the workflow state after modal is closed
|
||||
setTimeout(() => {
|
||||
// Remove the marketplace data from the workflow registry
|
||||
updateWorkflow(activeWorkflowId, {
|
||||
marketplaceData: null,
|
||||
})
|
||||
}, 100)
|
||||
} catch (error: any) {
|
||||
console.error('Error unpublishing workflow:', error)
|
||||
addNotification('error', `Failed to unpublish workflow: ${error.message}`, activeWorkflowId)
|
||||
@@ -350,13 +381,8 @@ export function MarketplaceModal({ open, onOpenChange }: MarketplaceModalProps)
|
||||
const renderMarketplaceInfo = () => {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-12 text-muted-foreground">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<div className="animate-spin">
|
||||
<BrainCircuit className="h-5 w-5" />
|
||||
</div>
|
||||
<p className="text-sm">Loading marketplace information...</p>
|
||||
</div>
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<LoadingAgent size="md" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -379,12 +405,6 @@ export function MarketplaceModal({ open, onOpenChange }: MarketplaceModalProps)
|
||||
<div className="flex items-start justify-between">
|
||||
<h3 className="text-xl font-medium leading-tight">{marketplaceInfo.name}</h3>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-1.5 rounded-md px-2 py-1">
|
||||
<Star className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
<span className="text-xs font-medium text-muted-foreground">
|
||||
{marketplaceInfo.stars}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 rounded-md px-2 py-1">
|
||||
<Eye className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
<span className="text-xs font-medium text-muted-foreground">
|
||||
|
||||
@@ -11,6 +11,7 @@ import ReactFlow, {
|
||||
useReactFlow,
|
||||
} from 'reactflow'
|
||||
import 'reactflow/dist/style.css'
|
||||
import { LoadingAgent } from '@/components/ui/loading-agent'
|
||||
import { createLogger } from '@/lib/logs/console-logger'
|
||||
import { useExecutionStore } from '@/stores/execution/store'
|
||||
import { useNotificationStore } from '@/stores/notifications/store'
|
||||
@@ -460,7 +461,13 @@ function WorkflowContent() {
|
||||
}
|
||||
}, [setSubBlockValue])
|
||||
|
||||
if (!isInitialized) return null
|
||||
if (!isInitialized) {
|
||||
return (
|
||||
<div className="flex h-[calc(100vh-4rem)] w-full items-center justify-center">
|
||||
<LoadingAgent size="lg" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { Folder } from 'lucide-react'
|
||||
import { Folder, LucideIcon } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { WorkflowMetadata } from '@/stores/workflows/registry/types'
|
||||
@@ -12,9 +12,15 @@ interface FolderSectionProps {
|
||||
title: string
|
||||
workflows: WorkflowMetadata[]
|
||||
defaultOpen?: boolean
|
||||
icon?: LucideIcon
|
||||
}
|
||||
|
||||
export function FolderSection({ title, workflows, defaultOpen = true }: FolderSectionProps) {
|
||||
export function FolderSection({
|
||||
title,
|
||||
workflows,
|
||||
defaultOpen = true,
|
||||
icon: Icon = Folder,
|
||||
}: FolderSectionProps) {
|
||||
const [isOpen, setIsOpen] = useState(defaultOpen)
|
||||
const pathname = usePathname()
|
||||
|
||||
@@ -30,8 +36,41 @@ export function FolderSection({ title, workflows, defaultOpen = true }: FolderSe
|
||||
return null
|
||||
}
|
||||
|
||||
// Calculate height based on number of workflows, with a maximum of 4
|
||||
const navItemHeight = 32 // Height of each NavItem in pixels
|
||||
const gapSize = 12 // Gap between items (3 in rem units = ~12px)
|
||||
const maxItems = 4
|
||||
|
||||
// Calculate height for a single item (no gaps)
|
||||
const singleItemHeight = navItemHeight
|
||||
|
||||
// Calculate heights for different number of items
|
||||
const twoItemsHeight = navItemHeight * 2 + gapSize
|
||||
const threeItemsHeight = navItemHeight * 3 + gapSize * 2
|
||||
const fourItemsHeight = navItemHeight * 4 + gapSize * 3
|
||||
|
||||
// Max height is always the height of 4 items
|
||||
const maxHeight = fourItemsHeight
|
||||
const needsScroll = workflows.length > maxItems
|
||||
|
||||
// Get exact height based on number of workflows (up to 4)
|
||||
const getExactHeight = () => {
|
||||
switch (Math.min(workflows.length, maxItems)) {
|
||||
case 1:
|
||||
return singleItemHeight
|
||||
case 2:
|
||||
return twoItemsHeight
|
||||
case 3:
|
||||
return threeItemsHeight
|
||||
case 4:
|
||||
return fourItemsHeight
|
||||
default:
|
||||
return 'auto'
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="flex flex-col items-center w-full">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
@@ -42,7 +81,7 @@ export function FolderSection({ title, workflows, defaultOpen = true }: FolderSe
|
||||
isOpen ? 'bg-accent' : ''
|
||||
}`}
|
||||
>
|
||||
<Folder className="!h-5 !w-5" />
|
||||
<Icon className="!h-5 !w-5" />
|
||||
<span className="sr-only">{title}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
@@ -50,19 +89,36 @@ export function FolderSection({ title, workflows, defaultOpen = true }: FolderSe
|
||||
</Tooltip>
|
||||
|
||||
{isOpen && (
|
||||
<div className="mt-3 flex flex-col items-center gap-3">
|
||||
{workflows.map((workflow) => (
|
||||
<NavItem key={workflow.id} href={`/w/${workflow.id}`} label={workflow.name}>
|
||||
<div
|
||||
className="h-4 w-4 rounded-full"
|
||||
style={{
|
||||
backgroundColor:
|
||||
workflow.color ||
|
||||
(workflow.marketplaceData?.status === 'temp' ? '#808080' : '#3972F6'),
|
||||
}}
|
||||
/>
|
||||
</NavItem>
|
||||
))}
|
||||
<div className="mt-3 flex flex-col items-center w-full">
|
||||
<div
|
||||
className={`flex flex-col items-center w-full ${
|
||||
needsScroll ? 'overflow-y-auto [&::-webkit-scrollbar]:hidden' : ''
|
||||
}`}
|
||||
style={{
|
||||
height: workflows.length <= maxItems ? `${getExactHeight()}px` : `${maxHeight}px`,
|
||||
minHeight: workflows.length === 1 ? `${singleItemHeight}px` : undefined,
|
||||
maxHeight: `${maxHeight}px`,
|
||||
scrollbarWidth: 'none',
|
||||
msOverflowStyle: 'none',
|
||||
}}
|
||||
>
|
||||
<ul className="flex flex-col items-center gap-3 w-full">
|
||||
{workflows.map((workflow) => (
|
||||
<li key={workflow.id} className="flex justify-center w-full h-8 flex-shrink-0">
|
||||
<NavItem href={`/w/${workflow.id}`} label={workflow.name}>
|
||||
<div
|
||||
className="h-4 w-4 rounded-full flex-shrink-0"
|
||||
style={{
|
||||
backgroundColor:
|
||||
workflow.color ||
|
||||
(workflow.marketplaceData?.status === 'temp' ? '#808080' : '#3972F6'),
|
||||
}}
|
||||
/>
|
||||
</NavItem>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -9,10 +9,12 @@ export function NavItem({
|
||||
href,
|
||||
label,
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
href: string
|
||||
label: string
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}) {
|
||||
const pathname = usePathname()
|
||||
|
||||
@@ -25,7 +27,8 @@ export function NavItem({
|
||||
'flex !h-9 !w-9 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:text-foreground md:h-8 md:w-8',
|
||||
{
|
||||
'bg-accent': pathname === href,
|
||||
}
|
||||
},
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -119,7 +119,12 @@ export function Sidebar() {
|
||||
<FolderSection title="My Workflows" workflows={regularWorkflows} defaultOpen={true} />
|
||||
|
||||
{/* Marketplace Workflows folder */}
|
||||
<FolderSection title="Marketplace" workflows={tempWorkflows} defaultOpen={false} />
|
||||
<FolderSection
|
||||
title="Marketplace"
|
||||
workflows={tempWorkflows}
|
||||
defaultOpen={true}
|
||||
icon={Store}
|
||||
/>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
@@ -25,13 +25,10 @@ export function WorkflowCardSkeleton() {
|
||||
<Skeleton className="h-3 w-full mb-1" />
|
||||
<Skeleton className="h-3 w-4/5" />
|
||||
</CardContent>
|
||||
{/* Footer with author and stats skeletons */}
|
||||
{/* Footer with author and views skeletons */}
|
||||
<CardFooter className="p-4 pt-2 mt-auto flex justify-between items-center">
|
||||
<Skeleton className="h-3 w-1/4" />
|
||||
<div className="flex items-center space-x-3">
|
||||
<Skeleton className="h-3 w-10" />
|
||||
<Skeleton className="h-3 w-10" />
|
||||
</div>
|
||||
<Skeleton className="h-3 w-10" />
|
||||
</CardFooter>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Eye, Star } from 'lucide-react'
|
||||
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'
|
||||
@@ -81,9 +81,13 @@ export function WorkflowCard({ workflow, onHover }: WorkflowCardProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="block" aria-label={`View ${workflow.name} workflow`} onClick={handleClick}>
|
||||
<div
|
||||
className="block cursor-pointer"
|
||||
aria-label={`View ${workflow.name} workflow`}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<Card
|
||||
className="overflow-hidden transition-all hover:shadow-md flex flex-col h-full cursor-pointer"
|
||||
className="overflow-hidden transition-all hover:shadow-md flex flex-col h-full"
|
||||
onMouseEnter={handleMouseEnter}
|
||||
>
|
||||
{/* Workflow preview/thumbnail area */}
|
||||
@@ -91,7 +95,7 @@ export function WorkflowCard({ workflow, onHover }: WorkflowCardProps) {
|
||||
{isPreviewReady && workflow.workflowState ? (
|
||||
// Show interactive workflow preview if state is available
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="w-full h-full transform-gpu scale-[0.8]">
|
||||
<div className="w-full h-full transform-gpu scale-[0.9]">
|
||||
<WorkflowPreview workflowState={workflow.workflowState} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -124,11 +128,7 @@ export function WorkflowCard({ workflow, onHover }: WorkflowCardProps) {
|
||||
{/* Footer with author and stats */}
|
||||
<CardFooter className="p-4 pt-2 mt-auto flex justify-between items-center">
|
||||
<div className="text-xs text-muted-foreground">by {workflow.author}</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex items-center space-x-1">
|
||||
<Star className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
<span className="text-xs font-medium text-muted-foreground">{workflow.stars}</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center space-x-1">
|
||||
<Eye className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
<span className="text-xs font-medium text-muted-foreground">{workflow.views}</span>
|
||||
|
||||
@@ -408,7 +408,7 @@ function WorkflowPreviewContent({ workflowState }: WorkflowPreviewProps) {
|
||||
edgeTypes={edgeTypes}
|
||||
fitView
|
||||
fitViewOptions={{
|
||||
padding: 0.2,
|
||||
padding: 0,
|
||||
minZoom: 0.2,
|
||||
maxZoom: 3,
|
||||
}}
|
||||
@@ -430,12 +430,13 @@ function WorkflowPreviewContent({ workflowState }: WorkflowPreviewProps) {
|
||||
zoomOnPinch={false}
|
||||
zoomOnDoubleClick={false}
|
||||
panOnDrag={false}
|
||||
preventScrolling={true}
|
||||
preventScrolling={false}
|
||||
disableKeyboardA11y={true}
|
||||
attributionPosition="bottom-right"
|
||||
className="w-full h-full"
|
||||
style={{ background: 'transparent' }}
|
||||
className="w-full h-full pointer-events-none"
|
||||
style={{ background: 'transparent', pointerEvents: 'none' }}
|
||||
>
|
||||
<Background gap={12} size={1} className="opacity-30" />
|
||||
<Background gap={12} size={1} className="opacity-30 pointer-events-none" />
|
||||
</ReactFlow>
|
||||
)
|
||||
}
|
||||
@@ -448,7 +449,7 @@ function WorkflowPreviewContent({ workflowState }: WorkflowPreviewProps) {
|
||||
export function WorkflowPreview({ workflowState }: WorkflowPreviewProps) {
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<div className="h-full w-full">
|
||||
<div className="h-full w-full -m-1">
|
||||
<WorkflowPreviewContent workflowState={workflowState} />
|
||||
</div>
|
||||
</ReactFlowProvider>
|
||||
|
||||
@@ -16,7 +16,6 @@ export interface Workflow {
|
||||
name: string
|
||||
description: string
|
||||
author: string
|
||||
stars: number
|
||||
views: number
|
||||
tags: string[]
|
||||
thumbnail?: string
|
||||
@@ -41,7 +40,6 @@ export interface MarketplaceWorkflow {
|
||||
name: string
|
||||
description: string
|
||||
authorName: string
|
||||
stars: number
|
||||
views: number
|
||||
category: string
|
||||
createdAt: string
|
||||
@@ -94,7 +92,6 @@ export default function Marketplace() {
|
||||
name: item.name,
|
||||
description: item.description || '',
|
||||
author: item.authorName,
|
||||
stars: item.stars,
|
||||
views: item.views,
|
||||
tags: [item.category],
|
||||
workflowState: item.workflowState,
|
||||
@@ -105,7 +102,6 @@ export default function Marketplace() {
|
||||
name: item.name,
|
||||
description: item.description || '',
|
||||
author: item.authorName,
|
||||
stars: item.stars,
|
||||
views: item.views,
|
||||
tags: [item.category],
|
||||
workflowState: item.workflowState,
|
||||
@@ -121,7 +117,6 @@ export default function Marketplace() {
|
||||
name: item.name,
|
||||
description: item.description || '',
|
||||
author: item.authorName,
|
||||
stars: item.stars,
|
||||
views: item.views,
|
||||
tags: [item.category],
|
||||
workflowState: item.workflowState,
|
||||
|
||||
81
sim/components/ui/loading-agent.tsx
Normal file
81
sim/components/ui/loading-agent.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
'use client'
|
||||
|
||||
export interface LoadingAgentProps {
|
||||
/**
|
||||
* Size of the loading agent
|
||||
* @default 'md'
|
||||
*/
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
}
|
||||
|
||||
export function LoadingAgent({ size = 'md' }: LoadingAgentProps) {
|
||||
const pathLength = 40
|
||||
|
||||
// Size mappings for width and height
|
||||
const sizes = {
|
||||
sm: { width: 16, height: 18 },
|
||||
md: { width: 21, height: 24 },
|
||||
lg: { width: 30, height: 34 },
|
||||
}
|
||||
|
||||
const { width, height } = sizes[size]
|
||||
|
||||
return (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 21 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M15.6667 9.25H4.66667C2.64162 9.25 1 10.8916 1 12.9167V18.4167C1 20.4417 2.64162 22.0833 4.66667 22.0833H15.6667C17.6917 22.0833 19.3333 20.4417 19.3333 18.4167V12.9167C19.3333 10.8916 17.6917 9.25 15.6667 9.25Z"
|
||||
stroke="#802FFF"
|
||||
strokeWidth="1.8"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
// The magic: dash array & offset
|
||||
style={{
|
||||
strokeDasharray: pathLength,
|
||||
strokeDashoffset: pathLength,
|
||||
animation: 'dash 1s linear forwards',
|
||||
}}
|
||||
/>
|
||||
<path
|
||||
d="M10.1663 5.58464C11.1789 5.58464 11.9997 4.76382 11.9997 3.7513C11.9997 2.73878 11.1789 1.91797 10.1663 1.91797C9.15382 1.91797 8.33301 2.73878 8.33301 3.7513C8.33301 4.76382 9.15382 5.58464 10.1663 5.58464Z"
|
||||
stroke="#802FFF"
|
||||
strokeWidth="1.8"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
style={{
|
||||
strokeDasharray: pathLength,
|
||||
strokeDashoffset: pathLength,
|
||||
animation: 'dash 1s linear forwards',
|
||||
animationDelay: '0.5s', // if you want to stagger it
|
||||
}}
|
||||
/>
|
||||
<path
|
||||
d="M10.167 5.58594V9.2526M7.41699 16.5859V14.7526M12.917 14.7526V16.5859"
|
||||
stroke="#802FFF"
|
||||
strokeWidth="1.8"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
style={{
|
||||
strokeDasharray: pathLength,
|
||||
strokeDashoffset: pathLength,
|
||||
animation: 'dash 1s linear forwards',
|
||||
animationDelay: '1s',
|
||||
}}
|
||||
/>
|
||||
<style>
|
||||
{`
|
||||
@keyframes dash {
|
||||
to {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
2
sim/db/migrations/0027_careless_gamora.sql
Normal file
2
sim/db/migrations/0027_careless_gamora.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
DROP TABLE "marketplace_star" CASCADE;--> statement-breakpoint
|
||||
ALTER TABLE "marketplace" DROP COLUMN "stars";
|
||||
1185
sim/db/migrations/meta/0027_snapshot.json
Normal file
1185
sim/db/migrations/meta/0027_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -190,6 +190,13 @@
|
||||
"when": 1743654486007,
|
||||
"tag": "0026_daily_killraven",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 27,
|
||||
"version": "7",
|
||||
"when": 1743667795545,
|
||||
"tag": "0027_careless_gamora",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -78,10 +78,9 @@ export const workflow = pgTable('workflow', {
|
||||
runCount: integer('run_count').notNull().default(0),
|
||||
lastRunAt: timestamp('last_run_at'),
|
||||
variables: json('variables').default('{}'),
|
||||
marketplaceData: json('marketplace_data'), // Format: { id: string, status: 'owner' | 'temp' | 'star' }
|
||||
|
||||
marketplaceData: json('marketplace_data'), // Format: { id: string, status: 'owner' | 'temp' }
|
||||
|
||||
// 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),
|
||||
})
|
||||
@@ -189,32 +188,12 @@ export const marketplace = pgTable('marketplace', {
|
||||
.notNull()
|
||||
.references(() => user.id),
|
||||
authorName: text('author_name').notNull(),
|
||||
stars: integer('stars').notNull().default(0),
|
||||
views: integer('views').notNull().default(0),
|
||||
category: text('category'),
|
||||
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at').notNull().defaultNow(),
|
||||
})
|
||||
|
||||
export const marketplaceStar = pgTable(
|
||||
'marketplace_star',
|
||||
{
|
||||
id: text('id').primaryKey(),
|
||||
marketplaceId: text('marketplace_id')
|
||||
.notNull()
|
||||
.references(() => marketplace.id, { onDelete: 'cascade' }),
|
||||
userId: text('user_id')
|
||||
.notNull()
|
||||
.references(() => user.id),
|
||||
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||
},
|
||||
(table) => {
|
||||
return {
|
||||
userMarketplaceIdx: uniqueIndex('user_marketplace_idx').on(table.userId, table.marketplaceId),
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
export const userStats = pgTable('user_stats', {
|
||||
id: text('id').primaryKey(),
|
||||
userId: text('user_id')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export interface MarketplaceData {
|
||||
id: string
|
||||
status: 'owner' | 'temp' | 'star'
|
||||
id: string // Marketplace entry ID to track original marketplace source
|
||||
status: 'owner' | 'temp'
|
||||
}
|
||||
|
||||
export interface WorkflowMetadata {
|
||||
|
||||
@@ -94,6 +94,7 @@ export async function fetchWorkflowsFromDB(): Promise<void> {
|
||||
deployedAt,
|
||||
apiKey,
|
||||
createdAt,
|
||||
marketplaceData,
|
||||
} = workflow
|
||||
|
||||
// 1. Update registry store with workflow metadata
|
||||
@@ -104,6 +105,7 @@ export async function fetchWorkflowsFromDB(): Promise<void> {
|
||||
color: color || '#3972F6',
|
||||
// Use createdAt for sorting if available, otherwise fall back to lastSynced
|
||||
lastModified: createdAt ? new Date(createdAt) : new Date(lastSynced),
|
||||
marketplaceData: marketplaceData || null,
|
||||
}
|
||||
|
||||
// 2. Prepare workflow state data
|
||||
@@ -115,6 +117,7 @@ export async function fetchWorkflowsFromDB(): Promise<void> {
|
||||
deployedAt: deployedAt ? new Date(deployedAt) : undefined,
|
||||
apiKey,
|
||||
lastSaved: Date.now(),
|
||||
marketplaceData: marketplaceData || null,
|
||||
}
|
||||
|
||||
// 3. Initialize subblock values from the workflow state
|
||||
|
||||
Reference in New Issue
Block a user