Skip to content

Commit 5ba7a7e

Browse files
TheodoreSpeaksTheodore Li
andauthored
fix(resource) handle resource deletion deletion (#3568)
* Add handle dragging tab to input chat * Add back delete tools * Handle deletions properly with resources view * Fix lint * Add permisssions checking * Skip resource_added event when resource is deleted * Pass workflow id as context --------- Co-authored-by: Theodore Li <theo@sim.ai>
1 parent 0de4f73 commit 5ba7a7e

File tree

23 files changed

+569
-66
lines changed

23 files changed

+569
-66
lines changed

apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { lazy, Suspense, useCallback, useEffect, useMemo } from 'react'
44
import { Square } from 'lucide-react'
55
import { useRouter } from 'next/navigation'
66
import { Button, PlayOutline, Skeleton, Tooltip } from '@/components/emcn'
7-
import { BookOpen, SquareArrowUpRight } from '@/components/emcn/icons'
7+
import { BookOpen, FileX, SquareArrowUpRight, WorkflowX } from '@/components/emcn/icons'
88
import {
99
markRunToolManuallyStopped,
1010
reportManualRunToolStop,
@@ -62,9 +62,7 @@ export function ResourceContent({ workspaceId, resource, previewMode }: Resource
6262

6363
case 'workflow':
6464
return (
65-
<Suspense fallback={LOADING_SKELETON}>
66-
<Workflow key={resource.id} workspaceId={workspaceId} workflowId={resource.id} embedded />
67-
</Suspense>
65+
<EmbeddedWorkflow key={resource.id} workspaceId={workspaceId} workflowId={resource.id} />
6866
)
6967

7068
case 'knowledgebase':
@@ -228,6 +226,43 @@ export function EmbeddedKnowledgeBaseActions({
228226
)
229227
}
230228

229+
interface EmbeddedWorkflowProps {
230+
workspaceId: string
231+
workflowId: string
232+
}
233+
234+
function EmbeddedWorkflow({ workspaceId, workflowId }: EmbeddedWorkflowProps) {
235+
const workflowExists = useWorkflowRegistry((state) => Boolean(state.workflows[workflowId]))
236+
const hydrationPhase = useWorkflowRegistry((state) => state.hydration.phase)
237+
const hydrationWorkflowId = useWorkflowRegistry((state) => state.hydration.workflowId)
238+
const isMetadataLoaded = hydrationPhase !== 'idle' && hydrationPhase !== 'metadata-loading'
239+
const hasLoadError = hydrationPhase === 'error' && hydrationWorkflowId === workflowId
240+
241+
if (!isMetadataLoaded) return LOADING_SKELETON
242+
243+
if (!workflowExists || hasLoadError) {
244+
return (
245+
<div className='flex h-full flex-col items-center justify-center gap-[12px]'>
246+
<WorkflowX className='h-[32px] w-[32px] text-[var(--text-muted)]' />
247+
<div className='flex flex-col items-center gap-[4px]'>
248+
<h2 className='font-medium text-[20px] text-[var(--text-secondary)]'>
249+
Workflow not found
250+
</h2>
251+
<p className='text-[13px] text-[var(--text-muted)]'>
252+
This workflow may have been deleted or moved
253+
</p>
254+
</div>
255+
</div>
256+
)
257+
}
258+
259+
return (
260+
<Suspense fallback={LOADING_SKELETON}>
261+
<Workflow workspaceId={workspaceId} workflowId={workflowId} embedded />
262+
</Suspense>
263+
)
264+
}
265+
231266
interface EmbeddedFileProps {
232267
workspaceId: string
233268
fileId: string
@@ -242,8 +277,14 @@ function EmbeddedFile({ workspaceId, fileId, previewMode }: EmbeddedFileProps) {
242277

243278
if (!file) {
244279
return (
245-
<div className='flex h-full items-center justify-center'>
246-
<span className='text-[13px] text-[var(--text-muted)]'>File not found</span>
280+
<div className='flex h-full flex-col items-center justify-center gap-[12px]'>
281+
<FileX className='h-[32px] w-[32px] text-[var(--text-muted)]' />
282+
<div className='flex flex-col items-center gap-[4px]'>
283+
<h2 className='font-medium text-[20px] text-[var(--text-secondary)]'>File not found</h2>
284+
<p className='text-[13px] text-[var(--text-muted)]'>
285+
This file may have been deleted or moved
286+
</p>
287+
</div>
247288
</div>
248289
)
249290
}

apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-tabs/resource-tabs.tsx

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -160,12 +160,22 @@ export function ResourceTabs({
160160
[chatId, onRemoveResource]
161161
)
162162

163-
const handleDragStart = useCallback((e: React.DragEvent, idx: number) => {
164-
dragStartIdx.current = idx
165-
setDraggedIdx(idx)
166-
e.dataTransfer.effectAllowed = 'move'
167-
e.dataTransfer.setData('text/plain', String(idx))
168-
}, [])
163+
const handleDragStart = useCallback(
164+
(e: React.DragEvent, idx: number) => {
165+
dragStartIdx.current = idx
166+
setDraggedIdx(idx)
167+
e.dataTransfer.effectAllowed = 'copyMove'
168+
e.dataTransfer.setData('text/plain', String(idx))
169+
const resource = resources[idx]
170+
if (resource) {
171+
e.dataTransfer.setData(
172+
'application/x-sim-resource',
173+
JSON.stringify({ type: resource.type, id: resource.id, title: resource.title })
174+
)
175+
}
176+
},
177+
[resources]
178+
)
169179

170180
const handleDragOver = useCallback((e: React.DragEvent, idx: number) => {
171181
e.preventDefault()

apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -723,6 +723,38 @@ export function UserInput({
723723
[textareaRef, value, handleContextAdd, mentionMenu]
724724
)
725725

726+
const handleContainerDragOver = useCallback(
727+
(e: React.DragEvent) => {
728+
if (e.dataTransfer.types.includes('application/x-sim-resource')) {
729+
e.preventDefault()
730+
e.stopPropagation()
731+
e.dataTransfer.dropEffect = 'copy'
732+
return
733+
}
734+
files.handleDragOver(e)
735+
},
736+
[files]
737+
)
738+
739+
const handleContainerDrop = useCallback(
740+
(e: React.DragEvent) => {
741+
const resourceJson = e.dataTransfer.getData('application/x-sim-resource')
742+
if (resourceJson) {
743+
e.preventDefault()
744+
e.stopPropagation()
745+
try {
746+
const resource = JSON.parse(resourceJson) as MothershipResource
747+
handleResourceSelect(resource, false)
748+
} catch {
749+
// Invalid JSON — ignore
750+
}
751+
return
752+
}
753+
files.handleDrop(e)
754+
},
755+
[handleResourceSelect, files]
756+
)
757+
726758
useEffect(() => {
727759
if (wasSendingRef.current && !isSending) {
728760
textareaRef.current?.focus()
@@ -1006,8 +1038,8 @@ export function UserInput({
10061038
)}
10071039
onDragEnter={files.handleDragEnter}
10081040
onDragLeave={files.handleDragLeave}
1009-
onDragOver={files.handleDragOver}
1010-
onDrop={files.handleDrop}
1041+
onDragOver={handleContainerDragOver}
1042+
onDrop={handleContainerDrop}
10111043
>
10121044
{/* Context pills row */}
10131045
{contextManagement.selectedContexts.length > 0 && (

apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export interface UseChatReturn {
5555
resources: MothershipResource[]
5656
activeResourceId: string | null
5757
setActiveResourceId: (id: string | null) => void
58-
addResource: (resource: MothershipResource) => void
58+
addResource: (resource: MothershipResource) => boolean
5959
removeResource: (resourceType: MothershipResourceType, resourceId: string) => void
6060
reorderResources: (resources: MothershipResource[]) => void
6161
}
@@ -266,15 +266,18 @@ export function useChat(
266266

267267
const { data: chatHistory } = useChatHistory(initialChatId)
268268

269-
const addResource = useCallback((resource: MothershipResource) => {
269+
const addResource = useCallback((resource: MothershipResource): boolean => {
270+
if (resourcesRef.current.some((r) => r.type === resource.type && r.id === resource.id)) {
271+
return false
272+
}
273+
270274
setResources((prev) => {
271275
const exists = prev.some((r) => r.type === resource.type && r.id === resource.id)
272276
if (exists) return prev
273277
return [...prev, resource]
274278
})
275279
setActiveResourceId(resource.id)
276280

277-
// Persist to database if we have a chat ID
278281
const currentChatId = chatIdRef.current
279282
if (currentChatId) {
280283
fetch('/api/copilot/chat/resources', {
@@ -285,6 +288,7 @@ export function useChat(
285288
logger.warn('Failed to persist resource', err)
286289
})
287290
}
291+
return true
288292
}, [])
289293

290294
const removeResource = useCallback((resourceType: MothershipResourceType, resourceId: string) => {
@@ -552,7 +556,6 @@ export function useChat(
552556
)
553557
if (resource) {
554558
addResource(resource)
555-
invalidateResourceQueries(queryClient, workspaceId, resource.type, resource.id)
556559
onResourceEventRef.current?.()
557560
}
558561
}
@@ -565,6 +568,7 @@ export function useChat(
565568
if (resource?.type && resource?.id) {
566569
addResource(resource)
567570
invalidateResourceQueries(queryClient, workspaceId, resource.type, resource.id)
571+
568572
onResourceEventRef.current?.()
569573
if (resource.type === 'workflow') {
570574
if (ensureWorkflowInRegistry(resource.id, resource.title, workspaceId)) {
@@ -576,6 +580,20 @@ export function useChat(
576580
}
577581
break
578582
}
583+
case 'resource_deleted': {
584+
const resource = parsed.resource
585+
if (resource?.type && resource?.id) {
586+
removeResource(resource.type as MothershipResourceType, resource.id)
587+
invalidateResourceQueries(
588+
queryClient,
589+
workspaceId,
590+
resource.type as MothershipResourceType,
591+
resource.id
592+
)
593+
onResourceEventRef.current?.()
594+
}
595+
break
596+
}
579597
case 'tool_error': {
580598
const id = parsed.toolCallId || getPayloadData(parsed)?.id
581599
if (!id) break
@@ -612,7 +630,7 @@ export function useChat(
612630
}
613631
}
614632
},
615-
[workspaceId, queryClient, addResource]
633+
[workspaceId, queryClient, addResource, removeResource]
616634
)
617635

618636
const persistPartialResponse = useCallback(async () => {

apps/sim/app/workspace/[workspaceId]/home/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export type SSEEventType =
2323
| 'tool_result'
2424
| 'tool_error'
2525
| 'resource_added'
26+
| 'resource_deleted'
2627
| 'subagent_start'
2728
| 'subagent_end'
2829
| 'structured_result'

apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
Tooltip,
2222
Trash,
2323
} from '@/components/emcn'
24-
import { Database } from '@/components/emcn/icons'
24+
import { Database, DatabaseX } from '@/components/emcn/icons'
2525
import { SearchHighlight } from '@/components/ui/search-highlight'
2626
import { cn } from '@/lib/core/utils/cn'
2727
import { ALL_TAG_SLOTS, type AllTagSlot, getFieldTypeForSlot } from '@/lib/knowledge/constants'
@@ -1029,20 +1029,17 @@ export function KnowledgeBase({
10291029

10301030
if (error && !knowledgeBase) {
10311031
return (
1032-
<Resource
1033-
icon={Database}
1034-
title='Knowledge Base'
1035-
breadcrumbs={[
1036-
{
1037-
label: 'Knowledge Base',
1038-
onClick: () => router.push(`/workspace/${workspaceId}/knowledge`),
1039-
},
1040-
{ label: knowledgeBaseName },
1041-
]}
1042-
columns={DOCUMENT_COLUMNS}
1043-
rows={[]}
1044-
emptyMessage='Error loading knowledge base'
1045-
/>
1032+
<div className='flex h-full flex-col items-center justify-center gap-[12px]'>
1033+
<DatabaseX className='h-[32px] w-[32px] text-[var(--text-muted)]' />
1034+
<div className='flex flex-col items-center gap-[4px]'>
1035+
<h2 className='font-medium text-[20px] text-[var(--text-secondary)]'>
1036+
Knowledge base not found
1037+
</h2>
1038+
<p className='text-[13px] text-[var(--text-muted)]'>
1039+
This knowledge base may have been deleted or moved
1040+
</p>
1041+
</div>
1042+
</div>
10461043
)
10471044
}
10481045

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
Pencil,
3131
Plus,
3232
Table as TableIcon,
33+
TableX,
3334
Trash,
3435
TypeBoolean,
3536
TypeJson,
@@ -1231,8 +1232,14 @@ export function Table({
12311232

12321233
if (!isLoadingTable && !tableData) {
12331234
return (
1234-
<div className='flex h-full items-center justify-center'>
1235-
<span className='text-[13px] text-[var(--text-error)]'>Table not found</span>
1235+
<div className='flex h-full flex-col items-center justify-center gap-[12px]'>
1236+
<TableX className='h-[32px] w-[32px] text-[var(--text-muted)]' />
1237+
<div className='flex flex-col items-center gap-[4px]'>
1238+
<h2 className='font-medium text-[20px] text-[var(--text-secondary)]'>Table not found</h2>
1239+
<p className='text-[13px] text-[var(--text-muted)]'>
1240+
This table may have been deleted or moved
1241+
</p>
1242+
</div>
12361243
</div>
12371244
)
12381245
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import type { SVGProps } from 'react'
2+
3+
/**
4+
* Database-X icon component - cylinder database with an X mark indicating a missing or deleted knowledge base
5+
* @param props - SVG properties including className, fill, etc.
6+
*/
7+
export function DatabaseX(props: SVGProps<SVGSVGElement>) {
8+
return (
9+
<svg
10+
width='24'
11+
height='24'
12+
viewBox='-1 -2 24 24'
13+
fill='none'
14+
stroke='currentColor'
15+
strokeWidth='1.75'
16+
strokeLinecap='round'
17+
strokeLinejoin='round'
18+
xmlns='http://www.w3.org/2000/svg'
19+
{...props}
20+
>
21+
<ellipse cx='10.25' cy='3.75' rx='8.5' ry='3' />
22+
<path d='M1.75 3.75V9.75C1.75 11.41 5.55 12.75 10.25 12.75C14.95 12.75 18.75 11.41 18.75 9.75V3.75' />
23+
<path d='M1.75 9.75V12.5' />
24+
<path d='M18.75 9.75V15.75C18.75 17.41 14.95 18.75 10.25 18.75C9 18.75 7.75 18.6 6.75 18.3' />
25+
<path d='M1 16L5 20' />
26+
<path d='M5 16L1 20' />
27+
</svg>
28+
)
29+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type { SVGProps } from 'react'
2+
3+
/**
4+
* File-X icon component - document with an X mark indicating a missing or deleted file
5+
* @param props - SVG properties including className, fill, etc.
6+
*/
7+
export function FileX(props: SVGProps<SVGSVGElement>) {
8+
return (
9+
<svg
10+
width='24'
11+
height='24'
12+
viewBox='-1 -2 24 24'
13+
fill='none'
14+
stroke='currentColor'
15+
strokeWidth='1.75'
16+
strokeLinecap='round'
17+
strokeLinejoin='round'
18+
xmlns='http://www.w3.org/2000/svg'
19+
{...props}
20+
>
21+
<path d='M3.25 12.5V2.75C3.25 1.64543 4.14543 0.75 5.25 0.75H12.25L17.25 5.75V16.75C17.25 17.8546 16.3546 18.75 15.25 18.75H9.5' />
22+
<path d='M12.25 0.75V5.75H17.25' />
23+
<path d='M3.25 15L7.25 19' />
24+
<path d='M7.25 15L3.25 19' />
25+
</svg>
26+
)
27+
}

0 commit comments

Comments
 (0)