11'use client'
22
33import { useMemo , useState } from 'react'
4- import { Loader2 , Search } from 'lucide-react'
4+ import { Search } from 'lucide-react'
55import { useParams , useRouter } from 'next/navigation'
6- import {
7- Button ,
8- Check ,
9- SModalTabs ,
10- SModalTabsList ,
11- SModalTabsTrigger ,
12- toast ,
13- } from '@/components/emcn'
6+ import { Button , SModalTabs , SModalTabsList , SModalTabsTrigger } from '@/components/emcn'
147import { Input } from '@/components/ui'
158import { formatDate } from '@/lib/core/utils/formatting'
169import { RESOURCE_REGISTRY } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry'
1710import type { MothershipResourceType } from '@/app/workspace/[workspaceId]/home/types'
11+ import { DeletedItemSkeleton } from '@/app/workspace/[workspaceId]/settings/components/recently-deleted/deleted-item-skeleton'
1812import { useKnowledgeBasesQuery , useRestoreKnowledgeBase } from '@/hooks/queries/kb/knowledge'
1913import { useRestoreTable , useTablesList } from '@/hooks/queries/tables'
2014import { useRestoreWorkflow , useWorkflows } from '@/hooks/queries/workflows'
@@ -107,6 +101,7 @@ export function RecentlyDeleted() {
107101 const [ activeTab , setActiveTab ] = useState < ResourceType > ( 'all' )
108102 const [ searchTerm , setSearchTerm ] = useState ( '' )
109103 const [ restoringIds , setRestoringIds ] = useState < Set < string > > ( new Set ( ) )
104+ const [ restoredItems , setRestoredItems ] = useState < Map < string , DeletedResource > > ( new Map ( ) )
110105
111106 const workflowsQuery = useWorkflows ( workspaceId , { syncRegistry : false , scope : 'archived' } )
112107 const tablesQuery = useTablesList ( workspaceId , 'archived' )
@@ -124,6 +119,9 @@ export function RecentlyDeleted() {
124119 knowledgeQuery . isLoading ||
125120 filesQuery . isLoading
126121
122+ const error =
123+ workflowsQuery . error || tablesQuery . error || knowledgeQuery . error || filesQuery . error
124+
127125 const resources = useMemo < DeletedResource [ ] > ( ( ) => {
128126 const items : DeletedResource [ ] = [ ]
129127
@@ -168,9 +166,24 @@ export function RecentlyDeleted() {
168166 } )
169167 }
170168
169+ // Merge back restored items that are no longer in the query data
170+ const itemIds = new Set ( items . map ( ( i ) => i . id ) )
171+ for ( const [ id , resource ] of restoredItems ) {
172+ if ( ! itemIds . has ( id ) ) {
173+ items . push ( resource )
174+ }
175+ }
176+
171177 items . sort ( ( a , b ) => b . deletedAt . getTime ( ) - a . deletedAt . getTime ( ) )
172178 return items
173- } , [ workflowsQuery . data , tablesQuery . data , knowledgeQuery . data , filesQuery . data , workspaceId ] )
179+ } , [
180+ workflowsQuery . data ,
181+ tablesQuery . data ,
182+ knowledgeQuery . data ,
183+ filesQuery . data ,
184+ workspaceId ,
185+ restoredItems ,
186+ ] )
174187
175188 const filtered = useMemo ( ( ) => {
176189 let items = activeTab === 'all' ? resources : resources . filter ( ( r ) => r . type === activeTab )
@@ -195,42 +208,30 @@ export function RecentlyDeleted() {
195208 }
196209
197210 const onSuccess = ( ) => {
198- const href = getResourceHref ( resource . workspaceId , resource . type , resource . id )
199- toast . success ( `${ resource . name } restored` , {
200- icon : < Check className = 'h-[12px] w-[12px]' /> ,
201- action : { label : 'View' , onClick : ( ) => router . push ( href ) } ,
202- } )
203- }
204-
205- const onError = ( ) => {
206- toast . error ( `Failed to restore ${ resource . name } ` )
211+ setRestoredItems ( ( prev ) => new Map ( prev ) . set ( resource . id , resource ) )
207212 }
208213
209214 switch ( resource . type ) {
210215 case 'workflow' :
211- restoreWorkflow . mutate ( resource . id , { onSettled, onSuccess, onError } )
216+ restoreWorkflow . mutate ( resource . id , { onSettled, onSuccess } )
212217 break
213218 case 'table' :
214- restoreTable . mutate ( resource . id , { onSettled, onSuccess, onError } )
219+ restoreTable . mutate ( resource . id , { onSettled, onSuccess } )
215220 break
216221 case 'knowledge' :
217- restoreKnowledgeBase . mutate ( resource . id , { onSettled, onSuccess, onError } )
222+ restoreKnowledgeBase . mutate ( resource . id , { onSettled, onSuccess } )
218223 break
219224 case 'file' :
220225 restoreWorkspaceFile . mutate (
221226 { workspaceId : resource . workspaceId , fileId : resource . id } ,
222- { onSettled, onSuccess, onError }
227+ { onSettled, onSuccess }
223228 )
224229 break
225230 }
226231 }
227232
228233 return (
229- < div className = 'flex flex-col gap-[16px]' >
230- < p className = 'text-[13px] text-[var(--text-secondary)]' >
231- Items you delete are kept here for 30 days before being permanently removed.
232- </ p >
233-
234+ < div className = 'flex h-full flex-col gap-[18px]' >
234235 < div className = 'flex items-center gap-[8px] rounded-[8px] border border-[var(--border)] bg-transparent px-[8px] py-[5px] transition-colors duration-100 dark:bg-[var(--surface-4)] dark:hover:border-[var(--border-1)] dark:hover:bg-[var(--surface-5)]' >
235236 < Search
236237 className = 'h-[14px] w-[14px] flex-shrink-0 text-[var(--text-tertiary)]'
@@ -246,7 +247,7 @@ export function RecentlyDeleted() {
246247 </ div >
247248
248249 < SModalTabs value = { activeTab } onValueChange = { ( v ) => setActiveTab ( v as ResourceType ) } >
249- < SModalTabsList activeValue = { activeTab } className = 'border-[var(--border)] border-b ' >
250+ < SModalTabsList activeValue = { activeTab } className = 'border-b border- [var(--border)]' >
250251 { TABS . map ( ( tab ) => (
251252 < SModalTabsTrigger key = { tab . id } value = { tab . id } >
252253 { tab . label }
@@ -255,55 +256,81 @@ export function RecentlyDeleted() {
255256 </ SModalTabsList >
256257 </ SModalTabs >
257258
258- { isLoading ? (
259- < div className = 'flex items-center justify-center py-[48px]' >
260- < Loader2 className = 'h-5 w-5 animate-spin text-[var(--text-tertiary)]' />
261- </ div >
262- ) : filtered . length === 0 ? (
263- < div className = 'flex flex-col items-center justify-center py-[48px] text-[var(--text-tertiary)]' >
264- < p className = 'text-[13px]' >
259+ < div className = 'min-h-0 flex-1 overflow-y-auto' >
260+ { error ? (
261+ < div className = 'flex h-full flex-col items-center justify-center gap-[8px]' >
262+ < p className = 'text-[11px] leading-tight text-[#DC2626] dark:text-[#F87171]' >
263+ { error instanceof Error ? error . message : 'Failed to load deleted items' }
264+ </ p >
265+ </ div >
266+ ) : isLoading ? (
267+ < div className = 'flex flex-col gap-[8px]' >
268+ < DeletedItemSkeleton />
269+ < DeletedItemSkeleton />
270+ < DeletedItemSkeleton />
271+ </ div >
272+ ) : filtered . length === 0 ? (
273+ < div className = 'flex h-full items-center justify-center text-[14px] text-[var(--text-muted)]' >
265274 { showNoResults
266275 ? `No items found matching \u201c${ searchTerm } \u201d`
267276 : 'No deleted items' }
268- </ p >
269- </ div >
270- ) : (
271- < div className = 'flex flex-col' >
272- { filtered . map ( ( resource ) => {
273- const isRestoring = restoringIds . has ( resource . id )
274-
275- return (
276- < div
277- key = { resource . id }
278- className = 'flex items-center gap-[12px] rounded-[6px] px-[8px] py-[8px] hover:bg-[var(--bg-hover)]'
279- >
280- < ResourceIcon resource = { resource } />
281-
282- < div className = 'flex min-w-0 flex-1 flex-col' >
283- < span className = 'truncate font-medium text-[13px] text-[var(--text-primary)]' >
284- { resource . name }
285- </ span >
286- < span className = 'text-[12px] text-[var(--text-tertiary)]' >
287- { TYPE_LABEL [ resource . type ] }
288- { ' \u00b7 ' }
289- Deleted { formatDate ( resource . deletedAt ) }
290- </ span >
291- </ div >
292-
293- < Button
294- variant = 'default'
295- size = 'sm'
296- disabled = { isRestoring }
297- onClick = { ( ) => handleRestore ( resource ) }
298- className = 'shrink-0'
277+ </ div >
278+ ) : (
279+ < div className = 'flex flex-col gap-[8px]' >
280+ { filtered . map ( ( resource ) => {
281+ const isRestoring = restoringIds . has ( resource . id )
282+ const isRestored = restoredItems . has ( resource . id )
283+
284+ return (
285+ < div
286+ key = { resource . id }
287+ className = 'flex items-center gap-[12px] rounded-[6px] px-[8px] py-[8px] hover:bg-[var(--bg-hover)]'
299288 >
300- { isRestoring ? < Loader2 className = 'h-3.5 w-3.5 animate-spin' /> : 'Restore' }
301- </ Button >
302- </ div >
303- )
304- } ) }
305- </ div >
306- ) }
289+ < ResourceIcon resource = { resource } />
290+
291+ < div className = 'flex flex-col min-w-0 flex-1' >
292+ < span className = 'text-[13px] font-medium text-[var(--text-primary)] truncate' >
293+ { resource . name }
294+ </ span >
295+ < span className = 'text-[12px] text-[var(--text-tertiary)]' >
296+ { TYPE_LABEL [ resource . type ] }
297+ { ' \u00b7 ' }
298+ Deleted { formatDate ( resource . deletedAt ) }
299+ </ span >
300+ </ div >
301+
302+ { isRestored ? (
303+ < div className = 'flex items-center gap-[8px] shrink-0' >
304+ < span className = 'text-[13px] text-[var(--text-tertiary)]' > Restored</ span >
305+ < Button
306+ variant = 'default'
307+ size = 'sm'
308+ onClick = { ( ) =>
309+ router . push (
310+ getResourceHref ( resource . workspaceId , resource . type , resource . id )
311+ )
312+ }
313+ >
314+ View
315+ </ Button >
316+ </ div >
317+ ) : (
318+ < Button
319+ variant = 'default'
320+ size = 'sm'
321+ disabled = { isRestoring }
322+ onClick = { ( ) => handleRestore ( resource ) }
323+ className = 'shrink-0'
324+ >
325+ { isRestoring ? 'Restoring...' : 'Restore' }
326+ </ Button >
327+ ) }
328+ </ div >
329+ )
330+ } ) }
331+ </ div >
332+ ) }
333+ </ div >
307334 </ div >
308335 )
309336}
0 commit comments