Skip to content

Commit 5d2a2e8

Browse files
authored
Merge pull request #214 from itsmeakhil/akhil/optimise-nosql
Akhil/optimise nosql
2 parents 8879676 + e9e9ebc commit 5d2a2e8

11 files changed

Lines changed: 2739 additions & 17 deletions

apps/web/messages/en.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1556,7 +1556,10 @@
15561556
"dialogRenameCollectionTitle": "Rename collection",
15571557
"dialogRenameCollectionDescription": "Enter a new name for the collection.",
15581558
"placeholderRenameCollection": "Collection name",
1559-
"renameAction": "Rename"
1559+
"renameAction": "Rename",
1560+
"dialogDeleteBulkTitle": "Delete Collections",
1561+
"dialogDeleteBulkDescription": "This action cannot be undone. The following collections will be permanently deleted:",
1562+
"cancel": "Cancel"
15601563
},
15611564
"collectionItem": {
15621565
"newFolder": "New folder",

apps/web/src/components/api-client/api-client.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ export function ApiClient() {
8080
const [activeTabId, setActiveTabId] = React.useState<string>(tabs[0].id)
8181
const [isInitialized, setIsInitialized] = React.useState(false)
8282
const abortControllerRef = React.useRef<AbortController | null>(null)
83-
const { collections, addFolder, deleteItem, saveRequest, toggleFolder, createCollection, renameCollection, renameFolder, isLoading: collectionsLoading } = useCollections()
83+
const { collections, addFolder, deleteItem, saveRequest, toggleFolder, createCollection, renameCollection, renameFolder, deleteMultipleCollections, isLoading: collectionsLoading } = useCollections()
8484
const { history, addHistoryItem, clearHistory, deleteHistoryItem } = useHistory()
8585
const {
8686
environments,
@@ -530,7 +530,7 @@ export function ApiClient() {
530530
try {
531531
const parsed = parseCurlCommand(curl)
532532
const resolvedUrl = replaceUrlWithEnvBaseUrl(parsed.url)
533-
533+
534534
updateActiveTab({
535535
...parsed,
536536
url: resolvedUrl || activeTab.url,
@@ -543,6 +543,10 @@ export function ApiClient() {
543543
}
544544
}
545545

546+
const handleDeleteMultipleCollections = async (ids: string[]) => {
547+
await deleteMultipleCollections(ids)
548+
}
549+
546550
return (
547551
<div className="flex h-full min-h-0 w-full flex-col gap-4 mobile-nav-offset lg:flex-row">
548552
<div className="flex-1 flex flex-col gap-4 min-w-0 h-full">
@@ -575,6 +579,7 @@ export function ApiClient() {
575579
history={history}
576580
onClearHistory={clearHistory}
577581
onDeleteHistoryItem={deleteHistoryItem}
582+
onDeleteMultiple={handleDeleteMultipleCollections}
578583
/>
579584
</div>
580585
</SheetContent>
@@ -763,6 +768,7 @@ export function ApiClient() {
763768
history={history}
764769
onClearHistory={clearHistory}
765770
onDeleteHistoryItem={deleteHistoryItem}
771+
onDeleteMultiple={handleDeleteMultipleCollections}
766772
/>
767773
</div>
768774
)}

apps/web/src/components/api-client/collections/collections-sidebar.tsx

Lines changed: 107 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import * as React from "react"
44
import { Button } from "@/components/ui/button"
5+
import { Checkbox } from "@/components/ui/checkbox"
56
import { ScrollArea } from "@/components/ui/scroll-area"
67
import { Collection, CollectionFolder, CollectionRequest, HistoryRequest } from "../types"
78
import { CollectionItem } from "./collection-item"
@@ -42,6 +43,7 @@ interface CollectionsSidebarProps {
4243
history?: HistoryRequest[]
4344
onClearHistory?: () => void
4445
onDeleteHistoryItem?: (id: string) => void
46+
onDeleteMultiple?: (ids: string[]) => void
4547
}
4648

4749
export function CollectionsSidebar({
@@ -57,6 +59,7 @@ export function CollectionsSidebar({
5759
history,
5860
onClearHistory,
5961
onDeleteHistoryItem,
62+
onDeleteMultiple,
6063
}: CollectionsSidebarProps) {
6164
const t = useTranslations("ApiClient.collectionsSidebar")
6265
const tRoot = useTranslations("ApiClient")
@@ -89,6 +92,9 @@ export function CollectionsSidebar({
8992
const [renameCollectionName, setRenameCollectionName] = React.useState("")
9093
const [targetParentId, setTargetParentId] = React.useState<string | null>(null)
9194
const [targetCollectionId, setTargetCollectionId] = React.useState<string | null>(null)
95+
const [selectedCollections, setSelectedCollections] = React.useState<Set<string>>(new Set())
96+
const [deleteBulkDialogOpen, setDeleteBulkDialogOpen] = React.useState(false)
97+
const [isDeleting, setIsDeleting] = React.useState(false)
9298

9399
const handleAddFolder = () => {
94100
if (newFolderName && targetParentId) {
@@ -127,21 +133,50 @@ export function CollectionsSidebar({
127133
setRenameCollectionDialogOpen(true)
128134
}
129135

136+
const toggleCollectionSelection = (collectionId: string) => {
137+
setSelectedCollections(prev => {
138+
const next = new Set(prev)
139+
if (next.has(collectionId)) {
140+
next.delete(collectionId)
141+
} else {
142+
next.add(collectionId)
143+
}
144+
return next
145+
})
146+
}
147+
148+
const clearSelection = () => {
149+
setSelectedCollections(new Set())
150+
}
151+
130152
return (
131153
<div className="flex flex-col w-full h-full bg-background/50">
132154
<Tabs defaultValue="collections" className="flex-1 flex flex-col h-full min-h-0">
133155
<div className="px-4 py-3 border-b flex flex-col gap-3 shrink-0 bg-card/40 backdrop-blur-sm">
134156
<div className="flex items-center justify-between">
135157
<h3 className="font-semibold text-sm tracking-tight">{t("title")}</h3>
136-
<Button
137-
variant="ghost"
138-
size="icon"
139-
className="h-7 w-7 rounded-lg hover:bg-primary/10 hover:text-primary transition-colors"
140-
onClick={() => setNewCollectionDialogOpen(true)}
141-
title={t("newCollection")}
142-
>
143-
<FolderPlus className="h-4 w-4" />
144-
</Button>
158+
<div className="flex items-center gap-2">
159+
{selectedCollections.size > 0 && (
160+
<Button
161+
variant="destructive"
162+
size="sm"
163+
className="h-7 px-3 rounded-lg text-xs font-medium gap-2"
164+
onClick={() => setDeleteBulkDialogOpen(true)}
165+
>
166+
<Trash2 className="h-3.5 w-3.5" />
167+
Delete ({selectedCollections.size})
168+
</Button>
169+
)}
170+
<Button
171+
variant="ghost"
172+
size="icon"
173+
className="h-7 w-7 rounded-lg hover:bg-primary/10 hover:text-primary transition-colors"
174+
onClick={() => setNewCollectionDialogOpen(true)}
175+
title={t("newCollection")}
176+
>
177+
<FolderPlus className="h-4 w-4" />
178+
</Button>
179+
</div>
145180
</div>
146181
<TabsList className="w-full grid grid-cols-2 p-1 bg-muted/50 rounded-lg">
147182
<TabsTrigger value="collections" className="rounded-md text-xs font-medium">{t("tabCollections")}</TabsTrigger>
@@ -173,9 +208,18 @@ export function CollectionsSidebar({
173208
collections.map((collection) => (
174209
<div key={collection.id} className="mb-4">
175210
<div className="flex items-center justify-between px-2 py-1.5 mb-1 group rounded-md hover:bg-muted/50 transition-colors">
176-
<span className="text-[11px] font-bold text-muted-foreground uppercase tracking-wider truncate flex-1 mr-2 px-1">
177-
{collection.name}
178-
</span>
211+
<div className="flex items-center gap-2 flex-1 min-w-0">
212+
<div className="opacity-0 group-hover:opacity-100 transition-opacity flex items-center">
213+
<Checkbox
214+
checked={selectedCollections.has(collection.id)}
215+
onCheckedChange={() => toggleCollectionSelection(collection.id)}
216+
className="h-4 w-4"
217+
/>
218+
</div>
219+
<span className="text-[11px] font-bold text-muted-foreground uppercase tracking-wider truncate flex-1 px-1">
220+
{collection.name}
221+
</span>
222+
</div>
179223
<div className="flex items-center opacity-0 group-hover:opacity-100 transition-opacity">
180224
<Button
181225
variant="ghost"
@@ -200,14 +244,14 @@ export function CollectionsSidebar({
200244
<DropdownMenuItem onClick={() => openRenameCollectionDialog(collection)}>
201245
<Pencil className="h-4 w-4 mr-2" />
202246
{t("rename")}
203-
</DropdownMenuItem>
247+
</DropdownMenuItem>
204248
<DropdownMenuItem
205249
className="text-destructive focus:text-destructive"
206250
onClick={() => onDelete(collection.id)}
207251
>
208252
<Trash2 className="h-4 w-4 mr-2" />
209253
{t("delete")}
210-
</DropdownMenuItem>
254+
</DropdownMenuItem>
211255
</DropdownMenuContent>
212256
</DropdownMenu>
213257
</div>
@@ -432,6 +476,55 @@ export function CollectionsSidebar({
432476
</DialogFooter>
433477
</DialogContent>
434478
</Dialog>
479+
480+
<Dialog open={deleteBulkDialogOpen} onOpenChange={setDeleteBulkDialogOpen}>
481+
<DialogContent className="sm:max-w-[425px]">
482+
<DialogHeader>
483+
<DialogTitle>{t("dialogDeleteBulkTitle") || "Delete Collections"}</DialogTitle>
484+
<DialogDescription>
485+
{t("dialogDeleteBulkDescription") || "This action cannot be undone. The following collections will be permanently deleted:"}
486+
</DialogDescription>
487+
</DialogHeader>
488+
<div className="py-4">
489+
<div className="space-y-2 max-h-[200px] overflow-y-auto">
490+
{Array.from(selectedCollections).map(collectionId => {
491+
const collection = collections.find(c => c.id === collectionId)
492+
return (
493+
<div key={collectionId} className="flex items-center gap-2 px-3 py-2 bg-muted/50 rounded-md border border-border/50">
494+
<Trash2 className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
495+
<span className="text-sm font-medium truncate">{collection?.name || "Unknown"}</span>
496+
</div>
497+
)
498+
})}
499+
</div>
500+
</div>
501+
<DialogFooter>
502+
<Button variant="outline" onClick={() => setDeleteBulkDialogOpen(false)}>
503+
{t("cancel") || "Cancel"}
504+
</Button>
505+
<Button
506+
variant="destructive"
507+
disabled={isDeleting}
508+
onClick={async () => {
509+
setIsDeleting(true)
510+
try {
511+
await onDeleteMultiple?.(Array.from(selectedCollections))
512+
// Only close dialog and clear selection on successful deletion
513+
setDeleteBulkDialogOpen(false)
514+
clearSelection()
515+
} catch (error) {
516+
// Keep dialog open if deletion failed so user can retry
517+
console.error("Error deleting collections:", error)
518+
} finally {
519+
setIsDeleting(false)
520+
}
521+
}}
522+
>
523+
{isDeleting ? "Deleting..." : (t("delete") || "Delete")}
524+
</Button>
525+
</DialogFooter>
526+
</DialogContent>
527+
</Dialog>
435528
</div>
436529
)
437530
}

apps/web/src/components/api-client/collections/use-collections.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,66 @@ export function useCollections() {
367367
}
368368
}
369369

370+
const deleteMultipleCollections = async (ids: string[]) => {
371+
if (!user) return
372+
if (ids.length === 0) return
373+
374+
try {
375+
// Use Promise.allSettled to handle partial failures gracefully
376+
const deleteResults = await Promise.allSettled(
377+
ids.map((id) =>
378+
authedFetch(`/api/backend/api-client/collections/${id}`, { method: "DELETE" })
379+
)
380+
)
381+
382+
const successfulIds: string[] = []
383+
const failedIds: string[] = []
384+
385+
deleteResults.forEach((result, index) => {
386+
if (result.status === "fulfilled") {
387+
if (result.value.ok) {
388+
successfulIds.push(ids[index])
389+
} else {
390+
failedIds.push(ids[index])
391+
}
392+
} else {
393+
failedIds.push(ids[index])
394+
}
395+
})
396+
397+
// Remove successfully deleted collections from state
398+
if (successfulIds.length > 0) {
399+
setCollections((prev) => prev.filter((c) => !successfulIds.includes(c.id)))
400+
}
401+
402+
// Handle results with appropriate feedback
403+
if (failedIds.length === 0) {
404+
// All successful
405+
toast.success(ids.length === 1 ? "Collection deleted" : `${ids.length} collections deleted`)
406+
} else if (successfulIds.length === 0) {
407+
// All failed
408+
console.error("Failed to delete collections:", failedIds)
409+
toast.error(ids.length === 1 ? "Failed to delete collection" : "Failed to delete collections")
410+
throw new Error("All collections failed to delete")
411+
} else {
412+
// Partial failure
413+
const failedNames = failedIds
414+
.map(id => collections.find(c => c.id === id)?.name)
415+
.filter(Boolean)
416+
.join(", ")
417+
console.error("Partial failure deleting collections:", failedIds)
418+
toast.error(`Deleted ${successfulIds.length} of ${ids.length} collections. Failed: ${failedNames || "unknown"}`)
419+
throw new Error(`Partial failure: ${failedIds.length} collections failed to delete`)
420+
}
421+
} catch (error) {
422+
console.error("Failed to delete collections:", error)
423+
if (!(error instanceof Error) || !error.message.includes("Partial failure")) {
424+
toast.error("Failed to delete collections")
425+
}
426+
throw error // Re-throw so caller knows deletion failed
427+
}
428+
}
429+
370430
return {
371431
collections,
372432
addFolder,
@@ -376,6 +436,7 @@ export function useCollections() {
376436
createCollection,
377437
renameCollection,
378438
renameFolder,
439+
deleteMultipleCollections,
379440
isLoading
380441
}
381442
}

0 commit comments

Comments
 (0)