Studio: add clone-all-per-hotel, per-item status detail, retry, ETA, and auto-translate
CI / runtime-diagnostics (push) Skipped
CI / check (push) Successful in 36s
CI / release (push) Skipped
CI / deploy (push) Successful in 1m14s

- Add 'Clone all missing' button for clone sources with confirmation dialog
- Show per-item status badges: nitro, furnidata, catalog entry
- Add status filters: missing furnidata, missing catalog entry
- Add ETA and percentage to batch progress bar
- Add retry button for failed items after batch import
- Auto-translate all languages after clone-all completes
- Fix statusFilter 'all' bug that incorrectly filtered imported items
- Bulk-load FurnitureData + catalog references for O(1) per-item checks
This commit is contained in:
openhands committed 2026-09-01 17:29:12 +02:00
1 parent 1610aa1008
commit 1374bd332f
4 files changed
+580 -47

No files matched your search

+66 -9
View File
@@ -1,6 +1,7 @@
"use client";
import { Ban, Check, Loader2, ShieldCheck, X } from "lucide-react";
import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
@@ -32,6 +33,13 @@ function SummaryRow({
);
}
function formatEta(ms: number): string {
if (ms < 60_000) return `${Math.ceil(ms / 1000)}s`;
const min = Math.floor(ms / 60_000);
const sec = Math.ceil((ms % 60_000) / 1000);
return `${min}m ${sec}s`;
}
export function BatchProgress({
progress,
done,
@@ -39,8 +47,11 @@ export function BatchProgress({
failed,
cancelling,
verification,
startTime,
failedClassnames,
onCancel,
onDismiss,
onRetry,
}: {
progress: Map<string, BatchItemStatus>;
done: boolean;
@@ -48,8 +59,11 @@ export function BatchProgress({
failed: number;
cancelling?: boolean;
verification?: VerificationSummary | null;
startTime?: number;
failedClassnames?: string[];
onCancel?: () => void;
onDismiss?: () => void;
onRetry?: (classnames: string[]) => void;
}) {
const entries = [...progress.entries()];
const total = entries.length;
@@ -58,6 +72,27 @@ export function BatchProgress({
).length;
const percent = total > 0 ? Math.round((completed / total) * 100) : 0;
// Live ETA calculation
const [eta, setEta] = useState<string | null>(null);
useEffect(() => {
if (done || !startTime || completed === 0) {
setEta(null);
return;
}
const elapsed = Date.now() - startTime;
const avgPerItem = elapsed / completed;
const remaining = (total - completed) * avgPerItem;
setEta(formatEta(remaining));
const interval = setInterval(() => {
const elapsed = Date.now() - startTime;
const avgPerItem = elapsed / completed;
const remaining = (total - completed) * avgPerItem;
setEta(formatEta(remaining));
}, 2000);
return () => clearInterval(interval);
}, [done, startTime, completed, total]);
return (
<div className="border-t border-[var(--admin-border)] p-3">
<div className="mb-2 flex items-center justify-between gap-3">
@@ -81,6 +116,11 @@ export function BatchProgress({
<span className="text-xs font-mono text-[var(--admin-text-muted)]">
{completed}/{total}
</span>
{eta && (
<span className="text-xs font-mono text-[var(--admin-accent)]">
~{eta}
</span>
)}
<Loader2
size={14}
className="animate-spin text-[var(--admin-accent)]"
@@ -90,11 +130,16 @@ export function BatchProgress({
</div>
{!done && (
<div className="mb-2 h-1.5 overflow-hidden rounded-full bg-[var(--admin-accent)]/10">
<div
className="h-full rounded-full bg-[var(--admin-accent)] transition-all duration-300"
style={{ width: `${percent}%` }}
/>
<div className="mb-2 flex items-center gap-2">
<div className="h-1.5 flex-1 overflow-hidden rounded-full bg-[var(--admin-accent)]/10">
<div
className="h-full rounded-full bg-[var(--admin-accent)] transition-all duration-300"
style={{ width: `${percent}%` }}
/>
</div>
<span className="text-xs font-mono font-bold text-[var(--admin-text)]">
{percent}%
</span>
</div>
)}
@@ -165,10 +210,22 @@ export function BatchProgress({
<div className="mt-2 flex justify-end gap-2">
{done ? (
<Button size="sm" variant="secondary" onClick={onDismiss}>
<Check size={14} />
Dismiss
</Button>
<>
{failedClassnames && failedClassnames.length > 0 && onRetry && (
<Button
size="sm"
variant="outline"
onClick={() => onRetry(failedClassnames)}
>
<Loader2 size={14} />
Retry {failedClassnames.length} failed
</Button>
)}
<Button size="sm" variant="secondary" onClick={onDismiss}>
<Check size={14} />
Dismiss
</Button>
</>
) : (
<Button
size="sm"