Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions apps/sim/app/api/folders/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,23 @@ export async function DELETE(
)
}

// Check if deleting this folder would delete the last workflow(s) in the workspace
const workflowsInFolder = await countWorkflowsInFolderRecursively(
id,
existingFolder.workspaceId
)
const totalWorkflowsInWorkspace = await db
.select({ id: workflow.id })
.from(workflow)
.where(eq(workflow.workspaceId, existingFolder.workspaceId))

if (workflowsInFolder > 0 && workflowsInFolder >= totalWorkflowsInWorkspace.length) {
return NextResponse.json(
{ error: 'Cannot delete folder containing the only workflow(s) in the workspace' },
{ status: 400 }
)
}

// Recursively delete folder and all its contents
const deletionStats = await deleteFolderRecursively(id, existingFolder.workspaceId)

Expand Down Expand Up @@ -202,6 +219,34 @@ async function deleteFolderRecursively(
return stats
}

/**
* Counts the number of workflows in a folder and all its subfolders recursively.
*/
async function countWorkflowsInFolderRecursively(
folderId: string,
workspaceId: string
): Promise<number> {
let count = 0

const workflowsInFolder = await db
.select({ id: workflow.id })
.from(workflow)
.where(and(eq(workflow.folderId, folderId), eq(workflow.workspaceId, workspaceId)))

count += workflowsInFolder.length

const childFolders = await db
.select({ id: workflowFolder.id })
.from(workflowFolder)
.where(and(eq(workflowFolder.parentId, folderId), eq(workflowFolder.workspaceId, workspaceId)))

for (const childFolder of childFolders) {
count += await countWorkflowsInFolderRecursively(childFolder.id, workspaceId)
}

return count
}

// Helper function to check for circular references
async function checkForCircularReference(folderId: string, parentId: string): Promise<boolean> {
let currentParentId: string | null = parentId
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -339,12 +339,31 @@ export function CreateBaseModal({
<div ref={scrollContainerRef} className='min-h-0 flex-1 overflow-y-auto'>
<div className='space-y-[12px]'>
<div className='flex flex-col gap-[8px]'>
<Label htmlFor='name'>Name</Label>
<Label htmlFor='kb-name'>Name</Label>
{/* Hidden decoy fields to prevent browser autofill */}
<input
type='text'
name='fakeusernameremembered'
autoComplete='username'
style={{
position: 'absolute',
left: '-9999px',
opacity: 0,
pointerEvents: 'none',
}}
tabIndex={-1}
readOnly
/>
<Input
id='name'
id='kb-name'
placeholder='Enter knowledge base name'
{...register('name')}
className={cn(errors.name && 'border-[var(--text-error)]')}
autoComplete='off'
autoCorrect='off'
autoCapitalize='off'
data-lpignore='true'
data-form-type='other'
/>
</div>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,20 @@ export function ApiKeys({ onOpenChange, registerCloseHandler }: ApiKeysProps) {
<p className='font-medium text-[13px] text-[var(--text-secondary)]'>
Enter a name for your API key to help you identify it later.
</p>
{/* Hidden decoy fields to prevent browser autofill */}
<input
type='text'
name='fakeusernameremembered'
autoComplete='username'
style={{
position: 'absolute',
left: '-9999px',
opacity: 0,
pointerEvents: 'none',
}}
tabIndex={-1}
readOnly
/>
<EmcnInput
value={newKeyName}
onChange={(e) => {
Expand All @@ -499,6 +513,12 @@ export function ApiKeys({ onOpenChange, registerCloseHandler }: ApiKeysProps) {
placeholder='e.g., Development, Production'
className='h-9'
autoFocus
name='api_key_label'
autoComplete='off'
autoCorrect='off'
autoCapitalize='off'
data-lpignore='true'
data-form-type='other'
/>
{createError && (
<p className='text-[11px] text-[var(--text-error)] leading-tight'>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,12 +141,37 @@ export function MemberInvitationCard({
{/* Main invitation input */}
<div className='flex items-start gap-2'>
<div className='flex-1'>
{/* Hidden decoy fields to prevent browser autofill */}
<input
type='text'
name='fakeusernameremembered'
autoComplete='username'
style={{ position: 'absolute', left: '-9999px', opacity: 0, pointerEvents: 'none' }}
tabIndex={-1}
readOnly
/>
<input
type='email'
name='fakeemailremembered'
autoComplete='email'
style={{ position: 'absolute', left: '-9999px', opacity: 0, pointerEvents: 'none' }}
tabIndex={-1}
readOnly
/>
<Input
placeholder='Enter email address'
value={inviteEmail}
onChange={handleEmailChange}
disabled={isInviting || !hasAvailableSeats}
className={cn(emailError && 'border-red-500 focus-visible:ring-red-500')}
name='member_invite_field'
autoComplete='off'
autoCorrect='off'
autoCapitalize='off'
spellCheck={false}
data-lpignore='true'
data-form-type='other'
aria-autocomplete='none'
/>
{emailError && (
<p className='mt-1 text-[#DC2626] text-[11px] leading-tight dark:text-[#F87171]'>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,16 +55,31 @@ export function NoOrganizationView({

{/* Form fields - clean layout without card */}
<div className='space-y-4'>
{/* Hidden decoy field to prevent browser autofill */}
<input
type='text'
name='fakeusernameremembered'
autoComplete='username'
style={{ position: 'absolute', left: '-9999px', opacity: 0, pointerEvents: 'none' }}
tabIndex={-1}
readOnly
/>
<div>
<Label htmlFor='orgName' className='font-medium text-[13px]'>
<Label htmlFor='team-name-field' className='font-medium text-[13px]'>
Team Name
</Label>
<Input
id='orgName'
id='team-name-field'
value={orgName}
onChange={onOrgNameChange}
placeholder='My Team'
className='mt-1'
name='team_name_field'
autoComplete='off'
autoCorrect='off'
autoCapitalize='off'
data-lpignore='true'
data-form-type='other'
/>
</div>

Expand Down Expand Up @@ -116,31 +131,52 @@ export function NoOrganizationView({
</ModalHeader>

<div className='space-y-4'>
{/* Hidden decoy field to prevent browser autofill */}
<input
type='text'
name='fakeusernameremembered'
autoComplete='username'
style={{ position: 'absolute', left: '-9999px', opacity: 0, pointerEvents: 'none' }}
tabIndex={-1}
readOnly
/>
<div>
<Label htmlFor='org-name' className='font-medium text-[13px]'>
<Label htmlFor='org-name-field' className='font-medium text-[13px]'>
Organization Name
</Label>
<Input
id='org-name'
id='org-name-field'
placeholder='Enter organization name'
value={orgName}
onChange={onOrgNameChange}
disabled={isCreatingOrg}
className='mt-1'
name='org_name_field'
autoComplete='off'
autoCorrect='off'
autoCapitalize='off'
data-lpignore='true'
data-form-type='other'
/>
</div>

<div>
<Label htmlFor='org-slug' className='font-medium text-[13px]'>
<Label htmlFor='org-slug-field' className='font-medium text-[13px]'>
Organization Slug
</Label>
<Input
id='org-slug'
id='org-slug-field'
placeholder='organization-slug'
value={orgSlug}
onChange={(e) => setOrgSlug(e.target.value)}
disabled={isCreatingOrg}
className='mt-1'
name='org_slug_field'
autoComplete='off'
autoCorrect='off'
autoCapitalize='off'
data-lpignore='true'
data-form-type='other'
/>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -390,11 +390,26 @@ export function TemplateProfile() {
disabled={isUploadingProfilePicture}
/>
</div>
{/* Hidden decoy field to prevent browser autofill */}
<input
type='text'
name='fakeusernameremembered'
autoComplete='username'
style={{ position: 'absolute', left: '-9999px', opacity: 0, pointerEvents: 'none' }}
tabIndex={-1}
readOnly
/>
<Input
placeholder='Name'
value={formData.name}
onChange={(e) => updateField('name', e.target.value)}
className='h-9 flex-1'
name='profile_display_name'
autoComplete='off'
autoCorrect='off'
autoCapitalize='off'
data-lpignore='true'
data-form-type='other'
/>
</div>
{uploadError && <p className='text-[12px] text-[var(--text-error)]'>{uploadError}</p>}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'

import { useCallback, useState } from 'react'
import { useCallback, useMemo, useState } from 'react'
import clsx from 'clsx'
import { ChevronRight, Folder, FolderOpen } from 'lucide-react'
import { useParams, useRouter } from 'next/navigation'
Expand All @@ -15,7 +15,11 @@ import {
useItemRename,
} from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks'
import { SIDEBAR_SCROLL_EVENT } from '@/app/workspace/[workspaceId]/w/components/sidebar/sidebar'
import { useDeleteFolder, useDuplicateFolder } from '@/app/workspace/[workspaceId]/w/hooks'
import {
useCanDelete,
useDeleteFolder,
useDuplicateFolder,
} from '@/app/workspace/[workspaceId]/w/hooks'
import { useCreateFolder, useUpdateFolder } from '@/hooks/queries/folders'
import { useCreateWorkflow } from '@/hooks/queries/workflows'
import type { FolderTreeNode } from '@/stores/folders/store'
Expand Down Expand Up @@ -52,6 +56,9 @@ export function FolderItem({ folder, level, hoverHandlers }: FolderItemProps) {
const createFolderMutation = useCreateFolder()
const userPermissions = useUserPermissionsContext()

const { canDeleteFolder } = useCanDelete({ workspaceId })
const canDelete = useMemo(() => canDeleteFolder(folder.id), [canDeleteFolder, folder.id])

// Delete modal state
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false)

Expand Down Expand Up @@ -316,7 +323,7 @@ export function FolderItem({ folder, level, hoverHandlers }: FolderItemProps) {
disableCreate={!userPermissions.canEdit || createWorkflowMutation.isPending}
disableCreateFolder={!userPermissions.canEdit || createFolderMutation.isPending}
disableDuplicate={!userPermissions.canEdit}
disableDelete={!userPermissions.canEdit}
disableDelete={!userPermissions.canEdit || !canDelete}
/>

{/* Delete Modal */}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
useItemRename,
} from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks'
import {
useCanDelete,
useDeleteWorkflow,
useDuplicateWorkflow,
useExportWorkflow,
Expand Down Expand Up @@ -44,10 +45,14 @@ export function WorkflowItem({ workflow, active, level, onWorkflowClick }: Workf
const userPermissions = useUserPermissionsContext()
const isSelected = selectedWorkflows.has(workflow.id)

// Can delete check hook
const { canDeleteWorkflows } = useCanDelete({ workspaceId })

// Delete modal state
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false)
const [workflowIdsToDelete, setWorkflowIdsToDelete] = useState<string[]>([])
const [deleteModalNames, setDeleteModalNames] = useState<string | string[]>('')
const [canDeleteCaptured, setCanDeleteCaptured] = useState(true)

// Presence avatars state
const [hasAvatars, setHasAvatars] = useState(false)
Expand Down Expand Up @@ -172,10 +177,13 @@ export function WorkflowItem({ workflow, active, level, onWorkflowClick }: Workf
workflowNames: workflowNames.length > 1 ? workflowNames : workflowNames[0],
}

// Check if the captured selection can be deleted
setCanDeleteCaptured(canDeleteWorkflows(workflowIds))

// If already selected with multiple selections, keep all selections
handleContextMenuBase(e)
},
[workflow.id, workflows, handleContextMenuBase]
[workflow.id, workflows, handleContextMenuBase, canDeleteWorkflows]
)

// Rename hook
Expand Down Expand Up @@ -319,7 +327,7 @@ export function WorkflowItem({ workflow, active, level, onWorkflowClick }: Workf
disableRename={!userPermissions.canEdit}
disableDuplicate={!userPermissions.canEdit}
disableExport={!userPermissions.canEdit}
disableDelete={!userPermissions.canEdit}
disableDelete={!userPermissions.canEdit || !canDeleteCaptured}
/>

{/* Delete Confirmation Modal */}
Expand Down
Loading
Loading