refactor: enhance button loading states in various components for improved user experience

This commit is contained in:
Anish Sarkar 2026-03-29 16:26:31 +05:30
parent 75fd39c249
commit fec5c005eb
6 changed files with 42 additions and 91 deletions

View file

@ -986,9 +986,10 @@ export function DocumentsTableShell({
handleDeleteFromMenu();
}}
disabled={isDeleting}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
className="relative bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
{isDeleting ? <Spinner size="sm" /> : "Delete"}
<span className={isDeleting ? "opacity-0" : ""}>Delete</span>
{isDeleting && <Spinner size="sm" className="absolute" />}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
@ -1104,9 +1105,10 @@ export function DocumentsTableShell({
handleBulkDelete();
}}
disabled={isBulkDeleting}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
className="relative bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
{isBulkDeleting ? <Spinner size="sm" /> : "Delete"}
<span className={isBulkDeleting ? "opacity-0" : ""}>Delete</span>
{isBulkDeleting && <Spinner size="sm" className="absolute" />}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>

View file

@ -165,8 +165,9 @@ export function PromptsContent() {
<Button variant="ghost" size="sm" onClick={handleCancel}>
Cancel
</Button>
<Button size="sm" onClick={handleSave} disabled={isSaving}>
{isSaving ? <Spinner className="size-3.5" /> : editingId ? "Update" : "Create"}
<Button size="sm" onClick={handleSave} disabled={isSaving} className="relative">
<span className={isSaving ? "opacity-0" : ""}>{editingId ? "Update" : "Create"}</span>
{isSaving && <Spinner className="size-3.5 absolute" />}
</Button>
</div>
</div>

View file

@ -1,7 +1,7 @@
"use client";
import { useAtomValue, useSetAtom } from "jotai";
import { AlertTriangle, Cable, Settings } from "lucide-react";
import { AlertTriangle, Settings } from "lucide-react";
import { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from "react";
import { createPortal } from "react-dom";
import { statusInboxItemsAtom } from "@/atoms/inbox/status-inbox.atom";
@ -12,17 +12,14 @@ import {
import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-query.atoms";
import { searchSpaceSettingsDialogAtom } from "@/atoms/settings/settings-dialog.atoms";
import { currentUserAtom } from "@/atoms/user/user-query.atoms";
import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
import { Spinner } from "@/components/ui/spinner";
import { Tabs, TabsContent } from "@/components/ui/tabs";
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
import { useConnectorsSync } from "@/hooks/use-connectors-sync";
import { PICKER_CLOSE_EVENT, PICKER_OPEN_EVENT } from "@/hooks/use-google-picker";
import { useZeroDocumentTypeCounts } from "@/hooks/use-zero-document-type-counts";
import { cn } from "@/lib/utils";
import { ConnectorDialogHeader } from "./connector-popup/components/connector-dialog-header";
import { ConnectorConnectView } from "./connector-popup/connector-configs/views/connector-connect-view";
import { ConnectorEditView } from "./connector-popup/connector-configs/views/connector-edit-view";
@ -47,7 +44,7 @@ interface ConnectorIndicatorProps {
}
export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, ConnectorIndicatorProps>(
({ showTrigger = true }, ref) => {
(_props, ref) => {
const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom);
const setSearchSpaceSettingsDialog = useSetAtom(searchSpaceSettingsDialogAtom);
useAtomValue(currentUserAtom);
@ -74,8 +71,6 @@ export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, Connector
// Real-time document type counts via Zero (updates instantly as docs are indexed)
const documentTypeCounts = useZeroDocumentTypeCounts(searchSpaceId);
const documentTypesLoading = documentTypeCounts === undefined;
// Read status inbox items from shared atom (populated by LayoutDataProvider)
// instead of creating a duplicate useInbox("status") hook.
const statusInboxItems = useAtomValue(statusInboxItemsAtom);
@ -178,8 +173,6 @@ export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, Connector
inboxItems
);
const isLoading = connectorsLoading || documentTypesLoading;
// Get document types that have documents in the search space
const activeDocumentTypes = documentTypeCounts
? Object.entries(documentTypeCounts).filter(([, count]) => count > 0)
@ -205,40 +198,6 @@ export const ConnectorIndicator = forwardRef<ConnectorIndicatorHandle, Connector
return (
<Dialog open={isOpen} modal={false} onOpenChange={handleOpenChange}>
{showTrigger && (
<TooltipIconButton
data-joyride="connector-icon"
tooltip={
hasConnectors ? `Manage ${activeConnectorsCount} connectors` : "Connect your data"
}
side="bottom"
className={cn(
"size-[34px] rounded-full p-1 flex items-center justify-center transition-colors relative",
"hover:bg-muted-foreground/15 dark:hover:bg-muted-foreground/30",
"outline-none focus:outline-none focus-visible:outline-none font-semibold text-xs",
"border-0 ring-0 focus:ring-0 shadow-none focus:shadow-none"
)}
aria-label={
hasConnectors
? `View ${activeConnectorsCount} connectors`
: "Add your first connector"
}
onClick={() => handleOpenChange(true)}
>
{isLoading ? (
<Spinner size="sm" />
) : (
<>
<Cable className="size-4 stroke-[1.5px]" />
{activeConnectorsCount > 0 && (
<span className="absolute -top-0.5 right-0 flex items-center justify-center min-w-[16px] h-4 px-1 text-[10px] font-medium rounded-full bg-primary text-primary-foreground shadow-sm select-none">
{activeConnectorsCount > 99 ? "99+" : activeConnectorsCount}
</span>
)}
</>
)}
</TooltipIconButton>
)}
{isOpen &&
createPortal(

View file

@ -143,7 +143,7 @@ export const ConnectorCard: FC<ConnectorCardProps> = ({
size="sm"
variant={isConnected ? "secondary" : "default"}
className={cn(
"h-8 text-[11px] px-3 rounded-lg shrink-0 font-medium",
"relative h-8 text-[11px] px-3 rounded-lg shrink-0 font-medium items-center justify-center",
isConnected &&
"bg-white text-slate-700 hover:bg-slate-50 border-0 shadow-xs dark:bg-secondary dark:text-secondary-foreground dark:hover:bg-secondary/80",
!isConnected && "shadow-xs"
@ -151,19 +151,18 @@ export const ConnectorCard: FC<ConnectorCardProps> = ({
onClick={isConnected ? onManage : onConnect}
disabled={isConnecting || !isEnabled}
>
{isConnecting ? (
<Spinner size="xs" />
) : !isEnabled ? (
"Unavailable"
) : isConnected ? (
"Manage"
) : id === "youtube-crawler" ? (
"Add"
) : connectorType ? (
"Connect"
) : (
"Add"
)}
<span className={isConnecting ? "opacity-0" : ""}>
{!isEnabled
? "Unavailable"
: isConnected
? "Manage"
: id === "youtube-crawler"
? "Add"
: connectorType
? "Connect"
: "Add"}
</span>
{isConnecting && <Spinner size="xs" className="absolute" />}
</Button>
</div>
);

View file

@ -795,9 +795,10 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
confirmDeleteChat();
}}
disabled={isDeletingChat}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90 gap-2"
className="relative bg-destructive text-destructive-foreground hover:bg-destructive/90 items-center justify-center"
>
{isDeletingChat ? <Spinner size="sm" /> : tCommon("delete")}
<span className={isDeletingChat ? "opacity-0" : ""}>{tCommon("delete")}</span>
{isDeletingChat && <Spinner size="sm" className="absolute" />}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
@ -835,15 +836,11 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
<Button
onClick={confirmRenameChat}
disabled={isRenamingChat || !newChatTitle.trim()}
className="gap-2"
className="relative"
>
{isRenamingChat ? (
<>
<span className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
{tSidebar("renaming") || "Renaming"}
</>
) : (
tSidebar("rename") || "Rename"
<span className={isRenamingChat ? "opacity-0" : ""}>{tSidebar("rename") || "Rename"}</span>
{isRenamingChat && (
<span className="absolute h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
)}
</Button>
</DialogFooter>
@ -869,15 +866,11 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
confirmDeleteSearchSpace();
}}
disabled={isDeletingSearchSpace}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90 gap-2"
className="relative bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
{isDeletingSearchSpace ? (
<>
<span className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
{t("deleting")}
</>
) : (
tCommon("delete")
<span className={isDeletingSearchSpace ? "opacity-0" : ""}>{tCommon("delete")}</span>
{isDeletingSearchSpace && (
<span className="absolute h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
)}
</AlertDialogAction>
</AlertDialogFooter>
@ -903,15 +896,11 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
confirmLeaveSearchSpace();
}}
disabled={isLeavingSearchSpace}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90 gap-2"
className="relative bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
{isLeavingSearchSpace ? (
<>
<span className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
{t("leaving")}
</>
) : (
t("leave")
<span className={isLeavingSearchSpace ? "opacity-0" : ""}>{t("leave")}</span>
{isLeavingSearchSpace && (
<span className="absolute h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
)}
</AlertDialogAction>
</AlertDialogFooter>

View file

@ -807,9 +807,10 @@ export function DocumentsSidebar({
handleBulkDeleteSelected();
}}
disabled={isBulkDeleting}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
className="relative bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
{isBulkDeleting ? <Spinner size="sm" /> : "Delete"}
<span className={isBulkDeleting ? "opacity-0" : ""}>Delete</span>
{isBulkDeleting && <Spinner size="sm" className="absolute" />}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>