**Issues Fixed:** 1. **Empty Color Values** - Problem: Variation attributes showed 'Color:' with no value - Cause: Backend returned empty strings for some attributes - Fix: Filter empty values with .filter(([_, value]) => value) - Result: Only non-empty attributes displayed 2. **Desktop Should Use Dialog** - Problem: Both desktop and mobile used Drawer (bottom sheet) - Expected: Desktop = Dialog (modal), Mobile = Drawer - Fix: Added useMediaQuery hook, conditional rendering - Pattern: Same as Settings pages (Payments, Shipping, etc.) 3. **Duplicate Product+Variation Handling** - Problem: Same product+variation created new row each time - Expected: Should increment quantity of existing row - Fix: Check for existing item before adding - Logic: findIndex by product_id + variation_id, then increment qty **Changes to OrderForm.tsx:** - Added Dialog and useMediaQuery imports - Added isDesktop detection - Split variation selector into Desktop (Dialog) and Mobile (Drawer) - Fixed variationLabel to filter empty values - Added duplicate check logic before adding to cart - If exists: increment qty, else: add new item **Changes to PROJECT_SOP.md:** - Added Responsive Modal Pattern section - Documented Dialog/Drawer pattern with code example - Added rule 3: Same product+variation = increment qty - Added rule 6: Filter empty attribute values - Added rule 7: Responsive modals (Dialog/Drawer) **Result:** ✅ Color values display correctly (empty values filtered) ✅ Desktop uses Dialog (centered modal) ✅ Mobile uses Drawer (bottom sheet) ✅ Duplicate product+variation increments quantity ✅ UX matches Tokopedia/Shopee pattern ✅ Follows Settings page modal pattern
547 lines
21 KiB
TypeScript
547 lines
21 KiB
TypeScript
import React, { useState, useCallback } from 'react';
|
|
import { useQuery, useMutation, keepPreviousData } from '@tanstack/react-query';
|
|
import { api } from '@/lib/api';
|
|
import { Filter, PackageOpen, Trash2, RefreshCw } from 'lucide-react';
|
|
import { ErrorCard } from '@/components/ErrorCard';
|
|
import { getPageLoadErrorMessage } from '@/lib/errorHandling';
|
|
import { __ } from '@/lib/i18n';
|
|
import { useFABConfig } from '@/hooks/useFABConfig';
|
|
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card';
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle
|
|
} from '@/components/ui/alert-dialog';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Checkbox } from '@/components/ui/checkbox';
|
|
import { toast } from 'sonner';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectGroup,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import { formatRelativeOrDate } from "@/lib/dates";
|
|
import { Link, useNavigate } from 'react-router-dom';
|
|
import { Skeleton } from '@/components/ui/skeleton';
|
|
import { formatMoney, getStoreCurrency } from '@/lib/currency';
|
|
import DateRange from '@/components/filters/DateRange';
|
|
import OrderBy from '@/components/filters/OrderBy';
|
|
import { setQuery, getQuery } from '@/lib/query-params';
|
|
import { OrderCard } from './components/OrderCard';
|
|
import { FilterBottomSheet } from './components/FilterBottomSheet';
|
|
import { SearchBar } from './components/SearchBar';
|
|
|
|
function ItemsCell({ row }: { row: any }) {
|
|
const count: number = typeof row.items_count === 'number' ? row.items_count : 0;
|
|
const brief: string = row.items_brief || '';
|
|
const linesTotal: number | undefined = typeof row.lines_total === 'number' ? row.lines_total : undefined;
|
|
const linesPreview: number | undefined = typeof row.lines_preview === 'number' ? row.lines_preview : undefined;
|
|
const extra = linesTotal && linesPreview ? Math.max(0, linesTotal - linesPreview) : 0;
|
|
|
|
const label = `${count || '—'} item${count === 1 ? '' : 's'}`;
|
|
const inline = brief + (extra > 0 ? ` +${extra} more` : '');
|
|
|
|
return (
|
|
<div className="max-w-[280px] whitespace-nowrap overflow-hidden text-ellipsis">
|
|
<HoverCard openDelay={150}>
|
|
<HoverCardTrigger asChild>
|
|
<span className="cursor-help">
|
|
{label}
|
|
{inline ? <> · {inline}</> : null}
|
|
</span>
|
|
</HoverCardTrigger>
|
|
<HoverCardContent className="max-w-sm text-sm">
|
|
<div className="font-medium mb-1">{label}</div>
|
|
<div className="opacity-80 leading-relaxed">
|
|
{row.items_full || brief || 'No items'}
|
|
</div>
|
|
</HoverCardContent>
|
|
</HoverCard>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const statusStyle: Record<string, string> = {
|
|
pending: 'bg-amber-100 text-amber-800',
|
|
processing: 'bg-blue-100 text-blue-800',
|
|
completed: 'bg-emerald-100 text-emerald-800',
|
|
'on-hold': 'bg-slate-200 text-slate-800',
|
|
cancelled: 'bg-zinc-200 text-zinc-800',
|
|
refunded: 'bg-purple-100 text-purple-800',
|
|
failed: 'bg-rose-100 text-rose-800',
|
|
};
|
|
|
|
function StatusBadge({ value }: { value?: string }) {
|
|
const v = (value || '').toLowerCase();
|
|
const cls = statusStyle[v] || 'bg-slate-100 text-slate-800';
|
|
return (
|
|
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium capitalize ${cls}`}>{v || 'unknown'}</span>
|
|
);
|
|
}
|
|
|
|
export default function Orders() {
|
|
useFABConfig('orders'); // Add FAB for creating orders
|
|
const initial = getQuery();
|
|
const [page, setPage] = useState(Number(initial.page ?? 1) || 1);
|
|
const [status, setStatus] = useState<string | undefined>(initial.status || undefined);
|
|
const [dateStart, setDateStart] = useState<string | undefined>(initial.date_start || undefined);
|
|
const [dateEnd, setDateEnd] = useState<string | undefined>(initial.date_end || undefined);
|
|
const [orderby, setOrderby] = useState<'date'|'id'|'modified'|'total'>((initial.orderby as any) || 'date');
|
|
const [order, setOrder] = useState<'asc'|'desc'>((initial.order as any) || 'desc');
|
|
const [selectedIds, setSelectedIds] = useState<number[]>([]);
|
|
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
|
const [filterSheetOpen, setFilterSheetOpen] = useState(false);
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
|
const perPage = 20;
|
|
|
|
React.useEffect(() => {
|
|
setQuery({ page, status, date_start: dateStart, date_end: dateEnd, orderby, order });
|
|
}, [page, status, dateStart, dateEnd, orderby, order]);
|
|
|
|
const q = useQuery({
|
|
queryKey: ['orders', { page, perPage, status, dateStart, dateEnd, orderby, order }],
|
|
queryFn: () => api.get('/orders', {
|
|
page, per_page: perPage,
|
|
status,
|
|
date_start: dateStart,
|
|
date_end: dateEnd,
|
|
orderby,
|
|
order,
|
|
}),
|
|
placeholderData: keepPreviousData,
|
|
});
|
|
|
|
const data = q.data as undefined | { rows: any[]; total: number; page: number; per_page: number };
|
|
const nav = useNavigate();
|
|
const store = getStoreCurrency();
|
|
|
|
// Pull to refresh
|
|
const handleRefresh = useCallback(async () => {
|
|
setIsRefreshing(true);
|
|
await q.refetch();
|
|
setTimeout(() => setIsRefreshing(false), 500);
|
|
}, [q]);
|
|
|
|
// Filter orders by search query
|
|
const filteredOrders = React.useMemo(() => {
|
|
const rows = data?.rows;
|
|
if (!rows) return [];
|
|
if (!searchQuery.trim()) return rows;
|
|
|
|
const query = searchQuery.toLowerCase();
|
|
return rows.filter((order: any) =>
|
|
order.number?.toString().includes(query) ||
|
|
order.customer?.toLowerCase().includes(query) ||
|
|
order.status?.toLowerCase().includes(query)
|
|
);
|
|
}, [data, searchQuery]);
|
|
|
|
// Count active filters
|
|
const activeFiltersCount = React.useMemo(() => {
|
|
let count = 0;
|
|
if (status) count++;
|
|
if (dateStart || dateEnd) count++;
|
|
if (orderby !== 'date' || order !== 'desc') count++;
|
|
return count;
|
|
}, [status, dateStart, dateEnd, orderby, order]);
|
|
|
|
// Bulk delete mutation
|
|
const deleteMutation = useMutation({
|
|
mutationFn: async (ids: number[]) => {
|
|
const results = await Promise.allSettled(
|
|
ids.map(id => api.del(`/orders/${id}`))
|
|
);
|
|
const failed = results.filter(r => r.status === 'rejected').length;
|
|
return { total: ids.length, failed };
|
|
},
|
|
onSuccess: (result) => {
|
|
const { total, failed } = result;
|
|
if (failed === 0) {
|
|
toast.success(__('Orders deleted successfully'));
|
|
} else if (failed < total) {
|
|
toast.warning(__(`${total - failed} orders deleted, ${failed} failed`));
|
|
} else {
|
|
toast.error(__('Failed to delete orders'));
|
|
}
|
|
setSelectedIds([]);
|
|
setShowDeleteDialog(false);
|
|
q.refetch();
|
|
},
|
|
onError: () => {
|
|
toast.error(__('Failed to delete orders'));
|
|
setShowDeleteDialog(false);
|
|
},
|
|
});
|
|
|
|
// Checkbox handlers
|
|
const allIds = filteredOrders.map(r => r.id) || [];
|
|
const allSelected = allIds.length > 0 && selectedIds.length === allIds.length;
|
|
const someSelected = selectedIds.length > 0 && selectedIds.length < allIds.length;
|
|
|
|
const toggleAll = () => {
|
|
if (allSelected) {
|
|
setSelectedIds([]);
|
|
} else {
|
|
setSelectedIds(allIds);
|
|
}
|
|
};
|
|
|
|
const toggleRow = (id: number) => {
|
|
setSelectedIds(prev =>
|
|
prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id]
|
|
);
|
|
};
|
|
|
|
const handleDeleteClick = () => {
|
|
if (selectedIds.length > 0) {
|
|
setShowDeleteDialog(true);
|
|
}
|
|
};
|
|
|
|
const confirmDelete = () => {
|
|
deleteMutation.mutate(selectedIds);
|
|
};
|
|
|
|
const handleFiltersChange = (newFilters: any) => {
|
|
setPage(1);
|
|
setStatus(newFilters.status);
|
|
setDateStart(newFilters.dateStart);
|
|
setDateEnd(newFilters.dateEnd);
|
|
setOrderby(newFilters.orderby);
|
|
setOrder(newFilters.order);
|
|
};
|
|
|
|
const handleResetFilters = () => {
|
|
setStatus(undefined);
|
|
setDateStart(undefined);
|
|
setDateEnd(undefined);
|
|
setOrderby('date');
|
|
setOrder('desc');
|
|
setPage(1);
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-4 w-full pb-4">
|
|
{/* Mobile: Search + Filter */}
|
|
<div className="md:hidden">
|
|
<SearchBar
|
|
value={searchQuery}
|
|
onChange={setSearchQuery}
|
|
onFilterClick={() => setFilterSheetOpen(true)}
|
|
filterCount={activeFiltersCount}
|
|
/>
|
|
</div>
|
|
|
|
{/* Desktop: Top Bar with Filters */}
|
|
<div className="hidden md:block rounded-lg border border-border p-4 bg-card">
|
|
<div className="flex flex-col lg:flex-row lg:justify-between lg:items-center gap-3">
|
|
<div className="flex gap-3">
|
|
{selectedIds.length > 0 && (
|
|
<button
|
|
className="border rounded-md px-3 py-2 text-sm bg-red-600 text-white hover:bg-red-700 disabled:opacity-50 inline-flex items-center gap-2"
|
|
onClick={handleDeleteClick}
|
|
disabled={deleteMutation.isPending}
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
{__('Delete')} ({selectedIds.length})
|
|
</button>
|
|
)}
|
|
|
|
<button
|
|
className="border rounded-md px-3 py-2 text-sm hover:bg-accent disabled:opacity-50 inline-flex items-center gap-2"
|
|
onClick={handleRefresh}
|
|
disabled={q.isLoading || isRefreshing}
|
|
>
|
|
<RefreshCw className={`w-4 h-4 ${isRefreshing ? 'animate-spin' : ''}`} />
|
|
{__('Refresh')}
|
|
</button>
|
|
</div>
|
|
|
|
<div className="flex gap-2 items-center">
|
|
<Filter className="min-w-4 w-4 h-4 opacity-60" />
|
|
<Select
|
|
value={status ?? 'all'}
|
|
onValueChange={(v) => {
|
|
setPage(1);
|
|
setStatus(v === 'all' ? undefined : (v as typeof status));
|
|
}}
|
|
>
|
|
<SelectTrigger className="min-w-[140px]">
|
|
<SelectValue placeholder={__('All statuses')} />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectGroup>
|
|
<SelectItem value="all">{__('All statuses')}</SelectItem>
|
|
<SelectItem value="pending">{__('Pending')}</SelectItem>
|
|
<SelectItem value="processing">{__('Processing')}</SelectItem>
|
|
<SelectItem value="completed">{__('Completed')}</SelectItem>
|
|
<SelectItem value="on-hold">{__('On-hold')}</SelectItem>
|
|
<SelectItem value="cancelled">{__('Cancelled')}</SelectItem>
|
|
<SelectItem value="refunded">{__('Refunded')}</SelectItem>
|
|
<SelectItem value="failed">{__('Failed')}</SelectItem>
|
|
</SelectGroup>
|
|
</SelectContent>
|
|
</Select>
|
|
|
|
<DateRange
|
|
value={{ date_start: dateStart, date_end: dateEnd }}
|
|
onChange={(v) => { setPage(1); setDateStart(v.date_start); setDateEnd(v.date_end); }}
|
|
/>
|
|
|
|
<OrderBy
|
|
value={{ orderby, order }}
|
|
onChange={(v) => {
|
|
setPage(1);
|
|
setOrderby((v.orderby ?? 'date') as 'date' | 'id' | 'modified' | 'total');
|
|
setOrder((v.order ?? 'desc') as 'asc' | 'desc');
|
|
}}
|
|
/>
|
|
|
|
{activeFiltersCount > 0 && (
|
|
<button
|
|
className="text-sm text-muted-foreground hover:text-foreground underline text-nowrap"
|
|
onClick={handleResetFilters}
|
|
>
|
|
{__('Clear filters')}
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Mobile: Bulk Actions Bar */}
|
|
{selectedIds.length > 0 && (
|
|
<div className="md:hidden sticky top-0 z-30 bg-primary text-primary-foreground px-4 py-3 flex items-center justify-between shadow-lg">
|
|
<span className="font-medium">{selectedIds.length} {__('selected')}</span>
|
|
<div className="flex gap-2">
|
|
<Button
|
|
size="sm"
|
|
variant="secondary"
|
|
onClick={() => setSelectedIds([])}
|
|
>
|
|
{__('Cancel')}
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
variant="destructive"
|
|
onClick={handleDeleteClick}
|
|
disabled={deleteMutation.isPending}
|
|
>
|
|
<Trash2 className="w-4 h-4 mr-1" />
|
|
{__('Delete')}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Pull to Refresh Indicator */}
|
|
{isRefreshing && (
|
|
<div className="md:hidden flex justify-center py-2">
|
|
<RefreshCw className="w-5 h-5 animate-spin text-primary" />
|
|
</div>
|
|
)}
|
|
|
|
{/* Loading State */}
|
|
{q.isLoading && (
|
|
<div className="space-y-3">
|
|
{Array.from({ length: 5 }).map((_, i) => (
|
|
<Skeleton key={i} className="w-full h-24 rounded-lg" />
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Error State */}
|
|
{q.isError && (
|
|
<div>
|
|
<ErrorCard
|
|
title={__('Failed to load orders')}
|
|
message={getPageLoadErrorMessage(q.error)}
|
|
onRetry={() => q.refetch()}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* Mobile: Card List */}
|
|
{!q.isLoading && !q.isError && (
|
|
<>
|
|
<div className="md:hidden space-y-3">
|
|
{filteredOrders.length > 0 ? (
|
|
filteredOrders.map((order) => (
|
|
<OrderCard
|
|
key={order.id}
|
|
order={order}
|
|
selected={selectedIds.includes(order.id)}
|
|
onSelect={toggleRow}
|
|
currencyConfig={store}
|
|
/>
|
|
))
|
|
) : (
|
|
<div className="flex flex-col items-center justify-center py-12 text-center">
|
|
<PackageOpen className="w-12 h-12 opacity-40 mb-3" />
|
|
<div className="font-medium text-lg mb-1">{__('No orders found')}</div>
|
|
{searchQuery ? (
|
|
<p className="text-sm text-muted-foreground">{__('Try a different search term.')}</p>
|
|
) : status ? (
|
|
<p className="text-sm text-muted-foreground">{__('Try adjusting filters.')}</p>
|
|
) : (
|
|
<p className="text-sm text-muted-foreground">{__('Once you receive orders, they\'ll show up here.')}</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Desktop: Table */}
|
|
<div className="hidden md:block rounded-lg border overflow-hidden">
|
|
<table className="min-w-[800px] w-full text-sm">
|
|
<thead className="bg-muted/50">
|
|
<tr className="border-b">
|
|
<th className="w-12 p-3">
|
|
<Checkbox
|
|
checked={allSelected}
|
|
onCheckedChange={toggleAll}
|
|
aria-label={__('Select all')}
|
|
className={someSelected ? 'data-[state=checked]:bg-gray-400' : ''}
|
|
/>
|
|
</th>
|
|
<th className="text-left p-3 font-medium">{__('Order')}</th>
|
|
<th className="text-left p-3 font-medium">{__('Date')}</th>
|
|
<th className="text-left p-3 font-medium">{__('Customer')}</th>
|
|
<th className="text-left p-3 font-medium">{__('Items')}</th>
|
|
<th className="text-left p-3 font-medium">{__('Status')}</th>
|
|
<th className="text-right p-3 font-medium">{__('Total')}</th>
|
|
<th className="text-center p-3 font-medium">{__('Actions')}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{filteredOrders.map((row) => (
|
|
<tr key={row.id} className="border-b hover:bg-muted/30 last:border-0">
|
|
<td className="p-3">
|
|
<Checkbox
|
|
checked={selectedIds.includes(row.id)}
|
|
onCheckedChange={() => toggleRow(row.id)}
|
|
aria-label={__('Select order')}
|
|
/>
|
|
</td>
|
|
<td className="p-3">
|
|
<Link className="underline underline-offset-2" to={`/orders/${row.id}`}>#{row.number}</Link>
|
|
</td>
|
|
<td className="p-3 min-w-32">
|
|
<span title={row.date ?? ""}>
|
|
{formatRelativeOrDate(row.date_ts)}
|
|
</span>
|
|
</td>
|
|
<td className="p-3">{row.customer || '—'}</td>
|
|
<td className="p-3">
|
|
<ItemsCell row={row} />
|
|
</td>
|
|
<td className="p-3"><StatusBadge value={row.status} /></td>
|
|
<td className="p-3 text-right tabular-nums font-mono">
|
|
{formatMoney(row.total, {
|
|
currency: row.currency || store.currency,
|
|
symbol: row.currency_symbol || store.symbol,
|
|
thousandSep: store.thousand_sep,
|
|
decimalSep: store.decimal_sep,
|
|
position: store.position,
|
|
decimals: store.decimals,
|
|
})}
|
|
</td>
|
|
<td className="p-3 text-center space-x-2">
|
|
<Link className="btn text-sm underline underline-offset-2" to={`/orders/${row.id}`}>{__('Open')}</Link>
|
|
<Link className="btn text-sm underline underline-offset-2" to={`/orders/${row.id}/edit`}>{__('Edit')}</Link>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
|
|
{filteredOrders.length === 0 && (
|
|
<tr>
|
|
<td className="p-8 text-center text-muted-foreground" colSpan={8}>
|
|
<div className="flex flex-col items-center gap-2">
|
|
<PackageOpen className="w-8 h-8 opacity-40" />
|
|
<div className="font-medium">{__('No orders found')}</div>
|
|
{status ? (
|
|
<p className="text-sm opacity-70">{__('Try adjusting filters.')}</p>
|
|
) : (
|
|
<p className="text-sm opacity-70">{__('Once you receive orders, they\'ll show up here.')}</p>
|
|
)}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* Pagination */}
|
|
{!q.isLoading && !q.isError && filteredOrders.length > 0 && (
|
|
<div className="flex items-center justify-center gap-2 px-4 md:px-0">
|
|
<button
|
|
className="border rounded-md px-3 py-2 text-sm disabled:opacity-50"
|
|
disabled={page <= 1}
|
|
onClick={() => setPage((p) => p - 1)}
|
|
>
|
|
{__('Previous')}
|
|
</button>
|
|
<div className="text-sm opacity-80">{__('Page')} {page}</div>
|
|
<button
|
|
className="border rounded-md px-3 py-2 text-sm disabled:opacity-50"
|
|
disabled={!data || page * perPage >= data.total}
|
|
onClick={() => setPage((p) => p + 1)}
|
|
>
|
|
{__('Next')}
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{/* Mobile: Filter Bottom Sheet */}
|
|
<FilterBottomSheet
|
|
open={filterSheetOpen}
|
|
onClose={() => setFilterSheetOpen(false)}
|
|
filters={{ status, dateStart, dateEnd, orderby, order }}
|
|
onFiltersChange={handleFiltersChange}
|
|
onReset={handleResetFilters}
|
|
activeFiltersCount={activeFiltersCount}
|
|
/>
|
|
|
|
{/* Delete Confirmation Dialog */}
|
|
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>{__('Delete Orders')}</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
{__('Are you sure you want to delete')} {selectedIds.length} {selectedIds.length === 1 ? __('order') : __('orders')}?
|
|
<br />
|
|
<span className="text-red-600 font-medium">{__('This action cannot be undone.')}</span>
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel
|
|
onClick={() => setShowDeleteDialog(false)}
|
|
disabled={deleteMutation.isPending}
|
|
>
|
|
{__('Cancel')}
|
|
</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
onClick={confirmDelete}
|
|
disabled={deleteMutation.isPending}
|
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
>
|
|
{deleteMutation.isPending ? __('Deleting...') : __('Delete')}
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</div>
|
|
);
|
|
}
|