Skip to content

Commit ed7ac93

Browse files
committed
reactquery best practices, UI alignment in restore
1 parent 709f91f commit ed7ac93

File tree

4 files changed

+122
-76
lines changed

4 files changed

+122
-76
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Skeleton } from '@/components/emcn'
2+
3+
/**
4+
* Skeleton component for recently deleted list items.
5+
*/
6+
export function DeletedItemSkeleton() {
7+
return (
8+
<div className='flex items-center gap-[12px] px-[8px] py-[8px]'>
9+
<Skeleton className='h-[14px] w-[14px] shrink-0 rounded-[3px]' />
10+
<div className='flex min-w-0 flex-1 flex-col gap-[2px]'>
11+
<Skeleton className='h-[14px] w-[120px]' />
12+
<Skeleton className='h-[12px] w-[180px]' />
13+
</div>
14+
<Skeleton className='h-[30px] w-[64px] shrink-0 rounded-[6px]' />
15+
</div>
16+
)
17+
}

apps/sim/app/workspace/[workspaceId]/settings/components/recently-deleted/recently-deleted.tsx

Lines changed: 101 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,14 @@
11
'use client'
22

33
import { useMemo, useState } from 'react'
4-
import { Loader2, Search } from 'lucide-react'
4+
import { Search } from 'lucide-react'
55
import { 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'
147
import { Input } from '@/components/ui'
158
import { formatDate } from '@/lib/core/utils/formatting'
169
import { RESOURCE_REGISTRY } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry'
1710
import type { MothershipResourceType } from '@/app/workspace/[workspaceId]/home/types'
11+
import { DeletedItemSkeleton } from '@/app/workspace/[workspaceId]/settings/components/recently-deleted/deleted-item-skeleton'
1812
import { useKnowledgeBasesQuery, useRestoreKnowledgeBase } from '@/hooks/queries/kb/knowledge'
1913
import { useRestoreTable, useTablesList } from '@/hooks/queries/tables'
2014
import { 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
}

apps/sim/hooks/queries/kb/knowledge.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ type KnowledgeQueryScope = 'active' | 'archived' | 'all'
1414

1515
export const knowledgeKeys = {
1616
all: ['knowledge'] as const,
17+
lists: () => [...knowledgeKeys.all, 'list'] as const,
1718
list: (workspaceId?: string, scope: KnowledgeQueryScope = 'active') =>
18-
[...knowledgeKeys.all, 'list', workspaceId ?? 'all', scope] as const,
19+
[...knowledgeKeys.lists(), workspaceId ?? 'all', scope] as const,
1920
detail: (knowledgeBaseId?: string) =>
2021
[...knowledgeKeys.all, 'detail', knowledgeBaseId ?? ''] as const,
2122
tagDefinitions: (knowledgeBaseId: string) =>
@@ -1232,7 +1233,7 @@ export function useRestoreKnowledgeBase() {
12321233
return res.json()
12331234
},
12341235
onSettled: () => {
1235-
queryClient.invalidateQueries({ queryKey: knowledgeKeys.all })
1236+
queryClient.invalidateQueries({ queryKey: knowledgeKeys.lists() })
12361237
},
12371238
})
12381239
}

apps/sim/hooks/queries/tables.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ export function useTablesList(workspaceId?: string, scope: TableQueryScope = 'ac
171171
},
172172
enabled: Boolean(workspaceId),
173173
staleTime: 30 * 1000,
174+
placeholderData: keepPreviousData,
174175
})
175176
}
176177

0 commit comments

Comments
 (0)