fix(envvars): use getEnv on the client-side when we need to inject vars for docker runtime, fix folder container & removed folder/subfolder creation modals (#751)

* use getEnv on the client-side when we need to inject vars for docker runtime, fix folder container & removed folder/subfolder creation modals

* consolidated naming utils

* additional type safety
This commit is contained in:
Waleed Latif
2025-07-22 16:08:04 -07:00
committed by GitHub
parent d8a7aaaf88
commit a217daca3c
22 changed files with 382 additions and 338 deletions
@@ -17,7 +17,7 @@ import { Button } from '@/components/ui/button'
import { Card, CardContent } from '@/components/ui/card'
import { CopyButton } from '@/components/ui/copy-button'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { env } from '@/lib/env'
import { getEnv } from '@/lib/env'
import { createLogger } from '@/lib/logs/console-logger'
import { cn } from '@/lib/utils'
import { ChatDeploy } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/chat-deploy/chat-deploy'
@@ -225,7 +225,7 @@ export function DeployModal({
}
const data = await response.json()
const endpoint = `${env.NEXT_PUBLIC_APP_URL}/api/workflows/${workflowId}/execute`
const endpoint = `${getEnv('NEXT_PUBLIC_APP_URL')}/api/workflows/${workflowId}/execute`
const inputFormatExample = getInputFormatExample()
setDeploymentInfo({
@@ -288,7 +288,7 @@ export function DeployModal({
}
// Update the local deployment info
const endpoint = `${env.NEXT_PUBLIC_APP_URL}/api/workflows/${workflowId}/execute`
const endpoint = `${getEnv('NEXT_PUBLIC_APP_URL')}/api/workflows/${workflowId}/execute`
const inputFormatExample = getInputFormatExample()
const newDeploymentInfo = {
@@ -597,7 +597,7 @@ export function DeployModal({
<DeployForm
apiKeys={apiKeys}
keysLoaded={keysLoaded}
endpointUrl={`${env.NEXT_PUBLIC_APP_URL}/api/workflows/${workflowId}/execute`}
endpointUrl={`${getEnv('NEXT_PUBLIC_APP_URL')}/api/workflows/${workflowId}/execute`}
workflowId={workflowId || ''}
onSubmit={onDeploy}
getInputFormatExample={getInputFormatExample}
@@ -13,7 +13,7 @@ import {
CommandList,
} from '@/components/ui/command'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { env } from '@/lib/env'
import { getEnv } from '@/lib/env'
import { createLogger } from '@/lib/logs/console-logger'
import {
type Credential,
@@ -301,7 +301,7 @@ export function GoogleDrivePicker({
showUploadFolders: true,
supportDrives: true,
multiselect: false,
appId: env.NEXT_PUBLIC_GOOGLE_PROJECT_NUMBER,
appId: getEnv('NEXT_PUBLIC_GOOGLE_PROJECT_NUMBER'),
// Enable folder selection when mimeType is folder
setSelectFolderEnabled: !!mimeTypeFilter?.includes('folder'),
callbackFunction: (data) => {
@@ -2,7 +2,7 @@
import { useEffect, useState } from 'react'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { env } from '@/lib/env'
import { getEnv } from '@/lib/env'
import type { SubBlockConfig } from '@/blocks/types'
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
@@ -169,8 +169,8 @@ export function FileSelectorInput({
}
// For Google Drive
const clientId = env.NEXT_PUBLIC_GOOGLE_CLIENT_ID || ''
const apiKey = env.NEXT_PUBLIC_GOOGLE_API_KEY || ''
const clientId = getEnv('NEXT_PUBLIC_GOOGLE_CLIENT_ID') || ''
const apiKey = getEnv('NEXT_PUBLIC_GOOGLE_API_KEY') || ''
// Render Google Calendar selector
if (isGoogleCalendar) {
@@ -5,10 +5,8 @@ import { logger } from '@sentry/nextjs'
import { File, Folder, Plus, Upload } from 'lucide-react'
import { useParams, useRouter } from 'next/navigation'
import { Button } from '@/components/ui/button'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { generateFolderName } from '@/lib/naming'
import { cn } from '@/lib/utils'
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/w/components/providers/workspace-permissions-provider'
import { useFolderStore } from '@/stores/folders/store'
@@ -25,8 +23,6 @@ export function CreateMenu({
isCollapsed,
isCreatingWorkflow = false,
}: CreateMenuProps) {
const [showFolderDialog, setShowFolderDialog] = useState(false)
const [folderName, setFolderName] = useState('')
const [isCreating, setIsCreating] = useState(false)
const [isOpen, setIsOpen] = useState(false)
const [pressTimer, setPressTimer] = useState<NodeJS.Timeout | null>(null)
@@ -61,10 +57,37 @@ export function CreateMenu({
}
}, [onCreateWorkflow, isCreatingWorkflow, router, workspaceId])
const handleCreateFolder = useCallback(() => {
const handleCreateFolder = useCallback(async () => {
setIsOpen(false)
setShowFolderDialog(true)
}, [])
if (isCreating) {
logger.info('Folder creation already in progress, ignoring request')
return
}
if (!workspaceId) {
logger.error('No workspaceId available for folder creation')
return
}
try {
setIsCreating(true)
// Generate folder name using fresh data from API
const folderName = await generateFolderName(workspaceId)
await createFolder({
name: folderName,
workspaceId: workspaceId,
})
logger.info(`Created folder: ${folderName}`)
} catch (error) {
logger.error('Failed to create folder:', { error })
} finally {
setIsCreating(false)
}
}, [createFolder, workspaceId])
const handleImportWorkflow = useCallback(() => {
setIsOpen(false)
@@ -146,30 +169,6 @@ export function CreateMenu({
}
}, [pressTimer])
const handleFolderSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!folderName.trim() || !workspaceId) return
setIsCreating(true)
try {
await createFolder({
name: folderName.trim(),
workspaceId: workspaceId,
})
setFolderName('')
setShowFolderDialog(false)
} catch (error) {
logger.error('Failed to create folder:', { error })
} finally {
setIsCreating(false)
}
}
const handleCancel = () => {
setFolderName('')
setShowFolderDialog(false)
}
return (
<>
<Popover open={isOpen} onOpenChange={setIsOpen}>
@@ -221,11 +220,15 @@ export function CreateMenu({
</button>
<button
className='flex w-full cursor-pointer items-center gap-2 rounded-md px-3 py-2 font-[380] text-card-foreground text-sm outline-none hover:bg-secondary/50 focus:bg-secondary/50'
className={cn(
'flex w-full cursor-pointer items-center gap-2 rounded-md px-3 py-2 font-[380] text-card-foreground text-sm outline-none hover:bg-secondary/50 focus:bg-secondary/50',
isCreating && 'cursor-not-allowed opacity-50'
)}
onClick={handleCreateFolder}
disabled={isCreating}
>
<Folder className='h-4 w-4' />
New folder
{isCreating ? 'Creating...' : 'New folder'}
</button>
{userPermissions.canEdit && (
@@ -246,36 +249,6 @@ export function CreateMenu({
disabled={!userPermissions.canEdit}
onClose={() => setIsOpen(false)}
/>
{/* Folder creation dialog */}
<Dialog open={showFolderDialog} onOpenChange={setShowFolderDialog}>
<DialogContent className='sm:max-w-[425px]'>
<DialogHeader>
<DialogTitle>Create New Folder</DialogTitle>
</DialogHeader>
<form onSubmit={handleFolderSubmit} className='space-y-4'>
<div className='space-y-2'>
<Label htmlFor='folder-name'>Folder Name</Label>
<Input
id='folder-name'
value={folderName}
onChange={(e) => setFolderName(e.target.value)}
placeholder='Enter folder name...'
autoFocus
required
/>
</div>
<div className='flex justify-end space-x-2'>
<Button type='button' variant='outline' onClick={handleCancel}>
Cancel
</Button>
<Button type='submit' disabled={!folderName.trim() || isCreating}>
{isCreating ? 'Creating...' : 'Create Folder'}
</Button>
</div>
</form>
</DialogContent>
</Dialog>
</>
)
}
@@ -4,17 +4,15 @@ import { useState } from 'react'
import { File, Folder, MoreHorizontal, Pencil, Trash2 } from 'lucide-react'
import { useParams } from 'next/navigation'
import { Button } from '@/components/ui/button'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { createLogger } from '@/lib/logs/console-logger'
import { generateSubfolderName } from '@/lib/naming'
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/w/components/providers/workspace-permissions-provider'
import { useFolderStore } from '@/stores/folders/store'
@@ -37,8 +35,6 @@ export function FolderContextMenu({
onStartEdit,
level,
}: FolderContextMenuProps) {
const [showSubfolderDialog, setShowSubfolderDialog] = useState(false)
const [subfolderName, setSubfolderName] = useState('')
const [isCreating, setIsCreating] = useState(false)
const params = useParams()
const workspaceId = params.workspaceId as string
@@ -46,14 +42,46 @@ export function FolderContextMenu({
// Get user permissions for the workspace
const userPermissions = useUserPermissionsContext()
const { createFolder, deleteFolder } = useFolderStore()
const { createFolder, deleteFolder, setExpanded } = useFolderStore()
const handleCreateWorkflow = () => {
// Ensure folder is expanded so user can see the new workflow
setExpanded(folderId, true)
onCreateWorkflow(folderId)
}
const handleCreateSubfolder = () => {
setShowSubfolderDialog(true)
const handleCreateSubfolder = async () => {
if (isCreating) {
logger.info('Subfolder creation already in progress, ignoring request')
return
}
if (!workspaceId) {
logger.error('No workspaceId available for subfolder creation')
return
}
try {
setIsCreating(true)
// Ensure parent folder is expanded so user can see the new subfolder
setExpanded(folderId, true)
// Generate subfolder name using fresh data from API
const subfolderName = await generateSubfolderName(workspaceId, folderId)
await createFolder({
name: subfolderName,
workspaceId: workspaceId,
parentId: folderId,
})
logger.info(`Created subfolder: ${subfolderName}`)
} catch (error) {
logger.error('Failed to create subfolder:', { error })
} finally {
setIsCreating(false)
}
}
const handleRename = () => {
@@ -76,31 +104,6 @@ export function FolderContextMenu({
}
}
const handleSubfolderSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!subfolderName.trim() || !workspaceId) return
setIsCreating(true)
try {
await createFolder({
name: subfolderName.trim(),
workspaceId: workspaceId,
parentId: folderId,
})
setSubfolderName('')
setShowSubfolderDialog(false)
} catch (error) {
logger.error('Failed to create subfolder:', { error })
} finally {
setIsCreating(false)
}
}
const handleCancel = () => {
setSubfolderName('')
setShowSubfolderDialog(false)
}
return (
<>
<DropdownMenu>
@@ -175,37 +178,6 @@ export function FolderContextMenu({
)}
</DropdownMenuContent>
</DropdownMenu>
{/* Subfolder creation dialog */}
<Dialog open={showSubfolderDialog} onOpenChange={setShowSubfolderDialog}>
<DialogContent className='sm:max-w-[425px]' onClick={(e) => e.stopPropagation()}>
<DialogHeader>
<DialogTitle>Create New Subfolder</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubfolderSubmit} className='space-y-4'>
<div className='space-y-2'>
<Label htmlFor='subfolder-name'>Folder Name</Label>
<Input
id='subfolder-name'
value={subfolderName}
onChange={(e) => setSubfolderName(e.target.value)}
placeholder='Enter folder name...'
maxLength={50}
autoFocus
required
/>
</div>
<div className='flex justify-end space-x-2'>
<Button type='button' variant='outline' onClick={handleCancel}>
Cancel
</Button>
<Button type='submit' disabled={!subfolderName.trim() || isCreating}>
{isCreating ? 'Creating...' : 'Create Folder'}
</Button>
</div>
</form>
</DialogContent>
</Dialog>
</>
)
}
@@ -304,7 +304,7 @@ export function FolderItem({
onChange={(e) => setEditValue(e.target.value)}
onKeyDown={handleKeyDown}
onBlur={handleInputBlur}
className='flex-1 border-0 bg-transparent p-0 text-muted-foreground text-sm outline-none focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0'
className='min-w-0 flex-1 border-0 bg-transparent p-0 text-muted-foreground text-sm outline-none focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0'
maxLength={50}
disabled={isRenaming}
onClick={(e) => e.stopPropagation()} // Prevent folder toggle when clicking input
@@ -314,7 +314,9 @@ export function FolderItem({
spellCheck='false'
/>
) : (
<span className='flex-1 select-none truncate text-muted-foreground'>{folder.name}</span>
<span className='min-w-0 flex-1 select-none truncate text-muted-foreground'>
{folder.name}
</span>
)}
{!isEditing && (
@@ -225,7 +225,7 @@ export function WorkflowItem({
onChange={(e) => setEditValue(e.target.value)}
onKeyDown={handleKeyDown}
onBlur={handleInputBlur}
className={`flex-1 border-0 bg-transparent p-0 font-medium text-sm outline-none focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 ${
className={`min-w-0 flex-1 border-0 bg-transparent p-0 font-medium text-sm outline-none focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 ${
active && !isDragOver ? 'text-foreground' : 'text-muted-foreground'
}`}
maxLength={100}
@@ -237,7 +237,7 @@ export function WorkflowItem({
spellCheck='false'
/>
) : (
<span className='flex-1 select-none truncate'>
<span className='min-w-0 flex-1 select-none truncate'>
{workflow.name}
{isMarketplace && ' (Preview)'}
</span>
@@ -567,7 +567,7 @@ export function FolderTree({
marketplaceWorkflows.length === 0 &&
folderTree.length === 0 &&
!isCollapsed && (
<div className='break-words px-2 py-1.5 text-muted-foreground text-xs'>
<div className='break-words px-2 py-1.5 pr-12 text-muted-foreground text-xs'>
No workflows or folders in {workspaceId ? 'this workspace' : 'your account'}. Create one
to get started.
</div>
@@ -16,6 +16,7 @@ import {
import { Button } from '@/components/ui/button'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Skeleton } from '@/components/ui/skeleton'
import { isDev } from '@/lib/environment'
import { createLogger } from '@/lib/logs/console-logger'
import { cn } from '@/lib/utils'
import { useUserPermissionsContext } from '../../../providers/workspace-permissions-provider'
@@ -251,20 +252,22 @@ export function WorkspaceSelector({
{/* Bottom Actions */}
<div className='mt-2 flex items-center gap-2 border-t pt-2'>
{/* Send Invite */}
<Button
variant='secondary'
size='sm'
onClick={userPermissions.canAdmin ? () => setShowInviteMembers(true) : undefined}
disabled={!userPermissions.canAdmin}
className={cn(
'h-8 flex-1 justify-center gap-2 rounded-[8px] font-medium text-muted-foreground text-xs transition-colors hover:bg-muted-foreground/10 hover:text-muted-foreground',
!userPermissions.canAdmin && 'cursor-not-allowed opacity-50'
)}
>
<Send className='h-3 w-3' />
<span>Invite</span>
</Button>
{/* Send Invite - Hide in development */}
{!isDev && (
<Button
variant='secondary'
size='sm'
onClick={userPermissions.canAdmin ? () => setShowInviteMembers(true) : undefined}
disabled={!userPermissions.canAdmin}
className={cn(
'h-8 flex-1 justify-center gap-2 rounded-[8px] font-medium text-muted-foreground text-xs transition-colors hover:bg-muted-foreground/10 hover:text-muted-foreground',
!userPermissions.canAdmin && 'cursor-not-allowed opacity-50'
)}
>
<Send className='h-3 w-3' />
<span>Invite</span>
</Button>
)}
{/* Create Workspace */}
<Button
@@ -8,6 +8,7 @@ import { ScrollArea } from '@/components/ui/scroll-area'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { useSession } from '@/lib/auth-client'
import { createLogger } from '@/lib/logs/console-logger'
import { generateWorkspaceName } from '@/lib/naming'
import { cn } from '@/lib/utils'
import {
getKeyboardShortcutText,
@@ -287,13 +288,18 @@ export function Sidebar() {
setIsCreatingWorkspace(true)
logger.info('Creating new workspace')
// Generate workspace name using utility function
const workspaceName = await generateWorkspaceName()
logger.info(`Generated workspace name: ${workspaceName}`)
const response = await fetch('/api/workspaces', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'Untitled workspace',
name: workspaceName,
}),
})
+2 -2
View File
@@ -11,7 +11,7 @@ import {
} from 'react'
import { useParams } from 'next/navigation'
import { io, type Socket } from 'socket.io-client'
import { env } from '@/lib/env'
import { getEnv } from '@/lib/env'
import { createLogger } from '@/lib/logs/console-logger'
const logger = createLogger('SocketContext')
@@ -154,7 +154,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
// Generate initial token for socket authentication
const token = await generateSocketToken()
const socketUrl = env.NEXT_PUBLIC_SOCKET_URL || 'http://localhost:3002'
const socketUrl = getEnv('NEXT_PUBLIC_SOCKET_URL') || 'http://localhost:3002'
logger.info('Attempting to connect to Socket.IO server', {
url: socketUrl,
@@ -1,4 +1,4 @@
import { env } from '@/lib/env'
import { getEnv } from '@/lib/env'
import { createLogger } from '@/lib/logs/console-logger'
import { getAllBlocks } from '@/blocks'
import type { BlockOutput } from '@/blocks/types'
@@ -477,7 +477,7 @@ export class AgentBlockHandler implements BlockHandler {
) {
logger.info('Using HTTP provider request (browser environment)')
const url = new URL('/api/providers', env.NEXT_PUBLIC_APP_URL || '')
const url = new URL('/api/providers', getEnv('NEXT_PUBLIC_APP_URL') || '')
const response = await fetch(url.toString(), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
+2 -2
View File
@@ -18,13 +18,13 @@ import {
linkedErrorsIntegration,
makeFetchTransport,
} from '@sentry/nextjs'
import { env } from './lib/env'
import { env, getEnv } from './lib/env'
import { isProd } from './lib/environment'
// Only in production
if (typeof window !== 'undefined' && isProd) {
const client = new BrowserClient({
dsn: env.NEXT_PUBLIC_SENTRY_DSN || undefined,
dsn: getEnv('NEXT_PUBLIC_SENTRY_DSN') || undefined,
environment: env.NODE_ENV || 'development',
transport: makeFetchTransport,
stackParser: defaultStackParser,
+5 -5
View File
@@ -1,20 +1,20 @@
import { stripeClient } from '@better-auth/stripe/client'
import { emailOTPClient, genericOAuthClient, organizationClient } from 'better-auth/client/plugins'
import { createAuthClient } from 'better-auth/react'
import { env } from '@/lib/env'
import { env, getEnv } from '@/lib/env'
import { isDev, isProd } from '@/lib/environment'
export function getBaseURL() {
let baseURL
if (env.VERCEL_ENV === 'preview') {
baseURL = `https://${env.NEXT_PUBLIC_VERCEL_URL}`
baseURL = `https://${getEnv('NEXT_PUBLIC_VERCEL_URL')}`
} else if (env.VERCEL_ENV === 'development') {
baseURL = `https://${env.NEXT_PUBLIC_VERCEL_URL}`
baseURL = `https://${getEnv('NEXT_PUBLIC_VERCEL_URL')}`
} else if (env.VERCEL_ENV === 'production') {
baseURL = env.BETTER_AUTH_URL || env.NEXT_PUBLIC_APP_URL
baseURL = env.BETTER_AUTH_URL || getEnv('NEXT_PUBLIC_APP_URL')
} else if (env.NODE_ENV === 'development') {
baseURL = env.NEXT_PUBLIC_APP_URL || env.BETTER_AUTH_URL || 'http://localhost:3000'
baseURL = getEnv('NEXT_PUBLIC_APP_URL') || env.BETTER_AUTH_URL || 'http://localhost:3000'
}
return baseURL
-1
View File
@@ -128,7 +128,6 @@ export interface BillingSummary {
billingStatus: 'ok' | 'warning' | 'exceeded'
}
// API Response Types
export interface SubscriptionAPIResponse {
isPaid: boolean
isPro: boolean
+9 -9
View File
@@ -142,15 +142,15 @@ export const env = createEnv({
},
experimental__runtimeEnv: {
NEXT_PUBLIC_APP_URL: getEnv('NEXT_PUBLIC_APP_URL'),
NEXT_PUBLIC_VERCEL_URL: process.env.VERCEL_URL, // Keep as-is (server-only)
NEXT_PUBLIC_SENTRY_DSN: getEnv('NEXT_PUBLIC_SENTRY_DSN'),
NEXT_PUBLIC_GOOGLE_CLIENT_ID: getEnv('NEXT_PUBLIC_GOOGLE_CLIENT_ID'),
NEXT_PUBLIC_GOOGLE_API_KEY: getEnv('NEXT_PUBLIC_GOOGLE_API_KEY'),
NEXT_PUBLIC_GOOGLE_PROJECT_NUMBER: getEnv('NEXT_PUBLIC_GOOGLE_PROJECT_NUMBER'),
NEXT_PUBLIC_SOCKET_URL: getEnv('NEXT_PUBLIC_SOCKET_URL'),
NODE_ENV: process.env.NODE_ENV, // Keep as-is (build-time)
NEXT_TELEMETRY_DISABLED: process.env.NEXT_TELEMETRY_DISABLED, // Keep as-is (build-time)
NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
NEXT_PUBLIC_VERCEL_URL: process.env.NEXT_PUBLIC_VERCEL_URL,
NEXT_PUBLIC_SENTRY_DSN: process.env.NEXT_PUBLIC_SENTRY_DSN,
NEXT_PUBLIC_GOOGLE_CLIENT_ID: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID,
NEXT_PUBLIC_GOOGLE_API_KEY: process.env.NEXT_PUBLIC_GOOGLE_API_KEY,
NEXT_PUBLIC_GOOGLE_PROJECT_NUMBER: process.env.NEXT_PUBLIC_GOOGLE_PROJECT_NUMBER,
NEXT_PUBLIC_SOCKET_URL: process.env.NEXT_PUBLIC_SOCKET_URL,
NODE_ENV: process.env.NODE_ENV,
NEXT_TELEMETRY_DISABLED: process.env.NEXT_TELEMETRY_DISABLED,
},
})
+238
View File
@@ -0,0 +1,238 @@
/**
* Utility functions for generating names for all entities (workspaces, folders, workflows)
*/
import type { WorkflowFolder } from '@/stores/folders/store'
import type { Workspace } from '@/stores/organization/types'
export interface NameableEntity {
name: string
}
interface WorkspacesApiResponse {
workspaces: Workspace[]
}
interface FoldersApiResponse {
folders: WorkflowFolder[]
}
const ADJECTIVES = [
'Blazing',
'Crystal',
'Golden',
'Silver',
'Mystic',
'Cosmic',
'Electric',
'Frozen',
'Burning',
'Shining',
'Dancing',
'Flying',
'Roaring',
'Whispering',
'Glowing',
'Sparkling',
'Thunder',
'Lightning',
'Storm',
'Ocean',
'Mountain',
'Forest',
'Desert',
'Arctic',
'Tropical',
'Midnight',
'Dawn',
'Sunset',
'Rainbow',
'Diamond',
'Ruby',
'Emerald',
'Sapphire',
'Pearl',
'Jade',
'Amber',
'Coral',
'Ivory',
'Obsidian',
'Marble',
'Velvet',
'Silk',
'Satin',
'Linen',
'Cotton',
'Wool',
'Cashmere',
'Denim',
'Neon',
'Pastel',
'Vibrant',
'Muted',
'Bold',
'Subtle',
'Bright',
'Dark',
]
const NOUNS = [
'Phoenix',
'Dragon',
'Eagle',
'Wolf',
'Lion',
'Tiger',
'Panther',
'Falcon',
'Hawk',
'Raven',
'Swan',
'Dove',
'Butterfly',
'Firefly',
'Dragonfly',
'Hummingbird',
'Galaxy',
'Nebula',
'Comet',
'Meteor',
'Star',
'Moon',
'Sun',
'Planet',
'Asteroid',
'Constellation',
'Aurora',
'Eclipse',
'Solstice',
'Equinox',
'Horizon',
'Zenith',
'Castle',
'Tower',
'Bridge',
'Garden',
'Fountain',
'Palace',
'Temple',
'Cathedral',
'Lighthouse',
'Windmill',
'Waterfall',
'Canyon',
'Valley',
'Peak',
'Ridge',
'Cliff',
'Ocean',
'River',
'Lake',
'Stream',
'Pond',
'Bay',
'Cove',
'Harbor',
'Island',
'Peninsula',
'Archipelago',
'Atoll',
'Reef',
'Lagoon',
'Fjord',
'Delta',
'Cake',
'Cookie',
'Muffin',
'Cupcake',
'Pie',
'Tart',
'Brownie',
'Donut',
'Pancake',
'Waffle',
'Croissant',
'Bagel',
'Pretzel',
'Biscuit',
'Scone',
'Crumpet',
]
/**
* Generates the next incremental name for entities following pattern: "{prefix} {number}"
*
* @param existingEntities - Array of entities with name property
* @param prefix - Prefix for the name (e.g., "Workspace", "Folder", "Subfolder")
* @returns Next available name (e.g., "Workspace 3")
*/
export function generateIncrementalName<T extends NameableEntity>(
existingEntities: T[],
prefix: string
): string {
// Create regex pattern for the prefix (e.g., /^Workspace (\d+)$/)
const pattern = new RegExp(`^${prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')} (\\d+)$`)
// Extract numbers from existing entities that match the pattern
const existingNumbers = existingEntities
.map((entity) => entity.name.match(pattern))
.filter((match) => match !== null)
.map((match) => Number.parseInt(match![1], 10))
// Find next available number (highest + 1, or 1 if none exist)
const nextNumber = existingNumbers.length > 0 ? Math.max(...existingNumbers) + 1 : 1
return `${prefix} ${nextNumber}`
}
/**
* Generates the next workspace name
*/
export async function generateWorkspaceName(): Promise<string> {
const response = await fetch('/api/workspaces')
const data = (await response.json()) as WorkspacesApiResponse
const workspaces = data.workspaces || []
return generateIncrementalName(workspaces, 'Workspace')
}
/**
* Generates the next folder name for a workspace
*/
export async function generateFolderName(workspaceId: string): Promise<string> {
const response = await fetch(`/api/folders?workspaceId=${workspaceId}`)
const data = (await response.json()) as FoldersApiResponse
const folders = data.folders || []
// Filter to only root-level folders (parentId is null)
const rootFolders = folders.filter((folder) => folder.parentId === null)
return generateIncrementalName(rootFolders, 'Folder')
}
/**
* Generates the next subfolder name for a parent folder
*/
export async function generateSubfolderName(
workspaceId: string,
parentFolderId: string
): Promise<string> {
const response = await fetch(`/api/folders?workspaceId=${workspaceId}`)
const data = (await response.json()) as FoldersApiResponse
const folders = data.folders || []
// Filter to only subfolders of the specified parent
const subfolders = folders.filter((folder) => folder.parentId === parentFolderId)
return generateIncrementalName(subfolders, 'Subfolder')
}
/**
* Generates a creative workflow name using random adjectives and nouns
* @returns A creative workflow name like "blazing-phoenix" or "crystal-dragon"
*/
export function generateCreativeWorkflowName(): string {
const adjective = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)]
const noun = NOUNS[Math.floor(Math.random() * NOUNS.length)]
return `${adjective.toLowerCase()}-${noun.toLowerCase()}`
}
+3 -3
View File
@@ -1,4 +1,4 @@
import { env } from '@/lib/env'
import { getEnv } from '@/lib/env'
import { isProd } from '@/lib/environment'
/**
@@ -10,7 +10,7 @@ export function getBaseUrl(): string {
return window.location.origin
}
const baseUrl = env.NEXT_PUBLIC_APP_URL
const baseUrl = getEnv('NEXT_PUBLIC_APP_URL')
if (baseUrl) {
if (baseUrl.startsWith('http://') || baseUrl.startsWith('https://')) {
return baseUrl
@@ -32,7 +32,7 @@ export function getBaseDomain(): string {
const url = new URL(getBaseUrl())
return url.host // host includes port if specified
} catch (_e) {
const fallbackUrl = env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
const fallbackUrl = getEnv('NEXT_PUBLIC_APP_URL') || 'http://localhost:3000'
try {
return new URL(fallbackUrl).host
} catch {
+2 -2
View File
@@ -1,6 +1,6 @@
import { eq } from 'drizzle-orm'
import { NextResponse } from 'next/server'
import { env } from '@/lib/env'
import { getEnv } from '@/lib/env'
import { createLogger } from '@/lib/logs/console-logger'
import { db } from '@/db'
import { userStats, workflow as workflowTable } from '@/db/schema'
@@ -24,7 +24,7 @@ export async function updateWorkflowRunCounts(workflowId: string, runs = 1) {
// Get the origin from the environment or use direct DB update as fallback
const origin =
env.NEXT_PUBLIC_APP_URL || (typeof window !== 'undefined' ? window.location.origin : '')
getEnv('NEXT_PUBLIC_APP_URL') || (typeof window !== 'undefined' ? window.location.origin : '')
if (origin) {
// Use absolute URL with origin
+4 -3
View File
@@ -1,13 +1,14 @@
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
import { createLogger } from '@/lib/logs/console-logger'
import { generateCreativeWorkflowName } from '@/lib/naming'
import { clearWorkflowVariablesTracking } from '@/stores/panel/variables/store'
import { API_ENDPOINTS } from '../../constants'
import { useSubBlockStore } from '../subblock/store'
import { useWorkflowStore } from '../workflow/store'
import type { BlockState } from '../workflow/types'
import type { DeploymentStatus, WorkflowMetadata, WorkflowRegistry } from './types'
import { generateUniqueName, getNextWorkflowColor } from './utils'
import { getNextWorkflowColor } from './utils'
const logger = createLogger('WorkflowRegistry')
@@ -568,7 +569,7 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: options.name || generateUniqueName(),
name: options.name || generateCreativeWorkflowName(),
description: options.description || 'New workflow',
color: options.marketplaceId ? '#808080' : getNextWorkflowColor(),
workspaceId,
@@ -829,7 +830,7 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
// Generate workflow metadata with marketplace properties
const newWorkflow: WorkflowMetadata = {
id,
name: metadata.name || generateUniqueName(),
name: metadata.name || generateCreativeWorkflowName(),
lastModified: new Date(),
description: metadata.description || 'Imported from marketplace',
color: metadata.color || getNextWorkflowColor(),
-150
View File
@@ -71,156 +71,6 @@ export const WORKFLOW_COLORS = [
'#DF9F00', // Dark Amber
]
// Random adjectives and nouns for generating creative workflow names
const ADJECTIVES = [
'Blazing',
'Crystal',
'Golden',
'Silver',
'Mystic',
'Cosmic',
'Electric',
'Frozen',
'Burning',
'Shining',
'Dancing',
'Flying',
'Roaring',
'Whispering',
'Glowing',
'Sparkling',
'Thunder',
'Lightning',
'Storm',
'Ocean',
'Mountain',
'Forest',
'Desert',
'Arctic',
'Tropical',
'Midnight',
'Dawn',
'Sunset',
'Rainbow',
'Diamond',
'Ruby',
'Emerald',
'Sapphire',
'Pearl',
'Jade',
'Amber',
'Coral',
'Ivory',
'Obsidian',
'Marble',
'Velvet',
'Silk',
'Satin',
'Linen',
'Cotton',
'Wool',
'Cashmere',
'Denim',
'Neon',
'Pastel',
'Vibrant',
'Muted',
'Bold',
'Subtle',
'Bright',
'Dark',
]
const NOUNS = [
'Phoenix',
'Dragon',
'Eagle',
'Wolf',
'Lion',
'Tiger',
'Panther',
'Falcon',
'Hawk',
'Raven',
'Swan',
'Dove',
'Butterfly',
'Firefly',
'Dragonfly',
'Hummingbird',
'Galaxy',
'Nebula',
'Comet',
'Meteor',
'Star',
'Moon',
'Sun',
'Planet',
'Asteroid',
'Constellation',
'Aurora',
'Eclipse',
'Solstice',
'Equinox',
'Horizon',
'Zenith',
'Castle',
'Tower',
'Bridge',
'Garden',
'Fountain',
'Palace',
'Temple',
'Cathedral',
'Lighthouse',
'Windmill',
'Waterfall',
'Canyon',
'Valley',
'Peak',
'Ridge',
'Cliff',
'Ocean',
'River',
'Lake',
'Stream',
'Pond',
'Bay',
'Cove',
'Harbor',
'Island',
'Peninsula',
'Archipelago',
'Atoll',
'Reef',
'Lagoon',
'Fjord',
'Delta',
'Cake',
'Cookie',
'Muffin',
'Cupcake',
'Pie',
'Tart',
'Brownie',
'Donut',
'Pancake',
'Waffle',
'Croissant',
'Bagel',
'Pretzel',
'Biscuit',
'Scone',
'Crumpet',
]
// Generates a random name for a new workflow
export function generateUniqueName(): string {
const adjective = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)]
const noun = NOUNS[Math.floor(Math.random() * NOUNS.length)]
return `${adjective.toLowerCase()}-${noun.toLowerCase()}`
}
// Generates a random color for a new workflow
export function getNextWorkflowColor(): string {
// Simply return a random color from the available colors
+2 -2
View File
@@ -1,4 +1,4 @@
import { env } from '@/lib/env'
import { getEnv } from '@/lib/env'
import { isTest } from '@/lib/environment'
import { createLogger } from '@/lib/logs/console-logger'
import { getBaseUrl } from '@/lib/urls/utils'
@@ -15,7 +15,7 @@ const getReferer = (): string => {
try {
return getBaseUrl()
} catch (_error) {
return env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
return getEnv('NEXT_PUBLIC_APP_URL') || 'http://localhost:3000'
}
}