feat(studio): preserve work position and guide import completion
CI / check (push) Successful in 2m20s
CI / deploy (push) Successful in 18s
CI / publish-container (push) Successful in 1m22s

This commit is contained in:
Simo committed 2026-09-11 10:48:09 +02:00
1 parent c5a2b44807
commit 89af4eb1f6
6 files changed
+425 -10

No files matched your search

+84 -2
View File
@@ -17,6 +17,7 @@ import { adminFetch } from "@/lib/admin-fetch";
import { previewAutoCatalog } from "@/lib/furni/auto-catalog";
import { compareFurniture } from "@/lib/furni/studio-inspection";
import { FurnitureComparison } from "./furniture-inspector";
import { importRecommendation } from "./import-recommendation";
import { ImportSourceDiagnostics } from "./import-source-diagnostics";
import type { FurniItem } from "./studio-types";
import { useFurnitureInspection } from "./use-furniture-inspection";
@@ -39,6 +40,10 @@ export function ImportReview({
busy?: boolean;
onConfirm: (items: FurniItem[]) => void;
}) {
const preflightT = useTranslations("admin.studio.preflight");
const [reviewFilter, setReviewFilter] = useState<
"all" | "complete" | "conflict" | "verify"
>("all");
const diagnosticsT = useTranslations("admin.studio.importDiagnostics");
const completenessT = useTranslations("admin.studio.completeness");
const [attachments, setAttachments] = useState<Record<string, string>>({});
@@ -97,13 +102,26 @@ export function ImportReview({
compareFurniture(item, local).state !== "conflict"
);
}).length;
const pageItems = (step === "review" ? items : ready).slice(
const recommendations = new Map(
inspection.items.map((local) => [
local.classname,
importRecommendation(local),
]),
);
const reviewItems =
reviewFilter === "all"
? items
: items.filter(
(item) =>
recommendations.get(item.classname)?.action === reviewFilter,
);
const pageItems = (step === "review" ? reviewItems : ready).slice(
page * 50,
(page + 1) * 50,
);
const pageCount = Math.max(
1,
Math.ceil((step === "review" ? items : ready).length / 50),
Math.ceil((step === "review" ? reviewItems : ready).length / 50),
);
return (
<Dialog
@@ -220,10 +238,49 @@ export function ImportReview({
)}
{step === "review" ? (
<div className="space-y-2">
<fieldset
className="flex flex-wrap gap-2"
aria-label={preflightT("filterLabel")}
>
{(["all", "complete", "conflict", "verify"] as const).map(
(filter) => (
<Button
key={filter}
variant="outline"
aria-pressed={reviewFilter === filter}
onClick={() => {
setReviewFilter(filter);
setPage(0);
setExpanded("");
}}
>
{preflightT(`filters.${filter}`, {
count:
filter === "all"
? items.length
: items.filter(
(item) =>
recommendations.get(item.classname)
?.action === filter,
).length,
})}
</Button>
),
)}
</fieldset>
{reviewItems.length === 0 && (
<p
role="status"
className="p-3 text-sm text-[var(--admin-text-muted)]"
>
{preflightT("empty")}
</p>
)}
{pageItems.map((item) => {
const local = localByName.get(item.classname);
if (!local) return null;
const comparison = compareFurniture(item, local);
const recommendation = importRecommendation(local);
return (
<div
key={item.classname}
@@ -244,6 +301,31 @@ export function ImportReview({
<span className="block text-xs text-[var(--admin-text-muted)]">
{item.classname}
</span>
<span className="mt-1 block text-xs font-medium">
{preflightT(`actions.${recommendation.action}`)}
</span>
{recommendation.missing.length > 0 && (
<span className="mt-1 block text-xs text-[var(--admin-warning)]">
{preflightT("missing", {
parts: recommendation.missing
.map((part) =>
completenessT(`parts.${part}`),
)
.join(", "),
})}
</span>
)}
{recommendation.unknown.length > 0 && (
<span className="mt-1 block text-xs text-[var(--admin-text-muted)]">
{preflightT("unknown", {
parts: recommendation.unknown
.map((part) =>
completenessT(`parts.${part}`),
)
.join(", "),
})}
</span>
)}
</span>
<span>
{comparison.state === "new"