improvement: marketplace, sidebar, loading (#221)

This commit is contained in:
Emir Karabeg
2025-04-03 01:15:49 -07:00
committed by GitHub
parent 2df1d13b05
commit 4cd90116ef
21 changed files with 1537 additions and 147 deletions

View File

@@ -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,

View File

@@ -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)
}
}

View File

@@ -112,7 +112,6 @@ export async function POST(request: NextRequest) {
.values({
...marketplaceEntry,
createdAt: new Date(),
stars: 0,
views: 0,
})
.returning()

View File

@@ -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) {

View File

@@ -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

View File

@@ -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">

View File

@@ -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 (
<>

View File

@@ -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>

View File

@@ -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}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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,

View 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>
)
}

View File

@@ -0,0 +1,2 @@
DROP TABLE "marketplace_star" CASCADE;--> statement-breakpoint
ALTER TABLE "marketplace" DROP COLUMN "stars";

File diff suppressed because it is too large Load Diff

View File

@@ -190,6 +190,13 @@
"when": 1743654486007,
"tag": "0026_daily_killraven",
"breakpoints": true
},
{
"idx": 27,
"version": "7",
"when": 1743667795545,
"tag": "0027_careless_gamora",
"breakpoints": true
}
]
}
}

View File

@@ -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')

View File

@@ -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 {

View File

@@ -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